Skip to content

Gateway-Node 协议设计 ​

状态:设计稿(协议未冻结,迭代中) 相关文档:

1. 背景与目标 ​

Nahida Bot 的 Gateway 已经具备 REST API + SSE 事件流,WebUI 只消费公开 API。但部分能力(Desktop 桌宠远程表现控制、未来 Python worker 节点、远程 GPU 推理节点)需要双向、长连接、可被 Gateway 主动调用的通道。SSE 是单向的,REST 又缺乏主动推送能力,因此需要一套基于 WebSocket 的 Gateway-Node 协议。

本协议的设计目标:

  • 语言无关:Python 与 Rust/Tauri Desktop 各自实现一份 SDK,不通过 FFI 共享实现。
  • 双向:Gateway 可向 Node 下发请求(capability.invoke),Node 也可向 Gateway 上报事件与请求。
  • 可演进:协议带版本字段,未知字段向前兼容,破坏性变更走新主版本。
  • 可测试:通过 tests/fixtures/gateway_node/*.json 固定报文样例,Python/Rust 双方 parse 同一批 fixtures 保证一致。

非目标:

  • 不在通用 envelope 中承载音频、图片等大二进制。媒体走 media_id / Gateway Media API;desktop.computer.screenshot 的单帧、限尺寸 JPEG capability response 是 Windows Computer Use MVP 的受控例外。
  • 不在首版做复杂二进制协议或 protobuf 代码生成。
  • 高权限本机能力不随协议默认开放。当前文件读取、命令执行、截屏和输入注入 仅在 admin + actor 唯一绑定 + Desktop 本地显式策略授权后开放;其余保持关闭。

2. 设计原则 ​

2.1 JSON over WebSocket ​

V1 使用 JSON 文本帧。理由:调试方便、与 FastAPI/Pydantic/serde/TypeScript 全兼容、协议演化期调整成本低。Node 业务数据量小(状态事件、能力调用参数),不需要首版优化为 binary frame。

2.2 不使用 FFI ​

Desktop 不通过 Rust→Python FFI 复用业务逻辑。两个进程通过 WebSocket 说同一种协议,边界清晰,便于分布式部署和跨语言扩展。

2.3 协议分层 ​

text
Wire Protocol
  JSON over WebSocket 的稳定约定(envelope、auth、heartbeat、request/response/event、error)。

Python Protocol SDK
  Pydantic models + dispatcher + Gateway node session manager + Python node client。

Rust Protocol SDK
  serde structs + async websocket client + reconnect/heartbeat + Tauri command bridge(Desktop)。

3. 传输与连接 ​

3.1 Endpoint ​

  • WebSocket endpoint:/api/nodes/ws
  • 默认本地连接:ws://127.0.0.1:6185/api/nodes/ws
  • 远程连接必须使用 wss://,或明确配置为不安全模式(仅开发环境)。

3.2 鉴权载体 ​

连接鉴权支持两种方式:

  1. 长期 node token:通过 HTTP Sec-WebSocket-Protocol 子协议或 query 参数 ?token=<node_token> 携带。query 参数方式兼容浏览器/WebView 的 WebSocket 构造限制。
  2. 一次性 pairing token:配对流程中签发,换取长期 node token 后失效。

首版 query token 路径必须可用,因为 WebView 的 WebSocket 无法设置自定义 Authorization header。

3.3 默认网络边界 ​

  • 本地开发和个人使用默认只监听 127.0.0.1。
  • 远程 Gateway 必须要求 WSS 或明确配置为不安全模式。
  • Gateway CORS 不应长期保持生产环境 *。

4. Envelope ​

所有报文使用统一 envelope。envelope 字段使用 snake_case(wire format),各语言内部可转 camelCase。

4.1 基础字段 ​

字段类型必需说明
versionstring是协议版本,如 "1.0"
kindstring是request / response / event / heartbeat
idstring条件request/response 关联 ID(kind 为 request/response 时必需)
methodstring条件request 的方法名(kind=request 时必需)
eventstring条件event 的事件名(kind=event 时必需)
okbool条件response 是否成功(kind=response 时必需)
payloadobject否业务数据
errorobject否错误对象(见 4.7)
metaobject否trace、时间戳、兼容标记等扩展信息

4.2 Request ​

kind="request",必须带 id 和 method,可选 payload。由任一端发起,对端必须回一个相同 id 的 response。

json
{
  "version": "1.0",
  "kind": "request",
  "id": "req_01JZ000001",
  "method": "node.register",
  "payload": { "node_id": "desktop-local" }
}

4.3 Response ​

kind="response",id 对应 request,ok=true 时带 payload,ok=false 时带 error。

json
{
  "version": "1.0",
  "kind": "response",
  "id": "req_01JZ000001",
  "ok": true,
  "payload": { "accepted": true }
}

4.4 Event ​

kind="event",带 event 名,无 id(事件不需要回应)。用于单向通知。

json
{
  "version": "1.0",
  "kind": "event",
  "event": "agent.message.completed",
  "payload": { "session_id": "milky:private:10001", "text": "计划已整理好。" }
}

4.5 Heartbeat ​

kind="heartbeat",payload 含 type: "ping" 或 "pong",可选 ts。heartbeat 不要求严格的 request/response 配对,但 pong 应引用最近 ping 的 ts。

json
{ "version": "1.0", "kind": "heartbeat", "payload": { "type": "ping", "ts": 1752100000000 } }

4.6 ID 生成约定 ​

  • request id 由发起端生成,建议使用 ULID 或 {prefix}_{timestamp}_{random}。
  • 同一连接内 id 不可重复。
  • response id 必须等于对应 request 的 id;收到未知 id 的 response 应忽略并记录警告。

4.7 Error 对象 ​

json
{
  "code": "capability_denied",
  "message": "Node is not allowed to execute this capability",
  "retryable": false,
  "details": {}
}
字段类型必需说明
codestring是机器可读错误码(见第 9 节)
messagestring是人类可读说明
retryablebool否是否值得重试,默认 false
detailsobject否附加诊断信息

5. 连接生命周期 ​

5.1 完整流程 ​

text
Node                           Gateway
  │                               │
  │  WebSocket connect + token    │
  ├──────────────────────────────►│
  │                               │  校验 token
  │                               │
  │  node.register (capabilities) │
  ├──────────────────────────────►│
  │                               │
  │  node.register response       │
  │   (session_id, heartbeat)     │
  │◄──────────────────────────────┤
  │                               │
  │  heartbeat ping/pong          │
  │◄─────────────────────────────►│
  │                               │
  │  event: agent.message.*       │  (Gateway → Node)
  │◄──────────────────────────────┤
  │                               │
  │  request: capability.invoke   │  (Gateway → Node)
  │◄──────────────────────────────┤
  │  response                      │
  ├──────────────────────────────►│
  │                               │
  │  request: node.state.updated  │  (Node → Gateway)
  ├──────────────────────────────►│

5.2 连接前鉴权 ​

WebSocket 握手阶段携带 token。Gateway 在接受升级前校验:

  1. token 存在且未过期/未撤销。
  2. token 类型正确(node token 或 pairing token)。
  3. 握手成功后进入「已认证但未注册」状态,此时只允许 node.register,其他 method 返回 not_registered 错误。

设计选择:把鉴权放在握手阶段(而非连接后第一帧),是因为 WebSocket 握手失败语义清晰(401/403),且避免半连接状态。

5.3 注册(node.register) ​

Node 连接成功后必须先发送 node.register:

json
{
  "version": "1.0",
  "kind": "request",
  "id": "req_register_001",
  "method": "node.register",
  "payload": {
    "node_id": "desktop-local",
    "display_name": "Nahida Desktop",
    "node_type": "desktop",
    "capabilities": [
      {
        "name": "desktop.live2d.set_expression",
        "version": "1.0",
        "direction": "gateway_to_node",
        "risk": "low",
        "description": "Set Live2D expression by expression id"
      }
    ],
    "metadata": { "platform": "windows", "app_version": "0.1.0" }
  }
}

Gateway 响应:

json
{
  "version": "1.0",
  "kind": "response",
  "id": "req_register_001",
  "ok": true,
  "payload": {
    "accepted": true,
    "session_id": "node_session_01JZ0000AB",
    "heartbeat_interval_ms": 15000,
    "heartbeat_timeout_ms": 45000,
    "server_time": "2026-07-09T12:00:00Z"
  }
}

注册约束:

  • node_id 在 Gateway 范围内唯一。重复 node_id 连接时,Gateway 按「踢旧连接」策略处理(旧连接收到 node.duplicate_connection event 后被关闭)。
  • node_type 首版枚举:desktop / worker / tool-host。
  • capabilities 可为空(纯消费事件的 node)。
  • 注册后 node 进入「在线」状态,开始心跳。

5.4 心跳 ​

Gateway 按 heartbeat_interval_ms 间隔发送 ping,Node 必须在 heartbeat_timeout_ms 内回 pong。超时未回,Gateway 判定 Node 离线,关闭连接。

Node 也可主动发 ping(例如检测链路活性),Gateway 必须回 pong。

text
Gateway ping → Node pong (主路径)
Node ping → Gateway pong (可选)

5.5 重连 ​

Node 必须实现:

  • 指数退避重连,初始 1s,上限 30s。
  • 重连后重新鉴权、重新 node.register。
  • 重连期间本地状态进入 disconnected,但不崩溃。
  • Gateway 侧:Node 重连后视为同一 node_id 的会话恢复,重新建立在线状态。

5.6 状态机 ​

text
disconnected ──connect──► authenticating ──ok──► registering ──accepted──► online
                                │                       │                      │
                              fail                   reject                 heartbeat timeout
                                │                       │                      │
                                ▼                       ▼                      ▼
                            disconnected            disconnected            offline (待重连)

Gateway 侧维护的 Node 会话状态:authenticating / registering / online / offline。offline 表示曾在线但当前断开,保留最近状态一段时间供查询。

6. Methods(请求方法注册表) ​

6.1 Node → Gateway ​

method说明payload 关键字段
node.register注册节点与能力node_id, node_type, capabilities, metadata
node.state.updated上报本地状态摘要state(窗口/模型/TTS/渲染模式摘要)
node.unsubscribe取消订阅部分事件events: list[str]
node.input.submitNode 端用户输入消息session_id, text(见 6.3)
node.capability.result异步 capability 结果回传invoke_id, status, result(可选异步路径)

6.2 Gateway → Node ​

method说明payload 关键字段
capability.invoke调用 Node 能力invoke_id, capability, arguments
capability.cancel取消进行中的能力调用invoke_id
node.subscribe告知 Node 将推送哪些事件events: list[str]
node.set_event_filter调整事件过滤策略filter

6.3 node.input.submit 语义 ​

Desktop 桌宠气泡输入框的消息通过此方法注入到指定 session。Gateway 将其标记为 source=node 的用户消息,进入 Agent Loop。注意:这不能伪装成普通用户消息,必须在 turn metadata 中标记来源,便于审计。

6.4 Capability 调用 ​

json
{
  "version": "1.0",
  "kind": "request",
  "id": "req_invoke_099",
  "method": "capability.invoke",
  "payload": {
    "invoke_id": "inv_01JZ0000ZZ",
    "capability": "desktop.live2d.set_expression",
    "arguments": { "expression": "happy" }
  }
}

成功响应:

json
{
  "version": "1.0",
  "kind": "response",
  "id": "req_invoke_099",
  "ok": true,
  "payload": { "applied": true }
}

失败响应(capability 不存在 / 未授权 / 执行异常):

json
{
  "version": "1.0",
  "kind": "response",
  "id": "req_invoke_099",
  "ok": false,
  "error": {
    "code": "capability_not_found",
    "message": "Capability desktop.live2d.set_expression not registered",
    "retryable": false
  }
}

约束:

  • Gateway 在调用前必须校验 Node 已注册该 capability,且调用方(user/session/plugin)被授权。
  • Node 端必须二次校验本地 allowlist,不能只依赖 Gateway。
  • 所有 capability.invoke 写入 audit log(调用方、node_id、capability、arguments 摘要、结果、耗时)。
  • arguments 由 capability schema 约束;非法字段 schema 校验失败返回 invalid_arguments。
  • 高风险 capability(文件、命令、媒体采集)首版不开放。

desktop.surface.sync 是低风险的宿主渲染能力。其 arguments 是带单调 revision 的 完整 desired-state snapshot;Desktop 必须校验 surface 数量、枚举、字符串长度、进度范围 和重复所有权 ID,并忽略旧 revision。协议与插件边界见 Plugin 运行时与 UI Surface。

7. Events ​

事件是单向通知(kind="event"),不需要回应。

7.1 Gateway → Node(订阅式) ​

Node 通过 node.register 或 node.subscribe 声明感兴趣的事件类别。默认订阅集合由 Gateway 配置决定(例如 Desktop 默认订阅当前 session 的 agent 事件)。

event说明payload 关键字段
agent.message.startedAgent 开始处理session_id
agent.message.completedAgent 回复完成session_id, text, display_plan
agent.message.errorAgent 处理出错session_id, error
cron.fired定时任务触发job_id, session_id, prompt
plugin.error插件错误plugin_id, error
gateway.shutdownGateway 即将关闭reason, retry_after_ms
node.duplicate_connection同 node_id 新连接取代当前连接new_session_id
capability.revoked能力授权被撤销capability

agent.message.completed 的 display_plan 字段承载 desktop-app.md §9.3 的 DisplayPlan,用于驱动 Desktop Live2D/TTS 表现。Gateway 保留干净文本进入 transcript/memory,DisplayPlan 只作为 metadata 透传给订阅的 node。

7.2 Node → Gateway ​

event说明payload 关键字段
node.state.updated上报状态摘要(也可走 request,event 为轻量通知)state
node.model.imported用户导入模型元数据model_id, name, capabilities
node.event.customNode 自定义事件(透传,不参与 core 语义)type, data

7.3 事件过滤与隐私 ​

  • Gateway 不向 Node 推送其无权感知的 session 事件。
  • 事件 payload 不包含 base64 媒体、临时 URL、raw_event、reasoning 原文。
  • DisplayPlan 在进入 Node 前已完成 schema 校验与降级。

8. 鉴权与配对 ​

8.1 配对流程(pairing) ​

V2 Desktop Node 推荐配对流程,避免长期 token 手动复制:

text
1. 用户在 Desktop App 输入 Gateway 地址。
2. Desktop App POST /api/nodes/pairing/start
   → Gateway 生成一次性 pairing code(短码,例如 6 位)。
3. 用户在 WebUI 授权页输入 pairing code(或扫码)确认。
4. Gateway POST 返回长期 node token + node_id。
5. Tauri Rust side 把 node token 存入系统安全存储(keychain)。
6. Desktop 用 node token 建立 WebSocket。

8.2 Token 类型 ​

token用途生命周期
pairing token / code一次性换取 node token短(分钟级)
node tokenWebSocket 连接鉴权长,可撤销/过期
(现有) bearer token / webui sessionREST/SSE 鉴权现有体系

node token 与现有 bearer/webui 体系并行,由 node_auth service 独立管理。token 存储建议:

  • Python 端:哈希存储(不存明文),记录签发时间、过期、撤销、关联 node_id。
  • Desktop 端:系统 keychain,不写普通 JSON/YAML。

8.3 授权策略 ​

Gateway 维护 capability 授权策略:哪些 user/session/plugin 可以调用哪些 node 的哪些 capability。Node 注册 capability 不等于默认信任。调用入口、类型化 actor、策略模型、审批和持久审计见 Gateway-Node 调用入口与授权设计。公开入口启用后采用 fail-closed;owner 也需要显式 grant,高风险调用还需要 approval。

Node credential 与人员身份分离。配对/签发时,Gateway 可为 credential 绑定:

  • actor_account_key:该 credential 获准代表的账号,例如 desktop:user:owner;不能从 node_id 推导。
  • conversation_id:该 credential 默认使用的短期 history lane,例如 conversation:private:owner-desktop。

运行时由 Gateway 从已验证 token 恢复这两个绑定,客户端声明的 person 或 node id 均不能替代 actor account。IdentityStore 再独立解析 actor_account_key -> person_id;worker/tool-host 可不绑定人员账号,但不能 提交人员输入。

9. 错误码 ​

错误码是稳定契约,新增允许,已有码语义不破坏性变更。

code含义retryable 典型值
auth_failed鉴权失败(token 无效/过期)false
auth_required未鉴权就发非 register 请求false
not_registered未注册就发业务请求false
register_rejected注册被拒(node_id 冲突策略等)视情况
capability_not_found能力未注册false
capability_denied调用方未授权false
capability_local_deniedNode 本地 allowlist 拒绝false
invalid_argumentsarguments schema 校验失败false
capability_failed能力执行异常视情况
capability_timeout能力调用超时true
capability_cancelled能力调用被取消false
method_not_found未知 methodfalse
node_input_unavailableNode 输入入口或目标 channel 当前不可用true
rate_limited频率超限true
internal_errorGateway 内部错误true
unknown_request_idresponse 引用了未知 request idfalse

错误响应必须包含 code 和 message。retryable 帮助 Node/Gateway 决定是否重试。

10. 版本与兼容 ​

10.1 版本字段 ​

version 字段格式 "<major>.<minor>"。

  • minor 升级:新增字段、新增 method/event/code、放宽约束。双方必须向前兼容(忽略未知字段、忽略未知 method 时返回 method_not_found 而非断连)。
  • major 升级:破坏性变更。Gateway 可同时支持多个主版本 endpoint(如 /api/nodes/ws 与 /api/nodes/ws/v2),Node 通过握手声明版本。

10.2 兼容策略 ​

  • 接收端遇到未知字段:忽略(Pydantic 默认行为,Rust serde 用 #[serde(default)] + deny_unknown_fields=false)。
  • 接收端遇到未知 method:返回 method_not_found,不断连。
  • 接收端遇到未知 event:忽略并记录 debug 日志。
  • 接收端遇到未知 error.code:按 retryable=false 处理,展示 message。
  • version 缺失或无法解析:Gateway 拒绝连接,Node 重连时降级重试。

10.3 协议冻结时机 ​

协议进入「稳定」状态后(里程碑 3 完成且 Desktop Rust 对齐后),默认只做向后兼容变更。破坏性变更必须走新主版本。

11. 模块布局 ​

11.1 Python 侧 ​

text
nahida_bot/gateway/
  node_protocol/
    __init__.py
    schemas.py        # Pydantic envelope / payload / capability / error models
    dispatcher.py     # request routing / response matching / event fanout
    sessions.py       # node session 状态机、心跳、capability registry
    errors.py         # 错误码常量、NodeProtocolError 异常树
    auth.py           # node token / pairing token 校验
    routes.py         # FastAPI WebSocket endpoint(/api/nodes/ws)
  services/
    node_registry.py  # 在线/离线节点表、查询、状态摘要
    node_auth.py      # token 签发/撤销/存储、pairing 流程
    node_invoker.py   # capability.invoke 调用、超时、audit
  routes/
    nodes.py          # REST: /api/nodes, /api/nodes/{id}, pairing/*
text
nahida_bot/node/
  __init__.py
  client.py           # Python Node WebSocket client SDK
  capabilities.py     # capability 注册辅助、本地 allowlist

11.2 Rust/Tauri 侧(Desktop,后续阶段) ​

text
desktop/src-tauri/src/gateway_node/
  protocol.rs         # serde envelope / payload / error structs
  client.rs           # WebSocket connect / reconnect / heartbeat / request-response matching
  dispatcher.rs       # 处理 Gateway 发来的 capability.invoke
  capabilities.rs     # 注册 Live2D / notification / window capabilities
  auth.rs             # pairing / node token / 安全存储

11.3 Schema 一致性 ​

不通过 FFI 共享实现。一致性保证:

  • 本文档作为协议说明(single source of truth)。
  • tests/fixtures/gateway_node/*.json 保存跨语言报文样例。
  • Python 单测必须能 parse 所有 fixtures,并序列化出等价 JSON。
  • Rust 单测必须能 parse 同一批 fixtures。
  • 协议稳定前允许 Python/Rust 类型手写,避免过早引入代码生成链路。
  • Pydantic 可导出 JSON Schema,后续可用于生成 TypeScript/Rust 类型。

12. Fixtures ​

固定报文样例作为跨语言契约。每个 fixture 是一个独立 JSON 文件,文件名即语义。

text
tests/fixtures/gateway_node/
  auth_register.request.json
  auth_register.response.json
  auth_register_rejected.response.json
  heartbeat_ping.json
  heartbeat_pong.json
  node_state_updated.event.json
  capability_invoke_live2d.request.json
  capability_invoke_live2d.response.json
  capability_denied.response.json
  capability_timeout.response.json
  agent_message_completed.event.json
  gateway_shutdown.event.json
  node_duplicate_connection.event.json
  node_input_submit.request.json

fixtures 约定:

  • 真实 id / session_id 使用示例前缀(req_ / node_session_ / inv_),便于人工识别。
  • 时间戳使用 ISO8601 字符串或整数毫秒,保持同一 fixture 内一致。
  • 每个 fixture 应附一行注释(_comment 字段,parse 时忽略)说明用途。
  • fixtures 改动必须在文档同步说明,并更新 Python/Rust 双方测试。

13. 安全设计 ​

13.1 Token 存储 ​

  • Desktop App 不应把长期 token 明文写入普通 JSON/YAML。
  • Tauri side 使用系统安全存储或平台 keychain。
  • token 支持撤销、过期和重新配对。

13.2 Capability 权限 ​

  • Node 注册 capability 后,Gateway 不默认信任全部能力。
  • Gateway 保存授权策略:哪些 user/session/plugin 可调用哪些 node capability。
  • 所有 capability.invoke 写入 audit log。
  • Node 端二次校验本地 allowlist。

13.3 高风险能力 ​

以下能力已在显式声明、双端授权和审计边界下分阶段开放:

  • 只读文本文件与命令执行:受 Desktop disabled/scoped/full_access 模式裁决。
  • 单帧截屏与输入注入:另外受 computerUse.allowScreenCapture / computerUse.allowInput 独立开关裁决;工具仅对 admin 开放且禁止 subagent。
  • 截屏响应只在 capability 调用中短暂承载 base64;Gateway 校验后将图片注册到 TTL MediaStore,对 Agent 仅暴露 actor-bound media_id。分析与发送可复用该 artifact,tool transcript 不记录原始像素或内部缓存路径。

写入本机文件、录音、摄像头与全局键盘监听仍不开放。Computer Use 的像素、 坐标和视觉模型链路详见 Desktop 设计 10.4。

13.4 事件隐私 ​

  • Gateway 不向 Node 推送无权感知的 session 事件。
  • 事件 payload 不含 base64 媒体、临时 URL、raw_event、reasoning 原文。
  • DisplayPlan 进入 Node 前完成 schema 校验与降级。
  • Node 上报的状态/模型元数据只含非敏感摘要(模型 ID、名称、能力),不上传模型文件本体。

13.5 网络边界 ​

  • 本地默认 127.0.0.1。
  • 远程必须 WSS 或明确不安全模式。
  • Node 端校验 Gateway 证书;自签证书走显式信任配置。

14. 测试策略 ​

14.1 协议测试 ​

  • Python Pydantic models parse 所有 fixtures。
  • Rust serde models parse 同一批 fixtures。
  • Python/Rust 都能序列化出符合 schema 的 envelope。
  • request/response id 匹配、错误响应、未知字段兼容性均有测试。

14.2 Gateway 测试 ​

  • WebSocket 鉴权成功/失败(无效 token、过期 token、缺失 token)。
  • node.register 注册 capability、重复 node_id 处理。
  • heartbeat 超时断开。
  • capability.invoke 成功、失败(未授权/未注册)、超时、取消。
  • node offline 后 Gateway 不再调用该节点。
  • 事件订阅过滤:Node 只收到授权范围内的事件。

14.3 Node Client 测试 ​

  • 连接、鉴权、注册、重连、心跳。
  • 收到 Gateway event 能正确转交上层。
  • 收到 capability.invoke 能 dispatch 到本地 handler。
  • 本地 allowlist 拒绝非法 capability 调用。

14.4 集成测试 ​

  • Python node ↔ Gateway 全链路:注册 → 订阅事件 → 收到 agent 事件 → capability 被调用 → 状态上报。
  • 断线重连后会话恢复。
  • capability 超时/取消/异常路径。

15. 与现有系统的集成边界 ​

15.1 与 EventBroadcaster 的关系 ​

现有 EventBroadcaster(SSE)把核心 EventBus 事件 fan-out 给 HTTP 客户端。Node 协议复用同一批核心事件源(MessageReceived/MessageSent/AgentRunStarted/AgentRunFinished/PluginErrorOccurred),但:

  • Node 协议有自己的事件转换层:核心事件 → node envelope event payload(含 DisplayPlan 等额外字段)。
  • Node 订阅是 per-node 的,受授权过滤;SSE 是 per-client token 的全量(当前实现)。
  • 两者共享 EventBus 订阅,但 fan-out 路径独立,避免相互阻塞。

15.2 与 AgentOrchestrator / AgentRunExecutor 的关系 ​

agent-orchestration.md 已为远程执行预留 AgentRunExecutor 接口(LocalAgentRunExecutor + 未来 RemoteNodeRunExecutor)。本协议为 RemoteNodeRunExecutor 提供传输层,但首版不实现远程 agent run。首版 Node 协议聚焦 Desktop 表现控制与事件投递。

15.3 与 WebHostService 的关系 ​

Node WebSocket endpoint 是独立长连接通道,不经过 WebHostService(webhook 扩展点)。两者并存:webhook 走 /webhooks/{path},node 走 /api/nodes/ws。

15.4 Python Worker Node 的能力来源(设计方向,暂不实现) ​

当前 nahida_bot/node/ 是 SDK + 测试对端,没有定义「一个 Python node 进程启动时能力从哪来」。长期方向是复用现有插件系统:同一个插件,装在 gateway 端就是 gateway 的工具(通过 api.register_tool),装在 node 端就由 node 把它的能力暴露成 capability 给 gateway 远程调用。位置决定角色,不搞第二套插件系统。

核心机制:

text
node 进程                              gateway 进程
┌─────────────────────┐               ┌──────────────────────────┐
│ 插件 on_load        │               │ AgentLoop                │
│  api.register_tool( │               │  调用 web_search         │
│    "web_search",h)  │               │      ↓                   │
│       ↓             │               │ ToolRegistry 查找        │
│ NodeBotAPI          │               │  发现 web_search 在 node │
│  → capability       │──register────►│  → NodeToolBridge        │
│    "tool.web_search"│               │      ↓                   │
│  + 本地 handler h   │◄──invoke──────│ NodeInvocationService    │
│      ↓ 执行 h       │──response────►│      ↓                   │
│  返回结果           │               │ 结果回到 Agent           │
└─────────────────────┘               └──────────────────────────┘

需要的新组件:

  1. NodeBotAPI(实现 BotAPI 协议):node 端的 BotAPI 实现,按方法类别分流。
  2. NodeToolBridge(gateway 侧):监听 node register,把符合 token scope、schema 和 exposure policy 的 tool.* capability 注册成 ToolEntry(handler 统一走 NodeInvocationService);node 断线时按 node_session_id 安全注销。详细约束见 调用入口与授权设计。

BotAPI 的 ~40 个方法在 node 端分三类处理:

类别方法node 端策略
工具/命令注册register_tool / register_command转 capability(tool.{name}),本地保留 handler
事件订阅subscribe / on_event告诉 gateway 订阅,事件经 WebSocket 转发
workspace/memory/llmworkspace_read / memory_search / llm_chatRPC 回 gateway(需新增 node→gateway 方法)
session/message_routerrequest_agent_response / start_new_session不支持(依赖 EventBus.context.app 反向引用)
channel/providerregister_channel / register_provider_type不支持(必须在 gateway)
plugin_dataplugin_data_get/set本地 SQLite(node 自带轻量存储)
logger/taskslogger / spawn_task本地实现,无依赖

已知约束(来自插件系统依赖边界审计):

  • plugin_data_* 在 repo 为 None 时硬抛 RuntimeError(而 memory/workspace 静默降级)。node 必须自带本地 plugin_data store,否则 kb、mcp 等插件 on_load 就会崩。
  • EventBus.context.app 反向引用是 node 端的天生障碍——start_new_session / get_active_session_id / get_session_run_status / request_agent_response 四个方法无法在 node 直接工作。
  • 不是所有插件都能跑在 node:channel/provider 插件必须在 gateway;依赖 message_router 的插件不适合。需要 manifest 加 node_compatible 或 requires 声明。

分阶段方案:

阶段范围验收
ANodeBotAPI 基础版:register_tool→capability、logger/spawn_task 本地、其余不支持纯计算/外部 API 插件(web_fetch、image_generate)能在 node 跑,gateway 自动桥接成 Agent tool
Bnode→gateway RPC:workspace_read/memory_search/llm_chat 远程调用依赖宿主服务的插件也能在 node 跑
Cmanifest node_compatible/requires 声明 + nahida-bot node CLIoperator 能指定插件目录启动 node

当前状态:暂不实现,优先推进 #9(Desktop,走 Rust node)。阶段 A 是后续 Python worker(GPU 推理节点等)的基础。

16. 待决问题 ​

  • 一个 Gateway 同时连接多个 Desktop Node 时,未指定 node_id 的调用如何选择目标。当前收敛为:只有一个“在线且已授权”的候选时自动选择,否则返回 ambiguous_target;不使用 owner 全权或任意首节点策略。
  • node.input.submit 注入的消息是否计入会话历史,以及是否允许触发工具调用。
  • DisplayPlan 由主 LLM 输出、规则推导器生成,还是二次 planner 生成(见 desktop-app.md §18)。
  • pairing code 是 6 位短码(人工输入)还是扫码;首版建议短码,扫码后置。
  • heartbeat 由 Gateway 单向驱动,还是双向(Node 也可主动 ping)。当前设计为双向,主路径 Gateway→Node。
  • Node 离线后 Gateway 保留最近状态多久供 REST 查询。
  • capability.invoke 是否支持流式/分片结果(例如长 TTS 音频分段)。首版不支持,走 SpeechArtifact 引用。
  • 是否需要 node-to-node 中转(Gateway 路由两个 node 通信)。首版不支持。
  • Python worker node 的能力来源:复用插件系统 + NodeBotAPI 方案(见 §15.4),暂不实现,待 #9 Desktop 落地后再推进。

17. 实施里程碑 ​

与 desktop-app.md §16 Phase 5/6/7 对齐:

里程碑范围验收
M1 协议设计本文档 + fixtures双方 review,fixtures 冻结为回归基线
M2 Python SDK(服务端)node_protocol/ + services/node_*Pydantic parse 全部 fixtures;dispatcher 单测通过
M3 Gateway 集成/api/nodes/* + WS endpoint + 事件桥 + pairingmock node 能连、鉴权、注册、收事件、被调用
M4 Python Node Clientnahida_bot/node/Python node ↔ Gateway 全链路集成测试通过
M5 Desktop Rust Nodedesktop/src-tauri/gateway_node/Desktop 作为 node 连接,Rust parse 同一批 fixtures
M6 Capability 桥接DisplayPlan 投递、capability 真实执行Gateway 能通过 capability 控制 Desktop 表现
M7 Python Worker NodeNodeBotAPI + NodeToolBridge + nahida-bot node CLI(见 §15.4)插件装在 node 端,Agent 无感知远程调用

里程碑 M1-M4 属于本轮 gateway+node 架构推进范围;M5-M6 属于 Desktop 接入,依赖协议稳定;M7 是 Python worker node 的长期方向,暂不实现。