真分层(df-project): - ProjectManager 加 can_transition/transition/is_terminal 状态机 - 创建加 EmptyName 校验,返回 Result 而非裸实体 - 新增 ProjectTransitionError 三种错误变体 - 调用方(idea.rs promote_idea)适配 Result 传播 双源合并: - TaskStatus::as_str 改为 const fn(允许 const 上下文调用) - task_state_machine 字符串常量从 TaskStatus enum 派生 - IdeaStatus/ProjectStatus as_str 同步改为 const fn(一致性) 架构债设计方案: - #8 IPC 错误结构化迁移路径(IpcError enum + 前端消费) - #15 AI 状态机抽离 df-ai-session(4 阶段) - #18 配置四源统一(AppConfig 单例 + 环境变量覆盖) - #19 SQLite 连接池(r2d2)
268 lines
10 KiB
Markdown
268 lines
10 KiB
Markdown
# 架构债迁移设计方案(2026-06-29)
|
||
|
||
> 创建: 2026-06-29 | 状态: 设计草案
|
||
> 关联: [全量走查报告-2026-06-28](../05-代码审查/全量走查报告-2026-06-28.md) §架构缺陷 #4/#6/#8 + SQLite 单连接 #14
|
||
> 适用范围: 本次推进剩余项 #8/#15/#18/#19 的迁移路径设计
|
||
|
||
---
|
||
|
||
## 一、概览
|
||
|
||
走查报告识别出 4 项架构债,影响可维护性或扩展性。本设计为每项给出**目标架构 + 迁移路径 + 风险评估**,**不在本批次实施代码**(每项都需独立 PR + e2e 验证)。
|
||
|
||
| 编号 | 问题 | 风险 | 建议时机 |
|
||
|------|------|------|---------|
|
||
| #15 | AI 核心状态机焊死 src-tauri | 高(重构 agentic loop) | Phase 4 之前 |
|
||
| #8 | IPC 错误拍平为 String | 中(前后端协议改造) | 下个迭代 |
|
||
| #18 | 配置四源并存 | 中(跨 crate 改读) | 与 #15 协同 |
|
||
| #19 | SQLite 单连接 Mutex | 中(DB 层基础) | 性能瓶颈出现时 |
|
||
|
||
---
|
||
|
||
## 二、#15 — AI 核心状态机抽离
|
||
|
||
### 现状
|
||
|
||
```
|
||
src-tauri/src/commands/ai/
|
||
├── mod.rs ← AiSession / PerConvState / AiChatEvent / SessionState(应属 df-ai)
|
||
├── agentic/mod.rs ← run_agentic_loop 主循环(应属 df-ai)
|
||
├── commands.rs ← IPC 层(应留在 src-tauri)
|
||
├── tool_registry ← 工具注册(IPC 层依赖,留在 src-tauri)
|
||
└── ...
|
||
|
||
crates/df-ai/src/
|
||
├── provider.rs ← LlmProvider trait(正确)
|
||
├── openai_compat.rs ← HTTP(正确)
|
||
├── anthropic_compat.rs← HTTP(正确)
|
||
├── coordinator.rs ← 空壳(应承担 agentic loop)
|
||
└── ...
|
||
```
|
||
|
||
### 目标架构
|
||
|
||
```
|
||
crates/df-ai-session/ ← 新 crate(避免 df-ai 引 reqwest 同时引会话状态)
|
||
├── session.rs ← AiSession / PerConvState / SessionState
|
||
├── agentic_loop.rs ← run_agentic_loop(纯逻辑,依赖 LlmProvider trait)
|
||
├── events.rs ← AiChatEvent enum
|
||
└── approvals.rs ← PendingApproval / ApprovalKind
|
||
|
||
src-tauri/src/commands/ai/
|
||
├── mod.rs ← 仅 IPC glue(invoke_handler 注册 + AppState 转发)
|
||
├── commands.rs ← ai_send_message / ai_approve / ai_reject
|
||
└── tool_registry.rs ← 工具注册(仍依赖 df-storage / df-types)
|
||
```
|
||
|
||
### 迁移路径(4 阶段)
|
||
|
||
**阶段 1**:创建 `df-ai-session` crate,仅迁移类型定义(`AiSession` / `AiChatEvent` 等),不改逻辑。src-tauri 改为 re-export。
|
||
|
||
**阶段 2**:迁移 `run_agentic_loop` 函数体。保持现有调用方接口不变(`pub async fn run_agentic_loop(...)`),仅改归属。
|
||
|
||
**阶段 3**:把 tool_registry 中的工具注册逻辑拆分为「工具定义」(可迁移)和「工具 handler」(依赖 IPC 层,保留)。
|
||
|
||
**阶段 4**:src-tauri 仅保留 IPC glue,所有 AI 状态机测试可在 df-ai-session 独立运行。
|
||
|
||
### 风险
|
||
|
||
- **循环依赖**:`agentic_loop` 需要 LlmProvider(df-ai-core)+ AiToolRegistry。AiToolRegistry 的 handler 依赖 src-tauri 的 storage/commands。**解决**:把 AiToolRegistry trait 下沉到 df-ai-core,handler 实现留在 src-tauri。
|
||
- **测试覆盖**:现有 AI 测试都在 src-tauri 集成测试,迁移后需补 df-ai-session 的单测。
|
||
|
||
---
|
||
|
||
## 三、#8 — IPC 错误结构化
|
||
|
||
### 现状
|
||
|
||
```rust
|
||
// src-tauri/src/commands/mod.rs:26
|
||
pub fn err_str<E: ToString>(e: E) -> String { e.to_string() }
|
||
|
||
// 所有 IPC 命令
|
||
async fn xxx() -> Result<T, String> { ... .map_err(err_str) }
|
||
```
|
||
|
||
df-types/error.rs 的 11 个变体(`NotFound`/`Validation`/`InvalidState { current, expected }`)在 IPC 边界全部 `.to_string()` 拍平。前端只能拿到字符串,无法分辨错误类型。
|
||
|
||
### 目标架构
|
||
|
||
```rust
|
||
// df-types/src/error.rs 新增 Serialize
|
||
#[derive(Debug, Serialize, Deserialize)]
|
||
#[serde(tag = "kind", content = "data")]
|
||
pub enum IpcError {
|
||
NotFound { entity: String, id: String },
|
||
Validation { field: String, reason: String },
|
||
InvalidState { entity: String, current: String, expected: Vec<String> },
|
||
Unauthorized { reason: String },
|
||
Internal { message: String },
|
||
}
|
||
|
||
// IPC 命令改为
|
||
async fn xxx() -> Result<T, IpcError> { ... }
|
||
```
|
||
|
||
Tauri invoke 自动 serialize 为 JSON:
|
||
```json
|
||
{ "kind": "invalid_state", "data": { "entity": "task", "current": "todo", "expected": ["in_progress"] } }
|
||
```
|
||
|
||
### 前端消费
|
||
|
||
```ts
|
||
// src/api/types.ts
|
||
type IpcError =
|
||
| { kind: 'not_found'; data: { entity: string; id: string } }
|
||
| { kind: 'validation'; data: { field: string; reason: string } }
|
||
| { kind: 'invalid_state'; data: { entity: string; current: string; expected: string[] } }
|
||
| { kind: 'unauthorized'; data: { reason: string } }
|
||
| { kind: 'internal'; data: { message: string } }
|
||
|
||
// 使用
|
||
try { await invoke(...) }
|
||
catch (e: unknown) {
|
||
const err = e as IpcError
|
||
if (err.kind === 'not_found') router.push('/projects')
|
||
else if (err.kind === 'invalid_state') toast.warning(`当前 ${err.data.current},期望 ${err.data.expected.join(',')}`)
|
||
}
|
||
```
|
||
|
||
### 迁移路径
|
||
|
||
**阶段 1**:df-types 新增 `IpcError` enum + `From<df_types::error::Error> for IpcError`。
|
||
|
||
**阶段 2**:src-tauri IPC 命令逐个从 `Result<T, String>` 改为 `Result<T, IpcError>`。**保持向后兼容**:旧前端读 `err.message` 时降级为字符串(IpcError 实现 `Display` 输出 message)。
|
||
|
||
**阶段 3**:前端 api 层加 `IpcError` 类型 + 在关键 view(Projects/Tasks/Ideas)按 kind 分支处理。
|
||
|
||
**阶段 4**:删除 `err_str` 辅助函数。
|
||
|
||
### 风险
|
||
|
||
- **Tauri invoke 错误序列化**:Tauri 默认把 `Result::Err` 经 JSON 传前端,但前端 `invoke().catch()` 拿到的是 `string` 或 `object`?**需验证**:Tauri v2 对自定义 error 类型的序列化行为。
|
||
- **向后兼容**:旧前端代码 `e?.toString()` 需仍工作。IpcError 的 Display 实现保证字符串友好。
|
||
|
||
---
|
||
|
||
## 四、#18 — 配置四源统一
|
||
|
||
### 现状(散落 env::var 读取)
|
||
|
||
| 读取点 | 用途 | 性质 |
|
||
|--------|------|------|
|
||
| `df-relay/src/main.rs:17` `DF_RELAY_ADDR` | relay 监听地址 | 启动配置 |
|
||
| `df-relay/src/relay.rs:38` `DF_RELAY_TOKEN` | relay 鉴权 | 启动配置(panic 兜底) |
|
||
| `df-nodes/src/script_node.rs:141,155` `DF_SCRIPT_BLACKLIST`/`WHITELIST` | 脚本策略 | 运行时读取 |
|
||
| `df-nodes/src/ai_node.rs:389-398` `GLM_BASE_URL`/`GLM_API_KEY`/`GLM_MODEL` | AINode 兜底 provider | 运行时读取 |
|
||
| `src-tauri/src/commands/ai/tool_registry.rs:2780` `DEVFLOW_ENV_PROBE_ENABLED` | 环境探测开关 | 运行时读取 |
|
||
| `src-tauri/src/main.rs:116-121` `APPDATA`/`HOME` | OS 路径 | 系统变量 |
|
||
| `df-execute/src/env_snapshot.rs:216,253` `SHELL`/`LANG` | Shell 探测 | 系统变量 |
|
||
|
||
### 目标架构
|
||
|
||
**不强行统一所有读取**(OS 路径 / Shell 探测等系统变量性质不同),而是:
|
||
|
||
1. **df-types 新增 `AppConfig` struct**(集中定义项目级配置项)
|
||
2. **启动时一次性读取 env + DB KV**,构造 `AppConfig` 实例
|
||
3. **AppState 持有 `Arc<AppConfig>`**,各模块经 state 读取
|
||
4. **文档化配置矩阵**:env / DB KV / 默认值 / panic 的关系
|
||
|
||
```rust
|
||
// df-types/src/config.rs
|
||
pub struct AppConfig {
|
||
pub relay: RelayConfig,
|
||
pub script_policy: ScriptPolicyConfig,
|
||
pub ai_node: AiNodeConfig,
|
||
pub env_probe: bool,
|
||
}
|
||
|
||
pub struct RelayConfig {
|
||
pub addr: String, // 默认 "127.0.0.1:8080"
|
||
pub token: String, // 缺失 panic
|
||
}
|
||
|
||
pub struct ScriptPolicyConfig {
|
||
pub blacklist: Vec<String>, // 默认 ["rm","del","format","shutdown","mkfs","dd"]
|
||
pub whitelist: Vec<String>, // 默认 []
|
||
}
|
||
|
||
// src-tauri/src/state.rs
|
||
pub struct AppState {
|
||
pub config: Arc<AppConfig>,
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 迁移路径
|
||
|
||
**阶段 1**:df-types 定义 `AppConfig`,在 src-tauri 启动时构造。
|
||
|
||
**阶段 2**:逐个替换散落的 `std::env::var` 为 `state.config.xxx`。**保持 env var 作为 override**(向后兼容)。
|
||
|
||
**阶段 3**:前端 Settings 增加「环境变量覆盖」可视化。
|
||
|
||
### 风险
|
||
|
||
- **df-relay 独立 binary**:relay 是独立进程,不经 src-tauri AppState。**解决**:df-relay 自己构造 `RelayConfig`(共享 df-types::config 类型)。
|
||
|
||
---
|
||
|
||
## 五、#19 — SQLite 连接池
|
||
|
||
### 现状
|
||
|
||
```rust
|
||
// crates/df-storage/src/db.rs:13-16
|
||
pub struct Database {
|
||
conn: Arc<Mutex<Connection>>, // 单连接,所有读写串行
|
||
}
|
||
```
|
||
|
||
注释自承 `TODO: 考虑使用 r2d2 连接池替代单连接 Mutex`。AI 对话高峰(多 tool_calls 并发 + SSE 落库)下排队。
|
||
|
||
### 目标架构
|
||
|
||
```rust
|
||
pub struct Database {
|
||
pool: r2d2::Pool<SqliteConnectionManager>,
|
||
}
|
||
|
||
impl Database {
|
||
pub fn conn(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>> {
|
||
self.pool.get().map_err(...)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 迁移路径
|
||
|
||
**阶段 1**:df-storage 加 `r2d2` + `r2d2_sqlite` 依赖,改 `Database` 内部为 pool。
|
||
|
||
**阶段 2**:所有 Repo 的 `conn.blocking_lock()` 改为 `pool.get()?`(spawn_blocking 内获取连接)。
|
||
|
||
**阶段 3**:WAL 模式 + 合理 pool_size(桌面应用 4-8 即可)。
|
||
|
||
### 风险
|
||
|
||
- **WAL 已开**(`db.rs:22 PRAGMA journal_mode=WAL`),读写不互斥,pool 能真正并行。
|
||
- **事务原子性**:现有批量插入用 `unchecked_transaction()`,pool 模式下需确认 `PooledConnection` 的 transaction 行为一致。
|
||
- **测试改造**:所有 Repo 测试的 `Database::open_in_memory()` 需改为 pool 模式。
|
||
|
||
---
|
||
|
||
## 六、本批次已落地(参考)
|
||
|
||
| 编号 | 改动 | 文件 |
|
||
|------|------|------|
|
||
| #17 | TaskStatus 字符串常量从 enum 派生(`as_str` 改 const fn) | `crates/df-types/src/types.rs` + `crates/df-nodes/src/task_state_machine.rs` |
|
||
| #16 | df-project 真分层(加状态机 + 业务约束 + 测试) | `crates/df-project/src/manager.rs` + `src-tauri/src/commands/idea.rs` |
|
||
| #18 部分 | 文档化配置矩阵(本设计文档) | 本文件 |
|
||
|
||
## 七、推进顺序建议
|
||
|
||
1. **#8 IPC 错误结构化** — 风险中等,收益直接(前端可差异化处理)
|
||
2. **#15 AI 状态机抽离** — 风险高但收益最大(解锁 CLI/独立测试),建议在 Phase 4 之前
|
||
3. **#18 配置统一** — 与 #15 协同(新 crate 也需要读配置)
|
||
4. **#19 连接池** — 性能瓶颈出现时再改(当前单用户桌面应用未必触发)
|