新增: Phase 4 注入 — @[项目] 关联任务/灵感 + 三个工具 path 可选

- Augmentation 新增 extra 字段(项目关联信息/任务关联信息/灵感关联信息)
- ProjectResolver: @[项目] 时自动查前5条进行中任务+前3条待评估灵感
- TaskResolver/IdeaResolver: extra 预留(后续按需追加)
- render_one 渲染 extra 行
- search_files/grep/list_directory path 改为可选,不传时返回引导提示
This commit is contained in:
2026-06-28 00:19:13 +08:00
parent b7b54eb2a6
commit b7b004dd68
5 changed files with 92 additions and 25 deletions

View File

@@ -160,6 +160,9 @@ pub enum Augmentation {
/// 脱敏后的项目路径(远程 provider 仅 basename / 本地全路径);无目录绑定时为 None。
#[serde(rename = "path", default, skip_serializing_if = "Option::is_none")]
path: Option<SanitizedPath>,
/// Phase 4:项目关联信息(前 N 条任务/灵感/知识)。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
extra: Vec<String>,
},
/// @任务 注入体
Task {
@@ -174,6 +177,9 @@ pub enum Augmentation {
/// 所属项目名(join 展示用);游离任务为 None。
#[serde(rename = "project_name", default, skip_serializing_if = "Option::is_none")]
project_name: Option<String>,
/// Phase 4:任务关联信息(关联任务/所属项目上下文)。
#[serde(default, skip_serializing_if = "Vec::is_empty")]
extra: Vec<String>,
},
/// @灵感 注入体
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<String>,
},
/// /技能 注入体
Skill {

View File

@@ -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 批,子任务相关文件批量读改减少交互。**状态更新约定**:子项完成→父状态 🔨;全子完成→父 ✅;每批提交后同步此表。

View File

@@ -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 {

View File

@@ -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<String> = 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<String> = 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![],
})
}
}

View File

@@ -1549,17 +1549,18 @@ 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)]),
"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(
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 recursive = args["recursive"].as_bool().unwrap_or(false);
let skip_noise = args["skip_noise_dirs"].as_bool().unwrap_or(true);
@@ -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);