Files
DevFlow/docs/02-架构设计/专项设计/架构债迁移设计-2026-06-29.md
绝尘 093fe172f8 新增: 项目状态机分层+任务状态枚举合并
真分层(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)
2026-06-29 22:16:45 +08:00

10 KiB
Raw Blame History

架构债迁移设计方案2026-06-29

创建: 2026-06-29 | 状态: 设计草案 关联: 全量走查报告-2026-06-28 §架构缺陷 #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 glueinvoke_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 层,保留)。

阶段 4src-tauri 仅保留 IPC glue所有 AI 状态机测试可在 df-ai-session 独立运行。

风险

  • 循环依赖agentic_loop 需要 LlmProviderdf-ai-core+ AiToolRegistry。AiToolRegistry 的 handler 依赖 src-tauri 的 storage/commands。解决:把 AiToolRegistry trait 下沉到 df-ai-corehandler 实现留在 src-tauri。
  • 测试覆盖:现有 AI 测试都在 src-tauri 集成测试,迁移后需补 df-ai-session 的单测。

三、#8 — IPC 错误结构化

现状

// 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() 拍平。前端只能拿到字符串,无法分辨错误类型。

目标架构

// 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

{ "kind": "invalid_state", "data": { "entity": "task", "current": "todo", "expected": ["in_progress"] } }

前端消费

// 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(',')}`)
}

迁移路径

阶段 1df-types 新增 IpcError enum + From<df_types::error::Error> for IpcError

阶段 2src-tauri IPC 命令逐个从 Result<T, String> 改为 Result<T, IpcError>保持向后兼容:旧前端读 err.message 时降级为字符串IpcError 实现 Display 输出 message

阶段 3:前端 api 层加 IpcError 类型 + 在关键 viewProjects/Tasks/Ideas按 kind 分支处理。

阶段 4:删除 err_str 辅助函数。

风险

  • Tauri invoke 错误序列化Tauri 默认把 Result::Err 经 JSON 传前端,但前端 invoke().catch() 拿到的是 stringobject需验证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 的关系
// 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>,
    // ...
}

迁移路径

阶段 1df-types 定义 AppConfig,在 src-tauri 启动时构造。

阶段 2:逐个替换散落的 std::env::varstate.config.xxx保持 env var 作为 override(向后兼容)。

阶段 3:前端 Settings 增加「环境变量覆盖」可视化。

风险

  • df-relay 独立 binaryrelay 是独立进程,不经 src-tauri AppState。解决df-relay 自己构造 RelayConfig(共享 df-types::config 类型)。

五、#19 — SQLite 连接池

现状

// crates/df-storage/src/db.rs:13-16
pub struct Database {
    conn: Arc<Mutex<Connection>>,   // 单连接,所有读写串行
}

注释自承 TODO: 考虑使用 r2d2 连接池替代单连接 Mutex。AI 对话高峰(多 tool_calls 并发 + SSE 落库)下排队。

目标架构

pub struct Database {
    pool: r2d2::Pool<SqliteConnectionManager>,
}

impl Database {
    pub fn conn(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>> {
        self.pool.get().map_err(...)
    }
}

迁移路径

阶段 1df-storage 加 r2d2 + r2d2_sqlite 依赖,改 Database 内部为 pool。

阶段 2:所有 Repo 的 conn.blocking_lock() 改为 pool.get()?spawn_blocking 内获取连接)。

阶段 3WAL 模式 + 合理 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 连接池 — 性能瓶颈出现时再改(当前单用户桌面应用未必触发)