diff --git a/Cargo.lock b/Cargo.lock index b448a3c..c0db3f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -969,6 +969,7 @@ dependencies = [ "df-types", "serde", "serde_json", + "thiserror 2.0.18", "tokio", "tracing", ] diff --git a/crates/df-nodes/src/task_state_machine.rs b/crates/df-nodes/src/task_state_machine.rs index 02e8f80..0458e9c 100644 --- a/crates/df-nodes/src/task_state_machine.rs +++ b/crates/df-nodes/src/task_state_machine.rs @@ -22,27 +22,29 @@ //! - cancelled → 终态 // ============================================================ -// 状态字符串常量 — 与 df-types::TaskStatus::as_str 一一对应 +// 状态字符串常量 — 从 df-types::TaskStatus::as_str 派生(单一真相源) // ============================================================ // -// 不复用 df-types::TaskStatus enum(独立模块定位 + 避免推进链判定耦合存储枚举类型), -// 但字符串值严格对齐(df-types::TaskStatus::as_str 产出的小写 snake_case), -// 保证状态机判定的 from/to 与数据库 status 列存值语义一致。 +// 任务 #17 合并:字符串常量不再独立定义,直接从 `TaskStatus::as_str()` 派生。 +// 消除 df-nodes 字符串常量与 df-types enum 之间的双源问题—— 任一处修改 enum 的 +// as_str 输出,本模块常量自动同步,编译期即可发现柡移。 + +use df_types::types::TaskStatus; /// 待开始 -pub const TODO: &str = "todo"; +pub const TODO: &str = TaskStatus::Todo.as_str(); /// 进行中 -pub const IN_PROGRESS: &str = "in_progress"; +pub const IN_PROGRESS: &str = TaskStatus::InProgress.as_str(); /// 代码审查中 -pub const IN_REVIEW: &str = "in_review"; +pub const IN_REVIEW: &str = TaskStatus::InReview.as_str(); /// 测试中 -pub const TESTING: &str = "testing"; +pub const TESTING: &str = TaskStatus::Testing.as_str(); /// 已完成(终态) -pub const DONE: &str = "done"; +pub const DONE: &str = TaskStatus::Done.as_str(); /// 已阻塞 -pub const BLOCKED: &str = "blocked"; +pub const BLOCKED: &str = TaskStatus::Blocked.as_str(); /// 已取消(终态) -pub const CANCELLED: &str = "cancelled"; +pub const CANCELLED: &str = TaskStatus::Cancelled.as_str(); /// 全部合法状态值(供输入校验与错误提示复用) pub const ALL_STATES: &[&str] = &[ diff --git a/crates/df-project/Cargo.toml b/crates/df-project/Cargo.toml index 32d8efa..4958250 100644 --- a/crates/df-project/Cargo.toml +++ b/crates/df-project/Cargo.toml @@ -11,3 +11,4 @@ tokio = { workspace = true } anyhow = { workspace = true } chrono = { workspace = true } tracing = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/df-project/src/manager.rs b/crates/df-project/src/manager.rs index 7cf9b3f..d3577e0 100644 --- a/crates/df-project/src/manager.rs +++ b/crates/df-project/src/manager.rs @@ -1,4 +1,10 @@ -//! 项目管理器 — 项目的 CRUD 与生命周期管理 +//! 项目管理器 — 项目的领域层(CRUD 构造 + 状态机 + 业务约束) +//! +//! 任务 #16 真分层:本 crate 不再只是"构造实体的工厂函数",而是承载项目领域规则。 +//! Storage 层(df-storage::ProjectRepo)仅负责持久化,状态合法性 / 业务约束在此。 +//! +//! 调用方(src-tauri commands/project.rs)推进项目状态时必须经 +//! `ProjectManager::can_transition` / `transition` 校验,防非法跳态。 use serde::{Deserialize, Serialize}; @@ -39,15 +45,32 @@ pub struct CreateProjectInput { pub tags: Vec, } -/// 项目管理器 +/// 项目状态机错误 +#[derive(Debug, thiserror::Error)] +pub enum ProjectTransitionError { + #[error("项目状态转换非法: {from:?} → {to:?}")] + IllegalTransition { + from: ProjectStatus, + to: ProjectStatus, + }, + #[error("项目名称不能为空")] + EmptyName, + #[error("项目已处在终态 {0:?},不可再推进")] + TerminalState(ProjectStatus), +} + +/// 项目管理器(无状态纯逻辑,实体构造 + 状态机 + 业务约束) pub struct ProjectManager; impl ProjectManager { /// 创建新项目 — 构造领域实体(不落库);持久化由调用方经 storage 层 ProjectRecord 映射完成 /// (见 commands/idea.rs::promote_idea)。领域层不依赖 storage,保持分层。 - pub fn create(input: CreateProjectInput) -> Project { + pub fn create(input: CreateProjectInput) -> Result { + if input.name.trim().is_empty() { + return Err(ProjectTransitionError::EmptyName); + } let now = chrono::Utc::now(); - Project { + Ok(Project { id: df_types::types::new_id(), name: input.name, description: input.description, @@ -57,11 +80,15 @@ impl ProjectManager { tags: input.tags, created_at: now, updated_at: now, - } + }) } /// 从想法创建项目 - pub fn create_from_idea(name: String, description: String, idea_id: IdeaId) -> Project { + pub fn create_from_idea( + name: String, + description: String, + idea_id: IdeaId, + ) -> Result { Self::create(CreateProjectInput { name, description, @@ -71,4 +98,118 @@ impl ProjectManager { }) } + /// 判断项目状态转换是否合法(状态机核心)。 + /// + /// 项目状态语义(与 ARCHITECTURE.md 一致): + /// - Planning → InProgress(开始), Cancelled(取消) + /// - InProgress → Testing(提交测试), Paused(暂停), Cancelled + /// - Testing → Completed(测试通过), InProgress(退回开发), Cancelled + /// - Completed → 终态(不可变) + /// - Paused → InProgress(恢复), Cancelled + /// - Cancelled → 终态 + pub fn can_transition(from: &ProjectStatus, to: &ProjectStatus) -> bool { + use ProjectStatus::*; + matches!((from, to), + (Planning, InProgress) | (Planning, Cancelled) + | (InProgress, Testing) | (InProgress, Paused) | (InProgress, Cancelled) + | (Testing, Completed) | (Testing, InProgress) | (Testing, Cancelled) + | (Paused, InProgress) | (Paused, Cancelled) + ) + } + + /// 执行状态转换,返回新状态或非法错误。 + /// + /// 调用方(如 IPC `update_project_status` / `advance_project`)应用本方法校验后 + /// 再写 storage,防跳态(如 Planning → Completed 跳过 Testing)。 + pub fn transition( + from: ProjectStatus, + to: ProjectStatus, + ) -> Result { + if !Self::can_transition(&from, &to) { + // 区分错误: 终态→任何 vs 一般非法 + if matches!(from, ProjectStatus::Completed | ProjectStatus::Cancelled) { + return Err(ProjectTransitionError::TerminalState(from)); + } + return Err(ProjectTransitionError::IllegalTransition { from, to }); + } + Ok(to) + } + + /// 是否为终态(不可再转换) + pub fn is_terminal(s: &ProjectStatus) -> bool { + matches!(s, ProjectStatus::Completed | ProjectStatus::Cancelled) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_rejects_empty_name() { + let err = ProjectManager::create(CreateProjectInput { + name: " ".into(), + description: String::new(), + idea_id: None, + priority: Priority::default(), + tags: vec![], + }).unwrap_err(); + assert!(matches!(err, ProjectTransitionError::EmptyName)); + } + + #[test] + fn main_path_planning_to_completed() { + use ProjectStatus::*; + assert!(ProjectManager::can_transition(&Planning, &InProgress)); + assert!(ProjectManager::can_transition(&InProgress, &Testing)); + assert!(ProjectManager::can_transition(&Testing, &Completed)); + } + + #[test] + fn illegal_skips_rejected() { + use ProjectStatus::*; + assert!(!ProjectManager::can_transition(&Planning, &Completed)); + assert!(!ProjectManager::can_transition(&Planning, &Testing)); + assert!(!ProjectManager::can_transition(&InProgress, &Completed)); + } + + #[test] + fn terminal_states_block_all() { + use ProjectStatus::*; + for term in [Completed, Cancelled] { + for to in [Planning, InProgress, Testing, Paused] { + assert!(!ProjectManager::can_transition(&term, &to)); + } + } + } + + #[test] + fn paused_round_trip() { + use ProjectStatus::*; + assert!(ProjectManager::can_transition(&InProgress, &Paused)); + assert!(ProjectManager::can_transition(&Paused, &InProgress)); + } + + #[test] + fn transition_returns_target_on_legal() { + use ProjectStatus::*; + assert_eq!( + ProjectManager::transition(Planning, InProgress).unwrap(), + InProgress + ); + } + + #[test] + fn transition_errors_on_illegal() { + use ProjectStatus::*; + let err = ProjectManager::transition(Planning, Completed).unwrap_err(); + assert!(matches!(err, ProjectTransitionError::IllegalTransition { .. })); + } + + #[test] + fn transition_errors_on_terminal_source() { + use ProjectStatus::*; + let err = ProjectManager::transition(Completed, InProgress).unwrap_err(); + assert!(matches!(err, ProjectTransitionError::TerminalState(_))); + } } diff --git a/crates/df-types/src/types.rs b/crates/df-types/src/types.rs index e8ec8fd..6a03ba8 100644 --- a/crates/df-types/src/types.rs +++ b/crates/df-types/src/types.rs @@ -72,7 +72,9 @@ pub enum IdeaStatus { impl IdeaStatus { /// 返回数据库存储用的小写字符串 - pub fn as_str(&self) -> &'static str { + /// + /// `const fn`(与 TaskStatus::as_str 对齐,允许调用方在 const 上下文使用)。 + pub const fn as_str(&self) -> &'static str { match self { IdeaStatus::Draft => "draft", IdeaStatus::PendingReview => "pending_review", @@ -128,7 +130,9 @@ pub enum ProjectStatus { impl ProjectStatus { /// 返回数据库存储用的小写字符串 - pub fn as_str(&self) -> &'static str { + /// + /// `const fn`(与 TaskStatus::as_str 对齐,允许调用方在 const 上下文使用)。 + pub const fn as_str(&self) -> &'static str { match self { ProjectStatus::Planning => "planning", ProjectStatus::InProgress => "in_progress", @@ -186,7 +190,10 @@ pub enum TaskStatus { impl TaskStatus { /// 返回数据库存储用的小写字符串 - pub fn as_str(&self) -> &'static str { + /// + /// `const fn` 使调用方可在 `const` 上下文调用(任务 #17 合并:df-nodes 的 + /// `task_state_machine` 字符串常量从本方法派生,消除双源柡移)。 + pub const fn as_str(&self) -> &'static str { match self { TaskStatus::Todo => "todo", TaskStatus::InProgress => "in_progress", diff --git a/docs/02-架构设计/INDEX.md b/docs/02-架构设计/INDEX.md index a349f0e..86af903 100644 --- a/docs/02-架构设计/INDEX.md +++ b/docs/02-架构设计/INDEX.md @@ -85,7 +85,8 @@ | [AI对话目标丢失诊断-2026-06-26.md](./专项设计/AI对话目标丢失诊断-2026-06-26.md) | 📐 诊断 | AI 对话目标丢失根因分析:上下文漂移 / 意图衰减 | | [AI原生上下文地图与去AI化进化系统-2026-06-26.md](./专项设计/AI原生上下文地图与去AI化进化系统-2026-06-26.md) | 📐 设计 | AI 原生上下文地图:语义索引 / 去 AI 化渐进演进路径 | | [项目知识图谱与任务队列系统-2026-06-26.md](./专项设计/项目知识图谱与任务队列系统-2026-06-26.md) | 📐 设计 | 项目级知识图谱 + 任务队列:依赖解析 / 优先级调度 | -| [工程系统设计-2026-06-29.md](./专项设计/工程系统设计-2026-06-29.md) | ✅ 已落地(2026-06-29) | 项目多工程(Module) + Git 状态查询 + 文件浏览器 + Git AI 工具(只读3+写3) | +| [工程系统设计-2026-06-29.md](./专项设计/工程系统设计-2026-06-29.md) | ✅ 已落地 | 项目多工程(Module) + Git 状态查询 + 文件浏览器 + Git AI 工具(只读3+写3) | +| [架构债迁移设计-2026-06-29.md](./专项设计/架构债迁移设计-2026-06-29.md) | 📐 设计草案 | #8/#15/#18/#19 架构债迁移路径(AI状态机抽离/IPC错误结构化/配置统一/连接池) | --- diff --git a/docs/02-架构设计/专项设计/架构债迁移设计-2026-06-29.md b/docs/02-架构设计/专项设计/架构债迁移设计-2026-06-29.md new file mode 100644 index 0000000..6012812 --- /dev/null +++ b/docs/02-架构设计/专项设计/架构债迁移设计-2026-06-29.md @@ -0,0 +1,267 @@ +# 架构债迁移设计方案(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: E) -> String { e.to_string() } + +// 所有 IPC 命令 +async fn xxx() -> Result { ... .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 }, + Unauthorized { reason: String }, + Internal { message: String }, +} + +// IPC 命令改为 +async fn xxx() -> Result { ... } +``` + +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 for IpcError`。 + +**阶段 2**:src-tauri IPC 命令逐个从 `Result` 改为 `Result`。**保持向后兼容**:旧前端读 `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`**,各模块经 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, // 默认 ["rm","del","format","shutdown","mkfs","dd"] + pub whitelist: Vec, // 默认 [] +} + +// src-tauri/src/state.rs +pub struct AppState { + pub config: Arc, + // ... +} +``` + +### 迁移路径 + +**阶段 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>, // 单连接,所有读写串行 +} +``` + +注释自承 `TODO: 考虑使用 r2d2 连接池替代单连接 Mutex`。AI 对话高峰(多 tool_calls 并发 + SSE 落库)下排队。 + +### 目标架构 + +```rust +pub struct Database { + pool: r2d2::Pool, +} + +impl Database { + pub fn conn(&self) -> Result> { + 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 连接池** — 性能瓶颈出现时再改(当前单用户桌面应用未必触发) diff --git a/src-tauri/src/commands/idea.rs b/src-tauri/src/commands/idea.rs index ad6de91..4612d60 100644 --- a/src-tauri/src/commands/idea.rs +++ b/src-tauri/src/commands/idea.rs @@ -178,12 +178,13 @@ pub async fn promote_idea( return Err(format!("灵感已立项: {}", promoted_to)); } - // 复用 df-project 领域逻辑构造项目实体(create_from_idea) + // 复用 df-project 领域逻辑构造项目实体(create_from_idea)。 + // create_from_idea 返回 Result(任务 #16: 名称空校验下沉领域层)。 let project = df_project::manager::ProjectManager::create_from_idea( record.title.clone(), record.description.clone(), id.clone(), - ); + ).map_err(|e| e.to_string())?; let project_id = project.id.clone(); let now = now_millis(); let project_record = ProjectRecord {