新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring AI中MCP工具链落地:从配置到实战踩坑指南

发布时间:2026/10/1 3:20:37来源:尧图网络
Spring AI中MCP工具链落地:从配置到实战踩坑指南
我第一次在Spring AI里把MCP工具链跑通的时候第一反应是之前手写Function Calling工具描述和JsonSchema的日子终于可以消停一会儿了。MCPModel Context Protocol这个名字听起来很“协议”但落到Spring AI项目里它其实就是在解决一件很具体的事让大模型应用能统一地发现外部工具、调用外部工具、接收外部工具返回的数据。Spring AI从1.0时代就开始把MCP作为核心能力来推提供了MCP Client和MCP Server两条完整路径。这篇文章我不会复读官方文档而是把我自己在Spring Boot项目里配置MCP Server、挂载MCP工具、让Agent真正调用工具的完整操作过程写下来包括环境配置、代码示例、验证方式以及我踩过的几个坑。适合已经搭过Spring AI但还没搞明白MCP怎么落地的开发者。1. 先搞清楚MCP在Spring AI里到底解决什么问题1.1 从Function Calling到MCP工具调用方式的自然演进在做AI应用之前大多数后端系统对外暴露能力的方式是REST接口模型要调用系统能力需要有人提前写好工具描述告诉模型“这个工具叫什么、参数是什么、返回什么”。这就是Function CallingSpring AI里对应的就是Tool注解和ToolCallback。但问题在于每个系统都自己定义工具描述方式A系统的工具规范和B系统的工具规范可能完全不一样。今天你要接文件系统写一套明天要接数据库再写一套后天要接浏览器自动化又得重新适配。这种重复劳动本质上是“工具协议”没有统一。MCP就是干这个的它把“工具发现”“工具调用”“结果返回”这三个动作标准化了。一个MCP Server可以暴露文件读写、SQL查询、HTTP请求、浏览器操作等任意能力而MCP Client只需要按协议去请求就能拿到工具列表并执行调用。Spring AI选择把MCP纳入生态等于帮你把外部工具接入过程中的协议层问题一并解决掉。1.2 Spring AI在MCP生态里的位置既是Client又是Server很多人第一次接触MCP时容易把这个概念想窄了以为MCP只是“AI去调用别人工具”的通道。实际上在Spring AI里MCP的操作是双向的MCP ClientSpring Boot应用作为连接方去连接本地的stdio进程或远程MCP Server把外部工具注册给AI模型使用。MCP ServerSpring Boot应用作为提供方把自己业务类里的方法用Tool暴露成标准MCP工具供其他AI客户端或Agent调用。也就是说同一个Spring Boot项目既可以去“消费”外部MCP工具也可以“生产”自己的MCP工具。理解这一点很关键因为企业里往往两条都会用到既有需要调用第三方能力的场景也有需要把自己内部服务能力安全暴露给AI平台的场景。Spring AI官方提供的起步依赖也对应这两条线spring-ai-starter-mcp-client和spring-ai-starter-mcp-server。接下来的章节会把这两条线的操作都过一遍。1.3 什么时候值得上MCP什么时候还是写Function Calling更省事MCP虽好但也不是银弹。如果项目里只有两三个固定工具而且没有跨系统复用需求直接写Tool方法可能更轻快。但如果你遇到下面这些情况就值得认真考虑MCP了工具列表在持续扩展今天文件系统、明天数据库、后天内部OA。有多个AI应用都需要复用同一套工具能力希望用统一标准对外暴露。想直接消费开源社区已有的MCP Server生态而不是从零开发。Agent场景里需要动态发现工具而不是把工具硬编码在提示词里。我个人的判断标准是工具数量超过5个或者有三方系统接入者就走MCP否则先别过度设计。2. 先跑通一个标准MCP Clientstdio通道连接文件系统Server2.1 先落好依赖BOM和Starter在Spring AI里操作MCP第一步永远是先把依赖引入对。我建议直接用spring-ai-bom统一管理版本避免自己手写MCP相关依赖的版本号。dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后引入MCP Client的Starterdependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency这里特别提醒一句spring-ai-starter-mcp-client内部已经帮我们把MCP协议相关的SDK、自动配置、ToolCallback转换都装配好了。除非你确实要自己实现底层协议否则不要在业务模块里再手动引入io.modelcontextprotocol.sdk的各个包版本冲突的概率会成倍增加。如果你用的是Gradle同样建议在dependencyManagement里引入BOMimplementation platform(org.springframework.ai:spring-ai-bom:1.0.0) implementation org.springframework.ai:spring-ai-starter-mcp-client2.2 配置一个stdio类型的MCP ServerSpring AI的MCP Client通过配置就能连接MCP Server不需要写连接管理代码。最简单的场景就是连接一个由本地命令启动的MCP Server比如官方文件系统Server它是通过npx启动的Node.js进程。在application.yml里这样配置spring: ai: mcp: client: stdio: servers: - name: filesystem command: npx args: - -y - modelcontextprotocol/server-filesystem - /tmp/data env: NODE_ENV: production这段配置的含义是启动一个名为filesystem的MCP Server用npx运行官方的server-filesystem包并允许它访问/tmp/data目录。name字段会作为工具名前缀出现在模型工具列表里command和args对应真正的进程启动命令env则用于注入环境变量。配置完成后启动Spring Boot应用Spring AI会自动拉起这个进程通过标准输入输出与它通信。第一次启动时npx需要拉包耗时可能会长一些所以不要急着断言“连不上”。2.3 验证MCP工具是不是真的注册成功了配置对了不等于“工具进模型上下文了”需要验证。最直接的办法是注入已经注册好的ToolCallback列表把工具名打出来Component public class ToolListLogger implements ApplicationRunner { private final ListToolCallback toolCallbacks; public ToolListLogger(ListToolCallback toolCallbacks) { this.toolCallbacks toolCallbacks; } Override public void run(ApplicationArguments args) { toolCallbacks.forEach(tool - System.out.println(MCP Tool: tool.getToolDefinition().name())); } }如果配置正确你会看到类似filesystem_read_file、filesystem_write_file、filesystem_directory_tree这样的工具名出现。它们就是MCP Server通过协议暴露给模型的能力清单。再进一步可以写一个最简单的模型调用请求让Agent自己去调用工具ChatClient chatClient ChatClient.builder(chatModel).build(); String response chatClient.prompt() .user(帮我列出 /tmp/data 目录下的文件名称) .call() .content();当模型认为需要查看文件系统时它会自动选择名为filesystem_directory_tree或filesystem_read_file的工具并把工具参数按协议发回给本地进程拿到结果后再组织自然语言回答。这里就能直观感受到MCP的价值模型不需要知道文件系统API长什么样只需要知道有工具可用。2.4 在Windows上跑stdio进程的一个常见坑上面的配置在macOS和Linux上通常没问题但在Windows上很容易启动失败。原因是Spring AI通过ProcessBuilder启动进程而Windows下npx往往需要npx.cmd才能被正确解析。一种解决方式是把command改成npx.cmd同时在args里保持原来的参数。如果目录路径里有空格注意用working-directory属性显式指定工作目录spring: ai: mcp: client: stdio: servers: - name: filesystem command: npx.cmd args: - -y - modelcontextprotocol/server-filesystem - C:/Users/me/data working-directory: C:/projects/ai-agent这类问题不会在Linux上复现容易让人怀疑是自己配置写错了。先查命令是否在PATH里再看args的拆法是否符合yaml数组的预期最后确认工作目录。基本上按这个顺序排查stdio进程都能起来。3. 把一个Spring Boot应用改造成MCP Server把业务方法变成标准工具3.1 为什么企业里往往需要自建MCP ServerMCP Client只是“消费”外部工具但更多时候企业内部希望把自己独有的业务能力也暴露成MCP工具。比如你有一个订单中心、一个库存中心想让公司里的AI助手统一查询总不能每次都去外部装一个第三方Server。更合理的做法是直接用Spring Boot加Tool注解把业务方法变成标准MCP工具暴露成一个HTTP端点供内部MCP Client和AI平台调用。这样做的好处是工具归属权在自己手里鉴权、审计、参数校验都放在业务边界内不会被外部MCP Server的黑盒实现绑架。3.2 引入服务端Starter并配置端点自建MCP Server的操作比想象中简单。先在项目里引入服务端Starterdependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server/artifactId /dependency然后在application.yml里启用spring: ai: mcp: server: enabled: true name: internal-warehouse version: 1.0.0配合spring-boot-starter-webSpring AI会为MCP Server暴露一个HTTP端点默认路径一般是/mcp。这个端点就是标准的MCP协议入口任何MCP Client都可以通过它发现工具并调用前提是知道地址和鉴权信息。3.3 用Tool注解暴露业务工具的正确姿势MCP Server暴露工具底层依赖的还是Spring AI的Tool注解只是换了一个暴露给协议层的通道。先写一个普通服务类Service public class WarehouseTools { Tool(description 根据SKU查询当前库存余量返回JSON字符串包含sku和stock字段) public String queryStock(String sku) { int stock warehouseService.getStock(sku); return {\sku\:\ sku \,\stock\: stock }; } Tool(description 创建入库单入参为sku和quantity返回入库单号) public String createInboundOrder(String sku, int quantity) { String orderNo inboundService.createOrder(sku, quantity); return {\orderNo\:\ orderNo \}; } }然后需要把这些方法注册成ToolCallbackProviderConfiguration public class McpToolConfig { Bean public ToolCallbackProvider warehouseToolProvider(WarehouseTools warehouseTools) { return MethodToolCallbackProvider.builder() .toolObjects(warehouseTools) .build(); } }这里有个特别重要的细节Tool方法的入参和返回值一定要设计得足够“钝”。模型只会看你的方法名和描述来生成Json参数如果你定义的是一个复杂嵌套对象模型生成参数的失败率会显著提高。优先使用String、int、boolean或者简单的Map与JsonNode返回结果也最好是一段结构化字符串或JsonNode而不是一个需要模型自行解释的领域对象。3.4 工具方法内部的安全控制MCP Server一旦暴露收到的入参就都来自模型而模型生成参数的本质是“猜”所以工具方法内部必须当成不可信输入处理。我在生产环境里至少做三件事白名单校验sku、orderId如果不符合业务规则直接返回错误语义不要抛异常。操作日志记录调用来源、用户上下文、入参和出参。线上排查Agent发疯时全是靠日志定位。权限透传如果工具方法依赖当前用户身份可以用ToolContext参数接收上下文信息再从上下文里取用户标识。Tool(description 根据SKU查询当前库存余量) public String queryStock(String sku, ToolContext context) { String userId context.getContext().get(userId); if (!authService.hasWarehousePermission(userId)) { return {\error\:\no permission\}; } return stockService.query(sku); }ToolContext是Spring AI内置的上下文对象它不会进入模型生成参数的过程但能随工具调用一起传给方法是透传请求头和用户信息的常用通道。3.5 用标准MCP客户端自测Server自建Server完成后最简单的自测方式就是拿本机命令行工具或另一个Spring AI MCP Client项目连接它。如果不想新起项目也可以临时写一个带spring-ai-starter-mcp-client的测试配置把上面的/mcp端点的完整地址填到SSE或HTTP类型的server配置里启动后看ToolCallback列表里是否有queryStock和createInboundOrder。需要特别注意的是MCP协议传输方式也在迭代。老版本常用HTTPSSE新版本则更多是Streamable HTTP。Spring AI对不同传输方式的配置项不完全一样连接自建Server时一定要先确认Server端暴露的是哪种传输再去Client端选对应的配置入口否则会出现“地址明明可访问但工具发现不了”的怪问题。4. 让Agent真正调用MCP工具ChatClient和ToolCallingManager的组合4.1 自动装配倒是省事但别让它黑盒化当我们只配置了MCP Client而不写任何代码时Spring AI会自动把MCP Server暴露的工具转换为ToolCallback注册到ApplicationContext。这个机制很好用但也会带来一个困惑工具什么时候进入模型为什么同一个项目里有的模型能用工具、有的不能用原因在于ChatClient在构建时如果需要ToolCallback是从上下文里收集的。如果你显式指定了defaultTools那么外部自动注册的MCP工具就不一定会被带上。所以当你发现“MCP Server连上了但模型不调用工具”先检查自己有没有在ChatClient.builder()里手写defaultTools很可能你建造了一个“没有收录上下文默认工具”的Client。4.2 显式把MCP工具挂到ChatClient上我更推荐用显式的方式把MCP工具交给Agent组合清晰出问题也好排查Configuration public class AgentConfig { Bean public ChatClient chatClient(ChatModel chatModel, ListToolCallback mcpToolCallbacks) { return ChatClient.builder(chatModel) .defaultTools(mcpToolCallbacks.toArray(new ToolCallback[0])) .build(); } }这段代码的核心就是把Spring容器里已经存在的MCP工具全部注入到ChatClient里。其中的mcpToolCallbacks来源就是MCP Client starter自动配置生成的ToolCallback对象。这样配置后任何用这个ChatClient发起的提示词都会把工具列表带给模型模型在需要的时候自动生成工具调用指令。我用一个具体例子验证过这段链路MCP Server是自建的库存工具用户发出“SKU-001库存还有多少”ChatClient会把工具定义发给模型模型选择queryStock并传入参数{sku:SKU-001}框架执行工具后再把结果发回模型最终模型以自然语言回答。整个过程不需要手动编写一行“工具选择”逻辑。4.3 更底层的ToolCallingManager自己控制调用循环如果业务需求不只是“让模型回答一个问题”而是要精确控制工具执行顺序、审计每一次工具结果、甚至对工具结果做统一拦截那就别只用ChatClient可以用ToolCallingManager。Spring AI里的工具调用天然是一个循环模型返回工具调用请求框架解析并执行执行结果再作为消息回传模型再决定是继续调用还是输出最终结果。ToolCallingManager正是这个循环的核心编排角色。你需要做的是把MCP工具转换成ToolCallback列表然后交给它ToolCallingManager toolCallingManager ToolCallingManager.builder().build(); ChatClient chatClient ChatClient.builder(chatModel) .defaultTools(mcpToolCallbacks.toArray(new ToolCallback[0])) .defaultToolCallingManager(toolCallingManager) .build();当工具调用量特别大或出现“模型反复调用同一个工具”的情况时用ToolCallingManager可以更容易地加入重试、熔断和超时控制。它也让日志更清晰你能看到几轮对话中工具被调用了几次结果是什么而不是两眼一抹黑。4.4 给工具调用留好观测入口工具调用一旦多起来最危险的不是调用失败而是“模型开始乱选工具”你却毫无察觉。我的做法是在ToolCallback外面包一层审计类统一记录工具名、入参、耗时、结果摘要并输出一个traceId串到对话日志里。这样当用户反馈AI回答不对时可以直接追溯到是哪个工具返回了异常数据还是模型没选对工具。如果最后发现是模型选错工具优先改工具的description把触发条件写得更明确而不是一味调温度参数。5. 实战例子餐饮SaaS后台的AI助手通过MCP查库存和创建入库单5.1 场景设定从一个常见业务需求切入我最近在做餐饮SaaS后台的AI集成项目里面的一个核心场景就是运营人员在对话框里直接问库存、查订单甚至让AI帮忙创建入库单。这个场景非常典型因为既有“读操作”又有“写操作”又需要严格的权限控制。技术栈是Spring Boot Spring AI Alibaba模型层走的是标准OpenAI兼容接口。MCP部分我选择了自建Server把订单和库存相关的Tool方法暴露出来。这样比直接写Function Calling的好处是工具定义未来可以同时提供给公司内部多个AI渠道使用而不是只绑死在某一个ChatClient上。5.2 工具粒度和写操作设计工具粒度是整个链路能否成功的关键。我在这个项目里只暴露了三个工具Tool(description 查询指定SKU当前可用库存数量参数传SKU编码返回JSON) public String queryStock(String sku) { ... } Tool(description 查询订单基本信息参数传订单号返回JSON包含订单状态和金额) public String queryOrder(String orderNo) { ... } Tool(description 创建入库单参数为sku、quantity。注意此操作会实际写入库存调用前必须确认用户意图明确。返回JSON包含入库单号) public String createInboundOrder(String sku, int quantity) { ... }读操作和写操作分开定义不要揉成一个“万能方法”。写操作的描述里我特意加上了“调用前必须确认用户意图明确”这句话提示模型不要因为用户随口一句就触发写入。当然描述归描述可靠的人为确认还是要靠交互流程。5.3 在Prompt里给Agent立规矩工具描述只是第一道防线真正不可靠的是模型对用户意图的推断。我在系统提示词里固定了一段规则涉及库存查询必须使用queryStock。涉及创建入库单或出库单必须先查询当前库存再向用户展示待确认信息得到明确确认后才调用写工具。工具返回error或no permission时如实转述结果不要自行编造。实际测试效果用户发“SKU-002库存还剩多少顺便帮我补10件”时模型先调用queryStock拿当前库存然后回复“当前SKU-002库存为X件是否确认创建10件入库单”只有用户回复“确认”后模型才调用createInboundOrder。这个链路本身就演示了MCP在Spring AI里最有价值的部分Agent已经能“看情况做事”而不只是简单问答。5.4 多租户下的权限必须挂在工具内部餐饮SaaS是典型的多租户系统A商户的库存和B商户的库存必须完全隔离。MCP工具拿到模型生成的参数时通常只会有sku、quantity不会自动带上租户ID。因此我在ToolContext里注入当前登录用户的租户信息工具方法内部再做一次租户数据源路由和越权校验。模型就算生成了某个sku也只能查到本租户的数据。这一点无论MCP做得再方便业务层都不能省。5.5 写操作失败后的补偿另一个容易被忽略的问题是工具执行成功但后续Agent对话中断了。比如createInboundOrder已经写库成功但模型流式输出断掉用户以为没成功又下了一条重复单。我的方案是让入库单工具支持幂等键也就是把“用户操作会话ID操作编号”作为一个参数传给工具如果工具发现同一幂等键已处理过直接返回原单号不再重复创建。这个细节在真实业务里比任何花哨的Agent编排都保命。6. 接MCP时最容易翻车的几个地方6.1 Spring AI版本和Spring Boot版本对不上MCP相关包跟着Spring AI版本走而Spring AI版本又和Spring Boot版本有对应关系。最典型的错误是项目里Spring Boot用的是3.3.x但手动引用了最新版Spring AI结果启动时出现NoSuchMethodError或McpSchema相关类冲突。我的建议是永远使用spring-ai-bom统一管理并且只在升级Spring Boot版本后整体升级Spring AI BOM不要单独升级某一个starter。如果项目里有依赖平台强制的旧Boot版本就查一下官方支持的版本映射表选一个兼容的Spring AI版本。6.2 stdio进程起不来或连上了但工具为空stdio类型的MCP依赖本地命令最容易出问题。常见原因我总结了几个command对应的命令不在PATH里比如Windows下的npx需要写成npx.cmd。args在yaml里被写成了字符串而不是数组导致参数被当成一个整体传给进程。env变量缺少必要配置Node.js或Python进程启动后直接崩溃。工作目录不存在进程没有权限读写指定路径。遇到这类问题直接打开MCP Client的内部日志把进程启动的异常堆栈打出来。Spring AI有对应的debug开关通常在spring.ai.mcp.client.stdio.servers.*.env里给进程加DEBUG环境变量或调高Spring框架的日志级别能看到进程的stdout和stderr。6.3 远程MCP端点连接不上传输协议版本不匹配远程MCP连接时最常见的坑是Server端和Client端对同一个URL的传输方式理解不一致。老版Server暴露的是HTTPSSE新版则更偏向Streamable HTTP。Spring AI在配置远程端点时不同传输方式对应不同的配置入口。如果你按SSE方式配置但Server端已经切到Streamable HTTP很可能会发现“可以握手但工具列表为空”。最简单的方法自建MCP Server时在启动日志里明确打印出端点暴露的路径和传输方式然后写一个最简单的Client连上去验证不要直接在业务Agent里调试。6.4 工具数量太多把大模型上下文撑爆每个MCP工具定义都会占用模型上下文的token。当一个MCP Client接了好几个Server每个Server又暴露几十个工具时你会发现即使没有调用任何工具模型请求的token消耗也明显上涨。Spring AI的MCP Client配置通常支持按工具名筛选只暴露你当前Agent真正需要的工具。我在生产环境里的做法是按业务域拆分Agent每个Agent只连接1到2个MCP Server并过滤掉无关工具。宁可多起几个服务也不要让一个Agent背上几十个工具的上下文负担。6.5 不要把所有Tool方法都直接暴露到公网MCP Server本质是远程调用入口和REST接口一样需要做网关隔离、鉴权、限流。如果直接把带Tool的Spring Boot应用暴露到公网又没有加安全层会变成“任何人都能通过MCP协议调用你的内部工具”。所以Server端一定要在/mcp端点外层加Spring Security或者至少加一层自定义过滤器校验调用方身份。有个容易被忽略的点MCP工具方法内部的异常信息也会返回给模型模型可能把“SQL异常详情”“文件路径”这类内部信息直接告诉用户。所以在工具方法里不要返回底层异常堆栈要把异常转化为对外安全的语义化错误信息。我在实际项目中通常会给MCP工具再加一层统一出入口输入统一转成MapString, Object过滤输出统一包一层结果对象既方便模型解析也方便后续统计和审计。另一个小技巧是把MCP Server的启动交给Spring的SmartLifecycle管理避免AI首轮调用时因为进程还没拉起来而超时。MCP这条链路看着简单但真到生产环境细节都是在这些不起眼的地方积累出来的。希望这篇操作记录能帮你少走几步弯路。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

hindsight:面向LLM应用的开源可观测性基础设施 2026/10/1 6:20:51

hindsight:面向LLM应用的开源可观测性基础设施

1. 项目概述:hindsight 不是“事后诸葛亮”,而是一套可落地的 LLM 应用观测与调试基础设施“hindsight”这个词在日常语境里常被译作“后见之明”——事情发生之后才看清楚来龙去脉。但放在当前 LLM 工程实践的语境下,它早已脱离了哲学隐喻&a…

阅读更多 →
TensorFlow 2024完整指南:安装、训练与部署实战 2026/10/1 6:20:51

TensorFlow 2024完整指南:安装、训练与部署实战

1. TensorFlow到底是什么,为什么2024年还要学它TensorFlow,这个名字在深度学习圈子里几乎是“入门第一课”的代名词。不管你是刚毕业的学生、转行做AI的工程师,还是在生产环境里维护模型服务的后端开发者,大概率都跟它打过照面。它…

阅读更多 →
交叉小波变换原理与实战:从CWT到时频相关分析的完整指南 2026/10/1 6:20:51

交叉小波变换原理与实战:从CWT到时频相关分析的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
reverse-skill:面向AI与IoT的系统性逆向思维方法论 2026/10/1 6:20:50

reverse-skill:面向AI与IoT的系统性逆向思维方法论

1. 项目概述:什么是“reverse-skill”?它不是玄学,而是可拆解、可训练、可复用的逆向能力体系“reverse-skill”这个词乍看像黑话,但在我带过37个安全团队、审过2100份红队报告、亲手拆解过400款商用IoT固件和闭源AI服务接口之后&…

阅读更多 →
小米MiMo-V2.6开源大模型:MoE架构、MIT许可证与端侧部署实战 2026/10/1 6:20:50

小米MiMo-V2.6开源大模型:MoE架构、MIT许可证与端侧部署实战

1. 从“答题抢资格”到“登顶开源榜”:MiMo-V2.6 到底是个什么项目小米做开源大模型这件事,最早在圈内传开的时候,很多人的第一反应是“又一个手机厂商来蹭热度”。但 MiMo-V2.6 系列这次登顶全球开源大模型榜单,性质就完全不一样…

阅读更多 →
麒麟系统安装Docker实战指南:适配国产CPU与内核配置 2026/10/1 6:20:44

麒麟系统安装Docker实战指南:适配国产CPU与内核配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉