- Store default SQLite database at <config_dir>/data/picobot.db instead of
the workspace, keeping session data independent of the workspace; the
reload equivalence check and docs follow the new default
- Add per-skill enable/disable persisted in <config_dir>/skills_state.json;
skills default to enabled, disabled skills are excluded from prompts,
listings, and get_skill at load time
- Add mcp.servers[].enabled (default true); disabled servers are skipped
at activation and by health checks
- WebUI Tools page: switches for Skills and MCP servers, plus a concrete
MCP tool list with connection status and errors
- Add PUT /api/skills/{name} API and expose enabled/tool details in the
skills/status APIs
- Bump version to 1.15.0
390 lines
23 KiB
Markdown
390 lines
23 KiB
Markdown
# PicoBot 配置热重载设计与实现
|
||
|
||
本文档描述 PicoBot 1.3.0 配置热重载功能的设计目标、运行时模型、实现边界、失败语义和维护要求。代码与测试是最终事实来源;本文用于解释为什么采用当前方案,以及后续修改必须保持哪些不变量。
|
||
|
||
## 1. 背景
|
||
|
||
PicoBot 的配置并非只在一个全局对象中读取。Gateway 启动时会把配置拆分并复制到多个长生命周期组件:
|
||
|
||
- `SessionManager`、现有 `Session`、子 Agent 和 Scheduler 持有 Provider/模型配置。
|
||
- `ChannelManager` 按配置创建并启动飞书、CLI Chat 等 Channel。
|
||
- MCP 配置在启动时用于连接 Server,并把发现的工具注册到 `ToolRegistry`。
|
||
- Browser、文件上传、鉴权、后台任务并发度等配置在各自组件构造时固化。
|
||
- Gateway 的监听 socket、进程 cwd 和 SQLite 连接具有进程级生命周期。
|
||
|
||
因此,简单地重新读取 `config.json` 或替换一个 `Config` 指针并不能可靠生效。这样会造成请求处理组件混用新旧配置,例如新会话使用新模型、旧 Session 仍使用旧 Provider,或者配置显示飞书已禁用但旧连接仍在接收消息。
|
||
|
||
当前实现采用“运行代(runtime generation)切换”:先在旧运行代仍然服务时解析、校验并构造完整候选运行代;候选可用后排空当前交互工作,再回收旧运行代并激活新运行代。
|
||
|
||
## 2. 目标与非目标
|
||
|
||
### 2.1 目标
|
||
|
||
- 提供统一的 `picobot reload`、`/reload` 和 `reload_config` 工具入口。
|
||
- 在停止旧运行代前完成候选配置解析、关键字段校验和依赖构造。
|
||
- 配置错误或候选构造失败时继续使用旧运行代,不中断服务。
|
||
- 尽量让正在执行及已经排队的交互 Turn 完成,避免热重载直接截断发起重载的 Turn。
|
||
- 重新创建所有启动期固化配置的组件,使 Provider、Channel、MCP、Scheduler、Browser、鉴权和上传策略一致地切换。
|
||
- 保持监听 socket,不释放端口,避免切换期间被其他进程抢占。
|
||
- 保持运行期环境变量操作线程安全:热重载不得调用 `std::env::set_var`。
|
||
- 为所有入口提供相同的校验、排队和错误语义。
|
||
|
||
### 2.2 非目标
|
||
|
||
- 不支持热变更监听地址、workspace 或 SQLite 路径。
|
||
- 不承诺 WebSocket 连接无感迁移;切换会主动关闭旧连接,客户端需要重连。
|
||
- 不实现 nginx 式新旧 worker 长时间并行处理连接。PicoBot 是单进程、单 Gateway 运行代模型,采用保留 socket 的顺序切换。
|
||
- 不动态修改已经启动进程的环境变量;`.env` 新值只用于重新解析配置占位符和显式组件配置。
|
||
- 不把“请求已接受”解释为“切换已经完成”。触发方在候选运行代构造成功后收到响应,实际切换在排空阶段之后发生。
|
||
- 不赋予本地 admin token 调用管理 API 的新权限;重载 HTTP API 遵循现有设备鉴权边界。
|
||
|
||
## 3. 核心设计:Gateway 运行代
|
||
|
||
### 3.1 生命周期结构
|
||
|
||
Gateway 进程拥有两层生命周期:
|
||
|
||
```text
|
||
进程生命周期
|
||
├── 固定配置路径
|
||
├── 启动前进程环境快照
|
||
├── 启动 cwd
|
||
├── 原始监听 socket
|
||
├── ReloadController
|
||
└── 当前 Gateway 运行代(可替换)
|
||
├── GatewayState
|
||
├── SessionManager / Session workers
|
||
├── MessageBus / routers / outbound dispatcher
|
||
├── ChannelManager / Channel connections
|
||
├── Scheduler / MCP / tools
|
||
├── AuthManager / UploadRegistry
|
||
├── Axum Router / WebSocket connections
|
||
└── TaskSupervisor
|
||
```
|
||
|
||
进程级资源在 `gateway::run()` 外层只创建一次;运行代资源由 `GatewayState::from_config()` 重新构造。
|
||
|
||
### 3.2 为什么保留监听 socket
|
||
|
||
`gateway::run()` 首次启动时创建一个非阻塞 `std::net::TcpListener`,并在整个进程生命周期内持有它。每个运行代通过 `try_clone()` 获得一个 Tokio listener 交给 Axum。
|
||
|
||
切换时旧 Axum serve future 停止接受连接并退出,但原始 listener 仍然占有地址。旧运行代清理完成后,新运行代再克隆同一个 listener 开始接受连接。这样可以:
|
||
|
||
- 避免重新 bind 失败或端口被其他进程抢占。
|
||
- 保留内核 listen backlog;短暂切换窗口中的新 TCP 连接可能排队等待新运行代接收。
|
||
- 允许 Axum Router、鉴权 middleware 和 WebSocket state 随运行代完整替换。
|
||
|
||
这不是零停顿切换。旧运行代停止和受监督任务回收期间没有 Axum accept loop;当前回收宽限期上限为 10 秒,通常会更短。
|
||
|
||
## 4. 重载控制通道
|
||
|
||
重载协调类型位于 `src/gateway/reload.rs`:
|
||
|
||
- `ReloadHandle`:可克隆的请求端,注入 SessionManager、工具和 Gateway HTTP state。
|
||
- `ReloadController`:由 `gateway::run()` 独占,持有请求 receiver、启动环境快照和启动 cwd。
|
||
- `ReloadRequest`:包含 generation ID 与 oneshot response,用于把候选校验/构造结果返回触发方。
|
||
- `ReloadStatus`:记录 `preparing`、`draining`、`activating`、`active`、`failed` 相位、时间与最近错误。
|
||
|
||
控制通道使用容量为 8 的 Tokio MPSC 队列,并用原子 pending 标记保证同一时间最多只有一次重载。`ReloadHandle::request()` 使用 `try_send`:
|
||
|
||
- 已有重载尚未进入 `active` 或 `failed` 终态时立即返回 `another configuration reload is already pending`。
|
||
- Gateway 正在退出、receiver 已关闭时立即返回 `gateway is shutting down`。
|
||
- 请求成功入队后等待对应 oneshot 结果。
|
||
|
||
有界队列避免错误调用或模型重复调用形成无界重载积压;并发请求不会排队形成连续运行代切换,而是收到明确冲突错误。
|
||
|
||
## 5. 三种触发入口
|
||
|
||
三个入口只负责鉴权、参数适配和结果展示,最终都调用同一个 `ReloadHandle::request()`。
|
||
|
||
| 入口 | 实现 | 行为 |
|
||
|------|------|------|
|
||
| `picobot reload` | `src/main.rs`、`client::reload_gateway()` | 把 WebSocket/HTTP Gateway URL 转为 HTTP base URL,使用已保存的 TUI bearer token 调用 `POST /api/config/reload` |
|
||
| `/reload` | `SessionManager::execute_slash_command()` | 通过普通 slash command 路由执行,不进入 Agent 队列;结果作为 command output 返回当前 Channel |
|
||
| `reload_config` | `ReloadConfigTool` | 仅注册到根交互 Agent;无参数、独占执行,描述要求仅在用户明确要求重载时调用。子 Agent、Cron 和 managed scheduled Agent 无权获得该工具 |
|
||
|
||
`POST /api/config/reload` 返回 accepted generation;`GET /api/config/reload/status` 返回当前重载相位和最近错误。并发 `POST` 返回 409,Gateway 退出或候选准备失败返回 503,配置或不可变字段错误返回 400。
|
||
|
||
HTTP 路由属于现有 protected Router,因此:
|
||
|
||
- pairing 关闭时沿用 `PairingDisabled` 身份。
|
||
- pairing 开启时需要已配对的 bearer/cookie 凭据。
|
||
- 本地 `web_admin_token` 仍只允许用于既有的 loopback WebSocket 特例,不能绕过管理 API 鉴权。
|
||
|
||
pairing 开启但本机尚未保存 TUI token 时,`picobot reload` 会收到 HTTP 401;应先完成现有配对流程,或从一个已认证的聊天/WebUI 连接触发 `/reload`。
|
||
|
||
`GatewayState::new()` 只构造独立 state,没有运行代主循环,因此其中的 reload handle 明确不可用。正式 Gateway 必须通过 `gateway::run()` 启动,才能执行热重载。
|
||
|
||
## 6. 端到端时序
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Caller as CLI / Slash / Tool
|
||
participant RC as ReloadController
|
||
participant Old as Old GatewayState
|
||
participant New as Candidate GatewayState
|
||
participant HTTP as Axum / Listener
|
||
|
||
Caller->>RC: ReloadHandle::request()
|
||
RC->>RC: 重新读取 config + .env
|
||
RC->>RC: 校验 default agent、飞书凭据、不可变字段
|
||
RC->>New: GatewayState::from_config(candidate)
|
||
alt 解析、校验或构造失败
|
||
RC-->>Caller: Error
|
||
Note over Old: 旧运行代继续服务
|
||
else 候选构造成功
|
||
RC->>Old: 关闭 admission,拒绝新工作
|
||
RC-->>Caller: Accepted + generation / 等待当前任务后切换
|
||
RC->>Old: 等待 inbound、Session、Scheduler 与后台 Agent 排空(最多 60s)
|
||
RC->>Old: cancel WebSocket connections
|
||
RC->>HTTP: graceful shutdown 当前 serve future
|
||
RC->>Old: stop_all channels
|
||
RC->>Old: TaskSupervisor shutdown(10s)
|
||
RC->>New: start_all channels + message processing
|
||
RC->>HTTP: 从保留 listener 创建新 serve future
|
||
end
|
||
```
|
||
|
||
### 6.1 准备阶段
|
||
|
||
准备阶段在旧运行代继续提供服务时执行:
|
||
|
||
1. `load_candidate()` 重新读取当前 Gateway 启动时确定的配置文件。
|
||
2. 使用启动环境快照和启动 cwd 解析 `.env`、占位符与相对 workspace。
|
||
3. 校验 default agent 能解析为完整 `LLMProviderConfig`。
|
||
4. 若飞书启用,校验 `app_id` 和 `app_secret` 非空。
|
||
5. 比较不可热变更字段。
|
||
6. 调用 `GatewayState::from_config()` 构造候选运行代。
|
||
|
||
候选构造会重新创建 Storage handle、MemoryManager、MessageBus、ChannelManager、SessionManager、工具集、MCP 配置 wrapper、AuthManager 和 UploadRegistry。外部 Channel、MCP 连接、消息 routers、outbound dispatcher 和 Scheduler 在候选成为当前运行代前不会激活。`from_config(..., false)` 也不会再次修改进程 cwd 或释放默认配置文件。
|
||
|
||
MCP 的真实连接位于激活阶段,避免准备候选时启动双份 stdio 子进程或提前覆盖进程级 `MCP_SERVER_STATUS`。单个 MCP Server 连接失败沿用启动语义:记录错误并跳过其工具,不会让整个运行代激活失败。
|
||
|
||
SessionManager 构造期注册的通知消费者和周期清理任务已经归候选 `TaskSupervisor` 所有,但在激活前没有候选消息入口;周期清理也会跳过首次 interval tick。
|
||
|
||
### 6.2 接受响应
|
||
|
||
候选运行代构造成功并关闭旧代 admission 后,Controller 通过 oneshot 返回 generation 与消息:
|
||
|
||
```text
|
||
配置校验通过;Gateway 将在当前任务结束后切换到新配置。
|
||
```
|
||
|
||
此响应表示候选配置已经通过准备阶段,不表示切换完成。先返回响应有两个原因:
|
||
|
||
- `/reload` 的 command output 需要通过旧运行代发送给用户。
|
||
- `reload_config` 工具需要返回 tool result,让发起它的 Agent Turn 正常完成。
|
||
|
||
### 6.3 排空阶段
|
||
|
||
每个运行代有一个 `RuntimeAdmission`。Inbound 在进入会话 lane 前获取 activity guard,因此已经进入 lane 的消息也计入排空;关闭 admission 后新消息不再进入会话处理,并尽量收到“正在重新加载”提示。`/reload` 的 command output 使用 outbound delivery acknowledgement,guard 只有在回复实际投递成功或明确失败后才释放,不再依赖固定 sleep。
|
||
|
||
`SessionManager::wait_until_idle()` 同时检查所有已加载 Session:
|
||
|
||
- `current_cancel.is_some()` 表示当前有 Agent Turn 正在执行。
|
||
- Session MPSC sender 的剩余容量小于最大容量,表示仍有排队任务。
|
||
- 必须连续空闲 100ms 才认为稳定,避免 worker 刚取出任务、尚未设置 `current_cancel` 的竞态窗口。
|
||
|
||
Scheduler 在领取和执行任务前检查 admission,已执行任务持有 guard 到结果提交与投递完成;后台子 Agent 同样持有 guard。最长统一等待 60 秒。超时不会撤销已经接受的重载,而是记录 warning 并继续回收旧运行代;未完成工作随后会被取消。
|
||
|
||
Axum 自身会优雅等待已进入 handler 的 HTTP 请求;其 graceful shutdown 另有 10 秒硬上限,超过后 abort serve task。未纳入 admission 的维护型后台任务由 `TaskSupervisor` 的 10 秒有界关停负责。
|
||
|
||
### 6.4 切换与回收阶段
|
||
|
||
切换顺序是:
|
||
|
||
1. 取消旧 `connection_shutdown`,使 WebSocket handler 主动退出。
|
||
2. 取消当前 Axum generation shutdown token,停止接受新请求并等待已进入的请求完成。
|
||
3. 调用旧 `ChannelManager::stop_all()`,停止外部消息入口。
|
||
4. 取消旧 `TaskSupervisor`,最多等待 10 秒,超时任务被 abort。
|
||
5. 把候选 state 设为当前 state。
|
||
6. 启动候选 Channel、MCP、消息处理循环、outbound dispatcher 和 Scheduler。
|
||
7. 从原始 listener 克隆新 Tokio listener,构建并运行新 Axum Router。
|
||
|
||
旧 WebSocket 不跨运行代迁移。TUI/WebUI 重连后按原有 client scope 从 SQLite 恢复 dialog 和历史;运行中的内存 Session 不直接搬迁到新 SessionManager。
|
||
|
||
## 7. 配置与环境变量语义
|
||
|
||
### 7.1 启动加载
|
||
|
||
正常启动使用以下优先级解析配置:
|
||
|
||
```text
|
||
进程启动环境 > workspace/.env > config目录/.env
|
||
```
|
||
|
||
启动时合并的 `.env` 值会在单线程阶段写入进程环境,供后续 MCP 和工具子进程继承。
|
||
|
||
### 7.2 热重载加载
|
||
|
||
Gateway 在首次调用 `Config::load_from()` 前保存:
|
||
|
||
- 原始进程环境 `startup_process_env`。
|
||
- 切换 workspace 前的 `startup_cwd`。
|
||
|
||
热重载调用 `Config::load_for_reload()`:
|
||
|
||
- 重新读取 config 目录 `.env` 和 workspace `.env`。
|
||
- 继续以原始启动环境作为最高优先级,避免启动时注入进程环境的旧 `.env` 值错误覆盖新文件。
|
||
- 相对 `workspace_dir` 始终相对于启动 cwd 解析,不受 Gateway 已经 `chdir(workspace)` 影响。
|
||
- 只解析得到候选配置,不调用 `env::set_var`,避免多线程进程中修改全局环境。
|
||
|
||
因此,`.env` 的修改会影响配置中的 `<VAR_NAME>` 占位符和由配置显式传入的新组件。它不会改变现有进程环境;仅依赖继承环境、但没有通过配置显式传值的 Shell/子进程仍会看到启动时环境。需要变更这类继承环境时应完整重启 Gateway。
|
||
|
||
## 8. 热重载边界
|
||
|
||
### 8.1 可通过新运行代生效的配置
|
||
|
||
以下配置消费者会随 `GatewayState` 重建:
|
||
|
||
| 配置区域 | 新运行代中的效果 |
|
||
|----------|------------------|
|
||
| `providers`、`models`、`agents` | 新 SessionManager、Session、主 Agent、子 Agent 和 Scheduler Agent 使用新 Provider/模型参数 |
|
||
| `channels` | ChannelManager 重新创建并启动已启用 Channel,allowlist、凭据、媒体和实时投递策略更新 |
|
||
| `mcp` | 重新连接 MCP Server,并重新生成工具注册表 |
|
||
| `browser` | 根据新配置注册或移除 Browser 工具 |
|
||
| `memory` | 重建 MemoryManager;维护任务使用新的 retention 配置 |
|
||
| `gateway.scheduler` | 启用、关闭或按新并发/轮询/超时参数创建 Scheduler |
|
||
| `gateway.max_concurrent_background_tasks` | 新 SubAgentManager 使用新的并发上限 |
|
||
| `gateway.file_transfer` | 新 UploadRegistry 和 WebSocket capability 使用新限制 |
|
||
| `gateway.require_pairing` | 新 Router/AuthManager 使用新鉴权要求;已有 WebSocket 在切换时断开 |
|
||
|
||
配置中尚未被运行时代码消费的字段,在热重载后仍然不会产生功能效果。例如当前 `session_ttl_hours` 和 `cleanup_interval_minutes` 只完成了解析,尚未接入 Session 清理逻辑。
|
||
|
||
`client.gateway_url` 是 CLI 侧配置:`picobot reload` 在发请求前读取它来确定目标 Gateway,但它不是 Gateway 运行代设置。
|
||
|
||
### 8.2 必须完整重启的配置
|
||
|
||
| 字段 | 原因 |
|
||
|------|------|
|
||
| `gateway.host`、`gateway.port` | 原始监听 socket 在进程生命周期内固定;热重载不会重新 bind |
|
||
| `workspace_dir` | Gateway 已修改进程 cwd,工具路径、媒体路径和相对文件语义均依赖它 |
|
||
| `gateway.session_db_path` | Storage、SessionManager、Scheduler 和历史恢复必须共享同一个数据库身份 |
|
||
| 未显式进入配置组件的继承环境变量 | 热重载禁止运行期修改进程全局环境 |
|
||
|
||
候选值与当前值不一致时,`load_candidate()` 返回错误,并明确提示重启 Gateway。`workspace_dir` 在比较前按启动 cwd 解析并 canonicalize;通过校验后会被归一化为当前绝对 workspace 路径,避免候选构造时受当前 cwd 影响。
|
||
|
||
## 9. 原子性与失败语义
|
||
|
||
这里的“原子”是指请求处理组件的配置可见性:旧运行代不会被逐项改造成半套新配置。它不是数据库事务,也不是两个 worker 的瞬时指针交换;进程级 MCP status 的提前更新是下文记录的已知例外。
|
||
|
||
| 失败阶段 | 行为 |
|
||
|----------|------|
|
||
| 已有 pending 重载/控制器关闭 | 分别返回 409/503,不读取配置 |
|
||
| JSON、`.env`、占位符或默认 Agent 校验失败 | 返回错误,旧运行代保持不变 |
|
||
| 不可热变更字段发生变化 | 返回 restart-required 错误,旧运行代保持不变 |
|
||
| `GatewayState::from_config()` 构造失败 | 返回错误;候选被丢弃,其 TaskSupervisor 随对象释放取消;旧请求处理运行代保持不变 |
|
||
| 单个 MCP Server 连接或工具发现失败 | 记录 MCP 失败状态,候选继续构造且不注册该 Server 的工具;这不视为整体 reload 失败 |
|
||
| 等待交互空闲超过 60 秒 | 记录 warning,继续切换,旧运行中的剩余工作可能被取消 |
|
||
| 旧 Channel 停止失败 | 记录 error,继续回收其他组件和切换 |
|
||
| 旧受监督任务 10 秒内未退出 | TaskSupervisor abort 剩余任务,继续切换 |
|
||
| 候选激活阶段 `start_all()` 失败 | `gateway::run()` 返回错误;若由 systemd 管理,则按 service restart policy 重启 |
|
||
|
||
准备阶段成功后才向调用者返回 accepted。激活阶段仍可能遇到运行时错误,因此调用者不应把 accepted 当作健康检查;可使用 `GET /api/config/reload/status` 等待相同 generation 进入 `active`,并结合 `/health` 与客户端重连确认。
|
||
|
||
WebUI `PUT /api/config` 只负责原子写文件、恢复被掩码的 secret 并返回 `restart_required: true`;它不会隐式触发重载。显式的 `POST /api/config/reload` 将文件写入与运行代切换解耦,使用户可以批量编辑后主动决定生效时机。
|
||
|
||
## 10. 并发与生命周期不变量
|
||
|
||
维护或扩展热重载时必须保持以下约束:
|
||
|
||
1. 只有 `gateway::run()` 拥有 reload receiver 和当前运行代,其他组件只能持有 `ReloadHandle`。
|
||
2. 候选构造不得修改旧 `GatewayState`,也不得提前连接 MCP 或替换旧 MessageBus、Channel、ToolRegistry、MCP status 或 Router。
|
||
3. 不得在热重载路径调用 `env::set_var`;环境文件只允许在单线程首次启动时安装到进程环境。
|
||
4. 不得释放原始 listener 后再尝试 bind 同一地址。
|
||
5. 旧 WebSocket 必须观察 `connection_shutdown`,不能在新鉴权/配置运行代启动后继续无限存活。
|
||
6. 旧长生命周期任务必须由旧 `TaskSupervisor` 回收;候选任务必须由候选 Supervisor 所有。
|
||
7. 排空检查不得长时间持有 `SessionManagerInner` 或 Session mutex;当前实现先复制 Session Arc,再逐一短暂检查。
|
||
8. reload tool 必须保持 exclusive 且只对根交互 Agent 可见,避免后台或子 Agent 触发进程级切换。
|
||
9. admission 必须在会话 lane 入队前获取;Slash 回复、Scheduler job 和后台子 Agent 必须持有 guard 到其持久化/投递边界。
|
||
10. 不可热变更字段的比较必须按有效路径归一化,并发生在候选运行代构造之前。
|
||
11. 新增启动期配置消费者时,应确认它是否由 `GatewayState::from_config()` 重建,并更新本文的热重载边界表。
|
||
|
||
## 11. 关键实现位置
|
||
|
||
| 文件/符号 | 职责 |
|
||
|-----------|------|
|
||
| `src/gateway/reload.rs` | 重载请求通道、候选配置加载、不可变字段校验 |
|
||
| `src/gateway/mod.rs::run` | 持有 listener、当前运行代和切换主循环 |
|
||
| `src/gateway/mod.rs::GatewayState::from_config` | 构造一套完整运行代依赖 |
|
||
| `src/config/mod.rs::Config::load_for_reload` | 使用启动环境/cwd 安全重新解析配置,不修改进程环境 |
|
||
| `src/session/session.rs::wait_until_idle` | 检查活动 Turn、Session 队列和稳定空闲窗口 |
|
||
| `src/session/session.rs::execute_slash_command` | `/reload` 入口 |
|
||
| `src/tools/reload_config.rs` | Agent 可调用的独占重载工具 |
|
||
| `src/gateway/http.rs::reload_config` | 受保护的 `POST /api/config/reload` |
|
||
| `src/client/mod.rs::reload_gateway` | CLI HTTP 客户端与 bearer token 注入 |
|
||
| `src/main.rs::Command::Reload` | `picobot reload` CLI 定义 |
|
||
|
||
## 12. 测试策略
|
||
|
||
当前回归测试覆盖:
|
||
|
||
- 候选配置允许 Provider/模型等运行时字段变化。
|
||
- 相对 `workspace_dir` 按启动 cwd 正确解析。
|
||
- workspace 变化被拒绝且返回明确错误。
|
||
- `None` 与显式指向同一有效数据库路径(默认 `{config_dir}/data/picobot.db`)时允许重载。
|
||
- admission 关闭后拒绝新工作,并等待现有 activity guard 释放。
|
||
- command output 在 dispatcher 明确确认投递前不会释放处理任务。
|
||
- 真实子进程 Gateway 可完成 generation 2 切换;无效候选返回 400 且旧代 `/health` 继续可用。
|
||
- `/reload` alias 能解析到规范命令。
|
||
- 全量 Rust 单元测试验证 Gateway、Session、Channel、鉴权和 TaskSupervisor 既有行为。
|
||
- 离线协议集成测试验证 slash/WebSocket 相关协议没有退化。
|
||
- Clippy、Cargo build 和 WebUI check/build 验证完整构建链。
|
||
|
||
后续适合增加的集成测试:
|
||
|
||
1. 使用可控假 Provider 验证新 model 被下一 Turn 实际使用。
|
||
2. 在长 Turn 中调用 `reload_config`,验证 tool result 和最终消息投递后才断开。
|
||
3. Session 队列有积压时验证重载等待队列排空。
|
||
4. 排空超时、Channel stop 超时和候选激活失败的故障注入。
|
||
5. 重载前后鉴权策略变化以及旧 WebSocket 失效。
|
||
|
||
## 13. 运维使用
|
||
|
||
修改配置并保存后执行:
|
||
|
||
```bash
|
||
picobot reload
|
||
```
|
||
|
||
连接非默认 Gateway:
|
||
|
||
```bash
|
||
picobot reload --gateway-url https://gateway.example.com
|
||
```
|
||
|
||
也可以在支持 slash command 的聊天中发送:
|
||
|
||
```text
|
||
/reload
|
||
```
|
||
|
||
若配置修改涉及监听地址、workspace、数据库路径或必须进入进程继承环境的变量,应执行完整重启:
|
||
|
||
```bash
|
||
picobot service restart
|
||
```
|
||
|
||
建议的运维流程是:
|
||
|
||
1. 原子保存配置文件。
|
||
2. 执行 reload 并检查返回是否为候选已接受。
|
||
3. 使用已认证请求轮询 `GET /api/config/reload/status`,确认返回的 generation 进入 `active`。
|
||
4. 等待客户端重连,检查 `/health` 和关键 Channel。
|
||
5. 若激活失败,由 systemd 重启或人工恢复配置后再次启动。
|
||
|
||
## 14. 已知限制与演进方向
|
||
|
||
- 旧运行代回收与新运行代激活是顺序执行,存在短暂 accept/Channel intake 空窗。
|
||
- WebSocket 需要客户端自行重连,服务端没有连接迁移协议。
|
||
- 候选激活失败没有自动回滚到已经回收的旧运行代,依赖 systemd restart 或人工恢复。
|
||
- 同一时间只允许一个 pending 重载;并发请求返回 409,不做合并或排队。
|
||
- `.env` 不能热修改进程继承环境。
|
||
- admission 覆盖消息入口、Scheduler 与后台子 Agent;其他维护型任务仍依赖 TaskSupervisor 的有界关停。
|
||
|
||
可能的后续演进包括:
|
||
|
||
- 将 Channel 激活前检查拆成显式 `prepare()`,把更多运行时失败提前到旧运行代仍可回退的阶段。
|
||
- 在不破坏 Channel/Session 边界的前提下,引入动态 Router service,实现新旧 HTTP generation 短期重叠。
|
||
- 为 TUI/WebUI 增加 reload 完成通知和自动重连状态提示。
|