# Webhook 渠道与模拟外部系统 Demo 完整方案 方案日期:2026-09-01。状态:待实施。 本文设计一个参考 Telegram Bot API 的 Webhook 渠道。用户只需填写 `Base URL + Token` 并点击“绑定并连接”,DSH 即主动连接外部系统;外部系统不需要知道 DSH 地址。本文同时定义一个放在 `src/webhookDemo` 的最小模拟外部系统,提供单页聊天界面,用于本地联调渠道收发消息。 ## 1. 最终决策 Webhook 渠道只采用一套机制: ```text Base URL + Token ↓ POST /v1/getMe 验证身份 ↓ POST /v1/getUpdates 长轮询收消息 ↓ TextHarnessBridge 处理消息 ↓ POST /v1/sendMessage 发送回复 ``` 关键决定: - DSH 始终是主动发起连接的一方,不开放入站 Webhook,不要求公网地址。 - 协议固定为 HTTPS/HTTP + JSON + Bearer Token,不使用 WebSocket、SSE 或回调 URL。 - 接收消息使用 Telegram 风格的 `getUpdates(offset, timeout, limit)` 长轮询。 - V1 只支持文本、私聊和群聊;共享命令、Session、Workspace、Agent Preset、上下文增强、权限和主动投递继续复用现有机制。 - 渠道代码复用现有 Token Bot 生命周期和 `TextHarnessBridge`,只新增外部协议适配层。 - Demo 使用 Node.js 内置 HTTP 服务、一个 HTML 文件和内存数据,不增加运行时依赖,也不考虑持久化、集群或可靠性。 虽然产品名称为“Webhook 渠道”,其技术模型实际是“可配置 Base URL 的 Bot API 长轮询渠道”。名称不影响协议边界。 ## 2. 目标与范围 ### 2.1 必须实现 1. 设置页可以填写 Base URL 和 Token,并点击“绑定并连接”。 2. 绑定时调用外部系统 `getMe`,确认 Token、协议版本和机器人身份。 3. 绑定成功后自动启动长轮询;Host 重启后自动恢复。 4. 外部消息能够进入现有 Harness 会话、命令和权限链路。 5. Harness 最终文本能够通过外部系统发送回原会话。 6. 私聊和群聊使用稳定的外部会话 ID。 7. 支持现有连接状态、重试连接、移除机器人、连接测试、工作区、Agent Preset、上下文增强和文字主动投递。 8. Token 只进入 `ctx.credentials`,配置、状态、错误和日志不得返回 Token。 9. 一个 DSH 可以绑定多个 Webhook Connector;同一 Connector 重绑新 Token 时保留原 bot 状态。 10. 提供一个能直接启动的模拟外部系统及 HTML 聊天页面。 ### 2.2 V1 明确不做 - 不让外部系统 POST 到 DSH。 - 不实现 WebSocket、SSE、反向隧道或公共 Relay。 - 不支持图片、文件、语音、卡片、按钮、消息编辑、输入状态和 Reaction;图片与普通文件保留为后续协议扩展,不进入 V1 实现和 Demo。 - 不支持流式更新;只发送最终文字和现有明确错误提示。 - 不允许用户配置任意 API 路径或 JSON 映射模板。 - 不支持 OAuth、动态注册或自动创建外部机器人。 - 不实现 DSH 侧消息队列、离线补发或出站自动重试。 - 不为 Demo 实现数据库、磁盘持久化、幂等、限流、多租户、消息保留承诺或多消费者协调。 - 不重构所有渠道,也不建设新的通用 Connector 框架。 ## 3. 用户体验 Webhook 设置页显示两个输入项: ```text Base URL https://connector.example.com/dsh/ Token •••••••••••••••••••• [绑定并连接] ``` 点击后: 1. 浏览器通过现有 Connection RPC 把 `{ baseUrl, token }` 发送给 Host。 2. Host 严格校验 Base URL 和 Token。 3. Host 调用 `{baseUrl}/v1/getMe`。 4. 外部系统返回稳定机器人身份。 5. Host 保存非敏感配置,把 Token 写入凭据服务。 6. Runtime 启动 `getUpdates` 长轮询。 7. Harness 可达且首次 `getUpdates` 成功后,机器人状态显示“HTTP 长轮询运行正常”。 如果身份验证成功但长轮询暂时失败,机器人配置仍然保留,状态显示连接未就绪,并由现有 supervisor 自动重试。删除接入时停止长轮询、删除本地配置、Token、会话状态和 Workspace 绑定,不调用外部系统删除机器人。 ## 4. 总体架构 ```mermaid flowchart LR U[外部用户] --> E[外部系统] D[DSH WebhookRuntime] -->|getMe / getUpdates| E E -->|Update JSON| D D --> N[normalizeWebhookUpdate] N --> B[WebhookHarnessBridge] B --> H[Harness] H --> B B --> C[WebhookBotClient] C -->|sendMessage| E E --> U ``` 职责边界: | 组件 | 职责 | | --- | --- | | 外部系统 | 鉴权、机器人身份、接收其业务消息、提供 Update、把 DSH 回复发回业务会话 | | `WebhookApi` | Base URL、Bearer Token、HTTP 请求、响应校验和错误分类 | | `WebhookRuntime` | Harness 启动、身份复核、长轮询、cursor、重连状态和资源停止 | | `normalizeWebhookUpdate` | 外部 Update 转换成 `TextHarnessBridge.accept()` 所需语义 | | `WebhookBotClient` | 将共享文字交付转换为 `sendMessage` 请求 | | `WebhookHarnessBridge` | 只提供渠道 descriptor,业务处理复用 `TextHarnessBridge` | | `WebhookController` | 复用 Token Bot 的绑定、状态、重连、删除和主动投递生命周期 | ## 5. 外部 Connector 协议 V1 ### 5.1 公共约束 - 协议标识:`dsh-im-webhook-v1`。 - 所有接口使用 `POST` 和 `application/json; charset=utf-8`。 - 鉴权头为 `Authorization: Bearer `。 - 成功响应统一为 `{ "ok": true, "result": ... }`。 - 失败响应统一为 `{ "ok": false, "error": { "code", "message", "retry_after"? } }`。 - Token 长度为 20~4096 个字符;Token 不进入 URL、响应或日志。 - `update_id` 和消息序号必须是 JavaScript safe integer。 - `chat.id`、`from.id` 和机器人 `id` 是 1~128 字符的稳定字符串,不允许空白、控制字符和 `|`。 - V1 文本最大 32 KiB,单次 Update 数量最大 100。 - DSH 请求使用 `redirect: "error"`,外部系统不能通过重定向更换目标地址。 ### 5.2 Base URL 规则 用户填写的 Base URL: - 生产环境只允许 `https:`。 - 本地调试允许 `http://localhost`、`http://127.0.0.1` 和 `http://[::1]`。 - 禁止 username、password、query 和 hash。 - 允许非根路径,例如 `https://example.com/connectors/dsh/`。 - 保存前统一补齐结尾 `/`。 接口通过 `new URL("v1/getMe", baseUrl)` 等方式拼接。因此: ```text Base URL: https://example.com/connectors/dsh/ getMe: https://example.com/connectors/dsh/v1/getMe ``` Base URL 是用户明确配置的受信 Connector 地址,因此 V1 不额外阻止内网地址;但任何入站消息都不能改变 Base URL。 ### 5.3 `getMe`:验证绑定与取得身份 请求: ```http POST /v1/getMe Authorization: Bearer webhook-demo-token-123456 Content-Type: application/json; charset=utf-8 {} ``` 成功响应: ```json { "ok": true, "result": { "protocol_version": "dsh-im-webhook-v1", "id": "demo-bot", "name": "Webhook Demo Bot", "username": "webhook_demo", "is_bot": true } } ``` 要求: - `id` 在同一个 Base URL 下长期稳定。 - Token 轮换后仍应返回相同 `id`。 - `name` 必填,`username` 可省略。 - `protocol_version` 必须精确匹配;V1 不做协议协商。 - `is_bot` 必须为 `true`。 DSH 内部使用 `规范化 Base URL + "|" + 外部 id` 作为 `platformId`,再复用现有 `deriveTokenBotIdentity()` 生成 `botId` 和 `tokenRef`。因此同一 Base URL、同一外部 id 重绑会更新原机器人;Base URL 改变视为新的 Connector。 ### 5.4 `getUpdates`:长轮询收消息 请求: ```http POST /v1/getUpdates Authorization: Bearer webhook-demo-token-123456 Content-Type: application/json; charset=utf-8 { "offset": 123, "timeout": 25, "limit": 100 } ``` 成功响应: ```json { "ok": true, "result": [ { "update_id": 123, "message": { "message_id": 456, "date": 1788200000, "chat": { "id": "demo-chat", "type": "private", "title": "Demo Chat" }, "from": { "id": "demo-user", "name": "Demo User", "is_bot": false }, "text": "你好", "addressed": true } } ] } ``` 参数语义: | 字段 | 约束 | 语义 | | --- | --- | --- | | `offset` | safe integer,最小 0 | 只返回 `update_id >= offset` 的 Update;提交更大 offset 表示确认更早的 Update | | `timeout` | 0~30,默认 25 | 没有消息时最多保持请求的秒数;有消息立即返回 | | `limit` | 1~100,默认 100 | 单次最多返回的 Update 数量 | 消息约束: - `chat.type` 只允许 `private` 或 `group`。 - 私聊总是视为 addressed;群聊只有 `addressed: true` 才进入 Harness。 - `from.is_bot: true` 的消息被 DSH 忽略,避免机器人循环。 - `text` 是 V1 唯一输入内容;空文本 Update 被忽略。 - Update 按 `update_id` 升序返回。 - 正常超时返回 `result: []`,不是错误。 生产外部系统应自行实现未确认消息保存和 offset 语义。Demo 只在内存中近似实现,不提供可靠性承诺。 ### 5.5 `sendMessage`:接收 DSH 回复 请求: ```http POST /v1/sendMessage Authorization: Bearer webhook-demo-token-123456 Content-Type: application/json; charset=utf-8 { "delivery_id": "delivery-789", "chat_id": "demo-chat", "reply_to_message_id": 456, "text": "你好,我可以帮你处理任务。", "format": "markdown" } ``` 成功响应: ```json { "ok": true, "result": { "message_id": 789 } } ``` 约束: - `delivery_id` 是 DSH 为本次交付生成的不透明 ID。 - `chat_id` 必须来自入站消息或已保存的主动投递目标。 - `reply_to_message_id` 可省略。 - `format` 只允许 `plain` 或 `markdown`。 - 外部系统不支持 Markdown 时可以按纯文本显示,但不能丢失文字。 - 成功必须返回稳定 `message_id`。 - DSH 对最终发送不自动重试;生产外部系统可以按 `delivery_id` 做幂等,Demo 不做。 ### 5.6 错误和超时 建议错误响应: ```json { "ok": false, "error": { "code": "invalid_token", "message": "Token is invalid" } } ``` | HTTP 状态 | DSH 处理 | | --- | --- | | `400` | 明确协议错误;绑定失败或当前消息发送失败 | | `401` / `403` | 无效 Token 或权限不足;连接失败 | | `404` | 会话不存在;发送明确失败 | | `409` | 可用于报告另一个活跃轮询者;Runtime 失败后重连 | | `429` | 限流;读取 `retry_after`,由 supervisor 稍后重连 | | `5xx` | Connector 暂时不可用;长轮询重连;发送结果标记不确定 | 默认超时: - `getMe`:15 秒。 - `sendMessage`:15 秒。 - `getUpdates`:`timeout + 10` 秒,最大 40 秒。 网络错误、发送超时、无效 JSON和 `5xx` 如果发生在请求已经发出之后,映射为现有 unknown delivery 语义,不发送 fallback 副本,不自动重试。明确 `4xx` 映射为 failed。错误对象和日志只保留稳定 code、HTTP 状态和安全摘要,不包含 Token、Authorization Header 或完整响应体。 ## 6. DSH 渠道设计 ### 6.1 目录结构 ```text src/channels/webhook/ ├── config-store.mjs ├── harness-client.mjs ├── state-store.mjs ├── webhook-api.mjs ├── webhook-bridge.mjs ├── webhook-controller.mjs └── webhook-runtime.mjs plugin-src/host/channels/webhook/ ├── index.mjs ├── production.mjs └── rpc.mjs plugin-src/client/channels/webhook/ ├── api.js ├── index.js └── styles.js ``` 目录沿用 Telegram 渠道结构,避免引入新的组织方式。 ### 6.2 `WebhookApi` `webhook-api.mjs` 负责: - `normalizeWebhookBaseUrl(value)`。 - `inspectWebhookBinding({ baseUrl, token })`,内部调用 `getMe`。 - `WebhookApi.getMe()`。 - `WebhookApi.getUpdates({ offset, timeout, limit, signal })`。 - `WebhookApi.sendMessage({ deliveryId, chatId, replyToMessageId, text, format, signal })`。 - 统一 `{ ok, result }` 响应、超时、Abort、HTTP 状态和安全错误映射。 它不处理 Harness、Session、cursor、重连或 UI。 实现保持普通 `fetch` 注入点,测试使用 fake fetch。V1 不直接导入 Telegram 私有 HTTP transport,也不新建通用 Transport 框架;如果真实 Connector 后续出现长轮询占用连接问题,再独立提取共享 transport。 ### 6.3 配置和身份 `WebhookConfigStore` 继承 `TokenBotConfigStore`,通过 extension 保存并严格校验: ```json { "botId": "webhook_<24 hex>", "platformId": "https://connector.example.com/dsh/|remote-bot-id", "tokenRef": "DSH_WEBHOOK_BOT_TOKEN_<24 HEX>", "name": "CRM Assistant", "username": "crm_bot", "baseUrl": "https://connector.example.com/dsh/", "remoteBotId": "remote-bot-id", "createdAt": "...", "connectedAt": "..." } ``` 不保存 Token。`baseUrl` 是非敏感配置,但公共状态只显示协议和机器人身份,不必回传完整 Base URL;设置页如需展示,只显示 host 或脱敏后的地址。 ### 6.4 对共享 Token Bot 的最小扩展 当前 `TokenBotController` 和共享 RPC 假定绑定 payload 只有 `{ token }`。为避免复制整套 Controller,增加两个可选扩展点,默认行为完全不变: 1. `TokenBotController` 可选 `inspectBinding(payload)`: - 默认仍提取 `payload.token` 并调用现有 `inspectToken(token)`。 - Webhook 实现校验 `{ baseUrl, token }`,调用 `getMe`,返回 `{ token, platformId, name, username, configExtension }`。 - Controller 后续保存凭据、原子写配置、启动 Runtime、回滚、状态、重连和删除逻辑保持原样。 2. `createTokenBotRpcHandler()` 可选 `validateBindCredentials(payload)`: - 默认仍只接受 `{ token }`。 - Webhook 只接受 `{ baseUrl, token }`,未知字段拒绝。 这是本方案唯一需要修改的共享 Token Bot 绑定边界。不能把 Base URL 编码进 Token,也不能复制一份完整的 `TokenBotController` 或 RPC Handler。 共享 `createTokenProductionController()` 继续负责: - ConfigStore 和 StateStore。 - HarnessClient。 - 每 Bot Workspace、Agent Preset 和上下文增强。 - 当前统一访问策略。 - Controller 和 Runtime 装配。 - supervisor 自动重连。 - Delivery adapter 注册。 - 删除状态和关闭资源。 ### 6.5 `WebhookRuntime` 状态模型与 Telegram 保持一致: ```js { startedAt: null, ready: false, connectionState: 'idle', harnessReachable: false, lastCheckedAt: null, lastConnectedAt: null, lastError: null, ...createTextBridgeStatus() } ``` `start()`: 1. 调用 `stop()` 清理旧代 Runtime。 2. 设置 `connecting`。 3. `harness.ensureRunning()`。 4. 创建 `WebhookApi({ baseUrl: config.baseUrl, token })`。 5. 调用 `getMe`,再次确认外部身份与保存的 `platformId` 一致。 6. 创建 `WebhookBotClient` 和 `WebhookHarnessBridge`。 7. 从 `state.cursor()` 读取 offset;首次使用 `0`。 8. 立即执行一次 `getUpdates({ offset, timeout: 0, limit: 100 })`;处理返回结果并在成功后设置 `connected/ready`,不能让绑定操作等待 25 秒。 9. 启动使用 `timeout: 25` 的持续 poll task。 `poll()`: 1. 调用 `getUpdates({ offset: cursor, timeout: 25, limit: 100 })`。 2. 验证 Update 数组按 `update_id` 升序且没有无效字段。 3. 为本批消息捕获当前上下文增强快照。 4. 调用 `normalizeWebhookUpdate()`。 5. 对有效消息异步调用 `bridge.accept()`。 6. 每处理一个 Update,把 cursor 更新为 `update_id + 1` 并通过 `ConversationStateStore.setCursor()` 原子保存。 7. 空数组直接进入下一轮。 该确认时机与现有 Telegram Runtime 相同:Update 交给 Bridge 后推进 cursor,不等待一次 Harness 任务完成。V1 不额外建设 durable inbox。 `stop()`: - Abort 当前请求。 - 清空 API、Bridge 和 poll task 引用。 - 最多等待 poll task 和 `bridge.waitForIdle()` 各 2 秒。 - 状态恢复为 `idle`。 - 重复调用必须幂等。 poll task 非 Abort 错误时设置 `failed`,由现有 supervisor 负责重启整个 Runtime,不在内部增加第二套退避循环。 ### 6.6 消息标准化 `normalizeWebhookUpdate(update, { botId })` 只做 V1 所需转换: ```js { messageId: String(update.update_id), senderId: String(message.from.id), senderIsBot: message.from.is_bot === true, kind: message.chat.type === 'group' ? 'group' : 'direct', conversationId: String(message.chat.id), content: message.text, plainText: true, addressed: message.chat.type === 'private' || message.addressed === true, contextSource: () => ({ senderName: message.from.name }), replyTarget: { chatId: String(message.chat.id), replyToMessageId: message.message_id }, connectionTestTarget: { chatId: String(message.chat.id) } } ``` 不直接复用 `normalizeTelegramUpdate()`,因为它还包含 Telegram mention、topic、图片、文件和下载逻辑。Webhook normalizer 保持为一个小型纯函数,业务层仍复用 `TextHarnessBridge`。 ### 6.7 Bridge 和 BotClient `WebhookHarnessBridge` 仅设置 descriptor: ```js { key: 'webhook', label: 'Webhook', connectionLabel: ' HTTP 长轮询' } ``` 不声明 Reaction。 `WebhookBotClient` 只实现: - `sendText(target, text)`:按 plain 调用 `sendMessage`。 - `sendDelivery(target, block)`:传递 `plain/markdown` 格式并返回 `providerMessageIds`。 不实现 `openDeliveryStream`、`sendTyping`、`addReaction`、`sendImage` 或 `sendFile`。`TextHarnessBridge` 会使用现有非流式文字路径;遇到产物时沿用共享的明确失败提示,不静默丢失。 ### 6.8 主动投递 Webhook Delivery Target 使用一个目标类型: ```json { "targetId": "demo-chat", "kind": "chat", "route": { "chatId": "demo-chat" } } ``` Runtime 校验 `kind === "chat"` 且 `route.chatId` 是合法外部 ID,然后调用 `bridge.sendProactiveText({ chatId }, text)`。目标候选从共享状态中的 `direct:` 和 `group:` conversation key 生成。 需要在现有 Delivery 支持表、路由校验、候选解析和客户端字段定义中加入 `webhook`。不新增主动投递历史、重试或幂等逻辑。 ### 6.9 Host 和 Client 接入 Host: - 新增 `/webhook` RPC channel。 - 使用共享 Token Bot endpoint 名称。 - 在 `plugin-src/host/index.mjs` 注册 Webhook channel,并注入现有 `deliveryService`。 - Webhook 启动失败不得阻止其他渠道。 Client: - 复用 `createTokenChannelSettings()`。 - 自定义 `CredentialPanel`,内部继续使用现有 `CredentialBindingPanel`: - identity 字段显示为 `Base URL`。 - secret 字段显示为 `Token`。 - `credentialPayload({ identity, secret })` 返回 `{ baseUrl: identity, token: secret }`。 - 复用机器人卡片、状态轮询、连接检查、移除、Workspace、Agent Preset、上下文增强和设置按钮。 - 新增最小 Webhook 图标和样式,不复制页面布局。 ## 7. 模拟外部系统 Demo ### 7.1 目标 Demo 用于证明完整链路: ```text HTML 页面输入消息 ↓ Demo 写入内存 Update 队列 ↓ DSH getUpdates 拉到消息 ↓ Harness 生成回复 ↓ DSH sendMessage ↓ Demo HTML 页面显示机器人回复 ``` Demo 不模拟生产可靠性,只要便于本机启动、绑定和观察消息即可。 ### 7.2 目录结构 代码全部放在用户指定目录: ```text src/webhookDemo/ ├── server.mjs ├── index.html └── README.md ``` 不增加前端构建、CSS 文件、客户端框架或数据库。 ### 7.3 启动方式 建议增加 npm script: ```json { "scripts": { "demo:webhook": "node src/webhookDemo/server.mjs" } } ``` 默认启动: ```text Host: 127.0.0.1 Port: 8787 Base URL: http://127.0.0.1:8787/ Token: webhook-demo-token-123456 Chat URL: http://127.0.0.1:8787/ ``` 允许通过环境变量覆盖: - `WEBHOOK_DEMO_HOST`,默认 `127.0.0.1`。 - `WEBHOOK_DEMO_PORT`,默认 `8787`。 - `WEBHOOK_DEMO_TOKEN`,默认 `webhook-demo-token-123456`。 默认只监听 loopback,避免无意暴露 Demo Token。 ### 7.4 `server.mjs` 仅使用 Node 内置模块: - `node:http` 提供服务。 - `node:fs/promises` 读取 `index.html`。 - `node:url` 和 `node:path` 定位静态文件。 - `node:crypto` 生成 `delivery_id` 或消息 ID(如有需要)。 内存状态: ```js { updates: [], transcript: [], waiters: Set, nextUpdateId: Math.max(Date.now(), previous + 1), nextMessageId: 1 } ``` Update ID 使用当前毫秒时间并保证进程内单调递增。这样 Demo 重启后生成的 ID 通常仍大于 DSH 已保存的 cursor,不需要为了调试增加磁盘持久化。 Demo 路由: | 方法 | 路径 | 用途 | | --- | --- | --- | | `GET` | `/` | 返回聊天页 | | `GET` | `/demo/config` | 返回当前 Base URL、脱敏提示和 Demo Token,供本地页面展示 | | `GET` | `/demo/messages?after=` | 页面轮询聊天记录 | | `POST` | `/demo/messages` | 页面提交一条用户消息并生成 Update | | `POST` | `/v1/getMe` | Connector 身份接口 | | `POST` | `/v1/getUpdates` | 长轮询 Update | | `POST` | `/v1/sendMessage` | 接收并记录 DSH 回复 | 服务端约束: - API 请求体上限 64 KiB。 - `/v1/*` 必须验证 Bearer Token;`/demo/*` 只因默认 loopback 而不鉴权。 - `getUpdates` 最多保持 30 秒;请求关闭时移除 waiter。 - 页面提交固定使用: - `chat.id = "demo-chat"` - `chat.type = "private"` - `from.id = "demo-user"` - `from.name = "Demo User"` - 新用户消息同时写入 transcript 和 updates,并立即唤醒等待中的 `getUpdates`。 - `sendMessage` 把机器人消息写入 transcript,返回递增 message ID。 - 当 DSH 提交更大 offset 时,Demo 可以删除更早 Update;进程退出时全部数据丢失。 - `SIGINT/SIGTERM` 时关闭 server,并用空数组结束所有 pending poll。 为了便于测试,`server.mjs` 导出 `createWebhookDemoServer(options)`;直接执行文件时才监听默认端口。测试可传入 `port: 0` 获取随机端口。 ### 7.5 `index.html` 单个 HTML 文件内联少量 CSS 和 JavaScript,界面只包含: - 标题“Webhook Demo Chat”。 - 当前 Base URL 和 Token,附复制提示。 - DSH 长轮询状态提示:根据最近一次 `getUpdates` 时间显示“DSH 已连接/等待连接”。 - 消息列表,区分“Demo User”和“DSH Bot”。 - 文本输入框和发送按钮。 - 清晰展示服务端错误。 浏览器行为: 1. 加载 `/demo/config`。 2. 每 500~1000 毫秒调用 `/demo/messages?after=`。 3. 提交表单到 `/demo/messages`。 4. 使用 `textContent` 渲染消息,禁止把消息内容写入 `innerHTML`。 5. 页面卸载时不需要额外清理。 页面不直接调用 DSH,也不需要知道 DSH 地址。 ### 7.6 `README.md` 只记录: 1. 启动命令。 2. 默认 Base URL 和 Token。 3. 在 DSH 设置页绑定的方法。 4. 打开聊天页并发送消息的步骤。 5. Demo 的内存数据和无可靠性限制。 ## 8. 现有代码复用清单 | 现有组件 | 处理 | | --- | --- | | `TokenBotController` | 增加可选 binding hook 后复用生命周期、状态、重连、删除和主动投递 | | `TokenBotConfigStore` | 通过 extension 保存 Base URL 和 remote bot id | | `ConversationStateStore` | 原样复用 Session、seen message IDs 和 cursor | | `TextHarnessBridge` | 原样复用消息队列、命令、Session、审批、问题、历史、Workspace、Preset、权限和最终回复 | | `HarnessClient` | 通过薄 `WebhookHarnessClient` 设置日志/RPC 前缀 | | `createTokenProductionController` | 原样复用生产装配;只传入 Webhook 定义 | | `createTokenConnectionSupervisor` | 原样复用自动初始化和重连 | | `createTokenChannelSettings` | 原样复用设置页;只提供双字段 CredentialPanel | | `DeliveryService` | 原样复用目标管理和发送服务 | | `createDeliveryAdapter` | 增加 Webhook route case | | 共享上下文增强和访问策略 | 把 `webhook` 加入渠道集合后复用 | 不复用的 Telegram 专属部分: - `normalizeTelegramUpdate()`。 - Telegram 图片、文件和 rich-message 转换。 - Telegram mention、topic、command menu 和 webhook conflict 检查。 - Telegram Token 格式和 API URL。 - Telegram 私有 Undici transport;V1 先用普通 fetch 注入点。 ## 9. 预计文件改动 ### 9.1 新增 | 文件 | 用途 | | --- | --- | | `src/channels/webhook/webhook-api.mjs` | Connector HTTP API 和绑定检查 | | `src/channels/webhook/webhook-runtime.mjs` | 长轮询 Runtime、normalizer、BotClient | | `src/channels/webhook/webhook-controller.mjs` | Token Bot controller 定义 | | `src/channels/webhook/config-store.mjs` | Base URL 配置扩展和身份派生 | | `src/channels/webhook/state-store.mjs` | `ConversationStateStore` 薄子类 | | `src/channels/webhook/webhook-bridge.mjs` | descriptor + `TextHarnessBridge` 薄子类 | | `src/channels/webhook/harness-client.mjs` | `HarnessClient` 薄子类 | | `plugin-src/host/channels/webhook/*` | Host 装配和 RPC | | `plugin-src/client/channels/webhook/*` | 设置页、API 和最小样式 | | `src/webhookDemo/server.mjs` | 模拟 Connector 服务 | | `src/webhookDemo/index.html` | 简单聊天页 | | `src/webhookDemo/README.md` | Demo 使用说明 | | `test/channels/webhook/*.test.mjs` | 渠道单元、生产装配、RPC 和 UI 测试 | | `test/webhook-demo.test.mjs` | Demo 端到端 HTTP 测试 | ### 9.2 修改 | 文件/区域 | 改动 | | --- | --- | | `src/channels/shared/token-bot-controller.mjs` | 可选 `inspectBinding`,默认行为不变 | | `plugin-src/host/channels/shared/rpc.mjs` | 可选 bind payload validator,默认行为不变 | | `plugin-src/host/index.mjs` | 注册 Webhook Host 渠道 | | `plugin-src/client/index.js` | 注册 Webhook 设置页、RPC 和图标 | | `plugin-src/client/channel-logos.js` | Webhook 图标 | | `plugin-src/host/delivery-adapter.mjs` | `webhook` 和 `{ chatId }` route | | `plugin-src/host/delivery-suggestions.mjs` | Webhook 会话候选 | | `plugin-src/client/delivery-settings.js` | Webhook 主动投递表单 | | `src/channels/shared/context-enhancement.mjs` | 加入 `webhook` 渠道键 | | 访问策略渠道集合及对应 UI/RPC 测试 | 加入 Webhook | | `package.json` | 增加 `demo:webhook`;渠道描述从九个更新为十个 | | `scripts/verify-package.mjs` | 校验 Webhook Host、Runtime 和 RPC marker | | `README.md`、`README.en.md`、`CHANGELOG.md` | 渠道、绑定和 Demo 文档 | | `lib/index.js`、`lib/client.js` | 由现有 build 生成 | | 所有硬编码九渠道数组和测试 | 加入 `webhook`,保持原渠道基线不变 | 实施时先通过 `rg` 再次检索 `nine`、`九个/九种/九渠道` 和九渠道数组,避免遗漏包验证、Compact、History、Session 和 UI 测试中的渠道清单。 ## 10. 测试方案 ### 10.1 API 和安全 - 只接受合法 HTTPS 或 loopback HTTP Base URL。 - 拒绝 URL credentials、query、hash、无效协议和重定向。 - 所有请求使用 Bearer Token,错误和日志不泄漏 Token。 - `getMe` 拒绝错误协议版本、无效 bot id/name 和 `is_bot !== true`。 - 超时、Abort、无效 JSON、`4xx`、`429`、`5xx` 使用稳定错误语义。 - `sendMessage` 的 unknown delivery 不自动重试。 ### 10.2 Controller、配置和 RPC - `{ baseUrl, token }` 可以绑定并启动 Runtime。 - 其他 Token 渠道继续只接受 `{ token }`。 - 同 Base URL、同远端 id 重绑复用 botId;更换 Token 不丢 Session/Workspace。 - 不同 Base URL 即使返回相同远端 id,也产生不同 botId。 - 配置只包含 Base URL、remote id 和 tokenRef,不包含 Token。 - 配置写入失败恢复旧 Token;启动失败保留配置并显示可重试状态。 - 删除时停止 Runtime、删除 Token、配置、state 和 workspace。 - 公共 RPC 响应不返回 Base URL 中可能包含的敏感信息,更不返回 Token。 ### 10.3 Runtime 和 Bridge - 启动顺序为 Harness → getMe → 首次 getUpdates → connected。 - direct 消息进入 `TextHarnessBridge` 并通过 sendMessage 回复。 - 未 addressed 的 group 消息被拒绝且不回复。 - `from.is_bot`、重复 update 和无效 payload 被忽略。 - cursor 按 update 顺序持久化,重启后从保存位置继续。 - 空 poll 正常循环;poll 失败进入 failed,交给 supervisor 重连。 - stop 取消 poll、等待 Bridge、有界返回且幂等。 - plain 和 markdown 最终文本映射正确。 - 文件/图片请求走共享明确降级,不静默丢失。 - 连接测试在收到一条私聊后可以发送到记忆目标。 - 主动投递使用 `{ kind: "chat", route: { chatId } }`。 ### 10.4 Host、Client 和回归 - Webhook 渠道失败不阻止其他渠道激活。 - 设置页显示 Base URL + Token,并提交精确 payload。 - 状态、重连、删除、Workspace、Preset、上下文增强、权限和 Delivery 设置复用正常。 - 所有既有渠道共享 Controller/RPC 测试保持通过。 - 构建 bundle 和发布包包含新增 Host、Client 和 Runtime。 ### 10.5 Demo 使用随机端口启动真实 Demo server,验证: 1. 无 Token 调用 `/v1/getMe` 返回 `401`。 2. 正确 Token 返回 Demo bot identity。 3. `/demo/messages` 创建 Update,并立即释放等待中的 `getUpdates`。 4. offset 增长后旧 Update 不再返回。 5. `/v1/sendMessage` 把 DSH 消息写入浏览器 transcript。 6. `/demo/messages?after=` 只返回新增消息。 7. `/` 返回可用 HTML,消息使用文本渲染。 8. 关闭 server 时 pending poll 正常结束。 Demo 测试不覆盖持久化、幂等、多消费者、限流和跨进程恢复。 ## 11. 实施顺序 ### 阶段 1:协议和 API 1. 完成 Base URL 校验和 `WebhookApi`。 2. 锁定三个接口和错误契约。 3. 完成 API 单元测试。 ### 阶段 2:共享绑定扩展和渠道核心 1. 给共享 Token Controller/RPC 增加两个可选 hook。 2. 先跑全部既有 Token 渠道测试,确认默认行为不变。 3. 新增 ConfigStore、Controller、StateStore、Bridge、BotClient 和 Runtime。 4. 完成 poll、cursor、消息收发和生命周期测试。 ### 阶段 3:Host、Client 和主动投递 1. 接入 shared production 和 supervisor。 2. 注册 Host/RPC/Client 渠道。 3. 接入上下文增强、访问策略和 Delivery。 4. 更新渠道硬编码清单和 UI 测试。 ### 阶段 4:Demo 1. 实现 `server.mjs` 和真实 HTTP 测试。 2. 实现单页 `index.html`。 3. 编写 Demo README 和 npm script。 4. 使用真实 DSH 完成一轮本机收发联调。 ### 阶段 5:发布验证 1. 更新中英文 README、包描述、CHANGELOG 和 package verification。 2. 执行 `npm run check`。 3. `npm pack` 后在空临时目录安装并导入 Host/Client bundle。 4. 从发布包启动 Demo,完成一次 Base URL + Token 绑定和聊天闭环。 ## 12. 验收标准 满足以下条件即可认为 V1 完成: - 用户只填写 Base URL 和 Token 就能绑定,不需要 DSH 地址。 - 绑定会验证真实外部身份,Token 不进入配置、状态和日志。 - 外部系统通过长轮询向 DSH 提供文本消息。 - 私聊消息可以触发 Harness,并在同一个外部会话看到最终回复。 - 群聊 addressed 边界、机器人消息过滤和重复 Update 去重有效。 - Host 重启和短暂网络故障后可以从持久化 cursor 恢复轮询。 - 连接状态、自动重连、删除、连接测试、Workspace、Preset、上下文增强、权限和文字主动投递可用。 - 图片和文件等未支持能力产生明确提示,不静默丢失。 - Demo 一条命令启动,浏览器页面能够完成用户消息 → DSH → 机器人回复闭环。 - Demo 没有数据库、WebSocket、前端框架或新增第三方依赖。 - 既有渠道行为和测试没有退化,完整构建和包验证通过。 ## 13. 后续能力触发条件 V1 上线后只有出现真实需求和证据时才增加: - 外部 Connector 明确需要图片或文件时,在保持 `getMe/getUpdates/sendMessage` 不变的前提下,为 Update 增加可选 `message.attachments`,并增加基于受控 `file_id` 的 `downloadFile` 接口;不接受任意下载 URL 或把文件编码进 `getUpdates`。现有 `WebhookApi`、normalizer 和 `TextHarnessBridge` 分层能够承接该扩展,V1 不提前实现。 - 长轮询在真实代理环境出现连接竞争时,再提取共享私有 HTTP transport。 - 外部系统达到高并发且长轮询成为瓶颈时,再评估 WebSocket;不得与 V1 同时实施。 - 需要严格任务不丢失时,单独设计 durable inbox/outbox 和 Harness 幂等,不修改本协议的简单 cursor 基线。 - 需要多个 Connector 方言时,先由外部系统适配到本协议,不在 DSH 内引入任意 JSON 映射器。 在这些条件出现前,保持三个 API、文本消息和现有共享机制即可。