优化: miniapp渲染(图片/checkbox/代码高亮) + MCP规则共享下沉 + 文档重写 + 节点风格对齐

This commit is contained in:
lxy
2026-08-08 21:39:30 +08:00
parent 9eb2995a74
commit e7228231d1
11 changed files with 587 additions and 167 deletions
+42 -29
View File
@@ -110,13 +110,13 @@ pub fn all_tools() -> &'static Vec<&'static ToolSpec> {
spec("bind_directory", "为项目绑定本地代码目录(会做路径冲突检测,Medium 风险+审计日志)", object_schema(json!({"id": str_field("项目 ID"), "path": str_field("本地目录绝对路径")}), &["id", "path"]), Medium, bind_directory),
// ─── 任务 ───
spec("list_tasks", "列出所有未删除任务(可按 project_id/status 过滤;分页 offset/limit,默认 limit=50 上限 100)", object_schema(json!({"project_id": opt_str_field("按项目过滤(可空)"), "status": opt_str_field("按状态过滤(todo/in_progress/in_review/testing/blocked/done/cancelled,可空)"), "offset": int_field("偏移量(可空,默认 0)"), "limit": int_field("返回上限(可空,默认 50,上限 100)")}), &[]), Low, list_tasks),
spec("create_task", "创建任务(Medium 风险,默认允许+审计日志;可选 parent_id 父任务 ID,限 1 级嵌套,父任务自身不能是子任务)", object_schema(json!({"project_id": str_field("项目 ID"), "title": str_field("标题"), "description": str_field("描述"), "priority": int_field("优先级(可空,默认 0)"), "parent_id": opt_str_field("父任务 ID(可空)")}), &["project_id", "title"]), Medium, create_task),
spec("create_task", "创建任务(Medium 风险,默认允许+审计日志;可选 idea_id 关联灵感、queue 管理池(默认 todo,新建仅 backlog/todo/decision)、parent_id 父任务 ID(限 1 级嵌套,父任务自身不能是子任务)、content_json 结构化需求规格(须合法 JSON))", object_schema(json!({"project_id": str_field("项目 ID"), "title": str_field("标题"), "description": str_field("描述"), "priority": int_field("优先级(可空,默认 2=medium,值域 0..=3)"), "idea_id": opt_str_field("关联灵感 ID(可空)"), "queue": opt_str_field("管理池(可空,默认 todo,新建仅 backlog/todo/decision)"), "parent_id": opt_str_field("父任务 ID(可空,限 1 级嵌套)"), "content_json": opt_str_field("结构化需求规格 JSON(可空,须合法 JSON)")}), &["project_id", "title"]), Medium, create_task),
spec("update_task", "更新任务(部分更新:仅传需要改的字段,未传字段保留原值;状态须走 advance_task)", object_schema(json!({"id": str_field("任务 ID"), "project_id": opt_str_field("项目 ID(可空=保留原值)"), "title": opt_str_field("标题(可空=保留原值)"), "description": opt_str_field("描述(可空=保留原值)"), "expected_updated_at": int_field("乐观锁版本(可空):上次读取到的 updated_at 毫秒时间戳,不一致则拒绝写入")}), &["id"]), Medium, update_task),
spec("advance_task", "推进任务状态(传目标 status,内部读当前态+状态机校验,Medium 风险+审计日志)", object_schema(json!({"id": str_field("任务 ID"), "to": str_field("目标 status(todo/in_progress/in_review/testing/blocked/done/cancelled)")}), &["id", "to"]), Medium, advance_task),
spec("delete_task", "软删任务(进回收站)——High 风险,默认拒绝,请在 DevFlow 应用内执行", object_schema(json!({"id": str_field("任务 ID")}), &["id"]), High, delete_task),
// ─── 灵感 ───
spec("list_ideas", "列出所有想法/灵感(分页:offset/limit,默认 limit=50 上限 100)", object_schema(json!({"offset": int_field("偏移量(可空,默认 0)"), "limit": int_field("返回上限(可空,默认 50,上限 100)")}), &[]), Low, list_ideas),
spec("create_idea", "创建想法(Medium 风险,默认允许+审计日志)", object_schema(json!({"title": str_field("标题"), "description": str_field("描述"), "priority": int_field("优先级(可空,默认 0)")}), &["title"]), Medium, create_idea),
spec("create_idea", "创建想法(Medium 风险,默认允许+审计日志)", object_schema(json!({"title": str_field("标题"), "description": str_field("描述"), "priority": int_field("优先级(可空,默认 1)")}), &["title"]), Medium, create_idea),
spec("update_idea", "更新想法(部分更新:仅传需要改的字段,未传字段保留原值)", object_schema(json!({"id": str_field("想法 ID"), "title": opt_str_field("标题(可空=保留原值)"), "description": opt_str_field("描述(可空=保留原值)"), "expected_updated_at": int_field("乐观锁版本(可空):上次读取到的 updated_at 毫秒时间戳,不一致则拒绝写入")}), &["id"]), Medium, update_idea),
spec("delete_idea", "软删想法——High 风险,默认拒绝,请在 DevFlow 应用内执行", object_schema(json!({"id": str_field("想法 ID")}), &["id"]), High, delete_idea),
spec("evaluate_idea", "对想法做启发式评估(只读:只返分数不写库,基于 description/title 计算 feasibility/impact/urgency/overall)", object_schema(json!({"id": str_field("想法 ID")}), &["id"]), Low, evaluate_idea),
@@ -572,31 +572,45 @@ fn create_task(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
Err(r) => return Box::pin(std::future::ready(r)),
};
let description = arg_str_or(&args, "description", "");
let priority = arg_int_or(&args, "priority", 0);
medium_audit("create_task", &format!("{project_id}/{title}"));
Box::pin(async move {
// parent_id 可选(arg_str_or 给 "" 哨兵,空串视为 None)。非空时校验 1 级嵌套铁律:
// 父任务存在 + 父任务自身无 parent_id(防孙任务),违反返回明确错误(与 IPC create_task 同规则)。
let parent_id_raw = arg_str_or(&args, "parent_id", "");
let parent_id = if parent_id_raw.trim().is_empty() {
None
} else {
let pid = parent_id_raw.trim();
let repo = TaskRepo::new(&db);
match repo.get_by_id(pid).await {
Ok(Some(parent)) => {
if parent.parent_id.is_some() {
return CallToolResult::error(format!(
"父任务不能是子任务(1 级嵌套限制): {pid} 自身有 parent_id={:?}",
parent.parent_id
));
}
Some(pid.to_string())
}
Ok(None) => return CallToolResult::error(format!("父任务不存在: {pid}")),
Err(e) => return err_str(e),
}
let repo = TaskRepo::new(&db);
// 共享校验(与 GUI/IPC 同源,一处修改全局生效):queue / parent_id / content_json / priority 值域
let queue = match df_storage::crud::normalize_queue_for_create(
args.get("queue").and_then(|v| v.as_str()),
) {
Ok(q) => q,
Err(e) => return CallToolResult::error(e),
};
let parent_id = match df_storage::crud::validate_parent_id(
&repo,
args.get("parent_id").and_then(|v| v.as_str()),
)
.await
{
Ok(pid) => pid,
Err(e) => return CallToolResult::error(e),
};
let content_json = match df_storage::crud::validate_content_json(
args.get("content_json").and_then(|v| v.as_str()),
) {
Ok(c) => c,
Err(e) => return CallToolResult::error(e),
};
let priority = match df_storage::crud::normalize_priority(
args.get("priority").and_then(|v| v.as_i64()),
) {
Ok(p) => p,
Err(e) => return CallToolResult::error(e),
};
// 可选关联灵感(空串/缺省视为不关联,与 GUI create_task 同语义)
let idea_id = args
.get("idea_id")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(String::from);
let now = now_millis();
let rec = TaskRecord {
id: new_id(),
@@ -611,15 +625,14 @@ fn create_task(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
base_branch: None,
review_rounds: 0,
output_json: None,
idea_id: None,
idea_id,
module_id: None,
queue: "todo".to_string(),
queue,
parent_id,
content_json: None,
content_json,
created_at: now.clone(),
updated_at: now,
};
let repo = TaskRepo::new(&db);
match repo.insert(rec).await {
Ok(id) => {
let created = repo.get_by_id(&id).await.ok().flatten();
@@ -766,7 +779,7 @@ fn create_idea(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
Err(r) => return Box::pin(std::future::ready(r)),
};
let description = arg_str_or(&args, "description", "");
let priority = arg_int_or(&args, "priority", 0);
let priority = arg_int_or(&args, "priority", 1);
medium_audit("create_idea", &title);
Box::pin(async move {
let now = now_millis();