1. OpenClaw.NET 兼容性目录到底解决什么问题
OpenClaw.NET 的 Compatibility Catalog(兼容性目录)是一份集中管理插件与技能预期行为的清单文件,路径固定在compat/public-smoke.json。它要回答的核心问题是:某个 NPM 插件或 ClawHub 技能,在当前运行时到底能不能正常加载、能不能正确暴露工具与技能、失败时会不会给出明确诊断码。对于做 NativeAOT 发布的团队来说,这个问题尤其尖锐,因为 AOT 编译会砍掉大量反射路径,任何依赖动态加载的插件都可能在发布后才暴露问题。
我见过太多项目把兼容性验证散落在各个测试文件里,新增一个插件就补一段测试代码,时间一长没人说得清哪些插件被验证过、验证到什么程度。Compatibility Catalog 把这个过程收敛成一份 JSON 清单,构建期作为嵌入资源编译进OpenClaw.Core.dll,运行时不需要访问文件系统,对 NativeAOT 完全友好。清单里的每一条 entry 就是一份契约:声明这个插件应该以什么状态加载、应该暴露哪些工具名、如果预期不兼容应该报出哪些诊断码。
适合谁用?三类人最直接受益。第一类是负责发布流程的工程师,需要在 CI 里跑回归验证,确认新版本没有破坏已有插件。第二类是外部集成方,想自查自己的插件是否在兼容清单里、预期行为是什么。第三类是社区贡献者,新增插件或技能时需要往清单里追加条目并本地验证。这份指南会从清单结构讲到 NativeAOT 下的加载机制,再给出 CLI 与 REST API 两条调用路径的完整验证步骤,最后把 TaoToken 的统一 Key 与 API 通道接进来,让整个验证链路可以程序化消费。
需要先明确一点:Compatibility Catalog 不是插件市场,也不是运行时注册表。它是一份静态清单,描述的是"预期",实际加载结果由烟雾测试去断言。清单和测试代码分离,好处是新增条目不需要改测试逻辑,坏处是清单字段写错时编译期或运行期才会报出来。下面从清单结构开始拆。
2. TaoToken 前置准备与 NativeAOT 加载机制
在把 CLI 和 REST API 跑通之前,需要先理解清单在 NativeAOT 下是怎么被加载的,以及 TaoToken 的 Key 和 API 通道怎么接进来。这两件事看似无关,实际上都影响验证链路能否在 CI 里稳定运行。
先说 NativeAOT 的加载机制。compat/public-smoke.json在.csproj里以<EmbeddedResource>方式编译进OpenClaw.Core.dll,运行时通过程序集资源流读取,没有任何文件 I/O。反序列化走的是CoreJsonContext,这是一个基于JsonSerializerContext的源生成上下文,编译期就生成了序列化代码,完全规避反射。这意味着如果你往清单里加了新字段,但忘了在CoreJsonContext里声明对应类型,AOT 模式下启动就会报缺少元数据。这是 NativeAOT 项目最常见的坑之一,JIT 模式下可能正常,AOT 发布后才炸。
插件与主进程之间的通信走plugin-bridge.mjs,协议是 JSON-RPC over stdio。这样做的好处是避免在主进程里动态加载托管程序集,AOT 对动态加载的支持本来就有限。插件本身是 Node.js 侧的产物,通过npx和clawhub命令链路安装,所以 CI 环境里 Node.js 20 是硬依赖。
再说 TaoToken 的接入。TaoToken 提供统一的 Key 和 API 通道,Base URL 是https://taotoken.net/api,官网在https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。在 OpenClaw.NET 的兼容性验证场景里,TaoToken 主要承担两个角色:一是作为插件配置里的模型通道,configJson字段里的apiKey可以指向 TaoToken 的 Key;二是作为 REST API 验证时的外部调用目标,用来确认 Gateway 的通道就绪状态。
你需要先拿到一个 TaoToken 的 API Key。打开 API Keys 管理页https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite,创建一个新 Key,复制保存。这个 Key 后面会用在两处:插件条目的configJson里,以及 REST API 验证时的请求头。如果你还没决定用哪个模型,可以先到模型对话页https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite试一下通道是否正常。
对于长期做编码和 Agent 场景的团队,Coding Plan 页面https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite里有套餐说明,这里不展开价格,只提一点:兼容性烟雾测试在 CI 里是定时跑的,调用量取决于清单条目数量,选套餐时把这个因素算进去。
接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,里面有完整的参数说明。控制台在https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite,可以查看调用记录和配额。如果你用的是 Claude Code 类的工具链,Anthropic 兼容入口在https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code_anthropic&utm_campaign=rewrite,不过 OpenClaw.NET 的验证链路主要走 REST,这个入口作为备选了解即可。
前置准备清单:Node.js 20+、.NET SDK(版本跟项目global.json对齐)、一个 TaoToken API Key、OPENCLAW_PUBLIC_SMOKE=1环境变量。这四样齐了,后面的步骤才能跑通。
3. 可复制的目录配置片段与接入参数
这一节给出可以直接复制进项目的配置片段。路径和字段名都跟 OpenClaw.NET 的实际约定一致,不要随意改名,否则编译期校验或运行期反序列化会失败。
先看清单顶层结构。compat/public-smoke.json是一个带版本号的 JSON 对象,entries字段是条目数组:
{ "version": 2, "entries": [ { "id": "agentseo-plugin", "category": "ts-jiti-plugin", "kind": "npm-plugin", "spec": "@agentseo/openclaw-plugin@0.1.4", "packageName": "@agentseo/openclaw-plugin", "pluginId": "agentseo", "expectedStatus": "compatible", "configJson": "{\"apiKey\":\"test_key\"}", "expectedToolNames": ["agentseo_audit", "agentseo_keywords"], "expectedSkillNames": ["agentseo"] } ] }字段分三组。通用字段所有条目必填:id是场景唯一标识,在entries里不能重复;category是场景分类,取值有pure-skill、js-tool-plugin、ts-jiti-plugin、config-schema-plugin、unsupported-surface-plugin;kind是资源类型,取值clawhub-skill或npm-plugin。
技能专用字段在kind == "clawhub-skill"时必填:slug是 ClawHub 里的技能标识符,version是 SemVer 版本,expectedRelativePath是安装后的预期相对路径,比如skills/my-skill/SKILL.md。
插件专用字段在kind == "npm-plugin"时必填:spec是 NPM 包规范,packageName是包名,pluginId是插件唯一标识,expectedStatus是预期兼容性状态,取值compatible或incompatible。注意expectedStatus是必填的,编译期校验会拒绝缺失该字段的条目,这是负面场景和正面场景的分界线。
可选字段里,configJson是 JSON 字符串形式的示例配置,注意它是字符串不是对象,里面的引号要转义。installExtraPackages是需要额外安装的依赖包列表。expectedToolNames和expectedSkillNames只在compatible场景下用,用来断言工具和技能是否完整暴露。expectedDiagnosticCodes只在incompatible场景下用,断言错误码集合。
把 TaoToken 的 Key 接进configJson时,写法是这样:
{ "id": "taotoken-channel-plugin", "category": "js-tool-plugin", "kind": "npm-plugin", "spec": "@your-org/openclaw-plugin@1.0.0", "packageName": "@your-org/openclaw-plugin", "pluginId": "taotoken-channel", "expectedStatus": "compatible", "configJson": "{\"apiKey\":\"你的TaoTokenKey\",\"baseUrl\":\"https://taotoken.net/api\"}", "expectedToolNames": ["channel_probe"], "expectedSkillNames": ["channel-check"] }这里baseUrl用https://taotoken.net/api,不要加 UTM 参数,API 端点保持干净。apiKey在 CI 里不要硬编码,用环境变量注入,清单里可以写占位符,测试代码在加载前做替换。
再看.csproj里的嵌入资源配置,确保清单被编译进程序集:
<ItemGroup> <EmbeddedResource Include="compat/public-smoke.json"> <LogicalName>OpenClaw.Core.compat.public-smoke.json</LogicalName> </EmbeddedResource> </ItemGroup>LogicalName决定了运行时读取资源用的名字,测试代码里通过Assembly.GetManifestResourceStream拿到的就是这个逻辑名。如果你改了LogicalName,记得同步改读取代码。
CoreJsonContext的声明也要跟上,新增字段类型必须在这里注册:
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] [JsonSerializable(typeof(PublicSmokeManifest))] [JsonSerializable(typeof(CompatibilityEntry))] [JsonSerializable(typeof(List<CompatibilityEntry>))] internal partial class CoreJsonContext : JsonSerializerContext { }PublicSmokeManifest和CompatibilityEntry是你的模型类,字段名跟 JSON 里的 camelCase 对应。如果清单里加了新字段但模型类没加,反序列化会静默忽略;如果模型类加了字段但CoreJsonContext没注册,AOT 模式下会报缺少元数据。这两个方向都要检查。
4. CLI 与 REST API 验证步骤及成功结果
配置就位后,开始验证。先跑 CLI 路径,再跑 REST API 路径,最后跑烟雾测试。每一步都给出预期输出,方便你对照。
CLI 路径。OpenClaw CLI 提供compatibility catalog子命令,简写是compat catalog。先看全量清单:
openclaw compatibility catalog预期输出是表格形式,每行一个条目,列出id、category、kind、expectedStatus。如果清单为空或加载失败,会提示资源未找到,这时候回去检查.csproj的EmbeddedResource配置。
按状态过滤:
openclaw compatibility catalog --status compatible openclaw compatibility catalog --status incompatible按类型和分类组合过滤:
openclaw compatibility catalog --kind npm-plugin --category ts-jiti-pluginJSON 格式输出,适合程序化消费:
openclaw compatibility catalog --jsonJSON 输出的结构跟清单本身一致,但会经过PublicCompatibilityCatalog.CreateCatalog()转换成富目录,多出subject、installCommand、summary、scenarioType、guidance这些派生字段。比如installCommand对技能是openclaw clawhub install {slug},对插件是openclaw plugins install {spec} --dry-run。scenarioType把expectedStatus映射成positive或negative。
REST API 路径。Gateway 通过/api/integration/compatibility路由族暴露清单:
curl -s http://localhost:5000/api/integration/compatibility/catalog带过滤参数:
curl -s "http://localhost:5000/api/integration/compatibility/catalog?compatibilityStatus=compatible" curl -s "http://localhost:5000/api/integration/compatibility/catalog?kind=npm-plugin&category=ts-jiti-plugin"/catalog端点支持compatibilityStatus、kind、category三个查询参数。/export端点返回完整兼容性报告,包含运行时模式(AOT / JIT)、安全态势、通道就绪状态:
curl -s http://localhost:5000/api/integration/compatibility/export如果 Gateway 前面有鉴权,请求头里带上 TaoToken 的 Key:
curl -s -H "Authorization: Bearer 你的TaoTokenKey" \ http://localhost:5000/api/integration/compatibility/export成功结果的特征:/catalog返回的 JSON 里entries数组长度跟清单一致,每条 entry 的expectedStatus字段存在且取值合法。/export返回的报告里runtimeMode字段显示AOT或JIT,channelReadiness显示各通道的就绪状态。如果runtimeMode是AOT但清单加载失败,多半是CoreJsonContext缺类型声明。
烟雾测试路径。设置环境变量后跑测试:
export OPENCLAW_PUBLIC_SMOKE=1 dotnet test OpenClaw.Net.slnx --filter Category=PublicSmoke测试类PublicCompatibilitySmokeTests会读取清单并迭代执行。对 ClawHub 技能,通过npx clawhub安装并校验expectedRelativePath文件存在。对compatible插件,执行安装、加载,然后断言expectedToolNames和expectedSkillNames完整暴露。对incompatible插件,执行安装、加载,断言加载失败且诊断码集合至少包含expectedDiagnosticCodes里的全部条目。
成功输出是测试全部通过,TRX 报告里Outcome为Passed。如果某个条目断言失败,报告里会指出是哪个id、哪个断言维度失败。CI 里这个作业失败即视为整个流水线失败,需要在合并前修复。
5. 本篇常见错误排查
这一节对照真实报错,给出原因和解决方案。报错信息按出现频率排序。
报错一:plugin failed to load
测试报告里出现这个,通常是configJson格式错误或字段类型不匹配。configJson是字符串,里面的 JSON 要正确转义。比如{"apiKey":"test_key"}写成字符串是"{\"apiKey\":\"test_key\"}",少一个反斜杠就解析失败。解决方案是先用--dry-run验证:
openclaw plugins install @your-org/openclaw-plugin@1.0.0 --dry-run--dry-run会走配置校验但不实际加载,能提前暴露 schema 问题。
报错二:expected tool not found
插件加载成功但工具没暴露。原因可能是插件未声明该工具,或者expectedToolNames里工具名拼写错误。校对时注意大小写和下划线,工具名是精确匹配。解决方案是把插件实际暴露的工具名打印出来对照:
openclaw plugins inspect @your-org/openclaw-plugin@1.0.0 --tools报错三:编译期npm-plugin must declare expectedStatus
新条目缺少expectedStatus字段。NPM 插件条目必须显式指定compatible或incompatible,编译期校验会拒绝缺失该字段的条目。解决方案是补上字段,不要留空。
报错四:烟雾测试整体未运行
环境变量没设置。OPENCLAW_PUBLIC_SMOKE=1必须设置,否则测试整体跳过,报告里显示Skipped而不是Passed。CI 里检查这个变量是否在作业级别注入。
报错五:clawhub安装失败
Node.js 未安装或版本过低。npx clawhub需要 Node.js 20+。解决方案是安装 Node.js 20 并确保npx在 PATH 里。CI 里用actions/setup-node指定版本。
报错六:expectedDiagnosticCodes不匹配
错误码命名变更或新增。诊断码有config_one_of_mismatch、unsupported_cli_registration、unsupported_surface_call、schema_required_missing等。如果插件升级后错误码变了,清单里的expectedDiagnosticCodes要同步更新。查阅最新诊断码列表,必要时同步更新清单。
报错七:AOT 模式启动报缺少元数据
新增字段未在CoreJsonContext中声明。这是 NativeAOT 特有的坑,JIT 模式下可能正常,AOT 发布后才报。解决方案是在源生成上下文里添加对应类型:
[JsonSerializable(typeof(你的新类型))] internal partial class CoreJsonContext : JsonSerializerContext { }报错八:local proxy failed或401
REST API 验证时出现401,检查请求头里的Authorization是否正确带上 TaoToken Key。出现local proxy failed,检查 Gateway 是否正常启动、端口是否被占用。如果 Gateway 配置了上游通道,确认baseUrl指向https://taotoken.net/api且没有多余路径。
报错九:reading choices相关错误
这个报错通常出现在调用模型通道时,响应体里没有choices字段。检查请求体格式是否符合 OpenAI 兼容规范,model字段是否填了 TaoToken 支持的模型 ID。如果用的是 Claude Code 类入口,确认走的是 Anthropic 兼容路径而不是 OpenAI 路径。
排查顺序建议:先确认环境变量和 Node.js 版本,再确认清单 JSON 语法,然后确认CoreJsonContext类型注册,最后确认 TaoToken Key 和 Base URL。大部分问题在前两步就能定位。
6. 把验证链路接进 CI 与后续接入
清单和验证步骤跑通后,下一步是接进 CI。GitHub Actions 里public-compatibility-smoke作业承担回归验证,触发条件是定时执行或手动派发,依赖 Node.js 20,执行流程是dotnet test加--filter Category=PublicSmoke,报告产物是 TRX 格式并作为 artifact 上传。失败语义是任意条目断言失败即整个作业失败。
贡献新条目的流程:在compat/public-smoke.json的entries数组末尾追加条目,确保必填字段完整。NPM 插件必须包含expectedStatus、spec、packageName、pluginId;技能必须包含slug、version、expectedRelativePath。本地设置OPENCLAW_PUBLIC_SMOKE=1后执行dotnet test OpenClaw.Net.slnx --filter Category=PublicSmoke。如果引入了新的category或kind,需要同步升级清单顶层version字段、更新PublicCompatibilityCatalog中的枚举与转换逻辑、更新文档里的场景分类表格。
数据转换逻辑值得单独说一下。清单在运行时通过PublicCompatibilityCatalog.CreateCatalog()转换成富目录,核心映射规则是:subject按slug、packageName、pluginId、id的优先级取第一个非空值;installCommand对技能是openclaw clawhub install {slug},对插件是openclaw plugins install {spec} --dry-run;summary根据category和expectedStatus生成人类可读描述;scenarioType把compatible映射成positive、incompatible映射成negative;guidance是上下文相关的操作建议,比如配置 schema 错误时提示参考插件文档。
TaoToken 的接入点在这里也清晰了:插件条目的configJson里带 TaoToken Key 和 Base URL,REST API 验证时请求头带 Key,CI 里 Key 通过环境变量注入。如果你需要程序化消费兼容性报告,/export端点的输出可以直接对接外部门户或归档系统。接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,API Keys 管理在https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite,控制台在https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite。
最后提一个实操细节:清单里的version字段是顶层版本号,不是插件版本。新增category或kind时才需要升级它,普通条目追加不用动。这个字段的作用是让消费方知道清单结构有没有破坏性变更。如果你在 CI 里缓存了清单,版本号变了要重新拉取。