新增: 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:
@@ -160,6 +160,9 @@ pub enum Augmentation {
|
|||||||
/// 脱敏后的项目路径(远程 provider 仅 basename / 本地全路径);无目录绑定时为 None。
|
/// 脱敏后的项目路径(远程 provider 仅 basename / 本地全路径);无目录绑定时为 None。
|
||||||
#[serde(rename = "path", default, skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "path", default, skip_serializing_if = "Option::is_none")]
|
||||||
path: Option<SanitizedPath>,
|
path: Option<SanitizedPath>,
|
||||||
|
/// Phase 4:项目关联信息(前 N 条任务/灵感/知识)。
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
extra: Vec<String>,
|
||||||
},
|
},
|
||||||
/// @任务 注入体
|
/// @任务 注入体
|
||||||
Task {
|
Task {
|
||||||
@@ -174,6 +177,9 @@ pub enum Augmentation {
|
|||||||
/// 所属项目名(join 展示用);游离任务为 None。
|
/// 所属项目名(join 展示用);游离任务为 None。
|
||||||
#[serde(rename = "project_name", default, skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "project_name", default, skip_serializing_if = "Option::is_none")]
|
||||||
project_name: Option<String>,
|
project_name: Option<String>,
|
||||||
|
/// Phase 4:任务关联信息(关联任务/所属项目上下文)。
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
extra: Vec<String>,
|
||||||
},
|
},
|
||||||
/// @灵感 注入体
|
/// @灵感 注入体
|
||||||
Idea {
|
Idea {
|
||||||
@@ -185,6 +191,9 @@ pub enum Augmentation {
|
|||||||
status: IdeaStatus,
|
status: IdeaStatus,
|
||||||
#[serde(rename = "description")]
|
#[serde(rename = "description")]
|
||||||
description: String,
|
description: String,
|
||||||
|
/// Phase 4:灵感关联信息(已立项时所属项目)。
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
extra: Vec<String>,
|
||||||
},
|
},
|
||||||
/// /技能 注入体
|
/// /技能 注入体
|
||||||
Skill {
|
Skill {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ graph TD
|
|||||||
| **父③** AI对话体验 | 📋 待办 | ③.1 B-260619-04 ToolCard / ③.2 REFACTOR-260619-04 审批状态机 | — |
|
| **父③** 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) | — |
|
| **父④** 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 |
|
| **父⑤** 灵感模块 | ✅ 完成 | ⑤.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 / 双监听器 | — (穿插) |
|
| **父⑦** 技术债 | 📋 待办 | SMELL-P1-6 / conditions / CR缓存 / UX分页 / 审批超时 / miniapp / 双监听器 | — (穿插) |
|
||||||
|
|
||||||
> **推进路径**:父①先行(攒批提交)→ 父②主线 workflow → 父③/④并行 → 父⑤/⑥/⑦穿插。每父任务一个 workflow 批,子任务相关文件批量读改减少交互。**状态更新约定**:子项完成→父状态 🔨;全子完成→父 ✅;每批提交后同步此表。
|
> **推进路径**:父①先行(攒批提交)→ 父②主线 workflow → 父③/④并行 → 父⑤/⑥/⑦穿插。每父任务一个 workflow 批,子任务相关文件批量读改减少交互。**状态更新约定**:子项完成→父状态 🔨;全子完成→父 ✅;每批提交后同步此表。
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ fn render_one(aug: &Augmentation, out: &mut String, label: &str) {
|
|||||||
status,
|
status,
|
||||||
description,
|
description,
|
||||||
path,
|
path,
|
||||||
|
extra,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
out.push_str("【");
|
out.push_str("【");
|
||||||
@@ -85,6 +86,10 @@ fn render_one(aug: &Augmentation, out: &mut String, label: &str) {
|
|||||||
out.push_str(description);
|
out.push_str(description);
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
}
|
}
|
||||||
|
for line in extra {
|
||||||
|
out.push_str(line);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
}
|
}
|
||||||
Augmentation::Task {
|
Augmentation::Task {
|
||||||
@@ -92,6 +97,7 @@ fn render_one(aug: &Augmentation, out: &mut String, label: &str) {
|
|||||||
status,
|
status,
|
||||||
description,
|
description,
|
||||||
project_name,
|
project_name,
|
||||||
|
extra,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
out.push_str("【");
|
out.push_str("【");
|
||||||
@@ -111,12 +117,17 @@ fn render_one(aug: &Augmentation, out: &mut String, label: &str) {
|
|||||||
out.push_str(description);
|
out.push_str(description);
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
}
|
}
|
||||||
|
for line in extra {
|
||||||
|
out.push_str(line);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
}
|
}
|
||||||
Augmentation::Idea {
|
Augmentation::Idea {
|
||||||
title,
|
title,
|
||||||
status,
|
status,
|
||||||
description,
|
description,
|
||||||
|
extra,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
out.push_str("【");
|
out.push_str("【");
|
||||||
@@ -131,6 +142,10 @@ fn render_one(aug: &Augmentation, out: &mut String, label: &str) {
|
|||||||
out.push_str(description);
|
out.push_str(description);
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
}
|
}
|
||||||
|
for line in extra {
|
||||||
|
out.push_str(line);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
}
|
}
|
||||||
Augmentation::Skill {
|
Augmentation::Skill {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use df_storage::crud::{IdeaRepo, ProjectRepo, TaskRepo};
|
|||||||
use df_storage::db::Database;
|
use df_storage::db::Database;
|
||||||
use df_types::augmentation::{Augmentation, MentionRef, ResolveError, SanitizedPath};
|
use df_types::augmentation::{Augmentation, MentionRef, ResolveError, SanitizedPath};
|
||||||
use df_types::types::{IdeaStatus, ProjectStatus, TaskStatus};
|
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::registry::ResolverRegistry;
|
||||||
use crate::commands::ai::augmentation::sanitize::{sanitize_for, ProviderLocality};
|
use crate::commands::ai::augmentation::sanitize::{sanitize_for, ProviderLocality};
|
||||||
@@ -100,6 +101,43 @@ impl MentionResolver for ProjectResolver {
|
|||||||
status,
|
status,
|
||||||
description: record.description,
|
description: record.description,
|
||||||
path,
|
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,
|
status,
|
||||||
description: task.description,
|
description: task.description,
|
||||||
project_name,
|
project_name,
|
||||||
|
extra: vec![],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,6 +259,7 @@ impl MentionResolver for IdeaResolver {
|
|||||||
title: record.title,
|
title: record.title,
|
||||||
status,
|
status,
|
||||||
description: record.description,
|
description: record.description,
|
||||||
|
extra: vec![],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1549,18 +1549,19 @@ fn register_file_tools(
|
|||||||
})},
|
})},
|
||||||
);
|
);
|
||||||
registry.register(
|
registry.register(
|
||||||
"list_directory", "列出目录内容,返回文件和子目录列表(名称、类型、大小)",
|
"list_directory", "列出目录内容,返回文件和子目录列表(名称、类型、大小)。path 可选,不传时返回引导提示",
|
||||||
df_ai::ai_tools::object_schema(vec![("path", "string", true), ("recursive", "boolean", false), ("skip_noise_dirs", "boolean", false), ("max_depth", "integer", false)]),
|
df_ai::ai_tools::object_schema(vec![("path", "string", false), ("recursive", "boolean", false), ("skip_noise_dirs", "boolean", false), ("max_depth", "integer", false)]),
|
||||||
RiskLevel::Low,
|
RiskLevel::Low,
|
||||||
{ let allowed_dirs = allowed_dirs.clone(); Box::new(move |args: serde_json::Value| {
|
{ let allowed_dirs = allowed_dirs.clone(); Box::new(move |args: serde_json::Value| {
|
||||||
let allowed_dirs = allowed_dirs.clone();
|
let allowed_dirs = allowed_dirs.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let snap = allowed_dirs.read().await.clone();
|
let p = args["path"].as_str().unwrap_or("");
|
||||||
let resolved = resolve_workspace_path_with_allowed(
|
if p.is_empty() {
|
||||||
args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?,
|
return Ok(serde_json::json!({"error": "请指定搜索目录。可用 @[项目名] 引用已绑定的项目目录,或直接提供绝对路径。"}));
|
||||||
&snap,
|
}
|
||||||
)?;
|
let snap = allowed_dirs.read().await.clone();
|
||||||
let path = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?;
|
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 recursive = args["recursive"].as_bool().unwrap_or(false);
|
||||||
let skip_noise = args["skip_noise_dirs"].as_bool().unwrap_or(true);
|
let skip_noise = args["skip_noise_dirs"].as_bool().unwrap_or(true);
|
||||||
// BUG-260617-09: max_depth 由 LLM 参数控制,无上限时虽 entries 上限(1000)
|
// BUG-260617-09: max_depth 由 LLM 参数控制,无上限时虽 entries 上限(1000)
|
||||||
@@ -2128,7 +2129,7 @@ fn register_file_tools(
|
|||||||
let grep_schema = {
|
let grep_schema = {
|
||||||
let mut props = serde_json::Map::new();
|
let mut props = serde_json::Map::new();
|
||||||
props.insert("pattern".into(), serde_json::json!({ "type": "string", "description": "正则表达式(默认大小写敏感)。无特殊字符时等价字面包含匹配。必填" }));
|
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("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("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)" }));
|
props.insert("-n".into(), serde_json::json!({ "type": "boolean", "description": "content 模式是否含行号(默认 true)" }));
|
||||||
@@ -2138,21 +2139,22 @@ fn register_file_tools(
|
|||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": props,
|
"properties": props,
|
||||||
"required": ["pattern", "path"],
|
"required": ["pattern"],
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
registry.register(
|
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,
|
grep_schema,
|
||||||
RiskLevel::Low,
|
RiskLevel::Low,
|
||||||
{ let allowed_dirs = allowed_dirs.clone(); Box::new(move |args: serde_json::Value| {
|
{ let allowed_dirs = allowed_dirs.clone(); Box::new(move |args: serde_json::Value| {
|
||||||
let allowed_dirs = allowed_dirs.clone();
|
let allowed_dirs = allowed_dirs.clone();
|
||||||
Box::pin(async move {
|
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 snap = allowed_dirs.read().await.clone();
|
||||||
let resolved = resolve_workspace_path_with_allowed(
|
let resolved = resolve_workspace_path_with_allowed(p, &snap)?;
|
||||||
args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?,
|
|
||||||
&snap,
|
|
||||||
)?;
|
|
||||||
let root = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?;
|
let root = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?;
|
||||||
let pattern = args["pattern"].as_str()
|
let pattern = args["pattern"].as_str()
|
||||||
.ok_or_else(|| anyhow::anyhow!("缺少 pattern 参数"))?;
|
.ok_or_else(|| anyhow::anyhow!("缺少 pattern 参数"))?;
|
||||||
@@ -2273,16 +2275,17 @@ fn register_file_tools(
|
|||||||
// ── 文件搜索 (Low risk) ──
|
// ── 文件搜索 (Low risk) ──
|
||||||
registry.register(
|
registry.register(
|
||||||
"search_files", "在指定目录下搜索匹配模式(字符串包含匹配)的文件名,支持 offset/limit 分页。返回 results、total、has_more。默认 limit=50",
|
"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,
|
RiskLevel::Low,
|
||||||
{ let allowed_dirs = allowed_dirs.clone(); Box::new(move |args: serde_json::Value| {
|
{ let allowed_dirs = allowed_dirs.clone(); Box::new(move |args: serde_json::Value| {
|
||||||
let allowed_dirs = allowed_dirs.clone();
|
let allowed_dirs = allowed_dirs.clone();
|
||||||
Box::pin(async move {
|
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 snap = allowed_dirs.read().await.clone();
|
||||||
let resolved = resolve_workspace_path_with_allowed(
|
let resolved = resolve_workspace_path_with_allowed(p, &snap)?;
|
||||||
args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?,
|
|
||||||
&snap,
|
|
||||||
)?;
|
|
||||||
let path = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?;
|
let path = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?;
|
||||||
let pattern = args["pattern"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 pattern 参数"))?;
|
let pattern = args["pattern"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 pattern 参数"))?;
|
||||||
let recursive = args["recursive"].as_bool().unwrap_or(false);
|
let recursive = args["recursive"].as_bool().unwrap_or(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user