01 — Rust Workspace 架构:15 Crate 的职责划分
Rust Workspace 架构:15 Crate 的职责划分
学习目标
理解 CodeWhale 的 Cargo workspace 设计:15 个 crate 的职责划分、依赖方向、默认成员选择与渐进式拆分策略。
项目实践
Workspace 结构
各 Crate 职责
| Crate | 职责 | 代码量 |
|---|---|---|
| cli | 参数解析(clap)、配置加载、入口路由 | 入口点 |
| tui | 完整运行时:引擎、工具、子 Agent、TUI 渲染、HTTP/SSE | ~77K 行 |
| core | Agent 循环、会话管理、轮次编排、容量控制(从 tui 拆分中) | 正在拆分 |
| agent | 模型/provider 注册表,将模型 ID 解析为 provider 端点 | 注册表 |
| config | 配置加载、profile、环境变量优先级、CLI 运行时覆盖 | 配置 |
| tools | 共享工具调用原语,包括工具结果/错误/能力类型 | 原语 |
| mcp | MCP 客户端 + stdio 服务器,外部工具服务器集成 | MCP |
| state | SQLite 持久化层,线程/会话/任务存储 | SQLite |
| execpolicy | 审批/沙箱策略引擎,工具执行决策 | 策略 |
| hooks | 生命周期钩子(stdout、jsonl、webhook)pre/post 工具事件 | 钩子 |
| protocol | 请求/响应框架和协议类型 | 类型 |
| secrets | OS 密钥环集成,API 密钥存储 | 密钥 |
| tui-core | 事件驱动 TUI 状态机脚手架 | 脚手架 |
| app-server | HTTP/SSE + JSON-RPC 应用服务器传输 | 传输 |
| release | 发布工具,二进制自更新 | 发布 |
依赖方向
cli → tui → core → tools, config, agent, execpolicy, state, protocol → hooks, mcp, secretsapp-server → core, protocol关键约束:
core不依赖tui——引擎独立于 UItools是底层原语——被core和tui共用protocol是最底层——只包含类型定义,无业务逻辑
默认成员策略
default-members = ["crates/cli", "crates/app-server", "crates/tui"]默认构建三个二进制 crate。其余 crate 作为库 crate被依赖,不直接产出二进制。
工作空间依赖统一
[workspace.dependencies]tokio = { version = "1.49.0", features = ["full"] }reqwest = { version = "0.13.1", default-features = false, features = ["json", "rustls", "socks"] }rusqlite = { version = "0.32.1", features = ["bundled"] }axum = { version = "0.8.5", features = ["json"] }所有 crate 共享版本,避免依赖不一致。
Rust 版本约束
rust-version = "1.88"edition = "2024"代码库大量使用 let_chains(if/while 条件中的 let 链式绑定),Rust 1.88 将其稳定化。旧版工具链会报 “feature edition2024 is required” 而非晦涩的 E0658 错误。
问题与规避
陷阱:crate 间循环依赖
当拆分出 core crate 时,需要仔细处理与 tui crate 的依赖方向。如果 core 依赖 tui 中的类型,会形成循环依赖。
规避:
- 共享类型下沉到
protocolcrate - 共享原语下沉到
toolscrate core只依赖protocol、tools、config等底层 crate
陷阱:依赖版本冲突
15 个 crate 使用不同版本的同一依赖会导致构建失败。
规避:workspace.dependencies 统一管理,所有 crate 通过 workspace = true 引用。
陷阱:构建时间过长
全量构建 15 个 crate 可能很慢,尤其是包含 rusqlite(bundled SQLite)编译时。
规避:
- 默认只构建 3 个 binary crate
cargo check增量编译- CI 中利用缓存
设计取舍
为什么选择 Cargo workspace 而非单 crate
- 编译隔离:修改
mcpcrate 不需要重编译corecrate - 职责清晰:每个 crate 有明确的边界
- 渐进拆分:从单 crate 开始,逐步拆分出库 crate,不破坏现有功能
- 发布灵活性:可以独立发布库 crate 到 crates.io
为什么 tui crate 仍然最大(~77K 行)
README 注释明确说明:
“
crates/tui仍然是面向用户的运行时……其他 workspace crate 正在渐进式拆分,但它们尚未成为运行时的唯一真相来源。”
这是一种务实的渐进式重构策略:
- 先拆分出独立的底层 crate(protocol、tools、config)
- 然后拆分中间层(core、agent、mcp)
- 最后 tui 只作为薄层,编排这些 crate
当前处于第 2 阶段进行中。
rusqlite bundled vs 系统 SQLite
选择 bundled feature 的原因:
- 用户不需要预装 SQLite 开发库
- 跨平台一致性
- 代价:编译时间更长(需要编译 SQLite C 代码)
参考来源
Cargo.toml— 工作空间定义与依赖版本docs/ARCHITECTURE.md— 架构概览与 crate 职责描述docs/INSTALL.md— 平台特定的安装说明(含 Rust 版本要求)