docs/prds/conversations/remote/remote-agent.md
本文档覆盖「设置 → Agents → 远端 Agents」页面的全部功能,包括 Remote Agent 的列表展示、创建/编辑/删除 (CRUD)、连接测试、OpenClaw 配对握手、会话管理、消息收发与流式响应、工具调用展示、权限审批、连接状态管理。 基于静态代码分析和动态 UI 验证综合整理,经 DA 质疑(23 条)和 Tester 反馈(8 条)修正定稿。
用户故事:作为用户,我希望在远端 Agents 页面看到所有已配置的远端 Agent 列表,了解它们的名称、状态和协议类型。
前置条件:用户已进入「设置 → Agents → 远端 Agents」Tab
正常流程(用户视角):
?tab=remote)grid-cols-1)md:grid-cols-2)xl:grid-cols-3)line-clamp-2)line-clamp-2)状态标签渲染:
渲染条件:agent.status 存在且不为 'unknown' 时渲染标签。status 为 undefined 时同样不渲染。
颜色映射(statusColor 函数):
| status 值 | 颜色 |
|---|---|
'connected' | green |
'pending' | orange |
'error' | red |
其他(含 'unknown', undefined) | gray(但渲染层不渲染) |
空列表状态:
settings.remoteAgent.emptyTitle)+ "添加"按钮(settings.remoteAgent.emptyAction)"查看配置指南"链接:
openExternalUrl 在系统浏览器中打开 https://github.com/iOfficeAI/AionUi/wiki/Remote-Agent-Guide-Chinese异常情况:
console.error(通过 .catch(console.error)),无用户提示验收标准:
?tab=remote 双向同步status 为 'unknown' 或 undefined 时不显示状态标签settings.remoteAgent.emptyTitle)和添加按钮用户故事:作为用户,我希望通过表单配置一个新的远端 Agent,填写连接信息并保存,以便后续使用该 Agent 进行对话。
正常流程(用户视角):
🤖(\u{1F916},固定值非随机)settings.remoteAgent.namePlaceholderwss://example.com/gateway'password' 但 UI 不暴露此选项(见设计约束)settings.remoteAgent.tokenPlaceholderwss:// 开头时显示,Switch 默认关闭,附说明 settings.remoteAgent.allowInsecureHint内部机制 — 创建流程:
ipcBridge.remoteAgent.create.invoke(payload)id: UUIDdeviceId = 公钥 SHA256 指纹, devicePublicKey, devicePrivateKey)status: 'unknown'createdAt / updatedAt: 当前时间戳remote_agents 表agentRegistry.refreshRemoteAgents() 刷新检测列表(fire-and-forget)RemoteAgentConfig协议说明:
UI 当前不暴露协议选择器,新建 Agent 硬编码为 'openclaw' 协议。类型定义支持 'openclaw' | 'zeroclaw' | 'acp' 三种协议。编辑时从 DB 回填实际协议值。
异常情况:
settings.remoteAgent.nameRequired / settings.remoteAgent.urlRequired),使用 role="alert" + aria-live="assertive"验收标准:
settings.remoteAgent.addTitle)🤖(固定值)wss:// 时正确显示不安全连接开关用户故事:作为用户,我希望修改已配置的远端 Agent 信息(名称、URL、认证等),修改后重新验证连接。
正常流程(用户视角):
setActiveProtocol(editAgent.protocol))ipcBridge.remoteAgent.update.invoke({ id, updates })内部机制 — 更新流程:
Bridge 端逐字段映射到 DB 列名(仅更新 updates 中不为 undefined 的字段):
| 前端字段 | DB 列名 | 说明 |
|---|---|---|
name | name | |
protocol | protocol | |
url | url | |
authType | auth_type | |
authToken | auth_token | |
avatar | avatar | |
description | description | |
allowInsecure | allow_insecure | boolean → 0/1 |
异常情况:
验收标准:
settings.remoteAgent.editTitle)用户故事:作为用户,我希望删除不再需要的远端 Agent 配置。
正常流程(用户视角):
Modal.confirm,区别于创建/编辑使用的 AionModal 封装):
settings.remoteAgent.deleteConfirm("删除远程 Agent")settings.remoteAgent.deleteConfirmContent("确定要删除「{agent名称}」吗?",使用直角引号包裹名称)settings.remoteAgent.deleted("远程 Agent 已删除")内部机制:
ipcBridge.remoteAgent.delete.invoke({ id })db.deleteRemoteAgent(id)agentRegistry.refreshRemoteAgents()(fire-and-forget)异常情况:
验收标准:
settings.remoteAgent.deleted)建议验证策略:WebSocket 连接通过集成测试 mock;URL 验证和 SSRF 防护通过单元测试
用户故事:作为用户,我希望在保存前测试远端 Agent 的连接是否可用,确认 URL 和认证信息正确。
正常流程(用户视角):
loading={testing} prop 已传入)settings.remoteAgent.testSuccess(绿色勾号 "连接成功")settings.remoteAgent.testFailed(红色, 参数 { error })settings.remoteAgent.testError(红色, 参数 { error })内部机制:
url, authType, authToken, allowInsecuresettings.remoteAgent.urlRequired,不发起测试ipcBridge.remoteAgent.testConnection.invoke({...})validateWebSocketUrl):
ws://(支持裸 host:port 格式如 127.0.0.1:42617)ws: / wss: 协议(防 SSRF){ success: false, error: 'Invalid URL' }ws 库Authorization: Bearer {token}handshakeTimeout: 10_000, rejectUnauthorized: !allowInsecurews.on('open') → { success: true }ws.on('error') → { success: false, error: err.message }settled flag 确保只 resolve 一次异常情况:
Unsupported protocol: {protocol}Connection timed out (10s)待验证问题:
RemoteAgentManagement.tsx:398 传了 loading={testing} prop,但动态分析未观察到明显 spinner 效果。需在真实窗口确认 Arco Button type='outline' 模式下 loading prop 的视觉表现。验收标准:
settings.remoteAgent.urlRequired),不发起连接allowInsecure 正确控制 TLS 证书验证settings.remoteAgent.testSuccess / testFailed / testError)建议验证策略:握手协议通过集成测试 mock WebSocket;设备密钥生成通过单元测试
用户故事:作为用户,当我保存 OpenClaw 协议的远端 Agent 时,系统应自动与远端 Gateway 完成身份验证握手。
前置条件:Agent 协议为 'openclaw'
正常流程(用户视角):
settings.remoteAgent.handshaking,加载态)settings.remoteAgent.created 或 settings.remoteAgent.updated,弹窗关闭内部机制 — OpenClaw 握手协议:
ipcBridge.remoteAgent.handshake.invoke({ id })OpenClawGatewayConnection 实例Client Gateway
|── WebSocket connect ───────────────>|
| |
| 路径 A: Challenge 到达 (<750ms) |
|<── EVENT connect.challenge {nonce} ─|
|── REQ connect {v2 签名, 含 nonce} ─|
| |
| 路径 B: Challenge 未到达(750ms超时)|
|── REQ connect {v1 签名, 无 nonce} ─|
| |
|<── RES hello-ok {auth.deviceToken, | (成功)
| policy, features} |
| 或 |
|<── RES error {PAIRING_REQUIRED} ─| (需审批)
onHelloOk、onConnectError、onClose)共用同一 Promise,依赖 Promise 只能 resolve 一次的语义保证互斥设备认证签名:
使用 Ed25519 私钥对管道分隔字符串签名:
"v1|{deviceId}|{clientId}|{clientMode}|{role}|{scopes_csv}|{signedAtMs}|{token}""v2|{deviceId}|{clientId}|{clientMode}|{role}|{scopes_csv}|{signedAtMs}|{token}|{nonce}"版本自动判定: params.nonce ? 'v2' : 'v1'
Connect 请求参数:
| 参数 | 值 |
|---|---|
minProtocol / maxProtocol | 3 |
client.id | 'gateway-client' |
client.displayName | 'AionUI' |
client.mode | 'backend' |
caps | ['tool-events'](必须声明以接收 tool call 事件) |
role | 'operator' |
scopes | ['operator.admin'] |
技术说明: password 认证路径在 Bridge 层(remoteAgentBridge.ts:184)和 Connection 层(OpenClawGatewayConnection.ts:263)已实现,但当前 UI 不暴露 password 选项。如果通过直接修改 DB 将 auth_type 改为 'password',握手流程会正确使用 password 认证。
Hello-ok 处理:
deviceToken(远程场景: 回调 onDeviceTokenIssued → 写入 DB device_token 字段)status = 'connected', last_connected_at = Date.now()PAIRING_REQUIRED 判定: details.recommendedNextStep === 'wait_then_retry' 或 /pairing.required/i
异常情况:
conn.stop() + 返回 { status: 'error', error: 'Handshake timed out (15s)' }{ status: 'error', error: 'Connection closed (code): reason' }(若已被 onHelloOk/onConnectError resolve 则忽略)验收标准:
settings.remoteAgent.handshaking)用户故事:作为用户,当远端 Gateway 要求设备审批时,我希望看到等待界面和倒计时,并可以随时取消。
前置条件:F-RAGENT-06 握手返回 pending_approval
正常流程(用户视角):
settings.remoteAgent.pendingApproval("等待网关审批...")settings.remoteAgent.pendingApprovalHint("请在 OpenClaw Gateway 上批准此设备")settings.remoteAgent.pendingTimeRemaining("剩余时间:M:SS",从 5:00 开始)settings.remoteAgent.pendingCancel)pending(橙色标签)ipcBridge.remoteAgent.handshake.invoke)ok → 成功 toast(固定使用 settings.remoteAgent.created key,不区分创建/编辑场景,已知局限)+ 弹窗关闭settings.remoteAgent.pendingTimeout,warning 样式)倒计时机制:
remaining = max(0, PAIRING_TIMEOUT - (Date.now() - startedAt))M:SS(如 4:45)remaining <= 0 → 停止所有定时器 + pairingState = 'timeout'轮询机制:
handshake.invoke({ id })status === 'ok' → 停止轮询 + success toast + 关闭弹窗status === 'pending_approval' → 继续轮询catch {}),继续轮询(已知局限:无错误反馈)取消配对行为:
pairingState = 'idle'onSaved() 刷新列表 + onClose() 关闭弹窗pending异常情况:
catch {}),继续轮询验收标准:
settings.remoteAgent.created)settings.remoteAgent.pendingTimeout)(验证策略:单元测试 mock 时间)建议验证策略:集成测试;需 mock Gateway sessions API
用户故事:作为用户(或系统),当使用远端 Agent 发起对话时,系统应自动创建或恢复与 Gateway 的会话。
正常流程(系统视角):
RemoteAgentCore.start() 被调用'connecting'OpenClawGatewayConnection 并启动isConnected,100ms 间隔,默认参数 30 秒超时)'connected'resolveSession):
resumeKey → 尝试 sessions.resolve({ key: resumeKey })sessions.reset({ key: conversationId, reason: 'new' })sessions.resolve({ key: conversationId })conversationId 作为 sessionKey'session_active'会话持久化:
RemoteAgentManager.saveSessionKey(sessionKey):
type === 'remote')conversation.extra.sessionKeyresumeKey 恢复会话异常情况:
Remote agent connection timeout验收标准:
建议验证策略:集成测试验证流式事件处理;E2E 需要真实 Gateway
用户故事:作为用户,我希望向远端 Agent 发送消息后,能实时看到流式响应内容。
正常流程(用户视角):
内部机制 — 消息发送:
RemoteAgentManager.sendMessage(data):
cronBusyGuard.setProcessing(conversationId, true) 标记忙碌RemoteAgentCore.sendMessage()cronBusyGuard.setProcessing(conversationId, false) 在 finish 信号事件或 sendMessage 异常时触发RemoteAgentCore.sendMessage():
start()(已知局限:可能导致意外重连和会话重置)@"filepath" 格式,否则 @filepath,多文件空格连接后追加到消息内容前connection.chatSend({ sessionKey, message, idempotencyKey: UUID })内部机制 — 流式响应处理:
Gateway 通过 chat / chat.event 事件推送响应:
content 事件到渲染进程agentAssistantFallbackText → 使用 fallback(agent.event assistant stream 缓存)fetchAndEmitHistoryFallback(runId): 从最近 5 条消息中反向查找匹配当前 runId 的 assistant 消息(无 runId 时匹配任意 assistant 消息)handleEndTurn()handleEndTurn()handleEndTurn()事件转发路径:
RemoteAgentCore → RemoteAgentManager
→ ipcBridge.conversation.responseStream.emit() → 渲染进程对话 UI
→ channelEventBus.emitAgentMessage() → Telegram/Lark 频道
→ teamEventBus.emit('responseStream') → 团队协作(仅 finish/error)
异常情况:
{ success: false, error } + emit 错误消息验收标准:
建议验证策略:集成测试 mock agent event 解析
用户故事:作为用户,我希望看到远端 Agent 使用的工具调用过程和结果。
正常流程(用户视角):
内部机制:
Gateway 通过 agent / agent.event 事件推送工具调用信息:
stream: tool / tool_call:
start/update/partialResult → in_progress, result → completed/failedtoolData.name 或 toolData.title,fallback 为 "Tool Call"inferToolKind): 基于子串匹配(非单词边界),对组合命名的工具可能误判(如 'Breadcrumb' 匹配到 'read',已知局限)
read/view/list/search/grep/glob/find/get/fetch → 'read'write/edit/create/delete/patch/update/insert/remove → 'edit'exec/run/bash/shell/terminal → 'execute'NavigationInterceptor 检测 → 提取 URL → 创建预览消息AcpAdapter.convertSessionUpdate() 转换为 TMessage → emitstream: thinking / thought:
thought 信号事件(subject: "Thinking")stream: assistant:
agentAssistantFallbackText(当 chat delta 未覆盖时的后备文本)验收标准:
建议验证策略:集成测试 mock approval request event
用户故事:作为用户,当远端 Agent 需要执行敏感操作时,我希望收到权限请求并可以选择允许或拒绝。
正常流程(用户视角):
内部机制:
exec.approval.request 事件handleApprovalRequest():
pendingPermissions Map 中创建条目acp_permission 信号事件到渲染进程allow_once / allow_always / reject_onceRemoteAgentManager.handleSignalEvent():
acp_permission 转换为 IConfirmationthis.addConfirmation(confirmation)confirmMessage({ confirmKey, callId }) 从 pendingPermissions 找到条目 → resolve已知局限(重要):当前 pendingPermissions Map 中存入的 resolve 函数为空函数 (_response) => {},confirmMessage 调用 resolve 不产生实际效果。源码中未找到 exec.approval.respond 等将审批结果实际传回 Gateway 的调用。权限审批的用户选择可能未实际传递给 Gateway。需确认是否有其他代码路径(如 BaseAgentManager.confirm)处理了实际的 Gateway 通信。
验收标准:
建议验证策略:集成测试 mock WebSocket 断连/重连场景
用户故事:作为用户,我希望系统能自动管理与远端 Gateway 的连接状态,在断连时自动重连。
正常流程(系统视角):
tick 心跳事件心跳监控 (Tick Watch):
gap = Date.now() - lastTick > tickIntervalMs * 2 → 关闭连接 (code 4000, 'tick timeout')Math.max(tickIntervalMs, 1000)scheduleReconnect 清理旧 tickTimer → 重连成功后 startTickWatch(先 clear 再 setInterval)重建 timer重连策略:
1s → 2s → 4s → 8s → 16s → 30s → 30s → ...)lastSeq(事件序列追踪)onConnectError 通知上层closed flag 互锁:
stop() 设 closed = true → 阻止所有后续重连(start() 和 scheduleReconnect() 均检查此 flag)onHelloOk 调用 conn.stop() 后,onClose 触发的 scheduleReconnect 被 closed = true 正确阻止事件序列追踪:
seq 字段seq > lastSeq + 1) → 打印 warn(不做恢复,已知局限)连接状态更新到 DB:
| 触发点 | 状态 |
|---|---|
RemoteAgentManager.initCore() 成功 | connected |
RemoteAgentManager.initCore() 失败 | error |
handshake onHelloOk | connected |
handshake onConnectError (pairing required) | pending |
handshake onConnectError (other) | error |
验收标准:
┌──────────────────────────────────────────────────────────────────┐
│ 渲染进程 (Renderer) │
│ │
│ RemoteAgentManagement.tsx │
│ ├─ useSWR → ipcBridge.remoteAgent.list.invoke() → 列表 │
│ │ │
│ └─ RemoteAgentFormModal (AionModal) │
│ ├─ ipcBridge.remoteAgent.create.invoke() → 创建 │
│ ├─ ipcBridge.remoteAgent.update.invoke() → 编辑 │
│ ├─ ipcBridge.remoteAgent.testConnection.invoke()→ 连接测试 │
│ ├─ ipcBridge.remoteAgent.handshake.invoke() → 握手 │
│ └─ [5s 轮询] handshake.invoke() → 配对等待 │
│ │
│ handleDelete (Modal.confirm) │
│ └─ ipcBridge.remoteAgent.delete.invoke() → 删除 │
│ │
│ 对话窗口 (Chat) │
│ ├─ ipcBridge.conversation.responseStream.on() ← 消息流 │
│ └─ confirm → RemoteAgentManager.confirm() → 权限确认 │
└────────────────────┬─────────────────────────────────────────────┘
│ IPC Bridge (invoke/provider)
┌────────────────────▼─────────────────────────────────────────────┐
│ 主进程 (Main) │
│ │
│ remoteAgentBridge.ts (7 个 provider) │
│ ├─ list / get / create / update / delete → SQLite CRUD │
│ ├─ testConnection → validateWebSocketUrl + WebSocket (10s) │
│ └─ handshake → OpenClawGatewayConnection (15s) │
│ │
│ RemoteAgentManager → RemoteAgentCore → OpenClawGatewayConnection │
│ ├─ 会话: sessions.resolve / sessions.reset │
│ ├─ 消息: chat.send → chat events (delta/final/error) │
│ ├─ 工具: agent events (tool/thinking/assistant) │
│ └─ 权限: exec.approval.request → confirm │
└────────────────────┬─────────────────────────────────────────────┘
│
┌────────────────────▼─────────────────────────────────────────────┐
│ 外部依赖 │
│ ├─ Remote OpenClaw Gateway (WebSocket, 协议 v3) │
│ ├─ SQLite Database (remote_agents 表) │
│ └─ AgentRegistry (检测列表同步) │
└──────────────────────────────────────────────────────────────────┘
| 场景 | 类型 | i18n Key / 文案 |
|---|---|---|
| URL 为空时测试连接 | warning | settings.remoteAgent.urlRequired |
| 连接测试成功 | success | settings.remoteAgent.testSuccess |
| 连接测试失败(result) | error | settings.remoteAgent.testFailed (参数 { error }) |
| 连接测试异常(catch) | error | settings.remoteAgent.testError (参数 { error }) |
| 创建成功 | success | settings.remoteAgent.created |
| 编辑成功 | success | settings.remoteAgent.updated |
| 握手失败但已保存 | warning | 拼接: created/updated + error |
| 配对成功 | success | settings.remoteAgent.created(固定,不区分创建/编辑) |
| 删除成功 | success | settings.remoteAgent.deleted |
| 远程连接错误 | error (tips) | 直接文案 Connection error: {msg}(非 i18n) |
| 远程 Agent 启动失败 | error | 直接文案 Failed to start remote agent: {msg}(非 i18n) |
| 远程消息发送失败 | error | 直接文案 Failed to send message: {msg}(非 i18n) |
| 超时 | 位置 | 用途 | 备注 |
|---|---|---|---|
| 5,000 ms | UI 配对轮询间隔 | handshake 轮询 | 常量 PAIRING_POLL_INTERVAL |
| 300,000 ms (5min) | UI 配对总超时 | 配对等待上限 | 常量 PAIRING_TIMEOUT |
| 10,000 ms | Bridge testConnection | WebSocket 连接测试超时 | 含 handshakeTimeout |
| 15,000 ms | Bridge handshake | 握手超时 | |
| 30,000 ms | RemoteAgentCore.waitForConnection | 等待连接建立超时 | 默认参数值 |
| 70,000 ms | RemoteAgentCore.handleApprovalRequest | 权限请求超时 | 硬编码 |
| 750 ms | OpenClawGatewayConnection.queueConnect | connect challenge 等待 | |
| 1s → 30s | OpenClawGatewayConnection 重连退避 | 指数退避 | 每次翻倍 |
| 10 次 | OpenClawGatewayConnection 最大重连 | 重连上限 | |
| 30,000 ms | OpenClawGatewayConnection tick | 默认心跳间隔 | 可由 HelloOk.policy 覆盖 |
| # | 功能点 | 局限描述 |
|---|---|---|
| 1 | F-RAGENT-02 | 协议默认硬编码 'openclaw',UI 无协议选择器。类型支持三种协议但用户无法选择 |
| 2 | F-RAGENT-02 | 认证方式仅 none/bearer 两个选项,缺少 password(Bridge/Connection 层已实现 password 路径,但 UI 不暴露) |
| 3 | F-RAGENT-02/03 | 保存时 catch 块为空(无用户提示),finally 块恢复 saving 状态 |
| 4 | F-RAGENT-05 | allowInsecure 开关仅在 URL 以 wss:// 开头时显示 |
| 5 | F-RAGENT-05 | 测试连接按钮 loading 状态待验证:源码传了 loading prop 但动态分析未观察到 spinner 效果 |
| 6 | F-RAGENT-07 | 配对轮询中 handshake 异常完全忽略 (catch {}),用户无错误反馈 |
| 7 | F-RAGENT-07 | 取消配对后无"重试配对"独立入口,需通过编辑再保存触发 |
| 8 | F-RAGENT-07 | 配对成功 toast 固定使用 settings.remoteAgent.created key(不区分创建/编辑场景) |
| 9 | F-RAGENT-08 | waitForConnection 使用 100ms 轮询检测状态(busy-wait),非事件驱动 |
| 10 | F-RAGENT-09 | sendMessage 在连接断开时自动重新 start(),可能导致意外重连和会话重置 |
| 11 | F-RAGENT-10 | 工具类型推断基于子串匹配而非单词边界,组合命名的工具可能误判(如 'Breadcrumb' 匹配 'read') |
| 12 | F-RAGENT-11 | 权限审批的用户选择可能未实际传回 Gateway:pendingPermissions 中 resolve 为空函数,未找到 exec.approval.respond 调用 |
| 13 | F-RAGENT-11 | 权限请求超时 70 秒硬编码,超时后自动 reject 无用户可见提示 |
| 14 | F-RAGENT-12 | 事件序列 gap 仅打印 warn,不做重新同步 |
| # | 约束 | 说明 |
|---|---|---|
| 1 | OpenClaw 协议版本 | 固定 v3 (OPENCLAW_PROTOCOL_VERSION = 3) |
| 2 | 设备密钥算法 | Ed25519,密钥对随 Agent 创建一次性生成 |
| 3 | 设备认证签名格式 | 管道分隔字符串,v1(无 nonce)/ v2(含 nonce)两种版本 |
| 4 | WebSocket 最大 payload | 25MB (maxPayload: 25 * 1024 * 1024) |
| 5 | 默认 Gateway 端口 | 18789 |
| 6 | 客户端标识 | gateway-client / backend / operator / operator.admin |
| 7 | Capabilities | caps: ['tool-events'] — 必须声明以接收 tool call 事件 |
| 8 | DB 存储 | SQLite remote_agents 表,字段 snake_case |
| 9 | 数据加载 | 使用 SWR(key: 'remote-agents.list'),支持自动重验证 |
| 10 | 弹窗组件 | 创建/编辑使用 AionModal 封装,删除确认使用 Arco 原生 Modal.confirm |
此功能与 Remote Agent 设置页无直接 UI 交互,降级为关联模块说明。
openclawConflictDetector.ts 检测 OpenClaw 的 Lark/Telegram channels 是否与 AionUi Channels 使用相同凭据:
channels.feishu.accounts[*].appId 与 AionUi appIdchannels.telegram.botToken 与 AionUi botToken~/.openclaw/openclaw.json → 遗留路径当前限制:冲突检测结果通过 console.warn 输出,无 UI 呈现。导出的 getConflictResolutionSteps() 提供解决方案建议文本,但尚未集成到任何 UI 组件中。