Back to Aionui

设置页 → Agents → 远端 Agents (F-RAGENT)

docs/prds/conversations/remote/remote-agent.md

1.9.2542.3 KB
Original Source

设置页 → Agents → 远端 Agents (F-RAGENT)

本文档覆盖「设置 → Agents → 远端 Agents」页面的全部功能,包括 Remote Agent 的列表展示、创建/编辑/删除 (CRUD)、连接测试、OpenClaw 配对握手、会话管理、消息收发与流式响应、工具调用展示、权限审批、连接状态管理。 基于静态代码分析和动态 UI 验证综合整理,经 DA 质疑(23 条)和 Tester 反馈(8 条)修正定稿。


(F-RAGENT-01) Remote Agent 列表展示 [已实现]

用户故事:作为用户,我希望在远端 Agents 页面看到所有已配置的远端 Agent 列表,了解它们的名称、状态和协议类型。

前置条件:用户已进入「设置 → Agents → 远端 Agents」Tab

正常流程(用户视角):

  1. 用户打开「设置 → Agents」页面,点击「远端 Agents」Tab(URL query: ?tab=remote
  2. 页面顶部显示说明横幅:"目前只支持远程连接 OpenClaw,其他 Agent 正在开发中。" + "查看配置指南"链接
  3. 右上角显示"+ 添加"按钮
  4. 下方以响应式网格布局展示已配置的 Agent 卡片
    • 小屏: 1 列 (grid-cols-1)
    • 中屏: 2 列 (md:grid-cols-2)
    • 大屏: 3 列 (xl:grid-cols-3)
  5. 每张卡片显示:
    • 居中的 Avatar(48x48px, 方形 borderRadius=12, emoji 字号 24px;无 avatar 时显示 Robot outline 图标 18px, @icon-park/react)
    • 名称(14px 加粗,最多 2 行截断 line-clamp-2
    • 标签区域:连接状态标签(条件渲染)+ 协议标签(始终渲染, arcoblue)
    • URL(12px 次级文字,最多 2 行截断 line-clamp-2
    • 底部操作按钮:"编辑" + "删除"(红色 danger 样式)

状态标签渲染

渲染条件:agent.status 存在且不为 'unknown' 时渲染标签。statusundefined 时同样不渲染。

颜色映射(statusColor 函数):

status 值颜色
'connected'green
'pending'orange
'error'red
其他(含 'unknown', undefinedgray(但渲染层不渲染)

空列表状态

  • 居中文案提示(14px, settings.remoteAgent.emptyTitle)+ "添加"按钮(settings.remoteAgent.emptyAction
  • 无卡片渲染

"查看配置指南"链接

  • 点击通过 openExternalUrl 在系统浏览器中打开 https://github.com/iOfficeAI/AionUi/wiki/Remote-Agent-Guide-Chinese
  • 说明横幅和弹窗内的警告 banner 中均有此链接

异常情况

  • 外部链接打开失败:仅 console.error(通过 .catch(console.error)),无用户提示

验收标准

  • Tab 切换正确,URL query parameter ?tab=remote 双向同步
  • 响应式网格在三种屏幕宽度下正确布局(1/2/3 列)
  • 卡片信息完整(头像、名称、状态标签、协议标签、URL、操作按钮)
  • status'unknown'undefined 时不显示状态标签
  • 空列表显示引导文案(settings.remoteAgent.emptyTitle)和添加按钮
  • "查看配置指南"链接在系统浏览器中打开目标 URL

(F-RAGENT-02) 创建 Remote Agent [已实现]

用户故事:作为用户,我希望通过表单配置一个新的远端 Agent,填写连接信息并保存,以便后续使用该 Agent 进行对话。

正常流程(用户视角):

  1. 用户点击"+ 添加"按钮
  2. 弹出"添加远程 Agent"弹窗(AionModal + 遮罩层)
  3. 弹窗顶部显示黄色警告 banner(说明文案 + "查看配置指南"链接)
  4. 用户填写表单:
    • Avatar:点击头像区域打开 Emoji 选择器(8 个分类 Tab),选中后立即应用。默认值 🤖\u{1F916},固定值非随机)
    • 名称(必填):Input size=large,placeholder settings.remoteAgent.namePlaceholder
    • URL(必填):placeholder wss://example.com/gateway
    • 认证方式(必填):下拉选择,选项:"无" (none) / "Bearer Token" (bearer)。默认"无"。注:类型定义支持 'password' 但 UI 不暴露此选项(见设计约束)
    • 认证令牌(条件必填):仅当认证方式为 "Bearer Token" 时显示,Input.Password(有眼睛图标切换可见性),placeholder settings.remoteAgent.tokenPlaceholder
    • 允许不安全连接(条件显示):仅当 URL 以 wss:// 开头时显示,Switch 默认关闭,附说明 settings.remoteAgent.allowInsecureHint
  5. 用户可选择先"测试连接"(见 F-RAGENT-05)
  6. 用户点击"保存"
  7. 表单验证通过后,创建配置并自动触发 OpenClaw 握手(见 F-RAGENT-06)

内部机制 — 创建流程

  1. 渲染进程调用 ipcBridge.remoteAgent.create.invoke(payload)
  2. 主进程 Bridge 生成:
    • id: UUID
    • OpenClaw 协议: 生成 Ed25519 密钥对(deviceId = 公钥 SHA256 指纹, devicePublicKey, devicePrivateKey
    • 其他协议: 无密钥生成
    • status: 'unknown'
    • createdAt / updatedAt: 当前时间戳
  3. 写入 SQLite remote_agents
  4. 触发 agentRegistry.refreshRemoteAgents() 刷新检测列表(fire-and-forget)
  5. 返回完整 RemoteAgentConfig

协议说明

UI 当前不暴露协议选择器,新建 Agent 硬编码为 'openclaw' 协议。类型定义支持 'openclaw' | 'zeroclaw' | 'acp' 三种协议。编辑时从 DB 回填实际协议值。

异常情况

  • 表单验证失败:名称或 URL 为空时,字段下方显示红色错误文字(settings.remoteAgent.nameRequired / settings.remoteAgent.urlRequired),使用 role="alert" + aria-live="assertive"
  • 创建/保存 API 失败:catch 块为空(无用户提示),finally 块恢复 saving 状态(已知局限)
  • DB 写入失败:抛出异常 "Failed to create remote agent"

验收标准

  • 弹窗标题为"添加远程 Agent"(settings.remoteAgent.addTitle
  • 表单验证:名称和 URL 为必填,Bearer Token 模式下令牌为必填
  • Emoji 选择器正常工作,默认头像为 🤖(固定值)
  • 认证方式切换时,令牌字段正确显示/隐藏
  • URL 为 wss:// 时正确显示不安全连接开关
  • 保存后自动触发 OpenClaw 握手流程
  • 创建成功后列表自动刷新(SWR mutate)

(F-RAGENT-03) 编辑 Remote Agent [已实现]

用户故事:作为用户,我希望修改已配置的远端 Agent 信息(名称、URL、认证等),修改后重新验证连接。

正常流程(用户视角):

  1. 用户点击卡片底部"编辑"按钮
  2. 弹出"编辑远程 Agent"弹窗(AionModal)
  3. 所有已保存数据完整回填:
    • Avatar emoji
    • 名称
    • URL
    • 认证方式(自动选中)
    • Token(密文显示)
    • 允许不安全连接(保持之前的状态)
    • 协议从 DB 回填(setActiveProtocol(editAgent.protocol)
  4. 用户修改字段后点击"保存"
  5. 调用 ipcBridge.remoteAgent.update.invoke({ id, updates })
  6. 对 OpenClaw 协议 Agent,编辑保存后同样触发握手流程

内部机制 — 更新流程

Bridge 端逐字段映射到 DB 列名(仅更新 updates 中不为 undefined 的字段):

前端字段DB 列名说明
namename
protocolprotocol
urlurl
authTypeauth_type
authTokenauth_token
avataravatar
descriptiondescription
allowInsecureallow_insecureboolean → 0/1

异常情况

  • 更新 API 失败:catch 块为空(无用户提示),finally 块恢复 saving 状态(已知局限)
  • Agent 已被删除:DB 返回失败,无特殊处理

验收标准

  • 弹窗标题为"编辑远程 Agent"(settings.remoteAgent.editTitle
  • 所有字段正确回填
  • 修改后保存成功,列表刷新
  • OpenClaw 协议 Agent 保存后触发握手流程
  • 关闭弹窗方式:取消按钮 / Escape 键 / X 按钮

(F-RAGENT-04) 删除 Remote Agent [已实现]

用户故事:作为用户,我希望删除不再需要的远端 Agent 配置。

正常流程(用户视角):

  1. 用户点击卡片底部"删除"按钮(红色 danger 样式)
  2. 弹出确认对话框(Arco 原生 Modal.confirm,区别于创建/编辑使用的 AionModal 封装):
    • 标题:settings.remoteAgent.deleteConfirm("删除远程 Agent")
    • 正文:settings.remoteAgent.deleteConfirmContent("确定要删除「{agent名称}」吗?",使用直角引号包裹名称)
    • 按钮:"取消" / "确定"(确定按钮为 danger 样式)
  3. 用户点击"确定"
  4. 卡片立即从列表中移除
  5. 显示成功 toast:settings.remoteAgent.deleted("远程 Agent 已删除")

内部机制

  1. 调用 ipcBridge.remoteAgent.delete.invoke({ id })
  2. Bridge 端: db.deleteRemoteAgent(id)
  3. 成功后: agentRegistry.refreshRemoteAgents()(fire-and-forget)
  4. 渲染进程: SWR mutate 刷新列表

异常情况

  • 删除 API 失败:当前无特殊错误处理

验收标准

  • 删除前弹出确认对话框,包含 Agent 名称
  • 确认后卡片立即从列表移除
  • 删除成功显示 toast 通知(settings.remoteAgent.deleted
  • 确认对话框关闭后 UI 无残留元素(Modal.confirm 默认行为)

(F-RAGENT-05) 连接测试 [已实现]

建议验证策略:WebSocket 连接通过集成测试 mock;URL 验证和 SSRF 防护通过单元测试

用户故事:作为用户,我希望在保存前测试远端 Agent 的连接是否可用,确认 URL 和认证信息正确。

正常流程(用户视角):

  1. 用户在创建/编辑弹窗中填写 URL 和认证信息
  2. 点击"测试连接"按钮(全宽 outline 样式,带刷新图标)
  3. 按钮进入 loading 状态(源码 loading={testing} prop 已传入)
  4. 测试完成后显示 toast:
    • 成功:settings.remoteAgent.testSuccess(绿色勾号 "连接成功")
    • 失败(result):settings.remoteAgent.testFailed(红色, 参数 { error }
    • 失败(catch):settings.remoteAgent.testError(红色, 参数 { error }

内部机制

  1. 从表单获取 url, authType, authToken, allowInsecure
  2. URL 为空 → warning toast settings.remoteAgent.urlRequired,不发起测试
  3. 调用 ipcBridge.remoteAgent.testConnection.invoke({...})
  4. Bridge 端处理:
    • URL 验证 (validateWebSocketUrl):
      • trim 输入
      • 无协议前缀 → 自动补 ws://(支持裸 host:port 格式如 127.0.0.1:42617
      • 仅允许 ws: / wss: 协议(防 SSRF)
      • 解析失败 → 返回 { success: false, error: 'Invalid URL' }
    • WebSocket 连接:
      • Node.js ws
      • headers: bearer 认证时添加 Authorization: Bearer {token}
      • 选项: handshakeTimeout: 10_000, rejectUnauthorized: !allowInsecure
      • 超时: 10 秒
      • ws.on('open'){ success: true }
      • ws.on('error'){ success: false, error: err.message }
      • 通过 settled flag 确保只 resolve 一次

异常情况

  • URL 为空:warning toast,不发起连接
  • 非 ws/wss 协议:返回 Unsupported protocol: {protocol}
  • 连接超时(10s):返回 Connection timed out (10s)
  • WebSocket 构造失败:catch 后返回错误信息
  • IPC 调用异常:catch → error toast

待验证问题

  • 源码 RemoteAgentManagement.tsx:398 传了 loading={testing} prop,但动态分析未观察到明显 spinner 效果。需在真实窗口确认 Arco Button type='outline' 模式下 loading prop 的视觉表现。

验收标准

  • URL 为空时给出 warning 提示(settings.remoteAgent.urlRequired),不发起连接
  • 仅允许 ws/wss 协议(SSRF 防护)
  • 支持裸 host:port 格式(自动补 ws://)
  • 10 秒超时后返回明确错误
  • Bearer 认证时正确传递 Authorization header
  • allowInsecure 正确控制 TLS 证书验证
  • 成功/失败通过 toast 反馈(settings.remoteAgent.testSuccess / testFailed / testError

(F-RAGENT-06) OpenClaw 握手与设备认证 [已实现]

建议验证策略:握手协议通过集成测试 mock WebSocket;设备密钥生成通过单元测试

用户故事:作为用户,当我保存 OpenClaw 协议的远端 Agent 时,系统应自动与远端 Gateway 完成身份验证握手。

前置条件:Agent 协议为 'openclaw'

正常流程(用户视角):

  1. 用户点击"保存"后,按钮显示"握手中..."(settings.remoteAgent.handshaking,加载态)
  2. 系统自动与远端 Gateway 进行身份验证
  3. 成功:toast settings.remoteAgent.createdsettings.remoteAgent.updated,弹窗关闭
  4. 需要审批:切换到配对等待界面(见 F-RAGENT-07)
  5. 失败:warning toast("已创建/更新 — 握手失败详情"),弹窗关闭

内部机制 — OpenClaw 握手协议

  1. 调用 ipcBridge.remoteAgent.handshake.invoke({ id })
  2. Bridge 端创建 OpenClawGatewayConnection 实例
  3. 协议握手流程:
    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}    ─|   (需审批)
    
  4. 三个回调(onHelloOkonConnectErroronClose)共用同一 Promise,依赖 Promise 只能 resolve 一次的语义保证互斥

设备认证签名

使用 Ed25519 私钥对管道分隔字符串签名:

  • v1 格式(无 nonce,750ms 超时强制 connect 时): "v1|{deviceId}|{clientId}|{clientMode}|{role}|{scopes_csv}|{signedAtMs}|{token}"
  • v2 格式(有 nonce,收到 challenge 后): "v2|{deviceId}|{clientId}|{clientMode}|{role}|{scopes_csv}|{signedAtMs}|{token}|{nonce}"

版本自动判定: params.nonce ? 'v2' : 'v1'

Connect 请求参数

参数
minProtocol / maxProtocol3
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 处理

  • 存储 Gateway 签发的 deviceToken(远程场景: 回调 onDeviceTokenIssued → 写入 DB device_token 字段)
  • 更新 DB: status = 'connected', last_connected_at = Date.now()

PAIRING_REQUIRED 判定: details.recommendedNextStep === 'wait_then_retry'/pairing.required/i

异常情况

  • Gateway 不可达:WebSocket 连接失败,15 秒超时后返回错误
  • Challenge 未到达:750ms 后强制发送 connect 请求(使用 v1 签名,无 nonce)
  • 设备令牌无效:本地场景可清除 token 并降级;远程场景直接返回错误
  • handshake 超时(15s):conn.stop() + 返回 { status: 'error', error: 'Handshake timed out (15s)' }
  • WebSocket 意外关闭:返回 { status: 'error', error: 'Connection closed (code): reason' }(若已被 onHelloOk/onConnectError resolve 则忽略)

验收标准

  • 保存后按钮显示"握手中..."加载态(settings.remoteAgent.handshaking
  • 握手成功:toast + 弹窗关闭 + 列表刷新
  • 需要审批:进入配对等待界面
  • 握手失败:warning toast 包含错误详情
  • 设备令牌正确持久化到 DB
  • 连接状态正确更新(connected / pending / error)
  • 15 秒超时后正确返回错误

(F-RAGENT-07) 配对等待与轮询 [已实现]

用户故事:作为用户,当远端 Gateway 要求设备审批时,我希望看到等待界面和倒计时,并可以随时取消。

前置条件:F-RAGENT-06 握手返回 pending_approval

正常流程(用户视角):

  1. 弹窗切换到等待审批界面:
    • 居中 Spinner 动画(Arco Spin, size=32)
    • 主文案:settings.remoteAgent.pendingApproval("等待网关审批...")
    • 副文案:settings.remoteAgent.pendingApprovalHint("请在 OpenClaw Gateway 上批准此设备")
    • 倒计时:settings.remoteAgent.pendingTimeRemaining("剩余时间:M:SS",从 5:00 开始)
    • 底部仅显示"取消"按钮(settings.remoteAgent.pendingCancel
  2. 同时,背景中 Agent 卡片已创建,状态为 pending(橙色标签)
  3. 系统每 5 秒轮询一次 Gateway(调用 ipcBridge.remoteAgent.handshake.invoke
  4. Gateway 审批通过 → 握手返回 ok → 成功 toast(固定使用 settings.remoteAgent.created key,不区分创建/编辑场景,已知局限)+ 弹窗关闭
  5. 5 分钟超时 → 显示超时文案(settings.remoteAgent.pendingTimeout,warning 样式)
  6. 用户点击"取消" → 停止轮询 + 弹窗关闭(Agent 保持 pending 状态)

倒计时机制

  • 总时长: 300,000 ms (5 分钟)
  • 更新间隔: 1 秒
  • 计算方式: remaining = max(0, PAIRING_TIMEOUT - (Date.now() - startedAt))
  • 格式化: M:SS(如 4:45
  • remaining <= 0 → 停止所有定时器 + pairingState = 'timeout'

轮询机制

  • 间隔: 5,000 ms
  • 每次调用 handshake.invoke({ id })
  • status === 'ok' → 停止轮询 + success toast + 关闭弹窗
  • status === 'pending_approval' → 继续轮询
  • 异常 → 忽略(catch {}),继续轮询(已知局限:无错误反馈)

取消配对行为

  1. 清除轮询定时器和倒计时定时器
  2. pairingState = 'idle'
  3. 触发 onSaved() 刷新列表 + onClose() 关闭弹窗
  4. Agent 卡片保留在列表中,状态保持 pending
  5. 无"重试配对"独立入口(需通过编辑再保存触发)

异常情况

  • 轮询中 handshake 异常:完全忽略(catch {}),继续轮询
  • 弹窗关闭后(afterClose):停止轮询 + 重置 pairingState + 重置表单

验收标准

  • 等待界面正确显示 Spinner + 主副文案 + 倒计时
  • 倒计时从 5:00 开始,每秒递减,格式 M:SS
  • 配对期间定期轮询 Gateway(验证策略:集成测试 mock 网络请求)
  • 审批通过后自动关闭弹窗并显示成功 toast(settings.remoteAgent.created
  • 配对超时后显示超时提示(settings.remoteAgent.pendingTimeout)(验证策略:单元测试 mock 时间)
  • 取消后停止轮询,Agent 保持 pending 状态
  • 弹窗关闭时清理所有定时器(验证策略:单元测试)

(F-RAGENT-08) Remote Agent 会话创建与恢复 [已实现]

建议验证策略:集成测试;需 mock Gateway sessions API

用户故事:作为用户(或系统),当使用远端 Agent 发起对话时,系统应自动创建或恢复与 Gateway 的会话。

正常流程(系统视角):

  1. RemoteAgentCore.start() 被调用
  2. emit 状态消息 'connecting'
  3. 创建 OpenClawGatewayConnection 并启动
  4. 等待连接建立(轮询 isConnected,100ms 间隔,默认参数 30 秒超时)
  5. emit 'connected'
  6. 解析会话 (resolveSession):
    • resumeKey → 尝试 sessions.resolve({ key: resumeKey })
    • 失败或无 resumeKey → sessions.reset({ key: conversationId, reason: 'new' })
    • reset 失败 → 降级 sessions.resolve({ key: conversationId })
    • 再失败 → 直接使用 conversationId 作为 sessionKey
  7. emit 'session_active'
  8. sessionKey 变更时通知上层持久化到 conversation extra

会话持久化

  • RemoteAgentManager.saveSessionKey(sessionKey):
    • 读取 conversation(确认 type === 'remote'
    • 更新 conversation.extra.sessionKey
    • 下次打开同一对话时可通过 resumeKey 恢复会话

异常情况

  • 连接超时(默认 30s):抛出 Remote agent connection timeout
  • sessions.resolve 失败:降级到 sessions.reset
  • sessions.reset 失败:降级到 sessions.resolve
  • 所有会话 API 失败:直接使用 conversationId 作为 sessionKey(已知局限:可能导致 Gateway 侧无匹配会话)

验收标准

  • 连接建立后自动创建/恢复会话
  • 支持会话恢复(通过 sessionKey)
  • 三级 fallback 机制正确执行
  • sessionKey 正确持久化到 conversation extra
  • 连接超时(默认 30s)返回明确错误

(F-RAGENT-09) 消息发送与流式响应 [已实现]

建议验证策略:集成测试验证流式事件处理;E2E 需要真实 Gateway

用户故事:作为用户,我希望向远端 Agent 发送消息后,能实时看到流式响应内容。

正常流程(用户视角):

  1. 用户在对话窗口输入消息并发送
  2. 页面显示连接状态提示(connecting → connected → session_active)
  3. Agent 开始流式回复:文字逐字/逐段出现
  4. 回复中可能穿插工具调用展示(见 F-RAGENT-10)
  5. 回复完成后,对话结束

内部机制 — 消息发送

  1. RemoteAgentManager.sendMessage(data):
    • cronBusyGuard.setProcessing(conversationId, true) 标记忙碌
    • 保存用户消息到 DB(非 silent 时)
    • 调用 RemoteAgentCore.sendMessage()
    • cronBusyGuard.setProcessing(conversationId, false) 在 finish 信号事件或 sendMessage 异常时触发
  2. RemoteAgentCore.sendMessage():
    • 检查连接状态,断开时自动重新 start()(已知局限:可能导致意外重连和会话重置)
    • 文件附件转换:路径含空格时用 @"filepath" 格式,否则 @filepath,多文件空格连接后追加到消息内容前
    • 调用 connection.chatSend({ sessionKey, message, idempotencyKey: UUID })

内部机制 — 流式响应处理

Gateway 通过 chat / chat.event 事件推送响应:

  • delta 事件
    • 携带累积文本
    • 智能增量计算:若累积文本以已接收文本开头 → 截取增量部分
    • emit content 事件到渲染进程
  • final 事件
    • 检查是否有遗漏文本(final 中的完整文本 > 已累积文本)
    • 若无 delta 到达但有 agentAssistantFallbackText → 使用 fallback(agent.event assistant stream 缓存)
    • 若仍无内容 → fetchAndEmitHistoryFallback(runId): 从最近 5 条消息中反向查找匹配当前 runId 的 assistant 消息(无 runId 时匹配任意 assistant 消息)
    • 触发 handleEndTurn()
  • aborted 事件:触发 handleEndTurn()
  • error 事件:emit 错误消息 + handleEndTurn()

事件转发路径

RemoteAgentCore → RemoteAgentManager
  → ipcBridge.conversation.responseStream.emit()  → 渲染进程对话 UI
  → channelEventBus.emitAgentMessage()             → Telegram/Lark 频道
  → teamEventBus.emit('responseStream')            → 团队协作(仅 finish/error)

异常情况

  • 连接断开时发送消息:自动重连后发送
  • chatSend 异常:返回 { success: false, error } + emit 错误消息
  • Gateway 返回 error 事件:emit 错误消息到对话界面
  • history fallback 中所有消息 runId 不匹配:无 fallback 文本,直接 handleEndTurn

验收标准

  • 用户消息正确保存到 DB
  • 流式文本逐步渲染
  • 文件附件正确转换(空格路径带双引号)
  • final 事件后对话状态正确结束
  • error 事件显示错误消息
  • 消息正确转发到频道和团队事件总线

(F-RAGENT-10) 工具调用展示 [已实现]

建议验证策略:集成测试 mock agent event 解析

用户故事:作为用户,我希望看到远端 Agent 使用的工具调用过程和结果。

正常流程(用户视角):

  1. Agent 回复过程中,工具调用以折叠卡片形式展示
  2. 卡片显示工具名称、状态(执行中/完成/失败)、参数/结果

内部机制

Gateway 通过 agent / agent.event 事件推送工具调用信息:

  • stream: tool / tool_call:

    • phase 映射: start/update/partialResultin_progress, resultcompleted/failed
    • 工具名称: toolData.nametoolData.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 → emit
  • stream: thinking / thought:

    • emit thought 信号事件(subject: "Thinking")
  • stream: assistant:

    • 缓存 agentAssistantFallbackText(当 chat delta 未覆盖时的后备文本)

验收标准

  • 工具调用状态正确映射(in_progress → completed/failed)
  • 工具类型(read/edit/execute)根据名称正确推断
  • 导航工具触发 URL 预览
  • Thinking 过程显示为 thought 信号

(F-RAGENT-11) 权限审批 [已实现]

建议验证策略:集成测试 mock approval request event

用户故事:作为用户,当远端 Agent 需要执行敏感操作时,我希望收到权限请求并可以选择允许或拒绝。

正常流程(用户视角):

  1. Agent 执行需要审批的操作时,对话界面弹出权限确认卡片
  2. 卡片显示:
    • 工具名称
    • 操作参数
    • 三个选项:Allow / Always Allow / Reject
  3. 用户选择后,UI 层处理完成
  4. 70 秒无响应自动拒绝

内部机制

  1. Gateway 发送 exec.approval.request 事件
  2. handleApprovalRequest():
    • pendingPermissions Map 中创建条目
    • emit acp_permission 信号事件到渲染进程
    • 默认选项: allow_once / allow_always / reject_once
    • 超时: 70 秒自动 reject
  3. RemoteAgentManager.handleSignalEvent():
    • acp_permission 转换为 IConfirmation
    • 调用 this.addConfirmation(confirmation)
  4. 用户选择后:
    • confirmMessage({ confirmKey, callId })pendingPermissions 找到条目 → resolve

已知局限(重要):当前 pendingPermissions Map 中存入的 resolve 函数为空函数 (_response) => {}confirmMessage 调用 resolve 不产生实际效果。源码中未找到 exec.approval.respond 等将审批结果实际传回 Gateway 的调用。权限审批的用户选择可能未实际传递给 Gateway。需确认是否有其他代码路径(如 BaseAgentManager.confirm)处理了实际的 Gateway 通信。

验收标准

  • 权限请求正确展示工具信息和选项
  • 用户选择后 UI 层正确响应
  • 70 秒超时自动拒绝
  • 多个并发权限请求互不干扰

(F-RAGENT-12) 连接状态管理与重连 [已实现]

建议验证策略:集成测试 mock WebSocket 断连/重连场景

用户故事:作为用户,我希望系统能自动管理与远端 Gateway 的连接状态,在断连时自动重连。

正常流程(系统视角):

  1. 连接建立后,Gateway 定期发送 tick 心跳事件
  2. 客户端监控 tick 间隔,超过 2 倍间隔未收到 → 判定断连
  3. 断连后自动重连(指数退避策略)
  4. 重连成功后恢复正常工作

心跳监控 (Tick Watch)

  • 默认间隔: 30,000 ms(可由 Gateway HelloOk.policy.tickIntervalMs 覆盖)
  • 检测逻辑: gap = Date.now() - lastTick > tickIntervalMs * 2 → 关闭连接 (code 4000, 'tick timeout')
  • 检测 timer 间隔: Math.max(tickIntervalMs, 1000)
  • 生命周期: scheduleReconnect 清理旧 tickTimer → 重连成功后 startTickWatch(先 clear 再 setInterval)重建 timer

重连策略

  • 最大重连次数: 10
  • 退避策略: 初始 1s, 每次翻倍, 最大 30s (1s → 2s → 4s → 8s → 16s → 30s → 30s → ...)
  • 重连时重置 lastSeq(事件序列追踪)
  • 达到上限 → 触发 onConnectError 通知上层

closed flag 互锁

  • stop()closed = true → 阻止所有后续重连(start()scheduleReconnect() 均检查此 flag)
  • handshake 场景: onHelloOk 调用 conn.stop() 后,onClose 触发的 scheduleReconnectclosed = true 正确阻止

事件序列追踪

  • 每个 event 帧可携带 seq 字段
  • 检测到 gap (seq > lastSeq + 1) → 打印 warn(不做恢复,已知局限)

连接状态更新到 DB

触发点状态
RemoteAgentManager.initCore() 成功connected
RemoteAgentManager.initCore() 失败error
handshake onHelloOkconnected
handshake onConnectError (pairing required)pending
handshake onConnectError (other)error

验收标准

  • 心跳超时(2 倍间隔未收到 tick)触发断连
  • 自动重连使用指数退避策略(1s-30s)
  • 最大重连次数为 10
  • 重连成功后恢复正常工作(含 tick timer 重建)
  • 连接状态正确同步到 DB(用于 UI 标签展示)

附录 A:IPC 通信链路

┌──────────────────────────────────────────────────────────────────┐
│ 渲染进程 (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 (检测列表同步)                                  │
└──────────────────────────────────────────────────────────────────┘

附录 B:Toast/Alert 消息汇总

场景类型i18n Key / 文案
URL 为空时测试连接warningsettings.remoteAgent.urlRequired
连接测试成功successsettings.remoteAgent.testSuccess
连接测试失败(result)errorsettings.remoteAgent.testFailed (参数 { error })
连接测试异常(catch)errorsettings.remoteAgent.testError (参数 { error })
创建成功successsettings.remoteAgent.created
编辑成功successsettings.remoteAgent.updated
握手失败但已保存warning拼接: created/updated + error
配对成功successsettings.remoteAgent.created(固定,不区分创建/编辑)
删除成功successsettings.remoteAgent.deleted
远程连接错误error (tips)直接文案 Connection error: {msg}(非 i18n)
远程 Agent 启动失败error直接文案 Failed to start remote agent: {msg}(非 i18n)
远程消息发送失败error直接文案 Failed to send message: {msg}(非 i18n)

附录 C:超时常量汇总

超时位置用途备注
5,000 msUI 配对轮询间隔handshake 轮询常量 PAIRING_POLL_INTERVAL
300,000 ms (5min)UI 配对总超时配对等待上限常量 PAIRING_TIMEOUT
10,000 msBridge testConnectionWebSocket 连接测试超时含 handshakeTimeout
15,000 msBridge handshake握手超时
30,000 msRemoteAgentCore.waitForConnection等待连接建立超时默认参数值
70,000 msRemoteAgentCore.handleApprovalRequest权限请求超时硬编码
750 msOpenClawGatewayConnection.queueConnectconnect challenge 等待
1s → 30sOpenClawGatewayConnection 重连退避指数退避每次翻倍
10 次OpenClawGatewayConnection 最大重连重连上限
30,000 msOpenClawGatewayConnection tick默认心跳间隔可由 HelloOk.policy 覆盖

附录 D:已知局限汇总

#功能点局限描述
1F-RAGENT-02协议默认硬编码 'openclaw',UI 无协议选择器。类型支持三种协议但用户无法选择
2F-RAGENT-02认证方式仅 none/bearer 两个选项,缺少 password(Bridge/Connection 层已实现 password 路径,但 UI 不暴露)
3F-RAGENT-02/03保存时 catch 块为空(无用户提示),finally 块恢复 saving 状态
4F-RAGENT-05allowInsecure 开关仅在 URL 以 wss:// 开头时显示
5F-RAGENT-05测试连接按钮 loading 状态待验证:源码传了 loading prop 但动态分析未观察到 spinner 效果
6F-RAGENT-07配对轮询中 handshake 异常完全忽略 (catch {}),用户无错误反馈
7F-RAGENT-07取消配对后无"重试配对"独立入口,需通过编辑再保存触发
8F-RAGENT-07配对成功 toast 固定使用 settings.remoteAgent.created key(不区分创建/编辑场景)
9F-RAGENT-08waitForConnection 使用 100ms 轮询检测状态(busy-wait),非事件驱动
10F-RAGENT-09sendMessage 在连接断开时自动重新 start(),可能导致意外重连和会话重置
11F-RAGENT-10工具类型推断基于子串匹配而非单词边界,组合命名的工具可能误判(如 'Breadcrumb' 匹配 'read')
12F-RAGENT-11权限审批的用户选择可能未实际传回 GatewaypendingPermissions 中 resolve 为空函数,未找到 exec.approval.respond 调用
13F-RAGENT-11权限请求超时 70 秒硬编码,超时后自动 reject 无用户可见提示
14F-RAGENT-12事件序列 gap 仅打印 warn,不做重新同步

附录 E:设计约束

#约束说明
1OpenClaw 协议版本固定 v3 (OPENCLAW_PROTOCOL_VERSION = 3)
2设备密钥算法Ed25519,密钥对随 Agent 创建一次性生成
3设备认证签名格式管道分隔字符串,v1(无 nonce)/ v2(含 nonce)两种版本
4WebSocket 最大 payload25MB (maxPayload: 25 * 1024 * 1024)
5默认 Gateway 端口18789
6客户端标识gateway-client / backend / operator / operator.admin
7Capabilitiescaps: ['tool-events'] — 必须声明以接收 tool call 事件
8DB 存储SQLite remote_agents 表,字段 snake_case
9数据加载使用 SWR(key: 'remote-agents.list'),支持自动重验证
10弹窗组件创建/编辑使用 AionModal 封装,删除确认使用 Arco 原生 Modal.confirm

附录 F:关联模块 — OpenClaw 凭据冲突检测

此功能与 Remote Agent 设置页无直接 UI 交互,降级为关联模块说明。

openclawConflictDetector.ts 检测 OpenClaw 的 Lark/Telegram channels 是否与 AionUi Channels 使用相同凭据:

  • Lark 冲突: 比较 channels.feishu.accounts[*].appId 与 AionUi appId
  • Telegram 冲突: 比较 channels.telegram.botToken 与 AionUi botToken
  • 配置读取路径: 环境变量 → ~/.openclaw/openclaw.json → 遗留路径

当前限制:冲突检测结果通过 console.warn 输出,无 UI 呈现。导出的 getConflictResolutionSteps() 提供解决方案建议文本,但尚未集成到任何 UI 组件中。