33 KiB
Webhook 渠道与模拟外部系统 Demo 完整方案
方案日期:2026-09-01。状态:待实施。
本文设计一个参考 Telegram Bot API 的 Webhook 渠道。用户只需填写 Base URL + Token 并点击“绑定并连接”,DSH 即主动连接外部系统;外部系统不需要知道 DSH 地址。本文同时定义一个放在 src/webhookDemo 的最小模拟外部系统,提供单页聊天界面,用于本地联调渠道收发消息。
1. 最终决策
Webhook 渠道只采用一套机制:
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 必须实现
- 设置页可以填写 Base URL 和 Token,并点击“绑定并连接”。
- 绑定时调用外部系统
getMe,确认 Token、协议版本和机器人身份。 - 绑定成功后自动启动长轮询;Host 重启后自动恢复。
- 外部消息能够进入现有 Harness 会话、命令和权限链路。
- Harness 最终文本能够通过外部系统发送回原会话。
- 私聊和群聊使用稳定的外部会话 ID。
- 支持现有连接状态、重试连接、移除机器人、连接测试、工作区、Agent Preset、上下文增强和文字主动投递。
- Token 只进入
ctx.credentials,配置、状态、错误和日志不得返回 Token。 - 一个 DSH 可以绑定多个 Webhook Connector;同一 Connector 重绑新 Token 时保留原 bot 状态。
- 提供一个能直接启动的模拟外部系统及 HTML 聊天页面。
2.2 V1 明确不做
- 不让外部系统 POST 到 DSH。
- 不实现 WebSocket、SSE、反向隧道或公共 Relay。
- 不支持图片、文件、语音、卡片、按钮、消息编辑、输入状态和 Reaction;图片与普通文件保留为后续协议扩展,不进入 V1 实现和 Demo。
- 不支持流式更新;只发送最终文字和现有明确错误提示。
- 不允许用户配置任意 API 路径或 JSON 映射模板。
- 不支持 OAuth、动态注册或自动创建外部机器人。
- 不实现 DSH 侧消息队列、离线补发或出站自动重试。
- 不为 Demo 实现数据库、磁盘持久化、幂等、限流、多租户、消息保留承诺或多消费者协调。
- 不重构所有渠道,也不建设新的通用 Connector 框架。
3. 用户体验
Webhook 设置页显示两个输入项:
Base URL https://connector.example.com/dsh/
Token ••••••••••••••••••••
[绑定并连接]
点击后:
- 浏览器通过现有 Connection RPC 把
{ baseUrl, token }发送给 Host。 - Host 严格校验 Base URL 和 Token。
- Host 调用
{baseUrl}/v1/getMe。 - 外部系统返回稳定机器人身份。
- Host 保存非敏感配置,把 Token 写入凭据服务。
- Runtime 启动
getUpdates长轮询。 - Harness 可达且首次
getUpdates成功后,机器人状态显示“HTTP 长轮询运行正常”。
如果身份验证成功但长轮询暂时失败,机器人配置仍然保留,状态显示连接未就绪,并由现有 supervisor 自动重试。删除接入时停止长轮询、删除本地配置、Token、会话状态和 Workspace 绑定,不调用外部系统删除机器人。
4. 总体架构
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 <token>。 - 成功响应统一为
{ "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) 等方式拼接。因此:
Base URL: https://example.com/connectors/dsh/
getMe: https://example.com/connectors/dsh/v1/getMe
Base URL 是用户明确配置的受信 Connector 地址,因此 V1 不额外阻止内网地址;但任何入站消息都不能改变 Base URL。
5.3 getMe:验证绑定与取得身份
请求:
POST /v1/getMe
Authorization: Bearer webhook-demo-token-123456
Content-Type: application/json; charset=utf-8
{}
成功响应:
{
"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:长轮询收消息
请求:
POST /v1/getUpdates
Authorization: Bearer webhook-demo-token-123456
Content-Type: application/json; charset=utf-8
{
"offset": 123,
"timeout": 25,
"limit": 100
}
成功响应:
{
"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 回复
请求:
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"
}
成功响应:
{
"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 错误和超时
建议错误响应:
{
"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 目录结构
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 保存并严格校验:
{
"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,增加两个可选扩展点,默认行为完全不变:
TokenBotController可选inspectBinding(payload):- 默认仍提取
payload.token并调用现有inspectToken(token)。 - Webhook 实现校验
{ baseUrl, token },调用getMe,返回{ token, platformId, name, username, configExtension }。 - Controller 后续保存凭据、原子写配置、启动 Runtime、回滚、状态、重连和删除逻辑保持原样。
- 默认仍提取
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 保持一致:
{
startedAt: null,
ready: false,
connectionState: 'idle',
harnessReachable: false,
lastCheckedAt: null,
lastConnectedAt: null,
lastError: null,
...createTextBridgeStatus()
}
start():
- 调用
stop()清理旧代 Runtime。 - 设置
connecting。 harness.ensureRunning()。- 创建
WebhookApi({ baseUrl: config.baseUrl, token })。 - 调用
getMe,再次确认外部身份与保存的platformId一致。 - 创建
WebhookBotClient和WebhookHarnessBridge。 - 从
state.cursor()读取 offset;首次使用0。 - 立即执行一次
getUpdates({ offset, timeout: 0, limit: 100 });处理返回结果并在成功后设置connected/ready,不能让绑定操作等待 25 秒。 - 启动使用
timeout: 25的持续 poll task。
poll():
- 调用
getUpdates({ offset: cursor, timeout: 25, limit: 100 })。 - 验证 Update 数组按
update_id升序且没有无效字段。 - 为本批消息捕获当前上下文增强快照。
- 调用
normalizeWebhookUpdate()。 - 对有效消息异步调用
bridge.accept()。 - 每处理一个 Update,把 cursor 更新为
update_id + 1并通过ConversationStateStore.setCursor()原子保存。 - 空数组直接进入下一轮。
该确认时机与现有 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 所需转换:
{
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:
{
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 使用一个目标类型:
{
"targetId": "demo-chat",
"kind": "chat",
"route": {
"chatId": "demo-chat"
}
}
Runtime 校验 kind === "chat" 且 route.chatId 是合法外部 ID,然后调用 bridge.sendProactiveText({ chatId }, text)。目标候选从共享状态中的 direct:<chatId> 和 group:<chatId> conversation key 生成。
需要在现有 Delivery 支持表、路由校验、候选解析和客户端字段定义中加入 webhook。不新增主动投递历史、重试或幂等逻辑。
6.9 Host 和 Client 接入
Host:
- 新增
/webhookRPC channel。 - 使用共享 Token Bot endpoint 名称。
- 在
plugin-src/host/index.mjs注册 Webhook channel,并注入现有deliveryService。 - Webhook 启动失败不得阻止其他渠道。
Client:
- 复用
createTokenChannelSettings()。 - 自定义
CredentialPanel,内部继续使用现有CredentialBindingPanel:- identity 字段显示为
Base URL。 - secret 字段显示为
Token。
- identity 字段显示为
credentialPayload({ identity, secret })返回{ baseUrl: identity, token: secret }。- 复用机器人卡片、状态轮询、连接检查、移除、Workspace、Agent Preset、上下文增强和设置按钮。
- 新增最小 Webhook 图标和样式,不复制页面布局。
7. 模拟外部系统 Demo
7.1 目标
Demo 用于证明完整链路:
HTML 页面输入消息
↓
Demo 写入内存 Update 队列
↓
DSH getUpdates 拉到消息
↓
Harness 生成回复
↓
DSH sendMessage
↓
Demo HTML 页面显示机器人回复
Demo 不模拟生产可靠性,只要便于本机启动、绑定和观察消息即可。
7.2 目录结构
代码全部放在用户指定目录:
src/webhookDemo/
├── server.mjs
├── index.html
└── README.md
不增加前端构建、CSS 文件、客户端框架或数据库。
7.3 启动方式
建议增加 npm script:
{
"scripts": {
"demo:webhook": "node src/webhookDemo/server.mjs"
}
}
默认启动:
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(如有需要)。
内存状态:
{
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=<seq> |
页面轮询聊天记录 |
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”。
- 文本输入框和发送按钮。
- 清晰展示服务端错误。
浏览器行为:
- 加载
/demo/config。 - 每 500~1000 毫秒调用
/demo/messages?after=<lastSeq>。 - 提交表单到
/demo/messages。 - 使用
textContent渲染消息,禁止把消息内容写入innerHTML。 - 页面卸载时不需要额外清理。
页面不直接调用 DSH,也不需要知道 DSH 地址。
7.6 README.md
只记录:
- 启动命令。
- 默认 Base URL 和 Token。
- 在 DSH 设置页绑定的方法。
- 打开聊天页并发送消息的步骤。
- 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,验证:
- 无 Token 调用
/v1/getMe返回401。 - 正确 Token 返回 Demo bot identity。
/demo/messages创建 Update,并立即释放等待中的getUpdates。- offset 增长后旧 Update 不再返回。
/v1/sendMessage把 DSH 消息写入浏览器 transcript。/demo/messages?after=只返回新增消息。/返回可用 HTML,消息使用文本渲染。- 关闭 server 时 pending poll 正常结束。
Demo 测试不覆盖持久化、幂等、多消费者、限流和跨进程恢复。
11. 实施顺序
阶段 1:协议和 API
- 完成 Base URL 校验和
WebhookApi。 - 锁定三个接口和错误契约。
- 完成 API 单元测试。
阶段 2:共享绑定扩展和渠道核心
- 给共享 Token Controller/RPC 增加两个可选 hook。
- 先跑全部既有 Token 渠道测试,确认默认行为不变。
- 新增 ConfigStore、Controller、StateStore、Bridge、BotClient 和 Runtime。
- 完成 poll、cursor、消息收发和生命周期测试。
阶段 3:Host、Client 和主动投递
- 接入 shared production 和 supervisor。
- 注册 Host/RPC/Client 渠道。
- 接入上下文增强、访问策略和 Delivery。
- 更新渠道硬编码清单和 UI 测试。
阶段 4:Demo
- 实现
server.mjs和真实 HTTP 测试。 - 实现单页
index.html。 - 编写 Demo README 和 npm script。
- 使用真实 DSH 完成一轮本机收发联调。
阶段 5:发布验证
- 更新中英文 README、包描述、CHANGELOG 和 package verification。
- 执行
npm run check。 npm pack后在空临时目录安装并导入 Host/Client bundle。- 从发布包启动 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、文本消息和现有共享机制即可。