From 3d8b75522917a1be4d740066a3c22dc33828bb59 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com>
Date: Fri, 3 Jul 2026 00:18:21 +0800
Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84:=20=E5=89=8D=E7=AB=AFDRY?=
=?UTF-8?q?=E6=94=B6=E5=8F=A3+=E5=90=8E=E7=AB=AF=E6=B5=8B=E8=AF=95?=
=?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AF=B9=E9=BD=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 useToast composable: 消除 AiChat/Settings/Projects 4处 toast 重复
统一默认3000ms(Projects原4000ms为操作类提示保留参数覆盖)
- 新增 utils/json.ts parseJsonArray: 消除 parseStack/parseTags/ModuleNode
3处JSON字符串数组解析重复
- 新增 utils/html.ts escapeHtml: 消除 useMarkdown/FilePreview 2处重复
- ProjectDetail score-bar 内联三元改用 scoreTier(消除最后一处阈值硬编码)
- ConversationSidebar 删除 formatTime 透传包装(直接用 formatRelative)
- 清理死代码: parseTs/stringifyError/ErrorSink/_Unused 改私有或删除
wrapNakedDiff 改私有(无外部 import)
- ModuleNode shortPath 改名 truncatedPath(与 useToolCard.shortPath 语义不同)
---
crates/df-ai/src/context/manager_tests.rs | 10 ++--
crates/df-nodes/src/human_node.rs | 12 ++--
crates/df-workflow/src/executor_helpers.rs | 18 +++---
src-tauri/src/commands/ai/audit/mod.rs | 1 +
.../src/commands/ai/augmentation/inject.rs | 7 +++
.../src/commands/ai/commands/conversation.rs | 8 +--
src-tauri/src/commands/ai/tool_registry.rs | 2 +-
src/components/AiChat.vue | 17 ++----
src/components/ai/ConversationSidebar.vue | 10 +---
src/components/project/FilePreview.vue | 5 +-
src/components/project/ModuleNode.vue | 15 ++---
src/composables/useMarkdown.ts | 6 +-
src/composables/useStoreAction.ts | 17 ++----
src/composables/useToast.ts | 59 +++++++++++++++++++
src/stores/knowledge.ts | 11 +---
src/utils/html.ts | 8 +++
src/utils/json.ts | 23 ++++++++
src/utils/project.ts | 10 +---
src/utils/time.ts | 4 +-
src/views/ProjectDetail.vue | 4 +-
src/views/Projects.vue | 21 ++-----
src/views/Settings.vue | 19 ++----
22 files changed, 163 insertions(+), 124 deletions(-)
create mode 100644 src/composables/useToast.ts
create mode 100644 src/utils/html.ts
create mode 100644 src/utils/json.ts
diff --git a/crates/df-ai/src/context/manager_tests.rs b/crates/df-ai/src/context/manager_tests.rs
index 93c5d13..96fb4c8 100644
--- a/crates/df-ai/src/context/manager_tests.rs
+++ b/crates/df-ai/src/context/manager_tests.rs
@@ -282,8 +282,8 @@ fn compress_old_messages_marks_compressed_and_returns_refs() {
assert_eq!(compressed[1].content, "旧回复1");
// status 已改 compressed
- assert_eq!(mgr.messages_mut()[0].message.status.as_deref(), Some("compressed"));
- assert_eq!(mgr.messages_mut()[1].message.status.as_deref(), Some("compressed"));
+ assert_eq!(mgr.messages_mut()[0].message.status.as_ref(), Some(&MessageStatus::Compressed));
+ assert_eq!(mgr.messages_mut()[1].message.status.as_ref(), Some(&MessageStatus::Compressed));
// 保护区外(本例 index 2)仍 active
assert!(mgr.messages_mut()[2].message.is_active(), "保护区外消息不应被动");
@@ -346,8 +346,8 @@ fn compress_old_messages_skips_already_inactive() {
);
// truncated 状态不被改成 compressed(保留原 truncated,语义不混淆)
assert_eq!(
- mgr.messages_mut()[0].message.status.as_deref(),
- Some("truncated"),
+ mgr.messages_mut()[0].message.status.as_ref(),
+ Some(&MessageStatus::Truncated),
"已 truncated 不应被改写为 compressed"
);
}
@@ -660,7 +660,7 @@ fn topic_field_survives_compress_old_messages() {
assert_eq!(mgr.messages_mut()[0].topic.as_deref(), Some("code"), "compressed 消息 topic 应保留");
assert_eq!(mgr.messages_mut()[1].topic.as_deref(), Some("file"), "compressed 消息 topic 应保留");
// status 改为 compressed
- assert_eq!(mgr.messages_mut()[0].message.status.as_deref(), Some("compressed"));
+ assert_eq!(mgr.messages_mut()[0].message.status.as_ref(), Some(&MessageStatus::Compressed));
}
#[test]
diff --git a/crates/df-nodes/src/human_node.rs b/crates/df-nodes/src/human_node.rs
index e9e40a0..e33aea0 100644
--- a/crates/df-nodes/src/human_node.rs
+++ b/crates/df-nodes/src/human_node.rs
@@ -218,7 +218,7 @@ mod tests {
node_id: node_id.to_string(),
inputs: HashMap::new(),
config,
- execution_id: execution_id.to_string(),
+ execution_id: execution_id.into(),
event_bus: event_bus.clone(),
node_status: StateMachine::new(),
}
@@ -235,7 +235,7 @@ mod tests {
) {
event_bus
.send(WorkflowEvent::HumanApprovalResponse {
- execution_id: execution_id.to_string(),
+ execution_id: execution_id.into(),
node_id: node_id.to_string(),
decision: decision.to_string(),
decisions: vec![],
@@ -254,7 +254,7 @@ mod tests {
) {
event_bus
.send(WorkflowEvent::HumanApprovalResponse {
- execution_id: execution_id.to_string(),
+ execution_id: execution_id.into(),
node_id: node_id.to_string(),
decision: String::new(),
decisions: decisions.iter().map(|s| s.to_string()).collect(),
@@ -477,8 +477,8 @@ mod tests {
dag.add_node("b".to_string(), Box::new(HumanNode));
dag.add_edge("a".to_string(), "b".to_string());
- let mut executor = DagExecutor::new(bus.clone(), exec_id.to_string());
- let sm = executor.state_machine(); // 共享状态机(spawn 后仍可读)
+ let mut executor = DagExecutor::new(bus.clone(), exec_id.into());
+ let sm = executor.state_machine(); // 共享状态机(spawn 后仍可读)
let run_handle = tokio::spawn(async move {
executor
@@ -537,7 +537,7 @@ mod tests {
let mut dag = Dag::new();
dag.add_node("h".to_string(), Box::new(HumanNode));
- let mut executor = DagExecutor::new(bus.clone(), exec_id.to_string());
+ let mut executor = DagExecutor::new(bus.clone(), exec_id.into());
let sm = executor.state_machine();
let run_handle = tokio::spawn(async move {
diff --git a/crates/df-workflow/src/executor_helpers.rs b/crates/df-workflow/src/executor_helpers.rs
index a567661..bdeb3ca 100644
--- a/crates/df-workflow/src/executor_helpers.rs
+++ b/crates/df-workflow/src/executor_helpers.rs
@@ -110,7 +110,7 @@ async fn node_config_overrides_global_in_node_context() {
let dag = registry.build_dag(&def).expect("build_dag");
- let mut executor = DagExecutor::new(EventBus::new(), "test-nodecfg".to_string());
+ let mut executor = DagExecutor::new(EventBus::new(), "test-nodecfg".into());
// 全局 config 写 foo=GLOBAL(验证被 n1 节点级覆盖,n2 回退用全局)
let outputs = executor
.run(&dag, serde_json::json!({ "foo": "GLOBAL" }))
@@ -138,7 +138,7 @@ async fn test_same_layer_runs_in_parallel() {
dag.add_node("a".to_string(), Box::new(SleepNode { sleep_ms: 100 }));
dag.add_node("b".to_string(), Box::new(SleepNode { sleep_ms: 100 }));
- let mut executor = DagExecutor::new(EventBus::new(), "test-exec".to_string());
+ let mut executor = DagExecutor::new(EventBus::new(), "test-exec".into());
let start = std::time::Instant::now();
let outputs = executor.run(&dag, serde_json::Value::Null).await.unwrap();
let elapsed = start.elapsed();
@@ -161,7 +161,7 @@ async fn test_layer_failure_aborts_following_layers() {
dag.add_node("c".to_string(), Box::new(SleepNode { sleep_ms: 10 }));
dag.add_edge("a".to_string(), "c".to_string());
- let mut executor = DagExecutor::new(EventBus::new(), "test-exec".to_string());
+ let mut executor = DagExecutor::new(EventBus::new(), "test-exec".into());
let err = executor
.run(&dag, serde_json::Value::Null)
.await
@@ -206,7 +206,7 @@ async fn test_cancelled_node_skips_set_failed() {
let mut dag = Dag::new();
dag.add_node("x".to_string(), Box::new(CancelSelfNode));
- let mut executor = DagExecutor::new(EventBus::new(), "test-cancel".to_string());
+ let mut executor = DagExecutor::new(EventBus::new(), "test-cancel".into());
let result = executor.run(&dag, serde_json::Value::Null).await;
// run 返回 Err(取消致中止后续层),但不 panic/bail transition
@@ -257,7 +257,7 @@ async fn test_cancelled_node_skips_set_completed() {
let mut dag = Dag::new();
dag.add_node("y".to_string(), Box::new(CancelSelfThenOkNode));
- let mut executor = DagExecutor::new(EventBus::new(), "test-cancel-ok".to_string());
+ let mut executor = DagExecutor::new(EventBus::new(), "test-cancel-ok".into());
let result = executor.run(&dag, serde_json::Value::Null).await;
// run 返回 Ok —— Ok 节点不应因 TOCTOU 取消被误判为失败
@@ -285,7 +285,7 @@ async fn test_cancelled_node_emits_node_cancelled_event() {
let bus = EventBus::new();
let mut rx = bus.subscribe();
- let mut executor = DagExecutor::new(bus, "test-cancel-event".to_string());
+ let mut executor = DagExecutor::new(bus, "test-cancel-event".into());
let _ = executor.run(&dag, serde_json::Value::Null).await;
// 收集所有事件(run 已结束,事件总线无新事件)
@@ -408,7 +408,7 @@ async fn conditions_eval_routes_by_edge_condition() {
"$.ok == false".to_string(),
);
- let mut executor = DagExecutor::new(EventBus::new(), "test-cond-route".to_string());
+ let mut executor = DagExecutor::new(EventBus::new(), "test-cond-route".into());
let outputs = executor
.run(&dag, serde_json::Value::Null)
.await
@@ -462,7 +462,7 @@ async fn conditions_eval_skip_keeps_pending() {
"$.ok == false".to_string(),
);
- let mut executor = DagExecutor::new(EventBus::new(), "test-cond-skip".to_string());
+ let mut executor = DagExecutor::new(EventBus::new(), "test-cond-skip".into());
let outputs = executor
.run(&dag, serde_json::Value::Null)
.await
@@ -504,7 +504,7 @@ async fn conditions_eval_unconditional_edge_always_passes() {
"$.ok == false".to_string(),
);
- let mut executor = DagExecutor::new(EventBus::new(), "test-cond-mix".to_string());
+ let mut executor = DagExecutor::new(EventBus::new(), "test-cond-mix".into());
let outputs = executor
.run(&dag, serde_json::Value::Null)
.await
diff --git a/src-tauri/src/commands/ai/audit/mod.rs b/src-tauri/src/commands/ai/audit/mod.rs
index c8278ef..dcdb895 100644
--- a/src-tauri/src/commands/ai/audit/mod.rs
+++ b/src-tauri/src/commands/ai/audit/mod.rs
@@ -485,6 +485,7 @@ pub(crate) async fn process_tool_calls(
#[cfg(test)]
mod tests {
use super::*;
+ use super::path_auth::extract_file_tool_paths;
/// grep 走单路径授权申请路径(NeedsAuth),非 search_files 盲拒(Denied)。
///
diff --git a/src-tauri/src/commands/ai/augmentation/inject.rs b/src-tauri/src/commands/ai/augmentation/inject.rs
index 64ee54d..fda0bef 100644
--- a/src-tauri/src/commands/ai/augmentation/inject.rs
+++ b/src-tauri/src/commands/ai/augmentation/inject.rs
@@ -189,6 +189,7 @@ mod tests {
status: ProjectStatus::InProgress,
description: "AI-native dev tool".into(),
path: Some(SanitizedPath::new("E:/wk-lab/devflow")),
+ extra: vec![],
};
let seg = build_augmentation_segment(&[aug], "zh");
assert!(seg.starts_with("--- 以下是用户选择的上下文参考"), "头部应隔离标注, got: {}", seg);
@@ -206,6 +207,7 @@ mod tests {
status: TaskStatus::Todo,
description: "do X".into(),
project_name: Some("devflow".into()),
+ extra: vec![],
};
let seg = build_augmentation_segment(&[aug], "zh");
assert!(seg.contains("【任务】Implement X"));
@@ -234,6 +236,7 @@ mod tests {
title: "Idea1".into(),
status: IdeaStatus::Approved,
description: String::new(),
+ extra: vec![],
},
Augmentation::Project {
id: "p1".into(),
@@ -241,6 +244,7 @@ mod tests {
status: ProjectStatus::Planning,
description: String::new(),
path: None,
+ extra: vec![],
},
];
let seg = build_augmentation_segment(&augs, "zh");
@@ -258,6 +262,7 @@ mod tests {
title: "Idea1".into(),
status: IdeaStatus::Approved,
description: String::new(),
+ extra: vec![],
};
let seg = build_augmentation_segment(&[aug], "en");
assert!(seg.starts_with("--- The following is the context"), "en 头部: {}", seg);
@@ -271,6 +276,7 @@ mod tests {
title: "Idea1".into(),
status: IdeaStatus::Approved,
description: String::new(),
+ extra: vec![],
};
let seg = build_augmentation_segment(&[aug], "fr");
assert!(seg.starts_with("--- 以下是用户选择的上下文参考"), "未知 lang 应按 zh 兜底");
@@ -284,6 +290,7 @@ mod tests {
status: ProjectStatus::Planning,
description: String::new(),
path: None,
+ extra: vec![],
};
let seg = build_augmentation_segment(&[aug], "zh");
assert!(!seg.contains("目录:"), "无 path 不应渲染目录行: {}", seg);
diff --git a/src-tauri/src/commands/ai/commands/conversation.rs b/src-tauri/src/commands/ai/commands/conversation.rs
index 1db60d4..d4d5173 100644
--- a/src-tauri/src/commands/ai/commands/conversation.rs
+++ b/src-tauri/src/commands/ai/commands/conversation.rs
@@ -637,7 +637,7 @@ pub async fn ai_conversation_export(
#[cfg(test)]
mod tests {
use super::*;
- use df_ai::provider::{ChatMessage, ContentPart, MessageRole, MessageStatus, ToolCall, ToolCallFunction, ToolType};
+ use df_ai::provider::{ChatMessage, ContentPart, MessageRole, MessageStatus, ToolCall, ToolCallFunction};
fn base_msg() -> ChatMessage {
ChatMessage {
@@ -821,7 +821,7 @@ mod tests {
serde_json::to_string(&msg.tool_calls).unwrap()
);
assert_eq!(back.model.as_deref(), Some("m"));
- assert_eq!(back.status.as_deref(), Some("compressed"));
+ assert_eq!(back.status.as_ref(), Some(&MessageStatus::Compressed));
assert_eq!(back.reasoning_content.as_deref(), Some("r"));
assert_eq!(back.timestamp, Some(123));
}
@@ -875,7 +875,7 @@ mod tests {
},
}]),
status: if i % 6 == 0 {
- Some("compressed".into())
+ Some(MessageStatus::Compressed)
} else {
None
},
@@ -980,7 +980,7 @@ mod tests {
id: Some("m_summary".into()),
role: MessageRole::Assistant,
content: "压缩摘要".into(),
- status: Some("compressed".into()),
+ status: Some(MessageStatus::Compressed),
..base_msg()
},
ChatMessage {
diff --git a/src-tauri/src/commands/ai/tool_registry.rs b/src-tauri/src/commands/ai/tool_registry.rs
index b2557e2..1b6c8ee 100644
--- a/src-tauri/src/commands/ai/tool_registry.rs
+++ b/src-tauri/src/commands/ai/tool_registry.rs
@@ -4081,7 +4081,7 @@ mod tests {
id: "proj-svc-1".to_string(),
name: "proj-svc-1".to_string(),
description: String::new(),
- status: "planning".to_string(),
+ status: ProjectStatus::Planning,
idea_id: None,
path: None,
stack: None,
diff --git a/src/components/AiChat.vue b/src/components/AiChat.vue
index d6f33ef..0faad00 100644
--- a/src/components/AiChat.vue
+++ b/src/components/AiChat.vue
@@ -128,7 +128,7 @@