mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-08 22:00:50 +08:00
update docs
This commit is contained in:
parent
129e9468a9
commit
559ea2c5a5
1 changed files with 876 additions and 0 deletions
876
docs/方案/Webhook渠道与模拟外部系统Demo完整方案.md
Normal file
876
docs/方案/Webhook渠道与模拟外部系统Demo完整方案.md
Normal file
|
|
@ -0,0 +1,876 @@
|
|||
# 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 <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)` 等方式拼接。因此:
|
||||
|
||||
```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:<chatId>` 和 `group:<chatId>` 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=<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”。
|
||||
- 文本输入框和发送按钮。
|
||||
- 清晰展示服务端错误。
|
||||
|
||||
浏览器行为:
|
||||
|
||||
1. 加载 `/demo/config`。
|
||||
2. 每 500~1000 毫秒调用 `/demo/messages?after=<lastSeq>`。
|
||||
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、文本消息和现有共享机制即可。
|
||||
Loading…
Add table
Add a link
Reference in a new issue