项目文件夹

文件
2026-07-13 13:20:22 +08:00

804 行
42 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# 设置页 → 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」TabURL query: `?tab=remote`
2. 页面顶部显示说明横幅:"目前只支持远程连接 OpenClaw,其他 Agent 正在开发中。" + "查看配置指南"链接
3. 右上角显示"+ 添加"按钮
4. 下方以响应式网格布局展示已配置的 Agent 卡片
- 小屏: 1 列 (`grid-cols-1`)
- 中屏: 2 列 (`md:grid-cols-2`)
- 大屏: 3 列 (`xl:grid-cols-3`)
5. 每张卡片显示:
- 居中的 Avatar48x48px, 方形 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 进行对话。
**正常流程**(用户视角):
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 列名 | 说明 |
| --------------- | ---------------- | ------------- |
| `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 配置。
**正常流程**(用户视角):
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.created``settings.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. 三个回调(`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` → 写入 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` → 使用 fallbackagent.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/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
- **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` 触发的 `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 标签展示)
---
## 附录 AIPC 通信链路
```
┌──────────────────────────────────────────────────────────────────┐
│ 渲染进程 (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 (检测列表同步) │
└──────────────────────────────────────────────────────────────────┘
```
---
## 附录 BToast/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 两个选项,缺少 passwordBridge/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 | 数据加载 | 使用 SWRkey: `'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 组件中。