# 配置热重载功能审核报告 > 状态:审查完成;主要意见已于 2026-07-21 落地。本文前六节保留首次审查快照,行号、测试数和“当前实现”描述可能已过时;以下处置表与代码为最新结论。 > > 本文是对 `docs/CONFIG_HOT_RELOAD_DESIGN.md` 描述的配置热重载功能及其当前未提交实现的综合审核。审核覆盖架构合理性、实现一致性、关键不变量和改进建议;具体行为以代码和测试为最终依据。相关实现位于 `src/gateway/reload.rs`、`src/gateway/mod.rs::run`、`src/config/mod.rs::load_for_reload`、`src/session/session.rs::wait_until_idle`、`src/tools/reload_config.rs` 与 `src/gateway/http.rs::reload_config`。 ## 0. Review 意见处置与实现结果 | 意见 | 处置 | 结论与实现 | |------|------|------------| | A1 / E1 MCP 准备期副作用 | 接收 | `connect_all()` 移到运行代激活阶段,候选构造不再启动双份 MCP 或提前覆盖全局 status。 | | A2 / E2 generation 与状态查询 | 接收 | 每次请求分配 generation;新增 `GET /api/config/reload/status`,状态为 `preparing/draining/activating/active/failed`。 | | A3 激活失败回滚 | 部分接收 | 接收“需要明确失败状态”,但驳回先启动新 Channel 再停旧 Channel。飞书长连接双开会重复消费事件,风险高于短暂切换空窗;完整 prepare/activate 或可恢复旧代留作后续。 | | A4 / L4 Agent 工具风险 | 部分接收 | 保留用户要求的 `reload_config`,但只允许根交互 Agent 使用;子 Agent、Cron、managed scheduled Agent 均剔除该工具,并保持 exclusive。驳回直接删除工具。 | | A5 / E5 后台任务排空 | 接收 | 引入运行代 admission/activity guard;Scheduler 已执行 job 与后台子 Agent 持有 guard,新任务在 drain 后不再进入。 | | I1 / E4 Slash 回复可能丢失 | 接收 | `/reload` 所在 inbound 在 lane 入队前持有 guard;command output 改为显式等待 outbound delivery acknowledgement,删除固定 500ms sleep。 | | I2 / F2 DB 路径误判 | 接收 | 比较归一化后的有效路径,`None` 与 `./picobot.db` 可判为同一数据库。 | | I3 零散预校验 | 部分接收 | 保留 default agent 与 Feishu 的快速错误提示;其余组件统一由候选 `from_config()` 构造验证,不继续扩张 ad-hoc 字段校验。长期采用显式 prepare contract。 | | I4 HTTP 错误码 | 接收 | pending 返回 409,退出/准备故障返回 503,配置与不可变字段错误返回 400。错误改为 `ReloadError` 类型。 | | I5 候选双 Storage | 驳回为正确性缺陷 | 同库多连接是 sqlx/SQLite 的正常模式,候选无消息入口;路径身份仍被强制保持一致。该点保留为测试与锁竞争观察项,而非阻止上线。 | | F1 集成测试 | 部分接收 | 新增真实 Gateway 子进程测试,覆盖成功切换到 generation 2、状态查询、无效候选不影响旧代健康。可控假 Provider、长 Turn 和故障注入仍待补充。 | | L1 prepare/activate 分离 | 方向接收 | 本轮已把 MCP activation 与候选构造分离;完整组件级接口留作后续架构演进。 | | L2 动态 Router | 暂缓 | 当前保留 listener 并为 Axum graceful shutdown 增加 10 秒硬上限;不为本功能引入动态 service 复杂度。 | | L3 客户端完成通知 | 暂缓 | 后端 generation/status 已具备;TUI/WebUI 展示可在后续独立实现。 | 首次 Review 未指出、但本轮一并修复的两个关键问题:候选构造原先直接在 `select!` 分支内 await,会暂停轮询旧 Axum serve 与进程信号;现在 serve 独立受监督运行,准备和排空阶段都继续响应服务退出。其次,原实现排空前没有关闭入口,持续新消息可能让排空永不稳定;现在 admission 先关闭再 drain。 ## 1. 审查范围与依据 ### 1.1 审查对象 - 设计文档:`docs/CONFIG_HOT_RELOAD_DESIGN.md` - 实现:当前未提交的 17 个文件变更与 2 个新增文件(`src/gateway/reload.rs`、`src/tools/reload_config.rs`),共 ~304 行净增 - 关联变更:`Cargo.toml`、`webui/package.json` 版本号 1.2.2 → 1.3.0;`README.md`、`AGENTS.md`、`docs/ARCHITECTURE.md`、`resources/skills/about-picobot/references/config.md` 文档同步 ### 1.2 审查依据 - 仓库既有架构边界与并发不变量(见 `docs/ARCHITECTURE.md`) - 现有相似机制(Channel 生命周期、TaskSupervisor、TurnController、OutboundDispatcher) - 验证命令:`cargo build`、`cargo clippy --all-targets --all-features -- -D warnings`、`cargo test --lib`(330 passed)、`webui && npm run check && npm run build` ### 1.3 验证结果 | 命令 | 结果 | |------|------| | `cargo build` | 通过 | | `cargo clippy --all-targets --all-features -- -D warnings` | 通过 | | `cargo test --lib` | 330 passed / 0 failed | | `cd webui && npm run check && npm run build` | 0 errors / 0 warnings | ## 2. 架构审查 ### 2.1 整体架构评估 **结论:架构方向正确,运行代(runtime generation)切换模型是 PicoBot 配置散落现状下的唯一可靠方案。** PicoBot Gateway 启动时把配置拆分复制到 `SessionManager`、`ChannelManager`、MCP、Scheduler、Browser、Auth、Upload 等长生命周期组件;`gateway.host/port`、进程 cwd、SQLite 连接具有进程级生命周期。在这种结构下,任何"原地替换 Config 指针"的方案都会导致请求处理组件混用新旧配置(新会话用新模型、旧 Session 仍用旧 Provider;或飞书配置显示禁用但旧连接仍在接收消息)。 运行代切换通过"先在旧代仍服务时构造完整候选代;候选可用后排空当前交互工作,再回收旧代并激活新代"避免了半套配置暴露。这一选择与 PicoBot 单进程、单 Gateway 模型契合,不引入 daemon/fork 层。 ### 2.2 运行代模型合理性 运行代模型的关键设计点均合理: | 设计点 | 评估 | |--------|------| | 保留原始 `std::net::TcpListener`,每代 `try_clone()` | ✅ 避免 bind 失败与端口抢占,保留内核 backlog | | 候选构造期间不修改旧 `GatewayState` | ✅ 避免半套配置暴露 | | `Config::load_for_reload` 使用启动环境快照、不调 `set_var` | ✅ 多线程运行期修改进程环境的危险被正确规避 | | 不可变字段(host/port/workspace/db_path)边界清晰 | ✅ 边界划分正确 | | 三个入口(CLI、`/reload`、`reload_config` 工具)共享控制通道 | ✅ 单一校验路径,语义一致 | | 拒绝 nginx 式新旧 worker 长期并行 | ✅ 单进程规模不值得这份复杂度 | | `ReloadHandle` 用有界 MPSC + `try_send` | ✅ 无界积压被正确拒绝 | ### 2.3 边界划分评估 热重载边界表(设计文档第 8 节)覆盖完整: - 可热重载:`providers`/`models`/`agents`、`channels`、`mcp`、`browser`、`memory`、`gateway.scheduler`、`gateway.max_concurrent_background_tasks`、`gateway.file_transfer`、`gateway.require_pairing` - 必须重启:`gateway.host`/`port`、`workspace_dir`、`gateway.session_db_path`、未进入配置组件的继承环境变量 边界划分与实现中 `load_candidate()` 的校验项一一对应。`workspace_dir` 在比较前按启动 cwd 解析并 canonicalize(`reload.rs:80-89`),与 `from_config` 中 `ensure_workspace_dir` 的 canonicalize 行为一致,比较基准正确。 ### 2.4 并发不变量评估 设计文档第 10 节列出的 10 条不变量在实现中均得到遵守: | 不变量 | 实现位置 | 遵守情况 | |--------|----------|----------| | 只有 `run()` 拥有 receiver 与当前运行代 | `mod.rs:331` ReloadController 在 `run()` 内创建 | ✅ | | 候选构造不修改旧 GatewayState | `mod.rs:399` `from_config` 创建独立 state | ✅ | | 不在热重载路径调 `env::set_var` | `config/mod.rs:611` `apply_to_process=false` | ✅ | | 不释放原始 listener 后 rebind | `mod.rs:345` listener 在 `run()` 内持有 | ✅ | | 旧 WebSocket 观察 `connection_shutdown` | `mod.rs:421` 切换前 cancel | ✅ | | 旧任务由旧 TaskSupervisor 回收 | `mod.rs:432-436` 旧 supervisor shutdown | ✅ | | 排空检查不长时间持有 Session mutex | `session.rs:2113-2124` 先克隆 Arc 再短锁 | ✅ | | reload tool 保持 exclusive | `reload_config.rs:49` `exclusive: true` | ✅ | | 不可变字段比较在候选构造之前 | `reload.rs:80-100` | ✅ | | 新增启动期配置消费者需更新边界表 | 文档约束 | ⚠️ 维护性约束,无机制强制 | ## 3. 实现审查 ### 3.1 与设计文档的一致性 实现与设计文档的关键路径高度一致: | 设计文档章节 | 实现位置 | 一致性 | |--------------|----------|--------| | §6.1 准备阶段:load_candidate → from_config | `mod.rs:386-408` | ✅ | | §6.2 接受响应:候选构造成功后通过 oneshot 返回 | `mod.rs:410-411` | ✅ | | §6.3 排空阶段:wait_until_idle 60s + 500ms 投递窗口 | `mod.rs:412-419` | ✅ | | §6.4 切换顺序:connection_shutdown → generation_shutdown → channel stop → task_supervisor → 新代启动 | `mod.rs:421-422, 429-436, 350-351` | ✅ | | §4 控制通道:容量 8、try_send、队列满立即返回错误 | `reload.rs:8, 47-54` | ✅ | | §7.2 环境变量语义:不修改进程环境 | `config/mod.rs:541-553` | ✅ | | §9 失败语义:候选构造失败时旧代不变 | `mod.rs:394-408` continue 不切换 | ✅ | ### 3.2 关键路径分析 #### 3.2.1 切换时序 `gateway::run` 的主循环(`mod.rs:349-441`)正确实现了运行代切换: ``` 外层 loop { start_all / start_message_processing // 新代激活 内层 loop { select! { serve | process_signal | reload_request } } serve.await // 等待旧 axum graceful shutdown channel_manager.stop_all // 停止旧 channel intake task_supervisor.cancel + shutdown(10s)// 回收旧受监督任务 state = next_state // 切换 } ``` `wait_for_shutdown_signal()` 在外层循环内创建(`mod.rs:365`),每代新建 future,不存在重复 poll 已完成 future 的 UB。 #### 3.2.2 候选构造与旧代隔离 `from_config`(`mod.rs:51-236`)为候选创建独立的 Storage、MessageBus、ChannelManager、SessionManager、ToolRegistry、AuthManager、UploadRegistry。候选的 `channel_manager.init()` 仅构造 Channel 对象,不调用 `start()`,因此不会与旧 channel 并发连接飞书 API。 #### 3.2.3 监听 socket 保留 `std::net::TcpListener`(`mod.rs:345`)在进程生命周期内持有;每代通过 `try_clone()`(`mod.rs:353`)获得新 fd。旧代 `serve` future 退出时仅释放克隆 fd,原始 socket 不释放,新代可重新克隆并 accept。内核 backlog 在切换窗口中暂存新 TCP 连接。 ### 3.3 测试覆盖评估 **结论:单元测试覆盖不足,集成测试完全缺失。** 当前测试: | 测试 | 位置 | 覆盖范围 | |------|------|----------| | `candidate_accepts_runtime_changes_and_rejects_workspace_changes` | `reload.rs:127` | load_candidate 的 model_id 变更接受与 workspace 拒绝 | | `resolve_slash_command("reload")` | `session.rs:3485` | slash 命令解析 | 缺失但设计文档第 12 节明确列出的测试: 1. 启动真实 Gateway、修改 model/channel 配置、HTTP 触发重载、验证新代生效 2. 配置无效时验证旧 WebSocket 与旧 Provider 仍可工作 3. 长 Turn 中调用 `reload_config`、验证 tool result 与最终消息投递后才断开 4. Session 队列积压时验证重载等待排空 5. 排空超时、Channel stop 超时、候选激活失败的故障注入 6. 重载前后鉴权策略变化与旧 WebSocket 失效 当前测试仅覆盖纯函数路径(`load_candidate`、slash 解析),未验证任何运行时切换行为。这是上线前的主要风险点。 ### 3.4 代码质量 - **Clippy**:`-D warnings` 通过 - **类型安全**:`ReloadHandle::unavailable()`(`reload.rs:39`)通过 drop receiver 使 `try_send` 返回 `Closed`,正确表达"Gateway 未由 `run()` 启动"的语义 - **错误处理**:候选构造失败时 `request.response.send(Err(...))` 后 `continue`,不切换;MCP 单 server 失败沿用启动语义(记录错误、跳过工具),不阻断候选构造 - **资源管理**:旧 TaskSupervisor 的 10s 有界 shutdown + abort 保证回收有硬时间边界 ## 4. 发现的问题 ### 4.1 架构层面问题 #### A1. MCP 在准备阶段连接,制造进程级副作用【中】 `from_config`(`mod.rs:171`)调用 `mcp::connect_all()`,立即建立 MCP 客户端连接或启动 stdio 子进程,并更新进程级 `MCP_SERVER_STATUS`(`mcp/mod.rs:42`)。这违反了设计文档第 10 节不变量 #2"候选构造不得修改旧 GatewayState"的精神——MCP status 虽非请求处理状态,但仍是旧代可见的进程级状态。 **具体影响:** - 候选构造期间,旧代的 `/mcp` 命令显示候选的连接状态而非旧代状态 - stdio MCP 子进程双份运行(旧代 + 候选)可能竞争资源或 stdin/stdout - 候选在 `connect_all` 之后失败(如 `ensure_default_maintenance_job` 失败,`mod.rs:194`),MCP 连接被 drop 但 `MCP_SERVER_STATUS` 仍显示 `connected: true`,旧代 `/mcp` 显示陈旧数据 设计文档第 6.1 节与第 14 节将此列为"已知例外"。但该例外的收益仅为"提前发现 MCP 失败"——而 MCP 单 server 失败本就被当非致命跳过(`mcp/mod.rs:171`),不需要提前连接来验证。 #### A2. 无 generation ID 与状态查询【中】 调用方只能得到准备阶段结果("配置校验通过;Gateway 将在当前任务结束后切换到新配置"),无法查询重载最终是否完成。运维需要翻日志或重连客户端确认切换状态。设计文档第 14 节将此列为"演进方向",但 generation ID(一个 atomic 计数器)+ `/api/config/reload/status` 端点成本极低,收益显著,应在 v1 内置。 #### A3. 候选激活失败无回滚【中-高】 切换顺序为:停旧 channel → 取消旧 TaskSupervisor → 切换 state → 启动新 channel(`mod.rs:429-351`)。若新代 `start_all()` 失败,`run()` 返回错误,依赖 systemd 拉起。但 channel 启动失败常为瞬时问题(飞书 5xx、端口冲突),丢掉本来正常服务的旧代去重启是可用性损失。旧代此时已被回收,无法回滚。 #### A4. `reload_config` Agent 工具的软约束【低-中】 `ReloadConfigTool`(`reload_config.rs`)注册到默认工具集,依赖 description"仅在用户明确要求重新加载配置时调用"约束 LLM。LLM compliance 是软约束,非可靠边界。`exclusive: true` 仅保证不与其他副作用工具并行,不保证调用时机正确。 #### A5. 排空仅覆盖交互 Session,不覆盖 Scheduler/后台 Agent【低】 `wait_until_idle` 仅检查内存中 Session 的 `current_cancel` 与 `agent_tx` 队列。Scheduler job、独立后台子 Agent、HTTP handler 不在排空范围内。Scheduler job 可能正在写 DB 或发送消息,被 TaskSupervisor 10s abort 截断可能留下不一致状态。设计文档第 6.3 节已承认此范围。 ### 4.2 实现层面问题 #### I1. `/reload` slash command 绕过 `wait_until_idle`【中】 `session.rs:2631` 显示 slash command 在 `handle_message` 内联处理,返回 `HandleResult::CommandOutput`,**不进入 session worker 队列**。因此 `wait_until_idle` 检查 `current_cancel`/`agent_tx.capacity()` 时看不到 `/reload` Turn 的活动状态,立即返回(仅 100ms 稳定 + 500ms sleep)。 slash command output 经 `process_inbound` → `publish_command_output` → outbound dispatcher → channel API 投递,整条链路必须在 ~600ms(+ serve graceful shutdown + 10s TaskSupervisor shutdown)内完成。对 CLI channel 足够,但对 Feishu 等远端 channel 较紧。设计文档第 6.3 节"为 slash command output 和终态投递留出发送窗口"承认了 500ms 窗口,但 500ms 是固定值,无背压保证。 实际窗口因 TaskSupervisor 的 10s shutdown 较宽,不会丢消息——但若 channel `stop_all()` 关闭了连接,in-flight 的 `send_message` 可能失败。 #### I2. `session_db_path` 比较为原始字符串【低】 `reload.rs:91` 直接比较 `current.gateway.session_db_path != candidate.gateway.session_db_path`。若用户把 `null` 改为 `"./picobot.db"`(解析后同一路径),会被误拒。保守是对的,但产生假阳性。`workspace_dir` 已做 canonicalize 比较,`session_db_path` 应保持一致。 #### I3. `load_candidate` 仅校验 Feishu 凭据【低】 `reload.rs:73-78` 仅校验飞书 `app_id`/`app_secret` 非空。其他 channel 配置(若有)、MCP 配置、Browser 配置等在 `from_config` 期间才验证,可能在那里失败。这与设计文档第 6.1 节一致("若飞书启用,校验 app_id 和 app_secret 非空"),但将失败发现延后到了候选构造阶段。 #### I4. `reload_config` HTTP handler 错误码语义【低】 `http.rs:355` 对所有错误用 `ApiError::bad_request`(400)。"gateway is shutting down"(队列关闭)更适合 503 Service Unavailable,"another configuration reload is already pending"更适合 409 Conflict。 #### I5. 候选 Storage 与旧 Storage 共享同一 SQLite 文件【低】 `from_config` 为候选创建新 Storage 连接到同一 `picobot.db`。候选的 background notification consumer 与 cleanup task 已归候选 TaskSupervisor,cleanup 跳过首次 tick,无入口触发 notification,故实际不写。理论上有并发写锁竞争可能,实际风险低。设计文档未显式说明此点。 ### 4.3 严重度分级 | 问题 | 严重度 | 影响 | |------|--------|------| | A3 候选激活失败无回滚 | 中-高 | 瞬时 channel 故障导致整个 Gateway 重启 | | A1 MCP 准备阶段连接 | 中 | 进程级 status 污染、子进程双份、失败后状态陈旧 | | A2 无 generation ID | 中 | 运维无法确认切换完成状态 | | I1 `/reload` 绕过排空 | 中 | 远端 channel output 投递窗口紧 | | A4 reload_config 软约束 | 低-中 | LLM 误调用风险 | | A5 排空不覆盖 Scheduler | 低 | 后台 job 被硬取消可能留下不一致 | | I2 session_db_path 假阳性 | 低 | 用户需重启而非重载 | | I3 仅校验 Feishu | 低 | 失败发现延后 | | I4 HTTP 错误码 | 低 | 语义不准确 | | I5 双 Storage 连接 | 低 | 理论并发风险 | ## 5. 改进建议 ### 5.1 必须修复(上线前) #### F1. 补充端到端集成测试 至少覆盖设计文档第 12 节列出的前 3 项: 1. 启动真实 Gateway、修改 model_id、HTTP 触发重载、验证新 model 在新 Turn 中生效 2. 配置无效(如 default agent 解析失败)时验证旧 WebSocket 与旧 Provider 仍可工作 3. 长 Turn 中调用 `reload_config`、验证 tool result 与最终消息投递后才断开 这些测试无法用单元测试替代,需启动真实 Gateway 进程。 #### F2. `session_db_path` 比较归一化 `reload.rs:91` 应将 `session_db_path` 相对于 workspace 解析并 canonicalize 后比较,与 `workspace_dir` 处理方式一致。`None` 与 `"picobot.db"`(默认值)应视为等价。 ### 5.2 建议增强(近期演进) #### E1. MCP 连接移出 `from_config`,消除"已知例外" 将 `mcp::connect_all()` 从 `from_config`(`mod.rs:171`)移到 `start_all()` 阶段,与 channel 启动同相位。收益: - 消除进程级 `MCP_SERVER_STATUS` 在候选构造期间被污染 - 消除 stdio 子进程双份运行 - 候选失败时无 MCP 连接泄漏 - 不再需要设计文档第 10 节不变量 #2 的"已知例外"声明 代价:MCP 连接失败从"候选构造失败"延后到"激活失败"。但 MCP 单 server 失败本就非致命(跳过该 server 工具),整体激活失败语义不变。 #### E2. 引入 generation ID 与 status 查询 在 `ReloadController` 增加 `Arc` generation 计数器与 `ReloadState` enum(`Idle`/`Preparing`/`Draining`/`Activating`/`Active`/`Failed`)。提供 `GET /api/config/reload/status` 端点。调用方在收到 "accepted" 后可轮询确认切换完成。成本极低(一个 atomic + 一个路由 + 一个 enum),运维收益显著。 #### E3. 旧代保留至新代 `start_all()` 成功 调整切换顺序为:先启动新 channel(新代 channel_manager.start_all),成功后再停止旧 channel。短暂双 channel 并存对飞书 webhook 幂等消息可容忍。代价是需处理两代 channel 并存的资源冲突(如媒体目录、飞书事件去重),但避免瞬时 channel 故障导致 Gateway 整体重启。 若实现成本过高,至少应在 `start_all()` 失败时尝试重启旧代 channel(旧 TaskSupervisor 已 cancel,可能无法恢复;需评估可行性)。 #### E4. `/reload` slash command 排空路径 两种方案: - **方案 A**:让 reload controller 记录触发源(channel, chat_id),等待该 inbound lane 的当前消息处理完成后再切换,而非泛化等待所有 session idle - **方案 B**:为 outbound dispatcher 增加显式 drain contract,在 `stop_all` 前等待 outbound 队列排空或超时 方案 A 更精确,方案 B 更通用。两者都比固定 500ms sleep 可靠。 #### E5. Scheduler 与后台 job 协作式 drain 在 Scheduler job 的协作取消边界检查 reload token,允许 job 在写 DB 前/后选择继续完成或退出。避免 TaskSupervisor 10s abort 截断 DB 写一半的 job。设计文档第 14 节已列出此项。 ### 5.3 演进方向(中长期) #### L1. `prepare()` / `activate()` 显式分离 将 `from_config` 拆为: - `construct()`:纯内存,无 I/O,可反复调用 - `prepare()`:可失败的 I/O(channel health check、MCP 连接、Storage ping),旧代仍服务 - `activate()`:开始接收消息 使更多失败前移到旧代仍可回退的阶段,而非等到 activate 才暴露。设计文档第 14 节已列出此项。 #### L2. 动态 Router service 引入新旧 HTTP generation 短期重叠,消除 accept/Channel intake 空窗。需不破坏 Channel/Session 边界。设计文档第 14 节已列出此项。 #### L3. WebUI/TUI reload 完成通知 客户端在 WebSocket 重连后显示 reload 完成状态与自动重连提示。依赖 E2 的 generation ID。 #### L4. 重新评估 `reload_config` Agent 工具 考虑移除该工具,仅保留 CLI 与 `/reload`。Agent 触发进程级状态切换的风险(A4)可能不抵边际收益。若保留,应在 SessionManager 层做调用方校验(如要求参数带确认 token),而非靠 description。 ## 6. 总体结论 ### 6.1 设计评估 设计文档质量高,边界清晰,不变量明确,失败语义完整。运行代切换模型是 PicoBot 当前架构下的正确选择。设计文档诚实地列出了已知限制(第 14 节),未掩饰缺陷。 主要设计层面的不足是把几件本应 v1 内置的能力(generation ID、MCP 相位对齐、旧代保留至新代激活成功)推迟到"演进方向",导致可用性与可观测性打了折扣。MCP 的"已知例外"(A1)是设计妥协被文档化的典型,留着会让后续维护者也认为"再来一个例外无所谓",应尽早消除而非长期承担。 ### 6.2 实现评估 实现忠实遵循设计,关键不变量均得到遵守。代码通过 `cargo build`、`cargo clippy -D warnings`、`cargo test --lib`(330 passed)与 WebUI `check/build`。版本号、文档、AGENTS.md 同步更新。 主要实现层面的不足是测试覆盖:仅 `load_candidate` 与 slash 解析有单元测试,无任何运行时切换行为的集成验证(I1-I5 中多数问题需要集成测试才能暴露)。设计文档第 12 节明确列出但未实现的 6 项集成测试是上线前的主要风险。 ### 6.3 上线建议 | 项 | 判定 | |----|------| | 架构方向 | ✅ 可接受 | | 实现一致性 | ✅ 可接受 | | 代码质量 | ✅ 可接受(Clippy/tests/build 全通过) | | 测试覆盖 | ⚠️ 不足,需补集成测试(F1) | | 已知限制 | ⚠️ 文档已承认,但 A1/A3 应优先修复 | **建议:在完成 F1(集成测试)与 F2(session_db_path 归一化)后可提交。A1(MCP 相位)、A3(无回滚)应列为后续优先修复项,不应长期承担。** ## 附录:关键文件与符号索引 | 文件/符号 | 职责 | 行号 | |-----------|------|------| | `src/gateway/reload.rs::ReloadController` | 重载控制通道、候选配置加载、不可变字段校验 | 19-36 | | `src/gateway/reload.rs::ReloadHandle` | 可克隆的请求端,注入 SessionManager/工具/HTTP state | 14-59 | | `src/gateway/reload.rs::load_candidate` | 候选配置解析与不可变字段校验 | 61-102 | | `src/gateway/mod.rs::run` | 持有 listener、当前运行代与切换主循环 | 318-441 | | `src/gateway/mod.rs::GatewayState::from_config` | 构造一套完整运行代依赖 | 51-236 | | `src/gateway/mod.rs::build_router` | 为每代构建 Axum Router | 448-489 | | `src/config/mod.rs::Config::load_for_reload` | 使用启动环境/cwd 安全重新解析配置 | 541-553 | | `src/config/mod.rs::Config::load_from_with_process_env` | 共享的配置加载实现,支持不写入进程环境 | 547-625 | | `src/session/session.rs::wait_until_idle` | 检查活动 Turn、Session 队列与稳定空闲窗口 | 2109-2144 | | `src/session/session.rs::execute_slash_command` | `/reload` slash 入口 | 2093-2099 | | `src/tools/reload_config.rs::ReloadConfigTool` | Agent 可调用的独占重载工具 | 6-52 | | `src/gateway/http.rs::reload_config` | 受保护的 `POST /api/config/reload` | 347-360 | | `src/client/mod.rs::reload_gateway` | CLI HTTP 客户端与 bearer token 注入 | 101-124 | | `src/main.rs::Command::Reload` | `picobot reload` CLI 定义 | 59-64, 132-138 | | `src/mcp/mod.rs::connect_all` | MCP 连接(当前在 from_config 期间调用,见 A1) | 126-181 | | `src/mcp/mod.rs::MCP_SERVER_STATUS` | 进程级 MCP 状态(A1 的副作用源) | 36-45 |