news 2026/8/25 11:00:44

前端URL安全白名单机制:从原理到OpenClaw项目实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前端URL安全白名单机制:从原理到OpenClaw项目实战

1. 项目概述:从一行代码看一个安全理念

如果你在维护一个前端项目,尤其是涉及用户上传、内容展示或者任何需要处理外部资源链接的场景,你大概率会碰到一个头疼的问题:如何安全地处理这些五花八门的URL?直接信任用户输入,无异于敞开大门欢迎不速之客;全部禁止,又会让功能变得极其难用。这时候,“白名单”机制就成了那个在安全与可用性之间走钢丝的平衡大师。今天要拆解的,就是OpenClaw这个项目中负责这项核心安全任务的模块——resolve-utils.ts

OpenClaw本身是一个功能集合或脚手架,其命名就带有“开放”与“抓取/控制”的双重意味,通常用于需要灵活解析和处理外部资源的场景。而resolve-utils.ts这个文件,从名字就能看出它的职责:解析(Resolve)工具集。它的核心任务,就是提供一套方法论和工具函数,来校验、解析并安全地转换URL,确保只有符合预设规则的“好”链接才能被放行。这不仅仅是简单的字符串匹配,它涉及到协议处理、域名校验、路径规范化、参数过滤等一系列细致入微的操作。理解这个模块,你学到的不仅是一段TypeScript代码,更是一套在前端层面构建资源访问安全边界的设计思想。无论你是前端开发者、安全爱好者,还是项目架构师,这个模块的深度剖析都能让你对“安全编码”有更落地的认识。

2. 模块核心设计思想与架构拆解

2.1 安全模型的基石:为何是白名单而非黑名单?

在开始看代码之前,我们必须先确立一个核心认知:在资源访问控制上,白名单(Allow List)策略在安全性上通常优于黑名单(Block List)。黑名单的思路是“我知道哪些是坏的,我把它们拦住”,但互联网上的恶意或不可信域名、新出现的攻击手法层出不穷,你永远无法穷举所有“坏”的。白名单则相反,它的逻辑是“我只允许我知道是好的”,这是一种默认拒绝的策略。resolve-utils.ts模块正是基于白名单思想构建的。

这种设计意味着,模块需要一个权威的、可管理的“好”的列表作为判断依据。这个列表的设计直接决定了模块的灵活性和安全性。一个粗糙的实现可能只硬编码几个域名,而一个健壮的实现,则会考虑列表的动态性、可配置性、甚至支持通配符和正则表达式来匹配一类域名。OpenClaw的这个模块,其架构必然围绕着如何高效、准确地将一个输入URL与白名单规则进行匹配来展开。这包括了规则的解析、存储数据结构的选择(数组、Set、Map或自定义树结构)、匹配算法的效率等。

2.2 模块的职责边界与接口设计

作为一个工具模块,resolve-utils.ts的接口设计一定追求清晰和单一职责。它不会去处理网络请求,也不会去渲染内容,它的输入是一个原始的URL字符串(可能还附带一些配置参数),输出则是一个经过校验和标准化后的、安全的URL字符串,或者一个表示失败的错误状态。

典型的函数签名可能会是这样:

function resolveAndValidateUrl( rawUrl: string, whitelist: Array<string | RegExp>, options?: ResolveOptions ): ResolveResult;

其中,ResolveResult可能是一个包含成功后的安全URL、原始URL、以及一些元数据的对象,也可能在失败时抛出一个自定义错误或返回一个特定的错误标识。

模块内部通常会拆分为几个子功能:

  1. URL解析与标准化:使用浏览器原生的URLAPI或Node.js的url模块,将字符串解析为结构化的对象,并处理诸如缺少协议、编码不一致等问题。
  2. 白名单匹配引擎:这是核心,实现将解析后的主机名(hostname)与白名单规则进行匹配的逻辑。
  3. 路径与参数安全处理:即使域名可信,路径和查询参数也可能存在问题(如路径遍历../../../、参数注入等),需要进行清洗或限制。
  4. 协议强制与降级:确保最终使用的协议是安全的(如强制使用https:),或根据环境进行协议降级处理。

3. 核心源码解析:逐行拆解实现细节

假设我们面对的是一个简化但核心逻辑完整的resolve-utils.ts实现。我们将聚焦几个关键函数。

3.1normalizeUrl:一切始于标准化

输入的URL可能千奇百怪:可能是相对路径/api/user,可能是协议省略的//example.com/img.jpg,还可能包含空格或中文等需要编码的字符。第一步必须是标准化。

import { URL } from 'url'; // 在Node环境下,或使用全局的URL /** * 标准化输入URL字符串 * @param rawUrl 原始URL字符串 * @param baseUrl 可选的基准URL,用于解析相对路径 * @returns 标准化后的URL对象,如果解析失败则返回null */ function normalizeUrl(rawUrl: string, baseUrl?: string): URL | null { try { // 处理无协议的情况,默认视为https。这是安全倾向的默认值。 let urlToParse = rawUrl; if (!rawUrl.startsWith('http://') && !rawUrl.startsWith('https://') && !rawUrl.startsWith('//')) { // 如果也不是协议相对链接,则尝试拼接基准或默认协议 if (baseUrl) { // 相对于baseUrl进行解析 return new URL(rawUrl, baseUrl); } else { // 没有baseUrl且无协议,默认尝试https urlToParse = `https://${rawUrl}`; } } else if (rawUrl.startsWith('//')) { // 协议相对链接,需要baseUrl来补全协议 if (!baseUrl) { throw new Error('Protocol-relative URL requires a baseUrl.'); } const base = new URL(baseUrl); urlToParse = `${base.protocol}${rawUrl}`; } const parsedUrl = new URL(urlToParse); // 额外的安全清理:确保hostname是小写的,便于后续比较 parsedUrl.hostname = parsedUrl.hostname.toLowerCase(); // 清理hash部分,通常白名单校验不关心hash parsedUrl.hash = ''; return parsedUrl; } catch (error) { // 解析失败,可能是格式极度不合法 console.warn(`[normalizeUrl] Failed to parse URL: ${rawUrl}`, error); return null; } }

关键点解析

  • 协议处理策略:代码体现了安全优先,默认尝试https。对于//开头的协议相对URL,必须提供baseUrl,否则无法确定协议,这是一个严谨的设计。
  • Hostname小写化:这是一个非常重要的细节。域名比较是大小写不敏感的,EXAMPLE.COMexample.com是同一个域名。提前统一转为小写,可以避免后续匹配时因大小写不一致导致的误判。
  • 清理Hash:URL的hash(#之后的部分)通常用于前端路由或锚点,不会发送到服务器,因此在做资源白名单校验时,一般可以忽略。将其清空可以简化后续的匹配逻辑。

3.2createWhitelistMatcher:构建高效匹配器

白名单规则可能简单如['example.com', 'cdn.example.org'],也可能复杂到支持子域名通配符如*.example.com。直接使用数组循环匹配在规则多时效率低。一个优化方案是预处理规则,构建一个高效的匹配器。

type WhitelistRule = string | RegExp; interface WhitelistMatcher { match(hostname: string): boolean; } /** * 创建白名单匹配器 * @param rules 白名单规则数组,支持字符串(精确匹配或通配符)和正则表达式 * @returns 一个匹配器对象 */ function createWhitelistMatcher(rules: WhitelistRule[]): WhitelistMatcher { // 预处理:分离精确匹配、通配符匹配和正则匹配 const exactSet = new Set<string>(); const wildcardPatterns: Array<{pattern: string, suffix: string}> = []; const regexPatterns: RegExp[] = []; for (const rule of rules) { if (typeof rule === 'string') { if (rule.includes('*')) { // 处理通配符,如 *.example.com // 将 *.example.com 转换为 .example.com 以便用 endsWith 匹配 const pattern = rule.toLowerCase(); if (pattern.startsWith('*.')) { const suffix = pattern.substring(1); // 得到 .example.com wildcardPatterns.push({ pattern, suffix }); } else { // 其他位置的通配符,暂时按正则处理,或视为无效 console.warn(`Complex wildcard pattern "${rule}" is not fully supported, treating as regex.`); regexPatterns.push(new RegExp(`^${pattern.replace(/\*/g, '.*')}$`)); } } else { // 精确匹配 exactSet.add(rule.toLowerCase()); } } else if (rule instanceof RegExp) { regexPatterns.push(rule); } } return { match(hostname: string): boolean { const host = hostname.toLowerCase(); // 1. 检查精确匹配 if (exactSet.has(host)) { return true; } // 2. 检查通配符匹配 for (const { suffix } of wildcardPatterns) { if (host === suffix.substring(1) || host.endsWith(suffix)) { // host === 'example.com' 匹配 *.example.com // host.endsWith('.example.com') 匹配所有子域名 return true; } } // 3. 检查正则匹配 for (const regex of regexPatterns) { if (regex.test(host)) { return true; } } return false; } }; }

关键点解析

  • 性能优化:通过预处理,将规则分类。匹配时,优先检查代价最小的精确匹配(Sethas操作是O(1)),然后是通配符(字符串endsWith),最后才是代价较高的正则匹配。这种分层策略能显著提升性能,尤其是在高频调用的场景下。
  • 通配符处理:将*.example.com转换为.example.com并用endsWith匹配,是一种经典且高效的做法。它允许a.example.comb.c.example.com都匹配成功。需要注意边界情况:example.com本身也应该匹配*.example.com吗?这取决于业务逻辑,上述代码通过host === suffix.substring(1)来处理,即example.com等于.example.com去掉开头的点。这是一个需要明确约定的点。
  • 规则的优先级:代码中并没有定义规则的优先级(如一个域名既被精确匹配又被通配符匹配)。通常,一旦匹配成功就返回,所以规则的顺序可能有影响。更复杂的实现可能需要定义优先级逻辑。

3.3resolveSecureUrl:核心校验流程

这是模块的入口函数,串联起整个校验流程。

interface ResolveOptions { whitelist: WhitelistRule[]; forceHttps?: boolean; // 是否强制使用HTTPS stripQuery?: boolean; // 是否移除查询参数(某些安全场景需要) allowedProtocols?: string[]; // 允许的协议,默认['http:', 'https:'] } interface ResolveResult { success: boolean; url?: string; // 校验并处理后的安全URL originalUrl?: string; error?: string; } /** * 解析并校验URL,返回安全版本 * @param inputUrl 输入URL * @param options 配置选项 * @returns 解析结果 */ function resolveSecureUrl(inputUrl: string, options: ResolveOptions): ResolveResult { const { whitelist, forceHttps = true, stripQuery = false, allowedProtocols = ['http:', 'https:'] } = options; // 1. 标准化 const parsedUrl = normalizeUrl(inputUrl); if (!parsedUrl) { return { success: false, originalUrl: inputUrl, error: 'URL_PARSE_FAILED' }; } // 2. 协议校验 if (!allowedProtocols.includes(parsedUrl.protocol)) { return { success: false, originalUrl: inputUrl, error: `PROTOCOL_NOT_ALLOWED. Allowed: ${allowedProtocols.join(', ')}` }; } // 3. 构建匹配器并校验白名单 const matcher = createWhitelistMatcher(whitelist); if (!matcher.match(parsedUrl.hostname)) { return { success: false, originalUrl: inputUrl, error: `HOSTNAME_NOT_IN_WHITELIST: ${parsedUrl.hostname}` }; } // 4. 安全增强处理 if (forceHttps && parsedUrl.protocol === 'http:') { parsedUrl.protocol = 'https:'; // 注意:如果标准端口是80,换到https后端口应变为443,URL对象通常会自动处理 } if (stripQuery) { parsedUrl.search = ''; // 清空查询字符串 } // 5. 返回最终的安全URL // 注意:这里将URL对象转回字符串。hash已在normalize阶段清空。 const finalUrl = parsedUrl.toString(); return { success: true, url: finalUrl, originalUrl: inputUrl }; }

关键点解析

  • 可配置性:通过options对象提供丰富的配置,使得模块可以适应不同安全级别的场景。例如,一个内部管理后台可能不需要forceHttps,而一个面向公网的内容展示页面则需要。
  • 清晰的错误处理:返回结构化的ResolveResult对象,而不是简单返回字符串或抛出异常,让调用方可以更灵活地处理成功和失败的情况。错误码(如URL_PARSE_FAILED)有助于定位问题。
  • 处理顺序:流程设计遵循“尽早失败”原则。先做最基本的解析和协议检查,失败则快速返回,避免不必要的白名单匹配开销。
  • 协议强制forceHttps的实现直接修改URL对象的protocol属性。这是一个需要留意的点:如果原始URL是http://example.com:8080,强制HTTPS后应该变成https://example.com:443还是保持:8080?通常,HTTPS的默认端口是443,URL对象在protocol改变时可能会自动调整端口,但最好在代码或文档中明确这一行为。

4. 高级特性与边界情况处理

一个工业级的白名单解析模块,还需要考虑更多边界情况和高级特性。

4.1 路径遍历与路径白名单

域名可信不代表路径可信。攻击者可能构造诸如https://trusted.com/../../../etc/passwd的URL(如果服务器配置不当)。因此,更严格的安全策略需要对路径进行规范化并检查。

import { normalize } from 'path'; // Node.js path模块,或使用浏览器兼容的polyfill function sanitizePath(url: URL): boolean { try { const pathname = url.pathname; // 使用路径规范化,它会解析掉 `.` 和 `..` const normalizedPath = normalize(pathname); // 如果规范化后的路径以`..`开头或包含`../`,说明存在路径遍历攻击 // 注意:normalize在浏览器端可能需polyfill,且处理方式略有不同 if (normalizedPath.startsWith('..') || normalizedPath.includes('/..')) { return false; } // 可选:可以进一步将规范化后的路径设回URL对象 // url.pathname = normalizedPath; return true; } catch { return false; } } // 在resolveSecureUrl函数中,白名单校验通过后,可以加入路径检查 // if (!sanitizePath(parsedUrl)) { // return { success: false, error: 'PATH_TRAVERSAL_DETECTED' }; // }

注意:前端进行路径遍历检查更多是一道额外的保险,真正的防护应该在服务端进行。因为最终请求是由浏览器或服务器发出的,攻击者完全可以绕过前端JS直接构造请求。

4.2 国际化域名(IDN)与同形异义字攻击

microsoft.commicrosоft.com看起来一样吗?注意第二个“o”是西里尔字母的小写о。这就是同形异义字攻击。国际化域名(如中文.cn)会转换为Punycode编码(xn--fiq228c.cn)。白名单匹配必须在同一编码层面进行。

function normalizeHostnameForComparison(hostname: string): string { // 1. 转换为小写 let normalized = hostname.toLowerCase(); // 2. 处理IDN:将Unicode域名转换为Punycode // 使用 `new URL()` 或专门的库如 `whatwg-url` 可以自动处理。 // 但为了显式控制,可以使用: try { // `domainToASCII` 是Node.js `url` 模块的函数,将Unicode域名转ASCII。 // 在浏览器中,URL构造函数本身会进行转换。 // 这里我们假设环境支持或已polyfill。 normalized = new URL(`http://${normalized}`).hostname; } catch (e) { // 转换失败,返回原值(或根据策略处理) } return normalized; } // 在createWhitelistMatcher的match函数中,对输入的hostname先进行此规范化处理。

核心要点:白名单规则也应该以Punycode形式存储,或者在进行匹配前,将输入和规则都转换为相同的编码形式(ASCII小写),以确保“视觉上”相同的域名能被正确匹配。

4.3 性能考量与缓存策略

在高频调用场景(如实时过滤大量用户生成内容),每次解析都创建新的匹配器、重新编译正则表达式是低效的。

// 简单的缓存:以规则数组的序列化字符串为key,缓存匹配器实例 const matcherCache = new Map<string, WhitelistMatcher>(); function getCachedMatcher(rules: WhitelistRule[]): WhitelistMatcher { const cacheKey = JSON.stringify(rules); // 注意:规则包含RegExp时,此方法不完美 if (!matcherCache.has(cacheKey)) { matcherCache.set(cacheKey, createWhitelistMatcher(rules)); } return matcherCache.get(cacheKey)!; } // 更高级的:使用WeakMap,或者对规则进行指纹计算(如hash)作为key。 // 如果规则不常变化,甚至可以将匹配器作为模块级变量单例化。

注意事项:缓存虽然提升了性能,但增加了内存占用,并且需要关注规则更新时的缓存失效问题。对于长期运行的前端SPA应用,需要谨慎设计缓存策略。

5. 实战集成与常见问题排查

5.1 在React/Vue项目中的集成示例

假设我们有一个用户评论组件,需要渲染评论中的链接,但必须确保安全。

// 在工具模块中 import { resolveSecureUrl } from './resolve-utils'; const RESOURCE_WHITELIST = [ 'example.com', '*.githubusercontent.com', 'cdn.jsdelivr.net', /^img\d+\.example\.org$/ // 正则匹配 img123.example.org 等形式 ]; export function safeResolveLink(href: string): string | null { const result = resolveSecureUrl(href, { whitelist: RESOURCE_WHITELIST, forceHttps: true, stripQuery: false, // 保留查询参数,如图片尺寸参数 }); return result.success ? result.url! : null; } // 在React组件中 function CommentText({ text }: { text: string }) { const renderTextWithLinks = () => { // 简单的URL正则匹配(实际应用可能需要更健壮的解析器) const urlRegex = /https?:\/\/[^\s]+/g; const parts = text.split(urlRegex); const matches = text.match(urlRegex) || []; return parts.flatMap((part, index) => { const elements = [<span key={`text-${index}`}>{part}</span>]; if (matches[index]) { const safeUrl = safeResolveLink(matches[index]); if (safeUrl) { elements.push( <a key={`link-${index}`} href={safeUrl} target="_blank" rel="noopener noreferrer"> {matches[index]} </a> ); } else { // 链接不安全,不渲染为可点击链接,或渲染为纯文本 elements.push(<span key={`unsafe-${index}`}>{matches[index]}</span>); } } return elements; }); }; return <p>{renderTextWithLinks()}</p>; }

5.2 常见问题排查速查表

在实际使用中,你可能会遇到以下问题:

问题现象可能原因排查步骤与解决方案
合法的白名单域名被拒绝1. 域名大小写不一致。
2. 规则是*.example.com,但输入是example.com(无子域名)。
3. 国际化域名编码问题。
4. URL包含端口、而规则未指定端口。
1. 在normalizeUrl和匹配前确保转为小写。
2. 检查通配符匹配逻辑是否支持根域名。修改createWhitelistMatcher中的匹配条件。
3. 确保输入域名和规则都使用Punycode或统一编码进行比较。
4. 白名单规则通常只匹配主机名(hostname),不包含端口。确认parsedUrl.hostname是否正确。
//开头的协议相对URL解析失败调用resolveSecureUrl时未提供baseUrl参数,而normalizeUrl函数需要它来补全协议。在调用resolveSecureUrl的上下文中,传入当前页面的基准URL(如window.location.origin)作为normalizeUrlbaseUrl参数。或者修改逻辑,对于协议相对URL,默认使用https:
性能问题,大量URL处理时慢1. 每次调用都重新创建匹配器。
2. 白名单规则过多,且匹配算法是线性O(n)。
3. 正则表达式规则过于复杂。
1. 引入匹配器缓存,如getCachedMatcher
2. 优化数据结构,对于大量精确匹配域名,使用Set。对于通配符,可考虑使用前缀树(Trie)进行优化。
3. 审视正则规则,看能否转换为字符串通配符匹配。
强制HTTPS后链接失效目标服务器可能不支持HTTPS,或者证书有问题。1. 根据场景调整forceHttps选项,对于内部HTTP服务可关闭。
2. 实现更智能的协议处理:先尝试HTTPS,如果失败(在前端可通过图片加载error事件探测),再降级回HTTP。但这需要更复杂的异步逻辑。
路径中包含..被错误拦截sanitizePath函数过于严格,可能拦截了合法的相对路径(虽然在前端完整URL中较少见)。明确业务需求。如果资源服务器支持合理的相对路径,可能需要调整路径检查逻辑,或者只对已知的危险模式进行拦截,而不是简单地禁止所有..

5.3 我的实操心得与避坑指南

  1. 白名单的维护是持续过程:不要试图一次把规则写全。项目初期可以收紧规则,只放行最核心的域名。随着业务发展,根据实际需求(如引入新的图床、CDN)逐步添加。建立一个规则添加的审批或记录流程。
  2. 测试用例要覆盖边界:为你的resolve-utils模块编写单元测试。重点测试:国际化域名、通配符边界(根域名、多级子域名)、协议相对URL、畸形URL、包含端口和认证信息的URL、路径遍历尝试等。
  3. 不要依赖前端安全做最终防护:前端白名单是增强用户体验和安全的第一道防线,但绝不能替代服务端的安全校验。恶意用户可以完全绕过你的前端JavaScript,直接向你的服务器或目标资源发送请求。服务端对重定向或代理请求进行同样的白名单校验至关重要。
  4. 谨慎使用正则表达式规则:正则表达式强大但危险,一个写得不好的正则可能导致性能灾难(回溯爆炸)或意想不到的匹配。如果可能,尽量使用字符串和通配符。必须使用正则时,要仔细测试,并考虑其性能影响。
  5. 错误信息要友好但不过于详细:给调用方返回的错误信息,要能区分是“格式错误”、“域名不在白名单”还是“系统错误”。但避免在错误信息中透露白名单的具体内容,以防信息泄露。
  6. 考虑SSR/SSG场景:如果你的应用是服务端渲染或静态生成,确保resolve-utils模块能在Node.js环境下正常运行。注意Node.js的URL实现与浏览器可能存在的细微差异,特别是对于无效URL的处理。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/25 10:59:55

2026七类网线口碑好的品牌有哪些?行业工程端真实口碑盘点

数字化基建持续推进背景下&#xff0c;万兆网络成为智慧园区、三甲医院、轨道交通、大型数据中心的基础建设标准&#xff0c;七类网线凭借 800MHz 高带宽、双层全域屏蔽、长期稳定传输的核心特性&#xff0c;成为高端综合布线工程的核心传输主材。对于弱电设计师、系统集成商、…

作者头像 李华
网站建设 2026/8/25 10:58:17

龙虾经济思想实验:当经济学法则遭遇无意识行为体

1. 项目概述&#xff1a;一个思想实验的诞生那天晚上&#xff0c;我和几个朋友在吃小龙虾&#xff0c;桌上堆满了虾壳&#xff0c;大家聊着各自工作的烦心事。有人抱怨加班&#xff0c;有人吐槽消费降级&#xff0c;突然一个念头冒了出来&#xff1a;如果这些龙虾能替我们去上班…

作者头像 李华
网站建设 2026/8/25 10:58:09

AI协作实战:从翻车到高效,我的Claude Teammate中医游戏开发心法

1. 项目缘起&#xff1a;当“中医游戏”遇上“AI队友”最近&#xff0c;我一直在琢磨怎么把传统的中医知识用更现代、更有趣的方式呈现出来。作为一个对游戏开发和AI应用都挺感兴趣的人&#xff0c;我自然想到了用游戏化的方式来科普中医。想法很简单&#xff1a;设计一个互动游…

作者头像 李华
网站建设 2026/8/25 10:57:36

Gemini 3.1 Pro Preview:从长上下文到深度推理的AI新纪元

1. 从“预览版”到“新纪元”&#xff1a;为什么这次更新值得关注&#xff1f;如果你最近关注AI领域&#xff0c;大概率已经被“Gemini 3.1 Pro Preview”这个名字刷屏了。这听起来像是一个常规的版本迭代&#xff0c;但圈内人讨论的焦点&#xff0c;却集中在那个看似不起眼的“…

作者头像 李华
网站建设 2026/8/25 10:57:09

AI Agent 面试题 350:如何设计Agent的工具调用优先级和排队机制?

&#x1f525; AI Agent 面试题 350&#xff1a;如何设计Agent的工具调用优先级和排队机制&#xff1f;摘要&#xff1a;本文深入解析了「如何设计Agent的工具调用优先级和排队机制&#xff1f;」这一 AI Agent 领域的核心面试题。文章从 API 编排与链式调用 的基本概念出发&am…

作者头像 李华
网站建设 2026/8/25 10:57:00

AI Agent 面试题 347:A2A协议中的多Agent任务协调机制详解

&#x1f525; AI Agent 面试题 347&#xff1a;A2A协议中的多Agent任务协调机制详解摘要&#xff1a;本文深入解析了「A2A协议中的多Agent任务协调机制详解」这一 AI Agent 领域的核心面试题。文章从 A2A 协议 的基本概念出发&#xff0c;系统性地剖析了 任务协调、多Agent 等…

作者头像 李华