42 KiB
设置页 → 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
正常流程(用户视角):
- 用户打开「设置 → Agents」页面,点击「远端 Agents」Tab(URL query:
?tab=remote) - 页面顶部显示说明横幅:"目前只支持远程连接 OpenClaw,其他 Agent 正在开发中。" + "查看配置指南"链接
- 右上角显示"+ 添加"按钮
- 下方以响应式网格布局展示已配置的 Agent 卡片
- 小屏: 1 列 (
grid-cols-1) - 中屏: 2 列 (
md:grid-cols-2) - 大屏: 3 列 (
xl:grid-cols-3)
- 小屏: 1 列 (
- 每张卡片显示:
- 居中的 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' 时渲染标签。status 为 undefined 时同样不渲染。
颜色映射(statusColor 函数):
| status 值 | 颜色 |
|---|---|
'connected' |
green |
'pending' |
orange |
'error' |
red |
其他(含 'unknown', undefined) |
gray(但渲染层不渲染) |
空列表状态:
- 居中文案提示(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 进行对话。
正常流程(用户视角):
- 用户点击"+ 添加"按钮
- 弹出"添加远程 Agent"弹窗(AionModal + 遮罩层)
- 弹窗顶部显示黄色警告 banner(说明文案 + "查看配置指南"链接)
- 用户填写表单:
- 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
- Avatar:点击头像区域打开 Emoji 选择器(8 个分类 Tab),选中后立即应用。默认值
- 用户可选择先"测试连接"(见 F-RAGENT-05)
- 用户点击"保存"
- 表单验证通过后,创建配置并自动触发 OpenClaw 握手(见 F-RAGENT-06)
内部机制 — 创建流程:
- 渲染进程调用
ipcBridge.remoteAgent.create.invoke(payload) - 主进程 Bridge 生成:
id: UUID- OpenClaw 协议: 生成 Ed25519 密钥对(
deviceId= 公钥 SHA256 指纹,devicePublicKey,devicePrivateKey) - 其他协议: 无密钥生成
status:'unknown'createdAt/updatedAt: 当前时间戳
- 写入 SQLite
remote_agents表 - 触发
agentRegistry.refreshRemoteAgents()刷新检测列表(fire-and-forget) - 返回完整
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、认证等),修改后重新验证连接。
正常流程(用户视角):
- 用户点击卡片底部"编辑"按钮
- 弹出"编辑远程 Agent"弹窗(AionModal)
- 所有已保存数据完整回填:
- Avatar emoji
- 名称
- URL
- 认证方式(自动选中)
- Token(密文显示)
- 允许不安全连接(保持之前的状态)
- 协议从 DB 回填(
setActiveProtocol(editAgent.protocol))
- 用户修改字段后点击"保存"
- 调用
ipcBridge.remoteAgent.update.invoke({ id, updates }) - 对 OpenClaw 协议 Agent,编辑保存后同样触发握手流程
内部机制 — 更新流程:
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 |
异常情况:
- 更新 API 失败:catch 块为空(无用户提示),finally 块恢复 saving 状态(已知局限)
- Agent 已被删除:DB 返回失败,无特殊处理
验收标准:
- 弹窗标题为"编辑远程 Agent"(
settings.remoteAgent.editTitle) - 所有字段正确回填
- 修改后保存成功,列表刷新
- OpenClaw 协议 Agent 保存后触发握手流程
- 关闭弹窗方式:取消按钮 / Escape 键 / X 按钮
(F-RAGENT-04) 删除 Remote Agent [已实现]
用户故事:作为用户,我希望删除不再需要的远端 Agent 配置。
正常流程(用户视角):
- 用户点击卡片底部"删除"按钮(红色 danger 样式)
- 弹出确认对话框(Arco 原生
Modal.confirm,区别于创建/编辑使用的 AionModal 封装):- 标题:
settings.remoteAgent.deleteConfirm("删除远程 Agent") - 正文:
settings.remoteAgent.deleteConfirmContent("确定要删除「{agent名称}」吗?",使用直角引号包裹名称) - 按钮:"取消" / "确定"(确定按钮为 danger 样式)
- 标题:
- 用户点击"确定"
- 卡片立即从列表中移除
- 显示成功 toast:
settings.remoteAgent.deleted("远程 Agent 已删除")
内部机制:
- 调用
ipcBridge.remoteAgent.delete.invoke({ id }) - Bridge 端:
db.deleteRemoteAgent(id) - 成功后:
agentRegistry.refreshRemoteAgents()(fire-and-forget) - 渲染进程: SWR mutate 刷新列表
异常情况:
- 删除 API 失败:当前无特殊错误处理
验收标准:
- 删除前弹出确认对话框,包含 Agent 名称
- 确认后卡片立即从列表移除
- 删除成功显示 toast 通知(
settings.remoteAgent.deleted) - 确认对话框关闭后 UI 无残留元素(Modal.confirm 默认行为)
(F-RAGENT-05) 连接测试 [已实现]
建议验证策略:WebSocket 连接通过集成测试 mock;URL 验证和 SSRF 防护通过单元测试
用户故事:作为用户,我希望在保存前测试远端 Agent 的连接是否可用,确认 URL 和认证信息正确。
正常流程(用户视角):
- 用户在创建/编辑弹窗中填写 URL 和认证信息
- 点击"测试连接"按钮(全宽 outline 样式,带刷新图标)
- 按钮进入 loading 状态(源码
loading={testing}prop 已传入) - 测试完成后显示 toast:
- 成功:
settings.remoteAgent.testSuccess(绿色勾号 "连接成功") - 失败(result):
settings.remoteAgent.testFailed(红色, 参数{ error }) - 失败(catch):
settings.remoteAgent.testError(红色, 参数{ error })
- 成功:
内部机制:
- 从表单获取
url, authType, authToken, allowInsecure - URL 为空 → warning toast
settings.remoteAgent.urlRequired,不发起测试 - 调用
ipcBridge.remoteAgent.testConnection.invoke({...}) - 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 }- 通过
settledflag 确保只 resolve 一次
- Node.js
- URL 验证 (
异常情况:
- 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 Buttontype='outline'模式下loadingprop 的视觉表现。
验收标准:
- 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'
正常流程(用户视角):
- 用户点击"保存"后,按钮显示"握手中..."(
settings.remoteAgent.handshaking,加载态) - 系统自动与远端 Gateway 进行身份验证
- 成功:toast
settings.remoteAgent.created或settings.remoteAgent.updated,弹窗关闭 - 需要审批:切换到配对等待界面(见 F-RAGENT-07)
- 失败:warning toast("已创建/更新 — 握手失败详情"),弹窗关闭
内部机制 — OpenClaw 握手协议:
- 调用
ipcBridge.remoteAgent.handshake.invoke({ id }) - Bridge 端创建
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 格式(无 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 / maxProtocol |
3 / 4(同时兼容 v3 与 v4 Gateway,2026.5.12 起 Gateway 默认要求 v4) |
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→ 写入 DBdevice_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
正常流程(用户视角):
- 弹窗切换到等待审批界面:
- 居中 Spinner 动画(Arco Spin, size=32)
- 主文案:
settings.remoteAgent.pendingApproval("等待网关审批...") - 副文案:
settings.remoteAgent.pendingApprovalHint("请在 OpenClaw Gateway 上批准此设备") - 倒计时:
settings.remoteAgent.pendingTimeRemaining("剩余时间:M:SS",从 5:00 开始) - 底部仅显示"取消"按钮(
settings.remoteAgent.pendingCancel)
- 同时,背景中 Agent 卡片已创建,状态为
pending(橙色标签) - 系统每 5 秒轮询一次 Gateway(调用
ipcBridge.remoteAgent.handshake.invoke) - Gateway 审批通过 → 握手返回
ok→ 成功 toast(固定使用settings.remoteAgent.createdkey,不区分创建/编辑场景,已知局限)+ 弹窗关闭 - 5 分钟超时 → 显示超时文案(
settings.remoteAgent.pendingTimeout,warning 样式) - 用户点击"取消" → 停止轮询 + 弹窗关闭(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 {}),继续轮询(已知局限:无错误反馈)
取消配对行为:
- 清除轮询定时器和倒计时定时器
pairingState = 'idle'- 触发
onSaved()刷新列表 +onClose()关闭弹窗 - Agent 卡片保留在列表中,状态保持
pending - 无"重试配对"独立入口(需通过编辑再保存触发)
异常情况:
- 轮询中 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 的会话。
正常流程(系统视角):
RemoteAgentCore.start()被调用- emit 状态消息
'connecting' - 创建
OpenClawGatewayConnection并启动 - 等待连接建立(轮询
isConnected,100ms 间隔,默认参数 30 秒超时) - emit
'connected' - 解析会话 (
resolveSession):- 有
resumeKey→ 尝试sessions.resolve({ key: resumeKey }) - 失败或无 resumeKey →
sessions.reset({ key: conversationId, reason: 'new' }) - reset 失败 → 降级
sessions.resolve({ key: conversationId }) - 再失败 → 直接使用
conversationId作为 sessionKey
- 有
- emit
'session_active' - sessionKey 变更时通知上层持久化到 conversation extra
会话持久化:
RemoteAgentManager.saveSessionKey(sessionKey):- 读取 conversation(确认
type === 'remote') - 更新
conversation.extra.sessionKey - 下次打开同一对话时可通过
resumeKey恢复会话
- 读取 conversation(确认
异常情况:
- 连接超时(默认 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 发送消息后,能实时看到流式响应内容。
正常流程(用户视角):
- 用户在对话窗口输入消息并发送
- 页面显示连接状态提示(connecting → connected → session_active)
- Agent 开始流式回复:文字逐字/逐段出现
- 回复中可能穿插工具调用展示(见 F-RAGENT-10)
- 回复完成后,对话结束
内部机制 — 消息发送:
RemoteAgentManager.sendMessage(data):cronBusyGuard.setProcessing(conversationId, true)标记忙碌- 保存用户消息到 DB(非 silent 时)
- 调用
RemoteAgentCore.sendMessage() cronBusyGuard.setProcessing(conversationId, false)在 finish 信号事件或 sendMessage 异常时触发
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 使用的工具调用过程和结果。
正常流程(用户视角):
- Agent 回复过程中,工具调用以折叠卡片形式展示
- 卡片显示工具名称、状态(执行中/完成/失败)、参数/结果
内部机制:
Gateway 通过 agent / agent.event 事件推送工具调用信息:
-
stream: tool / tool_call:
- phase 映射:
start/update/partialResult→in_progress,result→completed/failed - 工具名称:
toolData.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→ emit
- phase 映射:
-
stream: thinking / thought:
- emit
thought信号事件(subject: "Thinking")
- emit
-
stream: assistant:
- 缓存
agentAssistantFallbackText(当 chat delta 未覆盖时的后备文本)
- 缓存
验收标准:
- 工具调用状态正确映射(in_progress → completed/failed)
- 工具类型(read/edit/execute)根据名称正确推断
- 导航工具触发 URL 预览
- Thinking 过程显示为 thought 信号
(F-RAGENT-11) 权限审批 [已实现]
建议验证策略:集成测试 mock approval request event
用户故事:作为用户,当远端 Agent 需要执行敏感操作时,我希望收到权限请求并可以选择允许或拒绝。
正常流程(用户视角):
- Agent 执行需要审批的操作时,对话界面弹出权限确认卡片
- 卡片显示:
- 工具名称
- 操作参数
- 三个选项:Allow / Always Allow / Reject
- 用户选择后,UI 层处理完成
- 70 秒无响应自动拒绝
内部机制:
- Gateway 发送
exec.approval.request事件 handleApprovalRequest():- 在
pendingPermissionsMap 中创建条目 - emit
acp_permission信号事件到渲染进程 - 默认选项:
allow_once / allow_always / reject_once - 超时: 70 秒自动 reject
- 在
RemoteAgentManager.handleSignalEvent():- 将
acp_permission转换为IConfirmation - 调用
this.addConfirmation(confirmation)
- 将
- 用户选择后:
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 的连接状态,在断连时自动重连。
正常流程(系统视角):
- 连接建立后,Gateway 定期发送
tick心跳事件 - 客户端监控 tick 间隔,超过 2 倍间隔未收到 → 判定断连
- 断连后自动重连(指数退避策略)
- 重连成功后恢复正常工作
心跳监控 (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触发的scheduleReconnect被closed = true正确阻止
事件序列追踪:
- 每个 event 帧可携带
seq字段 - 检测到 gap (
seq > lastSeq + 1) → 打印 warn(不做恢复,已知局限)
连接状态更新到 DB:
| 触发点 | 状态 |
|---|---|
RemoteAgentManager.initCore() 成功 |
connected |
RemoteAgentManager.initCore() 失败 |
error |
handshake onHelloOk |
connected |
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 为空时测试连接 | 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) |
附录 C:超时常量汇总
| 超时 | 位置 | 用途 | 备注 |
|---|---|---|---|
| 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 覆盖 |
附录 D:已知局限汇总
| # | 功能点 | 局限描述 |
|---|---|---|
| 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,不做重新同步 |
附录 E:设计约束
| # | 约束 | 说明 |
|---|---|---|
| 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 |
附录 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 组件中。