From f4f69689870fc5e26765354b56fed6d5292de6de Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 13 Sep 2026 13:06:19 +0800 Subject: [PATCH] =?UTF-8?q?docs(sdk):=20=E9=80=9A=E9=81=93=E5=90=8D?= =?UTF-8?q?=E4=BC=9A=E6=8B=BC=E8=BF=9B=20LLM=20=E5=87=BD=E6=95=B0=E5=90=8D?= =?UTF-8?q?=EF=BC=8C=E5=86=99=E6=98=8E=E5=91=BD=E5=90=8D=E7=BA=A6=E6=9D=9F?= =?UTF-8?q?=EF=BC=88[A-Za-z0-9=5F-]{1,64}=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 生产事故(v1.3.0 部署后 agent 完全不应答)的根因之一就是这个约束没写清: 远程设备通道名 `device/` 里的 `/` 让 `output_send__device/` 无法通过上游的 函数名校验,上游对**整条请求**回 400(`Invalid 'tools[N].function.name'`), 网关 auto tier 全链条失败,内核只能报"所有 provider 都失败"。 这不是"某个工具不可用",而是**整个 agent 哑掉** —— 所以这条约束必须出现在 插件作者会看的地方(`RegisterOutputChannel` 文档 + README 的通道一节): - 通道名只允许 `[A-Za-z0-9_-]`,且总长要留出 `output_send__`(13) 的余量; - 名字若来自外部输入(设备自报 id 等),请在插件侧派生合规且唯一的名字。 内核侧**不做**净化/反解:通道名是插件自己的声明,就该由插件遵守契约。 --- README.md | 10 +++++++++- sdk/plugin.go | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 869c58e..e0ed1ee 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ type Plugin interface { |------|------|------| | 阶段钩子 | `RegisterStage(stage, handler, scope...)` | 注册阶段回调,scope 可选:`StageScopeGlobal`(全局,默认)或 `StageScopeOwnTools`(仅自己工具) | | 输入通道 | `RegisterInputChannel(name, def)` | 注册输入通道(**入站**:谁会往这个通道注入输入),def 为 `ChannelDef`(NoMemory/Cleaner) | -| 输出通道 | `RegisterOutputChannel(name, caps, desc, def, handler)` | 注册输出通道(**出站**:`output_send__` 的回复发给谁),def 为 `ChannelDef`,caps 为能力位掩码 | +| 输出通道 | `RegisterOutputChannel(name, caps, desc, def, handler)` | 注册输出通道(**出站**:`output_send__` 的回复发给谁),def 为 `ChannelDef`,caps 为能力位掩码。⚠️ 通道名只能用 `[A-Za-z0-9_-]`(见下方"输出通道"一节的命名约束) | | 工具注册 | `RegisterTool(name, def, handler)` | 注册工具供 LLM 调用 | | 插件 API | `RegisterPluginAPI(name)` | 注册插件 API 供其他插件访问 | | 图记忆 | `Memory()` | 访问图记忆 API(实体-关系存储) | @@ -139,6 +139,14 @@ sdk.RegisterInputChannel("qq", ChannelDef{ ### 输出通道 +> ⚠️ **命名约束(会进 LLM 函数名)**:内核按 `output_send__` 生成工具, +> 而上游对函数名的规范是 `^[a-zA-Z0-9_-]{1,64}$`。名字违规的后果不是 +> "这个工具不可用",而是**整条请求被 400 拒绝**(`Invalid 'tools[N].function.name'`), +> 网关 auto tier 全链条失败,表现成**整个 agent 不回应**。 +> 所以 `name` 只能用 `[A-Za-z0-9_-]`,并留出 `output_send__`(13 字符)的余量。 +> 名字若来自外部输入(设备自报 id 之类),请在插件侧派生一个合规且唯一的名字 —— +> 内核**不会**替你净化。 + ```go sdk.RegisterOutputChannel("my-channel", CapText|CapFile, "通道描述", ChannelDef{}, handler) ``` diff --git a/sdk/plugin.go b/sdk/plugin.go index 1f65774..72451a7 100644 --- a/sdk/plugin.go +++ b/sdk/plugin.go @@ -480,7 +480,15 @@ func (s *PluginSDK) RegisterPluginAPI(name string) error { // 入站(谁会往 注入输入)是另一件事,用 RegisterInputChannel 声明。 // 若该通道同时也是你的注入入口,两个都要登记。 // -// name: channel name (e.g. "qq", "webui") +// name: channel name (e.g. "qq", "webui")。 +// +// ❗**命名约束**:内核会把通道名拼进 LLM 的函数名(`output_send__`), +// 而上游对函数名的规范是 `^[a-zA-Z0-9_-]{1,64}$`。违反的后果不是"这个工具不可用", +// 而是**整条请求被上游 400 拒绝**(`Invalid 'tools[N].function.name'`), +// 网关的 auto tier 会全链条失败 —— 表现成"整个 agent 不说话了"。 +// 所以通道名只能用 `[A-Za-z0-9_-]`,且总长要留出 `output_send__`(13 字符)的余量。 +// 若通道名来自外部输入(设备自报 id 之类),请**在插件侧派生一个合规且唯一的名字**, +// 而不是把原始值直接当通道名。 // caps: bitmask of supported output capabilities (CapText, CapFile, etc.) // desc: description of the channel, expected meta format, and type enum // def: 通道在记忆计算层的行为(NoMemory/Cleaner)