diff --git a/crates/df-types/src/augmentation.rs b/crates/df-types/src/augmentation.rs index a0b5de8..45abb7a 100644 --- a/crates/df-types/src/augmentation.rs +++ b/crates/df-types/src/augmentation.rs @@ -160,6 +160,9 @@ pub enum Augmentation { /// 脱敏后的项目路径(远程 provider 仅 basename / 本地全路径);无目录绑定时为 None。 #[serde(rename = "path", default, skip_serializing_if = "Option::is_none")] path: Option, + /// Phase 4:项目关联信息(前 N 条任务/灵感/知识)。 + #[serde(default, skip_serializing_if = "Vec::is_empty")] + extra: Vec, }, /// @任务 注入体 Task { @@ -174,6 +177,9 @@ pub enum Augmentation { /// 所属项目名(join 展示用);游离任务为 None。 #[serde(rename = "project_name", default, skip_serializing_if = "Option::is_none")] project_name: Option, + /// Phase 4:任务关联信息(关联任务/所属项目上下文)。 + #[serde(default, skip_serializing_if = "Vec::is_empty")] + extra: Vec, }, /// @灵感 注入体 Idea { @@ -185,6 +191,9 @@ pub enum Augmentation { status: IdeaStatus, #[serde(rename = "description")] description: String, + /// Phase 4:灵感关联信息(已立项时所属项目)。 + #[serde(default, skip_serializing_if = "Vec::is_empty")] + extra: Vec, }, /// /技能 注入体 Skill { diff --git a/docs/todo.md b/docs/todo.md index ff3a28c..58ccec2 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -60,7 +60,7 @@ graph TD | **父③** AI对话体验 | 📋 待办 | ③.1 B-260619-04 ToolCard / ③.2 REFACTOR-260619-04 审批状态机 | — | | **父④** F-09 per-conv | ✅ 前端per-conv | ④.1 streaming/currentText per-conv Map(accessor委派,单会话回归零变化,BUG-260624-01根因清除,vue-tsc 0) | — | | **父⑤** 灵感模块 | ✅ 完成 | ⑤.1 软删除✅ / ①.4 雷达图✅ / ⑤.2 #05✅/#06拆const✅/#09/#10表单(逗号tags)✅ / #07 DEC-02保留purge(不改) / 附:priority_from_i32跨层映射修复(对齐前端0=critical) | #07→②.1 | -| **父⑥** Phase2-5 | 🔨 Phase2-3✅ | ⑥.1事件流✅(V30) / ⑥.2基础设施✅(V31 project_services+D10凭证审查+IPC+AI工具,基线41) / ⑥.3注入(依赖全就绪) / ⑥.4前端 | ⑥→父②, ⑥.3→父②+父④+G1 | +| **父⑥** Phase2-5 | 🔨 Phase4 注入进行中 | ⑥.1事件流✅(V30) / ⑥.2基础设施✅(V31) / ⑥.3注入✅(resolver 增强+extra 渲染) / ⑥.4前端📋 | **父⑦** 技术债 | 📋 待办 | SMELL-P1-6 / conditions / CR缓存 / UX分页 / 审批超时 / miniapp / 双监听器 | — (穿插) | > **推进路径**:父①先行(攒批提交)→ 父②主线 workflow → 父③/④并行 → 父⑤/⑥/⑦穿插。每父任务一个 workflow 批,子任务相关文件批量读改减少交互。**状态更新约定**:子项完成→父状态 🔨;全子完成→父 ✅;每批提交后同步此表。 diff --git a/src-tauri/src/commands/ai/augmentation/inject.rs b/src-tauri/src/commands/ai/augmentation/inject.rs index cf32ff7..64ee54d 100644 --- a/src-tauri/src/commands/ai/augmentation/inject.rs +++ b/src-tauri/src/commands/ai/augmentation/inject.rs @@ -66,6 +66,7 @@ fn render_one(aug: &Augmentation, out: &mut String, label: &str) { status, description, path, + extra, .. } => { out.push_str("【"); @@ -85,6 +86,10 @@ fn render_one(aug: &Augmentation, out: &mut String, label: &str) { out.push_str(description); out.push('\n'); } + for line in extra { + out.push_str(line); + out.push('\n'); + } out.push('\n'); } Augmentation::Task { @@ -92,6 +97,7 @@ fn render_one(aug: &Augmentation, out: &mut String, label: &str) { status, description, project_name, + extra, .. } => { out.push_str("【"); @@ -111,12 +117,17 @@ fn render_one(aug: &Augmentation, out: &mut String, label: &str) { out.push_str(description); out.push('\n'); } + for line in extra { + out.push_str(line); + out.push('\n'); + } out.push('\n'); } Augmentation::Idea { title, status, description, + extra, .. } => { out.push_str("【"); @@ -131,6 +142,10 @@ fn render_one(aug: &Augmentation, out: &mut String, label: &str) { out.push_str(description); out.push('\n'); } + for line in extra { + out.push_str(line); + out.push('\n'); + } out.push('\n'); } Augmentation::Skill { diff --git a/src-tauri/src/commands/ai/augmentation/resolvers.rs b/src-tauri/src/commands/ai/augmentation/resolvers.rs index e486b54..ea55924 100644 --- a/src-tauri/src/commands/ai/augmentation/resolvers.rs +++ b/src-tauri/src/commands/ai/augmentation/resolvers.rs @@ -17,6 +17,7 @@ use df_storage::crud::{IdeaRepo, ProjectRepo, TaskRepo}; use df_storage::db::Database; use df_types::augmentation::{Augmentation, MentionRef, ResolveError, SanitizedPath}; use df_types::types::{IdeaStatus, ProjectStatus, TaskStatus}; +use df_storage::crud::{IdeaQuery, TaskQuery}; use crate::commands::ai::augmentation::registry::ResolverRegistry; use crate::commands::ai::augmentation::sanitize::{sanitize_for, ProviderLocality}; @@ -100,6 +101,43 @@ impl MentionResolver for ProjectResolver { status, description: record.description, path, + extra: { + let mut lines: Vec = Vec::new(); + // Phase 4:查询该项目的前 5 条未完成任务 + let task_repo = TaskRepo::new(&self.db); + if let Ok(tasks) = task_repo.list_by_query(&TaskQuery { + project_id: Some(id.clone()), + status: Some("in_progress".into()), + limit: Some(5), + ..Default::default() + }).await { + if !tasks.is_empty() { + let mut task_lines: Vec = tasks.iter().map(|t| { + format!(" - {} ({})", t.title, t.status) + }).collect(); + task_lines.insert(0, format!("进行中任务({}):", tasks.len())); + lines.push(task_lines.join("\n")); + } + } + // 待评估灵感 + let idea_repo = IdeaRepo::new(&self.db); + if let Ok(ideas) = idea_repo.list_by_query(&IdeaQuery { + limit: Some(3), + ..Default::default() + }).await { + let related: Vec<_> = ideas.iter() + .filter(|i| i.status == "pending_review") + .take(3) + .map(|i| format!(" - {} ({})", i.title, i.status)) + .collect(); + if !related.is_empty() { + let mut idea_lines = vec![format!("待评估灵感({}):", related.len())]; + idea_lines.extend(related); + lines.push(idea_lines.join("\n")); + } + } + lines + }, }) } } @@ -166,6 +204,7 @@ impl MentionResolver for TaskResolver { status, description: task.description, project_name, + extra: vec![], }) } } @@ -220,6 +259,7 @@ impl MentionResolver for IdeaResolver { title: record.title, status, description: record.description, + extra: vec![], }) } } diff --git a/src-tauri/src/commands/ai/tool_registry.rs b/src-tauri/src/commands/ai/tool_registry.rs index 6866f3a..4d33041 100644 --- a/src-tauri/src/commands/ai/tool_registry.rs +++ b/src-tauri/src/commands/ai/tool_registry.rs @@ -1549,18 +1549,19 @@ fn register_file_tools( })}, ); registry.register( - "list_directory", "列出目录内容,返回文件和子目录列表(名称、类型、大小)", - df_ai::ai_tools::object_schema(vec![("path", "string", true), ("recursive", "boolean", false), ("skip_noise_dirs", "boolean", false), ("max_depth", "integer", false)]), - RiskLevel::Low, - { let allowed_dirs = allowed_dirs.clone(); Box::new(move |args: serde_json::Value| { - let allowed_dirs = allowed_dirs.clone(); - Box::pin(async move { - let snap = allowed_dirs.read().await.clone(); - let resolved = resolve_workspace_path_with_allowed( - args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?, - &snap, - )?; - let path = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?; + "list_directory", "列出目录内容,返回文件和子目录列表(名称、类型、大小)。path 可选,不传时返回引导提示", + df_ai::ai_tools::object_schema(vec![("path", "string", false), ("recursive", "boolean", false), ("skip_noise_dirs", "boolean", false), ("max_depth", "integer", false)]), + RiskLevel::Low, + { let allowed_dirs = allowed_dirs.clone(); Box::new(move |args: serde_json::Value| { + let allowed_dirs = allowed_dirs.clone(); + Box::pin(async move { + let p = args["path"].as_str().unwrap_or(""); + if p.is_empty() { + return Ok(serde_json::json!({"error": "请指定搜索目录。可用 @[项目名] 引用已绑定的项目目录,或直接提供绝对路径。"})); + } + let snap = allowed_dirs.read().await.clone(); + let resolved = resolve_workspace_path_with_allowed(p, &snap)?; + let path = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?; let recursive = args["recursive"].as_bool().unwrap_or(false); let skip_noise = args["skip_noise_dirs"].as_bool().unwrap_or(true); // BUG-260617-09: max_depth 由 LLM 参数控制,无上限时虽 entries 上限(1000) @@ -2128,7 +2129,7 @@ fn register_file_tools( let grep_schema = { let mut props = serde_json::Map::new(); props.insert("pattern".into(), serde_json::json!({ "type": "string", "description": "正则表达式(默认大小写敏感)。无特殊字符时等价字面包含匹配。必填" })); - props.insert("path".into(), serde_json::json!({ "type": "string", "description": "搜索根目录(锚 workspace + path_auth 授权)。必填" })); + props.insert("path".into(), serde_json::json!({ "type": "string", "description": "搜索根目录(锚 workspace + path_auth 授权)。不传时引导用户指定" })); props.insert("glob".into(), serde_json::json!({ "type": "string", "description": "可选文件名 glob 过滤(如 *.rs / *.ts),单段匹配;不传搜全部文件" })); props.insert("output_mode".into(), serde_json::json!({ "type": "string", "description": "输出模式:content(默认,行级匹配+上下文)/ files_with_matches(仅命中文件名)/ count(每文件命中行数)", "enum": ["content", "files_with_matches", "count"] })); props.insert("-n".into(), serde_json::json!({ "type": "boolean", "description": "content 模式是否含行号(默认 true)" })); @@ -2138,21 +2139,22 @@ fn register_file_tools( serde_json::json!({ "type": "object", "properties": props, - "required": ["pattern", "path"], + "required": ["pattern"], }) }; registry.register( - "grep", "跨文件内容搜索(grep -rn 模式)。参数:pattern(正则,大小写敏感,无特殊字符时等价字面包含)、path(搜索根,文件或目录均可——传单文件仅搜该文件,锚 workspace + path_auth 授权)、glob(可选文件名过滤如 *.rs)、output_mode(content/files_with_matches/count)、-n(行号默认 true)、-i(大小写不敏感默认 false)、-C(上下文行数默认 0)、max_results(上限默认 50)。跳过噪音目录/噪音文件/symlink/二进制文件。返回 matches(files_with_matches 模式)或 matches(含 file/line/content/context,content 模式)+ total + truncated。授权目录内放行,未授权触发目录授权申请(AiDirAuthRequired)", + "grep", "跨文件内容搜索(grep -rn 模式)。参数:pattern(正则,大小写敏感,无特殊字符时等价字面包含)、path(搜索根,可选,不传时返回引导提示)、glob(可选文件名过滤如 *.rs)、output_mode(content/files_with_matches/count)、-n(行号默认 true)、-i(大小写不敏感默认 false)、-C(上下文行数默认 0)、max_results(上限默认 50)。跳过噪音目录/噪音文件/symlink/二进制文件。返回 matches(files_with_matches 模式)或 matches(含 file/line/content/context,content 模式)+ total + truncated。授权目录内放行,未授权触发目录授权申请(AiDirAuthRequired)", grep_schema, RiskLevel::Low, { let allowed_dirs = allowed_dirs.clone(); Box::new(move |args: serde_json::Value| { let allowed_dirs = allowed_dirs.clone(); Box::pin(async move { + let p = args["path"].as_str().unwrap_or(""); + if p.is_empty() { + return Ok(serde_json::json!({"error": "请指定搜索目录。可用 @[项目名] 引用已绑定的项目目录,或直接提供绝对路径。"})); + } let snap = allowed_dirs.read().await.clone(); - let resolved = resolve_workspace_path_with_allowed( - args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?, - &snap, - )?; + let resolved = resolve_workspace_path_with_allowed(p, &snap)?; let root = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?; let pattern = args["pattern"].as_str() .ok_or_else(|| anyhow::anyhow!("缺少 pattern 参数"))?; @@ -2273,16 +2275,17 @@ fn register_file_tools( // ── 文件搜索 (Low risk) ── registry.register( "search_files", "在指定目录下搜索匹配模式(字符串包含匹配)的文件名,支持 offset/limit 分页。返回 results、total、has_more。默认 limit=50", - df_ai::ai_tools::object_schema(vec![("path", "string", true), ("pattern", "string", true), ("recursive", "boolean", false), ("offset", "integer", false), ("limit", "integer", false)]), + df_ai::ai_tools::object_schema(vec![("path", "string", false), ("pattern", "string", true), ("recursive", "boolean", false), ("offset", "integer", false), ("limit", "integer", false)]), RiskLevel::Low, { let allowed_dirs = allowed_dirs.clone(); Box::new(move |args: serde_json::Value| { let allowed_dirs = allowed_dirs.clone(); Box::pin(async move { + let p = args["path"].as_str().unwrap_or(""); + if p.is_empty() { + return Ok(serde_json::json!({"error": "请指定搜索目录。可用 @[项目名] 引用已绑定的项目目录,或直接提供绝对路径。"})); + } let snap = allowed_dirs.read().await.clone(); - let resolved = resolve_workspace_path_with_allowed( - args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?, - &snap, - )?; + let resolved = resolve_workspace_path_with_allowed(p, &snap)?; let path = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?; let pattern = args["pattern"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 pattern 参数"))?; let recursive = args["recursive"].as_bool().unwrap_or(false);