删除陈旧文档
This commit is contained in:
parent
954bfd1d75
commit
59ecb27c06
@ -1,369 +0,0 @@
|
||||
# PicoBot 代码质量分析报告
|
||||
|
||||
审查日期:2026-06-15
|
||||
|
||||
## 结论摘要
|
||||
|
||||
PicoBot 的总体架构方向是清晰的:Gateway 负责装配,Channel 只做收发,MessageBus 解耦输入输出,SessionManager 管理会话,AgentLoop 保持无状态并执行工具,Storage 统一持久化。这条主线是成立的,也已经具备较完整的 AI 助手运行时能力。
|
||||
|
||||
当前主要质量风险集中在三类:
|
||||
|
||||
1. 会话/CLI 路由语义不一致,导致多客户端隔离、加载会话、当前会话追踪不可靠。
|
||||
2. 若干公开控制接口是空实现或弱实现,协议层暴露的能力和后端实际行为不匹配。
|
||||
3. 工具和后台任务的资源边界偏弱,文件、shell、HTTP、长期任务在异常情况下容易突破预期的安全或稳定性边界。
|
||||
|
||||
如果只安排一轮修复,优先处理会话路由和控制接口。这些问题会直接影响用户看到的行为;工具安全和大模块拆分可以作为第二阶段。
|
||||
|
||||
## 修复状态
|
||||
|
||||
- 已修复:CLI 会话路由现在按每个 WebSocket client 的稳定 `chat_id` 隔离,普通输入、创建、列表、加载和 outbound 投递不再混用完整 `session_id` 与 `chat_id`。
|
||||
- 已修复:Dialog 控制接口已补齐当前会话查询、列表 current 标记、归档、清空历史和 `/delete` 删除当前会话后新建的行为;`include_archived` 现在由 Storage 查询生效。
|
||||
- 已修复:Session 主处理路径不再在持有 session mutex 时执行 memory recall、上下文压缩、标题 LLM 生成、消息持久化、`/stop` sub-agent 取消或清历史存储操作;慢操作改为锁外执行并用 `state_version`/`worker_generation` 防止陈旧结果覆盖当前会话。
|
||||
- 已修复:Bash 超时清理、文件读取大文件限制、HTTP DNS 私网校验、Bus 关闭退出、Cron `from` 语义和 PTY 工具接入等中等级问题已完成清扫。
|
||||
- 待处理:工具文件边界仍是后续质量风险。
|
||||
|
||||
## 主要发现
|
||||
|
||||
### 已修复:CLI 会话路由会破坏会话连续性和多客户端隔离
|
||||
|
||||
位置:
|
||||
|
||||
- `src/channels/cli_chat.rs:113-126`
|
||||
- `src/channels/cli_chat.rs:160-164`
|
||||
- `src/channels/cli_chat.rs:225-249`
|
||||
- `src/channels/cli_chat.rs:479-494`
|
||||
- `src/session/session.rs:1305-1310`
|
||||
|
||||
问题:
|
||||
|
||||
`Client.current_session_id` 存的是完整 session id,但 CLI channel 在多个地方把它当作 `chat_id` 使用。普通用户输入如果没有显式传 `chat_id`,会在 `src/channels/cli_chat.rs:119` 生成新的短 ID,而不是复用当前 client 的 chat scope。`CreateSession` 又把当前完整 session id 当成新会话的 chat_id。`LoadSession` 解析了传入 session id,但随后调用 `GetCurrentDialog`,而后端 `get_current_dialog()` 固定返回 `None`。
|
||||
|
||||
同时,`send()` 会把所有 `OutboundMessage` 广播给所有 CLI WebSocket client,没有按 `msg.chat_id` 或 client 当前会话过滤。这意味着一个客户端的回复可能出现在另一个客户端里。
|
||||
|
||||
影响:
|
||||
|
||||
- CLI 多轮对话可能落入不同 chat scope。
|
||||
- 创建/列出/加载会话得到的结果可能不符合 UI 预期。
|
||||
- 多个 CLI 客户端同时连接时存在串话。
|
||||
|
||||
建议:
|
||||
|
||||
- 将 client 状态拆成 `chat_id` 和 `current_session_id`,不要混用。
|
||||
- 注册 client 时生成稳定 `chat_id`,后续 `UserInput` 默认复用它。
|
||||
- `send()` 按 `OutboundMessage.chat_id` 精确投递;必要时维护 `chat_id -> clients` 映射。
|
||||
- `LoadSession` 应直接切换到指定 session,或通过 `SwitchDialog` 使用其中的 `dialog_id`。
|
||||
- 为 CLI WebSocket 增加多客户端路由测试。
|
||||
|
||||
### 已修复:Dialog 控制接口与协议承诺不一致
|
||||
|
||||
位置:
|
||||
|
||||
- `src/session/session.rs:996-997`
|
||||
- `src/session/session.rs:1305-1310`
|
||||
- `src/session/session.rs:1329-1349`
|
||||
- `src/session/session.rs:1378-1384`
|
||||
- `src/channels/cli_chat.rs:128-158`
|
||||
|
||||
问题:
|
||||
|
||||
后端暴露了 create/list/load/rename/archive/delete/clear 等 dialog 操作,但部分行为是空实现或语义错位:
|
||||
|
||||
- `/delete` 只创建新 session,并没有删除当前 session。
|
||||
- `get_current_dialog()` 固定返回 `Ok(None)`。
|
||||
- `list_dialogs()` 忽略 `include_archived`,且总是返回 `current_dialog_id = None`。
|
||||
- `archive_dialog()` 是空操作。
|
||||
- `clear_dialog_history()` 直接返回不可用,但 WebSocket 协议仍暴露 `clear_history`。
|
||||
|
||||
影响:
|
||||
|
||||
用户通过 slash command 和 WebSocket 调用同一类能力时,会得到不一致结果。前端难以基于协议实现可靠状态同步。
|
||||
|
||||
建议:
|
||||
|
||||
- 明确“archive/clear 是否支持”。不支持就从协议和命令列表移除;支持就实现到底。
|
||||
- `/delete` 应调用 `delete_dialog(current_session_id)`,再创建一个新的 current session。
|
||||
- `get_current_dialog()` 应读取 `current_sessions[channel:chat_id]` 并解析为 `UnifiedSessionId`。
|
||||
- `list_dialogs()` 返回真实 current dialog,并补上 archived 模型或移除 archived 参数。
|
||||
|
||||
### 高优先级:工具文件边界不符合“工作目录内工具”的架构约束
|
||||
|
||||
位置:
|
||||
|
||||
- `src/tools/mod.rs:56-62`
|
||||
- `src/tools/path_utils.rs:3-23`
|
||||
- `src/tools/bash.rs:146-185`
|
||||
|
||||
问题:
|
||||
|
||||
文件工具默认通过 `FileReadTool::new()`、`FileWriteTool::new()` 等注册,没有传入 workspace allowlist。`resolve_path()` 对绝对路径直接放行;即使传入 allowlist,也只是做 `Path::starts_with()` 的词法判断,没有 canonicalize,不能防御 `..`、符号链接等路径逃逸。
|
||||
|
||||
`bash` 默认工作目录是 `"."`,Gateway 启动时切到 workspace,这对相对路径有效,但 shell 命令仍然可以访问绝对路径。当前 denylist 只挡少数危险模式,不构成权限边界。
|
||||
|
||||
影响:
|
||||
|
||||
Agent 工具实际可以读写 workspace 外文件,和文档/架构里的“工作目录内操作”不一致。对于个人助手这可能是有意设计,但如果未来接入外部渠道、多用户或 MCP,风险会放大。
|
||||
|
||||
建议:
|
||||
|
||||
- 工具注册时传入 `workspace_dir`,默认所有文件工具限制在 workspace。
|
||||
- `resolve_path()` 使用 `std::fs::canonicalize` 或 `path_absolutize` 风格逻辑,并处理目标文件不存在时的父目录 canonicalize。
|
||||
- 写工具禁止跟随危险符号链接,或至少在文档中明确该能力是全文件系统权限。
|
||||
- shell 工具如果保留,应在配置中显式开关,并区分本地可信模式和渠道暴露模式。
|
||||
|
||||
### 已修复:Session 锁内执行过多异步操作
|
||||
|
||||
位置:
|
||||
|
||||
- `src/session/session.rs:1001-1018`
|
||||
- `src/session/session.rs:1604-1711`
|
||||
|
||||
问题:
|
||||
|
||||
`/compact` 在持有 session mutex 时执行压缩和持久化。agent worker 的 Phase 1 也在持有 session mutex 时执行用户消息落库、memory recall、上下文压缩、session meta 持久化和 agent 创建。其中 `compress_if_needed()` 可能触发 LLM 摘要,属于慢操作。
|
||||
|
||||
影响:
|
||||
|
||||
- 同一 session 的 slash command、stop、消息排队、状态查询会被慢操作阻塞。
|
||||
- 当压缩或存储出现抖动时,用户感觉像“卡死”。
|
||||
- 后续如果在这些慢操作里间接需要 session 状态,容易形成锁顺序问题。
|
||||
|
||||
已采取修复:
|
||||
|
||||
- 为 `Session` 增加 `state_version`,慢操作提交前检查会话是否已被 `/stop`、清历史或其它内存变更替换。
|
||||
- `/compact` 改为锁内取 history 快照,锁外压缩,锁内提交压缩结果,锁外持久化 meta。
|
||||
- agent worker Phase 1 改为锁内只创建用户消息、agent、cancel handle 和 history 快照;memory recall 与 context compression 都在锁外执行。
|
||||
- context overflow retry 的二次压缩移到锁外。
|
||||
- 标题生成改为锁内取 prompt/provider 快照,锁外调用 LLM,锁内应用标题,锁外持久化。
|
||||
- `add_message` 拆出内存更新和持久化快照,主消息路径在释放 session 锁后写入 SQLite。
|
||||
- `/stop` 和清历史不再持有 session 锁等待 sub-agent 取消或 Storage 操作。
|
||||
|
||||
### 已修复:Bash 超时不会显式终止子进程
|
||||
|
||||
位置:
|
||||
|
||||
- `src/tools/bash.rs:150-174`
|
||||
- `src/tools/bash.rs:180-207`
|
||||
|
||||
问题:
|
||||
|
||||
`timeout()` 包裹的是 `run_command()` future。超时后 future 被取消,但代码没有持有 child 句柄并显式 `kill()` / `wait()`。对于已经启动的长运行命令或子进程树,可能留下后台进程。
|
||||
|
||||
影响:
|
||||
|
||||
长任务、服务进程或卡住的 shell 命令会泄漏进程和资源,后续工具调用的行为也会变得不可预测。
|
||||
|
||||
已采取修复:
|
||||
|
||||
- Bash 一次性命令改用 `wait_with_output()`,避免 stdout/stderr 顺序读取造成 pipe 阻塞。
|
||||
- 子进程启用 `kill_on_drop(true)`,超时后丢弃等待 future 时会清理 child。
|
||||
- 新增大 stderr 输出测试,覆盖不会因为 stderr pipe 填满而卡住。
|
||||
- 持久/交互式进程通过已接入的 PTY 工具承载。
|
||||
|
||||
### 已修复:文件读取对大二进制文件没有输出上限
|
||||
|
||||
位置:
|
||||
|
||||
- `src/tools/file_read.rs:121-131`
|
||||
- `src/tools/file_read.rs:214-229`
|
||||
|
||||
问题:
|
||||
|
||||
`file_read` 先 `std::fs::read()` 读取整个文件。文本路径有 `MAX_CHARS` 截断,但二进制路径会完整 base64 编码后返回,没有大小限制。
|
||||
|
||||
影响:
|
||||
|
||||
读取大文件会造成内存膨胀、响应膨胀、上下文污染,甚至拖垮进程。
|
||||
|
||||
已采取修复:
|
||||
|
||||
- `file_read` 在读取前检查 metadata size,超过安全阈值直接拒绝。
|
||||
- 二进制 inline base64 增加单独大小上限,超限只返回错误和文件信息。
|
||||
- 含 NUL 字节内容按二进制处理,避免全 0 文件被 UTF-8 路径误判为文本。
|
||||
- 增加大文件和大二进制文件测试。
|
||||
|
||||
### 已修复:HTTP 私网防护只检查字面 host,未做 DNS 解析校验
|
||||
|
||||
位置:
|
||||
|
||||
- `src/tools/http_request.rs:31-59`
|
||||
|
||||
问题:
|
||||
|
||||
`http_request` 阻止 localhost、私网 IP 字面量和 `.local`,但普通域名不会解析后检查最终 IP。DNS rebinding 或内网域名解析到私网地址时,当前校验拦不住。
|
||||
|
||||
影响:
|
||||
|
||||
如果该工具暴露给非完全可信输入,存在 SSRF 风险。
|
||||
|
||||
已采取修复:
|
||||
|
||||
- `http_request` 和 `web_fetch` 在发送请求前通过 DNS 解析 host,并拒绝解析到 loopback、private、link-local、multicast、unspecified 的地址。
|
||||
- IPv6 unique-local 和 link-local 地址也纳入私网判定。
|
||||
- 禁用 reqwest 自动重定向,避免跳转到未校验的内网地址。
|
||||
- 增加端口解析和 IPv6 私网判断测试。
|
||||
|
||||
### 已修复:后台任务和主循环缺少监督与优雅关闭
|
||||
|
||||
位置:
|
||||
|
||||
- `src/bus/mod.rs:51-99`
|
||||
- `src/gateway/mod.rs:187-244`
|
||||
- `src/gateway/mod.rs:247-266`
|
||||
|
||||
问题:
|
||||
|
||||
Gateway 中多个长期任务通过 `tokio::spawn` 启动后没有保存 JoinHandle,也没有统一 cancellation token。MessageBus 的 `consume_*()` 在 channel 关闭时使用 `expect()` panic。
|
||||
|
||||
影响:
|
||||
|
||||
- 某个后台 loop 异常退出后,Gateway 不一定能发现。
|
||||
- 关闭流程只能 stop channel,无法系统性停止 scheduler、dispatcher、agent workers、notification publishers。
|
||||
- bus channel 关闭时更像崩溃,而不是可恢复状态。
|
||||
|
||||
已采取修复:
|
||||
|
||||
- `MessageBus::consume_inbound/consume_outbound/consume_control` 不再在 channel 关闭时 `expect()` panic,改为返回 `Option<T>`。
|
||||
- Gateway message processor 在 inbound/control bus 关闭时记录 warning 并退出 loop。
|
||||
- OutboundDispatcher 在 outbound bus 关闭时记录 warning 并退出 loop。
|
||||
- 这不是完整 runtime supervisor,但已消除 bus 关闭导致的 panic 崩溃路径,为后续集中 JoinHandle 管理留出接口。
|
||||
|
||||
### 已修复:Cron 计算函数没有按入参 `from` 计算 cron 下一次时间
|
||||
|
||||
位置:
|
||||
|
||||
- `src/scheduler/mod.rs:18-40`
|
||||
|
||||
问题:
|
||||
|
||||
`next_run_for_schedule(schedule, from)` 的注释说基于 `from` 计算,但 cron 分支创建了 `from_dt` 后没有传给 `cron_schedule`,实际使用的是 `upcoming(Utc)` 或 `upcoming(tz)` 的当前时间。
|
||||
|
||||
影响:
|
||||
|
||||
单元测试或补偿调度传入历史/未来时间时,结果不符合函数契约。线上 reschedule 当前使用 now,影响较小,但函数语义是错的。
|
||||
|
||||
已采取修复:
|
||||
|
||||
- cron 分支改用 `cron_schedule.after(&from_dt).next()`。
|
||||
- timezone 分支用 `from_dt.with_timezone(&tz)` 作为计算起点。
|
||||
- 增加 UTC 和 Asia/Shanghai 固定时间输入测试。
|
||||
|
||||
### 已修复:存在未接入或半接入代码,增加维护噪音
|
||||
|
||||
位置:
|
||||
|
||||
- `src/tools/pty.rs`
|
||||
- `src/tools/mod.rs:1-20`
|
||||
- `src/tools/mod.rs:49-88`
|
||||
|
||||
问题:
|
||||
|
||||
仓库里有完整 `pty.rs`,但 `tools/mod.rs` 没有声明 `pub mod pty`,`create_default_tools()` 也没有注册 PTY 工具。类似情况会让文档、计划和实现状态难以判断。
|
||||
|
||||
影响:
|
||||
|
||||
维护者会误以为功能已上线。未来改动容易遗漏测试和注册路径。
|
||||
|
||||
已采取修复:
|
||||
|
||||
- `src/tools/pty.rs` 已接入 `tools/mod.rs`,导出 `PtyManager`/`PtyTool`。
|
||||
- `create_default_tools()` 默认注册共享 `PtyManager` 的 `PtyTool`。
|
||||
- 修复 PTY 原本因未编译暴露不出的借用问题。
|
||||
|
||||
## 架构评价
|
||||
|
||||
### 做得好的地方
|
||||
|
||||
- 模块分层方向清楚:Channel、Bus、Session、Agent、Provider、Tool、Storage 边界基本可理解。
|
||||
- AgentLoop 设计为无状态,历史由 SessionManager 管理,这一点利于恢复、压缩和测试。
|
||||
- Provider 抽象简单直接,OpenAI-compatible 与 Anthropic 的差异被限制在 provider 层。
|
||||
- Storage 集中初始化 schema,便于部署单二进制应用。
|
||||
- Skill、memory、MCP、delegate 这几条扩展线已经形成统一的 ToolRegistry 接入点。
|
||||
|
||||
### 主要架构债务
|
||||
|
||||
- SessionManager 承担过多职责:会话生命周期、命令解析、memory recall、压缩、agent worker、任务取消、send_message 目标解析都在一个 2000 行文件内。
|
||||
- Channel 和 Session 对 chat_id/session_id/dialog_id 的边界没有类型保护,导致 CLI 层混用字符串。
|
||||
- Tool 权限模型不够显式:工具是否能访问全文件系统、是否能联网、是否能修改状态主要靠工具自身约定。
|
||||
- 后台任务生命周期分散:gateway loop、agent worker、notification publisher、scheduler、sub-agent task 各自 spawn,缺少统一管理。
|
||||
|
||||
## 模块级分析
|
||||
|
||||
### gateway
|
||||
|
||||
`GatewayState::new()` 是清晰的装配中心:配置、workspace、storage、memory、bus、session manager、channels、MCP、scheduler 都在这里接线。问题是启动后任务监督不足,且 scheduler 默认 `unwrap_or_default()` 会在省略 `gateway.scheduler` 时启用调度器,这和“省略配置是否代表开启”需要产品层确认。
|
||||
|
||||
### channels
|
||||
|
||||
Feishu channel 功能较厚,单文件接近 2000 行,建议后续按 API client、message parsing、media handling、outbound rendering 拆分。CLI channel 目前是质量风险最高的 channel,核心问题是会话身份混用和广播投递。
|
||||
|
||||
### bus
|
||||
|
||||
MessageBus 简洁,但当前消费者 API 通过 mutex 包住 receiver 并 `expect()`,更像“单消费者内部队列”。这没问题,但应该把“只能有一个 consumer”写进类型/文档,并把关闭作为正常状态处理。
|
||||
|
||||
### session
|
||||
|
||||
这是系统核心,也是债务最集中的模块。建议把 `session.rs` 拆成:
|
||||
|
||||
- `manager.rs`:SessionManager 状态和 dialog 生命周期
|
||||
- `worker.rs`:per-session agent worker 和 cancellation
|
||||
- `commands.rs`:slash command 执行
|
||||
- `outbound.rs`:OutboundMessenger 实现
|
||||
- `restore.rs`:storage 恢复与 tool call chain repair
|
||||
|
||||
拆分之前,先补行为测试,尤其是 CLI/WS session lifecycle。
|
||||
|
||||
### agent
|
||||
|
||||
AgentLoop 的职责相对聚焦:请求模型、执行工具、回填 tool result、循环直到 final response。需要关注的是工具并发的语义:`read_only()` 目前是工具自己声明,副作用工具不能错标。LoopDetector 有帮助,但属于 runtime guard,不应替代工具层的资源限制。
|
||||
|
||||
### providers
|
||||
|
||||
Provider 层整体可维护。OpenAI/Anthropic 的请求构造逻辑可以继续保留在 provider 内。建议补充请求脱敏策略:当前 debug log 和 `llm_calls` 会持久化完整 request/response,可能包含用户隐私、API 返回内容和文件内容。
|
||||
|
||||
### tools
|
||||
|
||||
工具体系覆盖面很强,但需要明确权限模型。建议新增统一的 `ToolExecutionContext`,包含 workspace、channel、session_id、权限策略、网络策略、输出预算。现在很多策略散落在各工具构造函数里,默认值容易失控。
|
||||
|
||||
### storage
|
||||
|
||||
Storage schema 初始化实用,但迁移方式是“CREATE IF NOT EXISTS + ALTER IGNORE”,适合早期迭代,不适合长期演进。建议引入 schema version 表或 sqlx migrations,至少把每次迁移记录下来。
|
||||
|
||||
### skills
|
||||
|
||||
Skill 加载优先级清晰,内置 skill 打包也实用。需要注意 `SkillsLoader` 使用同步文件系统扫描和 `std::sync::Mutex`,在请求路径频繁 `reload_if_changed()` 时可能造成阻塞。短期可以接受,长期建议缓存刷新放到后台 watcher。
|
||||
|
||||
## 建议修复路线
|
||||
|
||||
### P0:先修会话正确性
|
||||
|
||||
1. 修正 CLI `chat_id/current_session_id` 数据模型。
|
||||
2. 修正 CLI 出站按 client/chat_id 投递。
|
||||
3. 实现 `get_current_dialog()`、`list_dialogs()` current 返回。
|
||||
4. 修正 `/delete`、`clear_history`、`archive` 的真实行为或从协议移除。
|
||||
5. 增加 WebSocket session lifecycle 测试。
|
||||
|
||||
### P1:收紧工具和资源边界
|
||||
|
||||
1. 文件工具默认限制 workspace,路径 canonicalize。
|
||||
2. bash 超时杀进程,必要时引入进程组。
|
||||
3. file_read 增加文件大小上限和二进制输出上限。
|
||||
4. HTTP/web 工具增加 DNS 解析后的私网校验和重定向校验。
|
||||
5. 明确高危工具的配置开关。
|
||||
|
||||
### P2:降低架构复杂度
|
||||
|
||||
1. 拆分 `session.rs`、`feishu.rs`、`storage/mod.rs`、`browser.rs`。
|
||||
2. 引入任务 supervisor 和统一 shutdown token。
|
||||
3. 引入正式数据库迁移。
|
||||
4. 增加工具注册快照测试,避免死代码和文档漂移。
|
||||
|
||||
## 建议测试补充
|
||||
|
||||
- CLI 多客户端并发:两个 WebSocket client 同时发消息,互不串话。
|
||||
- CLI 不传 chat_id 的连续对话:所有消息应进入同一 session。
|
||||
- Load/switch/list/delete/clear 的完整 WebSocket 流程。
|
||||
- `/delete` 后旧 session 软删除、新 session 成为 current。
|
||||
- 文件路径逃逸:`../`、绝对路径、符号链接、workspace 前缀欺骗。
|
||||
- bash timeout 后检查子进程不存在。
|
||||
- cron `next_run_for_schedule()` 使用固定 `from` 的 deterministic 测试。
|
||||
- HTTP 工具对 DNS 解析到 `127.0.0.1` / `10.0.0.0/8` 的域名拒绝测试。
|
||||
@ -1,496 +0,0 @@
|
||||
# PicoBot 跨渠道交互式消息规划
|
||||
|
||||
规划日期:2026-06-16
|
||||
|
||||
## 背景
|
||||
|
||||
飞书交互式卡片可以让用户直接在消息卡片上点击按钮、提交选择或触发回调。这个能力很适合用于工具调用审批、快捷回复、任务确认、表单收集等 agent 交互。
|
||||
|
||||
参考项目调研结果:
|
||||
|
||||
- `reference/zeroclaw` 已实现飞书/Lark 工具审批卡片:发送 Card JSON 2.0 按钮卡片,收到 `card.action.trigger` 后解析 `approval_id` 和 `decision`,唤醒等待中的 approval future,并 PATCH 原卡片为已处理状态。
|
||||
- `reference/nanobot` 主要使用飞书 CardKit 做 agent 输出展示和流式更新,适合参考消息渲染体验,但没有完整的按钮回调驱动 agent 流程。
|
||||
- `reference/openlark` 是 SDK/API 封装,支持发送 interactive card 和 CardKit API,不包含完整 agent channel 编排。
|
||||
|
||||
PicoBot 当前飞书渠道已经会把普通 markdown 回复发送成 interactive card,但还缺少“用户在卡片上操作 -> 统一交互事件 -> Session/Agent/Tool 流程继续”的抽象。
|
||||
|
||||
## 目标
|
||||
|
||||
1. 支持飞书交互式卡片按钮回调。
|
||||
2. 设计成跨渠道能力,后续 Slack、Telegram、Discord、CLI chat 等渠道可以复用同一套交互语义。
|
||||
3. 支持渠道降级:不支持按钮的渠道也能用纯文本命令完成同样操作。
|
||||
4. 保持 PicoBot 现有边界:Channel 只做收发和渠道适配,SessionManager 管会话,AgentLoop 执行 LLM 和工具。
|
||||
5. 为工具调用审批、快捷回复和未来表单交互预留扩展点。
|
||||
|
||||
非目标:
|
||||
|
||||
- 本阶段不立即实现完整功能。
|
||||
- 不把飞书卡片细节泄漏到 AgentLoop 或工具层。
|
||||
- 不要求所有渠道同时支持原生交互组件。
|
||||
|
||||
## 核心原则
|
||||
|
||||
交互语义和渠道渲染分离。
|
||||
|
||||
Agent、工具或 Session 层只表达“我要一个 approval/quick reply/form interaction”。具体是飞书卡片按钮、Slack Block Kit、Telegram inline keyboard,还是 CLI 里显示编号选项,由 Channel 根据能力渲染。
|
||||
|
||||
回调也要统一。
|
||||
|
||||
飞书的 `card.action.trigger`、Telegram 的 `callback_query`、Slack 的 interaction payload 都应归一化成 PicoBot 内部的 `InteractionEvent`,再交给统一的处理器。
|
||||
|
||||
## 数据模型
|
||||
|
||||
建议新增一个 `interaction` 模块,定义渠道无关的数据结构。
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum InteractionKind {
|
||||
QuickReply,
|
||||
Approval,
|
||||
FormSubmit,
|
||||
Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum InteractionStyle {
|
||||
Default,
|
||||
Primary,
|
||||
Danger,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct InteractionAction {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub style: InteractionStyle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct InteractionPayload {
|
||||
pub interaction_id: String,
|
||||
pub kind: InteractionKind,
|
||||
pub title: Option<String>,
|
||||
pub body: String,
|
||||
pub actions: Vec<InteractionAction>,
|
||||
pub expires_at: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InteractionEvent {
|
||||
pub channel: String,
|
||||
pub chat_id: String,
|
||||
pub sender_id: String,
|
||||
pub interaction_id: String,
|
||||
pub action_id: String,
|
||||
pub action_value: String,
|
||||
pub timestamp: i64,
|
||||
pub metadata: std::collections::HashMap<String, String>,
|
||||
}
|
||||
```
|
||||
|
||||
`InteractionPayload` 用于 outbound 渲染,`InteractionEvent` 用于 inbound 回调。
|
||||
|
||||
## OutboundMessage 扩展
|
||||
|
||||
短期兼容方案:
|
||||
|
||||
- 继续使用 `OutboundMessage.metadata` 携带交互描述。
|
||||
- 例如:
|
||||
- `interaction.kind = "approval"`
|
||||
- `interaction.id = "<uuid>"`
|
||||
- `interaction.actions = "<json>"`
|
||||
|
||||
长期推荐方案:
|
||||
|
||||
```rust
|
||||
pub struct OutboundMessage {
|
||||
pub channel: String,
|
||||
pub chat_id: String,
|
||||
pub content: String,
|
||||
pub reply_to: Option<String>,
|
||||
pub media: Vec<MediaItem>,
|
||||
pub metadata: HashMap<String, String>,
|
||||
pub interaction: Option<InteractionPayload>,
|
||||
}
|
||||
```
|
||||
|
||||
推荐长期方案。它能避免把结构化交互塞进字符串 metadata,也让每个 Channel 的 `send()` 更清晰。
|
||||
|
||||
## Channel 能力声明
|
||||
|
||||
给 `Channel` 增加可选能力声明:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ChannelCapabilities {
|
||||
pub interactive_buttons: bool,
|
||||
pub forms: bool,
|
||||
pub message_update: bool,
|
||||
pub markdown_cards: bool,
|
||||
}
|
||||
|
||||
pub trait Channel {
|
||||
fn capabilities(&self) -> ChannelCapabilities {
|
||||
ChannelCapabilities::default()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
渠道能力示例:
|
||||
|
||||
| 渠道 | 原生按钮 | 表单 | 更新原消息 | 降级策略 |
|
||||
|------|----------|------|------------|----------|
|
||||
| Feishu | 是,interactive card | 可后续支持 | 是,PATCH message/card | 文本命令 |
|
||||
| Slack | 是,Block Kit | 是 | 是 | 文本命令 |
|
||||
| Telegram | 是,inline keyboard | 有限 | 可编辑消息 | 文本命令 |
|
||||
| Discord | 是,components | 有限 | 可编辑消息 | 文本命令 |
|
||||
| CLI chat | 否 | 否 | 局部可模拟 | 编号/命令输入 |
|
||||
| Webhook/Email/SMS | 否 | 否 | 通常否 | 纯文本命令或链接 |
|
||||
|
||||
## 渲染策略
|
||||
|
||||
每个 channel 实现一个渠道内的渲染函数:
|
||||
|
||||
```rust
|
||||
async fn send_interaction(
|
||||
&self,
|
||||
chat_id: &str,
|
||||
payload: &InteractionPayload,
|
||||
) -> Result<(), ChannelError>;
|
||||
```
|
||||
|
||||
也可以先不改 trait,在 `send()` 内部判断 `msg.interaction`。
|
||||
|
||||
飞书渲染:
|
||||
|
||||
- 使用 Card JSON 2.0。
|
||||
- `schema = "2.0"`。
|
||||
- body 用 markdown 展示 `payload.body`。
|
||||
- actions 渲染为 button。
|
||||
- 每个按钮的 callback value 写入:
|
||||
|
||||
```json
|
||||
{
|
||||
"interaction_id": "...",
|
||||
"action_id": "...",
|
||||
"action_value": "approve"
|
||||
}
|
||||
```
|
||||
|
||||
需要兼容飞书 Card 2.0 回调路径:
|
||||
|
||||
- `/action/value`
|
||||
- `/action/behaviors/0/value`
|
||||
|
||||
CLI 降级渲染:
|
||||
|
||||
```text
|
||||
需要确认:
|
||||
|
||||
Tool: bash
|
||||
Args: cargo test --lib
|
||||
|
||||
可选操作:
|
||||
1. Approve
|
||||
2. Deny
|
||||
3. Always approve
|
||||
|
||||
回复:
|
||||
/_interaction <interaction_id> approve
|
||||
/_interaction <interaction_id> deny
|
||||
/_interaction <interaction_id> always
|
||||
```
|
||||
|
||||
纯文本渠道都可以复用这个 fallback renderer。
|
||||
|
||||
## Inbound 回调归一化
|
||||
|
||||
飞书 WebSocket 当前在 `src/channels/feishu.rs` 里处理 `im.message.receive_v1`。需要新增对 `card.action.trigger` 的识别:
|
||||
|
||||
1. ACK 仍要尽快发送,飞书要求 3 秒内响应。
|
||||
2. 如果 event type 是 `card.action.trigger`,不要走普通消息解析。
|
||||
3. 从 event payload 中解析 `interaction_id`、`action_id`、`action_value`。
|
||||
4. 构造 `InteractionEvent` 发布给统一处理器。
|
||||
5. 对未知、过期或重复 interaction 返回成功但记录日志,不应导致渠道重连或报错。
|
||||
|
||||
如果短期不新增 interaction bus,可以把回调转成特殊 `InboundMessage`:
|
||||
|
||||
```text
|
||||
content = "/_interaction <interaction_id> <action_value>"
|
||||
metadata["event.kind"] = "interaction"
|
||||
metadata["interaction.id"] = "<interaction_id>"
|
||||
metadata["interaction.action_id"] = "<action_id>"
|
||||
metadata["interaction.action_value"] = "<action_value>"
|
||||
```
|
||||
|
||||
但必须由 SessionManager 或 InteractionManager 先拦截,不能把 `/_interaction` 当普通用户文本直接送进 LLM。
|
||||
|
||||
长期推荐新增 bus 通道:
|
||||
|
||||
```rust
|
||||
pub enum InboundEvent {
|
||||
Message(InboundMessage),
|
||||
Interaction(InteractionEvent),
|
||||
}
|
||||
```
|
||||
|
||||
或者在 `MessageBus` 上增加 `interaction_tx`。
|
||||
|
||||
## InteractionManager
|
||||
|
||||
建议新增 `InteractionManager`,集中管理 pending 交互状态,而不是让每个 Channel 各自维护。
|
||||
|
||||
职责:
|
||||
|
||||
- 生成 `interaction_id`。
|
||||
- 保存 pending interaction。
|
||||
- 处理超时和过期。
|
||||
- 接收 `InteractionEvent` 并解析成业务结果。
|
||||
- 对重复点击、未知 interaction、过期 interaction 做幂等处理。
|
||||
- 必要时通知 channel 更新原消息。
|
||||
|
||||
内部状态示例:
|
||||
|
||||
```rust
|
||||
pub struct PendingInteraction {
|
||||
pub id: String,
|
||||
pub kind: InteractionKind,
|
||||
pub channel: String,
|
||||
pub chat_id: String,
|
||||
pub sender_id: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
pub created_at: i64,
|
||||
pub expires_at: Option<i64>,
|
||||
pub status: InteractionStatus,
|
||||
pub responder: InteractionResponder,
|
||||
pub message_ref: Option<InteractionMessageRef>,
|
||||
}
|
||||
|
||||
pub struct InteractionMessageRef {
|
||||
pub channel: String,
|
||||
pub chat_id: String,
|
||||
pub message_id: String,
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
```
|
||||
|
||||
`InteractionResponder` 可以先支持 oneshot:
|
||||
|
||||
```rust
|
||||
pub enum InteractionResponder {
|
||||
Approval(tokio::sync::oneshot::Sender<ApprovalDecision>),
|
||||
InboundMessage,
|
||||
}
|
||||
```
|
||||
|
||||
后续如果需要持久化长期交互,oneshot 不够,需要落库。
|
||||
|
||||
## 工具审批流程
|
||||
|
||||
工具审批是第一批最适合落地的交互类型。
|
||||
|
||||
推荐流程:
|
||||
|
||||
1. AgentLoop 准备执行需要审批的工具。
|
||||
2. 调用 `InteractionManager::request_approval(...)`。
|
||||
3. InteractionManager 创建 `InteractionPayload`,通过 outbound 发送到原 channel/chat。
|
||||
4. AgentLoop 等待 oneshot,带 timeout。
|
||||
5. 用户在飞书卡片上点击 Approve/Deny/Always。
|
||||
6. FeishuChannel 收到 `card.action.trigger`,发布 `InteractionEvent`。
|
||||
7. InteractionManager resolve pending approval。
|
||||
8. AgentLoop 收到结果,继续执行或拒绝工具。
|
||||
9. 如果 channel 支持更新消息,InteractionManager 或 Channel 把原卡片更新成 resolved 状态。
|
||||
|
||||
审批 action 建议:
|
||||
|
||||
```rust
|
||||
approve -> ApprovalDecision::Approve
|
||||
deny -> ApprovalDecision::Deny
|
||||
always -> ApprovalDecision::AlwaysApprove
|
||||
```
|
||||
|
||||
`DenyWithEdit` 可后续支持,适合 ACP/Web/CLI 这类能输入文本的渠道。
|
||||
|
||||
## 快捷回复流程
|
||||
|
||||
快捷回复不是阻塞工具执行,而是把用户点击转成新的用户输入。
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "QuickReply",
|
||||
"body": "你想继续哪个操作?",
|
||||
"actions": [
|
||||
{ "label": "继续分析", "value": "继续分析" },
|
||||
{ "label": "生成报告", "value": "生成报告" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
用户点击后:
|
||||
|
||||
- `InteractionEvent.action_value` 转成一条普通 `InboundMessage.content`。
|
||||
- `sender_id` 和 `chat_id` 保留原用户和会话。
|
||||
- metadata 标记来源为 interaction,供审计或 UI 使用。
|
||||
|
||||
## 消息更新
|
||||
|
||||
支持原消息更新的渠道应在交互完成后更新 UI,避免重复点击。
|
||||
|
||||
飞书:
|
||||
|
||||
- 发送卡片后保存 `data.message_id`。
|
||||
- resolve 后 PATCH `/im/v1/messages/{message_id}`。
|
||||
- 卡片 schema 发送和更新都使用 Card JSON 2.0,参考项目指出跨版本 PATCH 可能返回成功但客户端不重渲染。
|
||||
|
||||
不支持更新的渠道:
|
||||
|
||||
- 发送一条新消息提示“已批准/已拒绝”。
|
||||
- 或仅在后台幂等拒绝重复点击。
|
||||
|
||||
## 安全和权限
|
||||
|
||||
交互回调必须校验:
|
||||
|
||||
- `interaction_id` 是否存在。
|
||||
- 是否已过期。
|
||||
- 是否已处理。
|
||||
- 点击用户是否允许处理该 interaction。
|
||||
- 当前 channel/chat 是否匹配。
|
||||
|
||||
对于工具审批,默认建议只有触发该 agent turn 的用户或允许列表用户可以审批。群聊里要特别注意 `sender_id`,不能只看 `chat_id`。
|
||||
|
||||
日志中避免记录原始飞书回调敏感字段:
|
||||
|
||||
- callback token
|
||||
- operator open_id/union_id/user_id/tenant_key
|
||||
- open_chat_id/open_message_id
|
||||
|
||||
可以记录脱敏后的 payload shape,用于排查飞书回调字段变化。
|
||||
|
||||
## 持久化策略
|
||||
|
||||
第一阶段可以只做内存 pending map:
|
||||
|
||||
- 适合短时工具审批。
|
||||
- 进程重启后旧按钮点击会变成 unknown/expired。
|
||||
- 实现简单。
|
||||
|
||||
后续如果要支持长期任务或跨重启交互,需要持久化:
|
||||
|
||||
- `interactions` 表保存 id、kind、channel、chat_id、sender_id、status、payload、created_at、expires_at。
|
||||
- `interaction_actions` 可选,或直接 JSON 存在 payload 中。
|
||||
- resolve 时事务更新 status,防止重复点击竞态。
|
||||
|
||||
## 与现有架构的关系
|
||||
|
||||
现有数据流:
|
||||
|
||||
```text
|
||||
Channel -> MessageBus -> SessionManager -> AgentLoop -> tools -> SessionManager -> MessageBus -> OutboundDispatcher -> Channel
|
||||
```
|
||||
|
||||
加入 interaction 后建议:
|
||||
|
||||
```text
|
||||
Outbound:
|
||||
AgentLoop/Tool approval -> InteractionManager -> MessageBus outbound -> OutboundDispatcher -> Channel renderer
|
||||
|
||||
Inbound:
|
||||
Channel callback -> InteractionEvent -> InteractionManager -> pending waiter / synthetic InboundMessage
|
||||
```
|
||||
|
||||
Channel 仍然只做渠道协议适配:
|
||||
|
||||
- 飞书负责 Card JSON 和 `card.action.trigger`。
|
||||
- Slack 负责 Block Kit 和 signing secret。
|
||||
- Telegram 负责 callback query。
|
||||
- CLI 负责文本命令 fallback。
|
||||
|
||||
InteractionManager 负责语义:
|
||||
|
||||
- 这是 approval 还是 quick reply。
|
||||
- 是否过期。
|
||||
- 是否有权限。
|
||||
- 应该唤醒哪个等待者。
|
||||
|
||||
SessionManager/AgentLoop 不需要知道飞书卡片格式。
|
||||
|
||||
## 分阶段实施计划
|
||||
|
||||
### 阶段 1:模型和 fallback
|
||||
|
||||
- 新增 `src/interaction/` 模块。
|
||||
- 定义 `InteractionPayload`、`InteractionAction`、`InteractionEvent`。
|
||||
- 增加 fallback text renderer。
|
||||
- 为 `OutboundMessage` 增加 `interaction: Option<InteractionPayload>`,或短期使用 metadata。
|
||||
- 增加单元测试覆盖序列化和 fallback 文本。
|
||||
|
||||
### 阶段 2:Feishu card action
|
||||
|
||||
- Feishu outbound 支持把 `InteractionPayload` 渲染为 Card JSON 2.0。
|
||||
- Feishu inbound 在 WebSocket frame 中识别 `card.action.trigger`。
|
||||
- 解析 `/action/value` 和 `/action/behaviors/0/value`。
|
||||
- 发布统一 `InteractionEvent`。
|
||||
- 保存 `message_id`,支持完成后 PATCH resolved card。
|
||||
- 增加 fixtures 测试真实/模拟的 `card.action.trigger` payload。
|
||||
|
||||
### 阶段 3:InteractionManager 和审批
|
||||
|
||||
- 新增内存版 `InteractionManager`。
|
||||
- 支持 request/resolve/timeout。
|
||||
- 接入工具执行前审批点。
|
||||
- 支持 Approve/Deny/AlwaysApprove。
|
||||
- 未知、过期、重复点击保持幂等。
|
||||
- 增加 agent/tool 审批单元测试。
|
||||
|
||||
### 阶段 4:其他渠道兼容
|
||||
|
||||
- CLI chat 支持 `/_interaction <id> <value>` fallback。
|
||||
- 其他不支持原生按钮的渠道使用纯文本 fallback。
|
||||
- 后续按需实现 Slack/Telegram/Discord 原生按钮。
|
||||
|
||||
### 阶段 5:持久化和高级交互
|
||||
|
||||
- 需要时落库 pending interaction。
|
||||
- 支持 quick reply 生成 synthetic inbound message。
|
||||
- 支持表单提交。
|
||||
- 支持 `DenyWithEdit`。
|
||||
- 支持长期任务交互和重启恢复。
|
||||
|
||||
## 测试计划
|
||||
|
||||
单元测试:
|
||||
|
||||
- `InteractionPayload` 序列化。
|
||||
- fallback text renderer 输出。
|
||||
- Feishu Card JSON 包含正确 callback value。
|
||||
- Feishu 回调同时支持 `/action/value` 和 `/action/behaviors/0/value`。
|
||||
- unknown/expired interaction 不报错。
|
||||
- 重复点击只 resolve 一次。
|
||||
|
||||
集成测试:
|
||||
|
||||
- 模拟 Feishu `card.action.trigger`,验证 pending approval 被唤醒。
|
||||
- 模拟超时,验证默认 deny。
|
||||
- CLI fallback 输入 `/_interaction`,验证能 resolve。
|
||||
- 不支持按钮的 channel 能收到可读 fallback 文本。
|
||||
|
||||
手工验证:
|
||||
|
||||
- 飞书群聊点击 Approve/Deny/Always。
|
||||
- 私聊点击。
|
||||
- 点击后原卡片更新为 resolved。
|
||||
- 重复点击不会重复执行工具。
|
||||
- 非触发用户点击时按权限策略处理。
|
||||
|
||||
## 开放问题
|
||||
|
||||
1. 工具审批应该由 AgentLoop 直接调用 InteractionManager,还是通过 SessionManager 代理?
|
||||
2. 群聊中是否只允许原始触发者审批,还是允许配置中的所有 allowed user 审批?
|
||||
3. `AlwaysApprove` 的作用域是本次会话、本 dialog、本 chat,还是全局工具策略?
|
||||
4. 是否需要第一阶段就修改 `OutboundMessage` 结构,还是先用 metadata 降低改动面?
|
||||
5. 飞书 CardKit 流式输出是否要与交互卡片统一,还是继续保持普通回复卡片和交互卡片两套路径?
|
||||
|
||||
@ -1,101 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1120" height="460" viewBox="0 0 1120 460" role="img" aria-labelledby="title desc">
|
||||
<title id="title">PicoBot message flow</title>
|
||||
<desc id="desc">Message flow from channel input through bus, session manager, agent loop, tools, provider, storage, outbound dispatcher, and back to the channel.</desc>
|
||||
<defs>
|
||||
<style>
|
||||
.bg { fill: #fbfbf8; }
|
||||
.lane { fill: #ffffff; stroke: #d6d3d1; stroke-width: 2; rx: 18; }
|
||||
.step { fill: #f4f4f5; stroke: #71717a; stroke-width: 1.6; rx: 12; }
|
||||
.in { fill: #e0f2fe; stroke: #0284c7; }
|
||||
.state { fill: #ecfdf5; stroke: #059669; }
|
||||
.agent { fill: #fff7ed; stroke: #ea580c; }
|
||||
.out { fill: #f5f3ff; stroke: #7c3aed; }
|
||||
.text { font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #18181b; }
|
||||
.title { font-size: 30px; font-weight: 800; }
|
||||
.label { font-size: 17px; font-weight: 700; }
|
||||
.small { font-size: 14px; fill: #52525b; }
|
||||
.arrow { stroke: #3f3f46; stroke-width: 2.4; fill: none; marker-end: url(#arrow); }
|
||||
.soft { stroke: #71717a; stroke-width: 2; fill: none; stroke-dasharray: 7 6; marker-end: url(#arrow-soft); }
|
||||
.num { fill: #18181b; font-size: 13px; font-weight: 800; }
|
||||
.badge { fill: #ffffff; stroke: #a1a1aa; }
|
||||
</style>
|
||||
<marker id="arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto">
|
||||
<path d="M2,2 L10,6 L2,10 Z" fill="#3f3f46" />
|
||||
</marker>
|
||||
<marker id="arrow-soft" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto">
|
||||
<path d="M2,2 L10,6 L2,10 Z" fill="#71717a" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect class="bg" width="1120" height="460" />
|
||||
<text class="text title" x="48" y="56">Message Flow</text>
|
||||
<text class="text small" x="48" y="82">A user message becomes a session-scoped agent run, then returns to the original channel.</text>
|
||||
|
||||
<rect class="lane" x="38" y="122" width="1044" height="236" />
|
||||
|
||||
<g transform="translate(70 182)">
|
||||
<rect class="step in" width="130" height="78" />
|
||||
<circle class="badge" cx="18" cy="18" r="13" />
|
||||
<text class="text num" x="14" y="23">1</text>
|
||||
<text class="text label" x="32" y="37">Channel</text>
|
||||
<text class="text small" x="22" y="60">CLI / Feishu</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(230 182)">
|
||||
<rect class="step" width="130" height="78" />
|
||||
<circle class="badge" cx="18" cy="18" r="13" />
|
||||
<text class="text num" x="14" y="23">2</text>
|
||||
<text class="text label" x="30" y="37">MessageBus</text>
|
||||
<text class="text small" x="29" y="60">inbound queue</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(390 182)">
|
||||
<rect class="step state" width="150" height="78" />
|
||||
<circle class="badge" cx="18" cy="18" r="13" />
|
||||
<text class="text num" x="14" y="23">3</text>
|
||||
<text class="text label" x="34" y="37">SessionManager</text>
|
||||
<text class="text small" x="31" y="60">dialog + context</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(580 182)">
|
||||
<rect class="step agent" width="130" height="78" />
|
||||
<circle class="badge" cx="18" cy="18" r="13" />
|
||||
<text class="text num" x="14" y="23">4</text>
|
||||
<text class="text label" x="34" y="37">AgentLoop</text>
|
||||
<text class="text small" x="23" y="60">LLM/tool loop</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(750 182)">
|
||||
<rect class="step agent" width="130" height="78" />
|
||||
<circle class="badge" cx="18" cy="18" r="13" />
|
||||
<text class="text num" x="14" y="23">5</text>
|
||||
<text class="text label" x="48" y="37">Tools</text>
|
||||
<text class="text small" x="29" y="60">side effects</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(920 182)">
|
||||
<rect class="step out" width="130" height="78" />
|
||||
<circle class="badge" cx="18" cy="18" r="13" />
|
||||
<text class="text num" x="14" y="23">6</text>
|
||||
<text class="text label" x="30" y="37">Response</text>
|
||||
<text class="text small" x="28" y="60">outbound bus</text>
|
||||
</g>
|
||||
|
||||
<path class="arrow" d="M200 221 H230" />
|
||||
<path class="arrow" d="M360 221 H390" />
|
||||
<path class="arrow" d="M540 221 H580" />
|
||||
<path class="arrow" d="M710 221 H750" />
|
||||
<path class="arrow" d="M880 221 H920" />
|
||||
|
||||
<path class="soft" d="M455 182 C460 120 620 116 652 181" />
|
||||
<text class="text small" x="500" y="126">recall memory + build prompt</text>
|
||||
|
||||
<path class="soft" d="M645 260 C640 322 482 326 458 260" />
|
||||
<text class="text small" x="496" y="334">persist messages and metadata</text>
|
||||
|
||||
<path class="soft" d="M815 182 C820 122 918 122 958 181" />
|
||||
<text class="text small" x="828" y="126">provider calls</text>
|
||||
|
||||
<path class="arrow" d="M985 260 C972 390 139 392 135 260" />
|
||||
<text class="text small" x="420" y="408">OutboundDispatcher routes the reply back to the same channel/chat scope.</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.6 KiB |
@ -1,88 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1120" height="620" viewBox="0 0 1120 620" role="img" aria-labelledby="title desc">
|
||||
<title id="title">PicoBot runtime architecture</title>
|
||||
<desc id="desc">High level architecture diagram showing channels, gateway, message bus, session manager, agent loop, tools, providers, storage, scheduler, skills, and MCP.</desc>
|
||||
<defs>
|
||||
<style>
|
||||
.bg { fill: #f8fafc; }
|
||||
.panel { fill: #ffffff; stroke: #cbd5e1; stroke-width: 2; rx: 18; }
|
||||
.box { fill: #f1f5f9; stroke: #64748b; stroke-width: 1.5; rx: 12; }
|
||||
.accent { fill: #e0f2fe; stroke: #0284c7; }
|
||||
.warm { fill: #fff7ed; stroke: #ea580c; }
|
||||
.green { fill: #ecfdf5; stroke: #059669; }
|
||||
.violet { fill: #f5f3ff; stroke: #7c3aed; }
|
||||
.text { font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #0f172a; }
|
||||
.small { font-size: 16px; }
|
||||
.label { font-size: 18px; font-weight: 700; }
|
||||
.title { font-size: 30px; font-weight: 800; }
|
||||
.note { font-size: 14px; fill: #475569; }
|
||||
.arrow { stroke: #334155; stroke-width: 2.4; fill: none; marker-end: url(#arrow); }
|
||||
.soft { stroke: #64748b; stroke-width: 2; fill: none; stroke-dasharray: 7 6; marker-end: url(#arrow-soft); }
|
||||
</style>
|
||||
<marker id="arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto">
|
||||
<path d="M2,2 L10,6 L2,10 Z" fill="#334155" />
|
||||
</marker>
|
||||
<marker id="arrow-soft" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto">
|
||||
<path d="M2,2 L10,6 L2,10 Z" fill="#64748b" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect class="bg" width="1120" height="620" />
|
||||
<text class="text title" x="48" y="56">PicoBot Runtime Architecture</text>
|
||||
<text class="text note" x="48" y="82">Channels stay thin, SessionManager owns conversation state, AgentLoop remains stateless.</text>
|
||||
|
||||
<rect class="panel" x="40" y="118" width="220" height="360" />
|
||||
<text class="text label" x="70" y="154">Channels</text>
|
||||
<rect class="box accent" x="70" y="185" width="160" height="58" />
|
||||
<text class="text small" x="106" y="220">CLI TUI</text>
|
||||
<rect class="box accent" x="70" y="263" width="160" height="58" />
|
||||
<text class="text small" x="104" y="298">Feishu/Lark</text>
|
||||
<rect class="box accent" x="70" y="341" width="160" height="58" />
|
||||
<text class="text small" x="109" y="376">WebSocket</text>
|
||||
<text class="text note" x="70" y="437">Only receive and send</text>
|
||||
|
||||
<rect class="panel" x="330" y="118" width="450" height="360" />
|
||||
<text class="text label" x="360" y="154">Gateway Core</text>
|
||||
<rect class="box" x="370" y="184" width="160" height="64" />
|
||||
<text class="text small" x="414" y="222">MessageBus</text>
|
||||
<rect class="box green" x="580" y="184" width="160" height="64" />
|
||||
<text class="text small" x="606" y="222">SessionManager</text>
|
||||
<rect class="box warm" x="580" y="288" width="160" height="64" />
|
||||
<text class="text small" x="621" y="326">AgentLoop</text>
|
||||
<rect class="box" x="370" y="288" width="160" height="64" />
|
||||
<text class="text small" x="402" y="326">Outbound</text>
|
||||
<text class="text small" x="399" y="346">Dispatcher</text>
|
||||
<rect class="box violet" x="475" y="390" width="200" height="54" />
|
||||
<text class="text small" x="514" y="423">Control Channel</text>
|
||||
|
||||
<rect class="panel" x="850" y="118" width="230" height="360" />
|
||||
<text class="text label" x="880" y="154">Capabilities</text>
|
||||
<rect class="box warm" x="880" y="185" width="170" height="50" />
|
||||
<text class="text small" x="929" y="216">Tools</text>
|
||||
<rect class="box warm" x="880" y="249" width="170" height="50" />
|
||||
<text class="text small" x="916" y="280">Providers</text>
|
||||
<rect class="box green" x="880" y="313" width="170" height="50" />
|
||||
<text class="text small" x="926" y="344">SQLite</text>
|
||||
<rect class="box violet" x="880" y="377" width="170" height="50" />
|
||||
<text class="text small" x="925" y="408">Skills</text>
|
||||
<text class="text note" x="880" y="457">MCP tools join ToolRegistry</text>
|
||||
|
||||
<rect class="box green" x="300" y="520" width="165" height="54" />
|
||||
<text class="text small" x="340" y="553">Scheduler</text>
|
||||
<rect class="box violet" x="505" y="520" width="165" height="54" />
|
||||
<text class="text small" x="552" y="553">Memory</text>
|
||||
<rect class="box accent" x="710" y="520" width="165" height="54" />
|
||||
<text class="text small" x="759" y="553">MCP</text>
|
||||
|
||||
<path class="arrow" d="M260 216 H370" />
|
||||
<path class="arrow" d="M530 216 H580" />
|
||||
<path class="arrow" d="M660 248 V288" />
|
||||
<path class="arrow" d="M740 320 H880" />
|
||||
<path class="arrow" d="M880 274 H750" />
|
||||
<path class="arrow" d="M580 320 H530" />
|
||||
<path class="arrow" d="M370 320 H260" />
|
||||
<path class="soft" d="M575 390 V352" />
|
||||
<path class="soft" d="M660 248 C750 250 800 330 880 338" />
|
||||
<path class="soft" d="M382 520 C420 470 520 465 610 352" />
|
||||
<path class="soft" d="M588 520 C600 470 620 420 650 352" />
|
||||
<path class="soft" d="M792 520 C825 470 860 430 880 402" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
Loading…
x
Reference in New Issue
Block a user