diff --git a/.gitignore b/.gitignore index 5bfc168..ff77e95 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,28 @@ tmp/ # AI 编排脚本(Claude Code Workflow 临时产物,非产品代码) workflows/ +# 临时分析脚本(团队分析/报告生成,非产品代码根目录级) +/analysis_*.txt +/analysis_*.py +/core_domains.txt +/core_domains.py +/final_report.txt +/grand_output.txt +/im_live_verify.txt +/liang_deep.py +/liangxianyou_deep.txt +/member_profiles.py +/read_all.py +/report_p2.txt +/report_wall.txt +/team_analysis.py +/team_deep_analysis.py +/team_member_profiles.txt +/team_report_final.txt +/tmp_*.py +/tmp_*.txt +/verify_im_live*.py + # MCP/SQLite 测试库(mcp_test2 --db 残留) devflow-dev.db devflow-dev.db-shm diff --git a/Batch.md b/Batch.md index 3c93c02..806d3d2 100644 --- a/Batch.md +++ b/Batch.md @@ -1,7 +1,7 @@ # DevFlow 批次推进记录 > 记录每个批次的提交 hash、改动内容和交付价值。 -> 最后更新: 2026-07-01 | 最新提交: `36ea090` +> 最后更新: 2026-07-02 | 最新提交: `b187404` --- @@ -431,6 +431,16 @@ - Coordinator 接入 run_agentic_loop 入口(plan_execution_enabled 时分解意图→输出 Plan) - **验证**: cargo check 通过 +## Batch 37 — 代码卫生与质量提升(String→newtype + 文件清理 + 文档同步) + +- **提交**: `b187404` → 待完成 +- **内容**: + - ExecutionId/ToolCallType/ToolType 裸 String → newtype(IPC 边界仍透明序列化为字符串) + - ChatMessage.status 从 `Option` → `Option` 枚举(Active/Truncated/Compressed/ArchivedSegment) + - .gitignore 添加分析脚本,tmp 文件清理 + - Batch.md/文档状态同步 +- **验证**: cargo check 通过 + ## 后续规划批次(待推进) > 设计文档:[多Agent并行执行与仲裁合并设计-2026-07-01.md](docs/02-架构设计/专项设计/多Agent并行执行与仲裁合并设计-2026-07-01.md) diff --git a/crates/df-ai-core/src/provider.rs b/crates/df-ai-core/src/provider.rs index 2bf22b7..4149cfe 100644 --- a/crates/df-ai-core/src/provider.rs +++ b/crates/df-ai-core/src/provider.rs @@ -95,14 +95,14 @@ impl ChatMessage { /// 无需每加一个状态就来这里改。当前取值 None/Some("active")/Some("truncated") /// 行为与旧反面排除完全等价(None=true / "active"=true / "truncated"=false)。 pub fn is_active(&self) -> bool { - matches!(self.status.as_deref(), None | Some("active")) + matches!(self.status, None | Some(MessageStatus::Active)) } } impl ToolDefinition { pub fn function(name: impl Into, description: impl Into, parameters: serde_json::Value) -> Self { Self { - tool_type: "function".into(), + tool_type: ToolType::new("function"), function: ToolFunction { name: name.into(), description: description.into(), parameters }, } } @@ -166,22 +166,22 @@ mod tests { // "active" let mut m = ChatMessage::user("hi"); - m.status = Some("active".to_string()); + m.status = Some(MessageStatus::Active); assert!(m.is_active(), "Some(active) 应 active"); // "truncated" — 当前取值,与旧实现等价(false) let mut m = ChatMessage::user("hi"); - m.status = Some("truncated".to_string()); + m.status = Some(MessageStatus::Truncated); assert!(!m.is_active(), "truncated 应不 active"); // "archived_segment" — 阶段2 待引入,白名单自动隔离 let mut m = ChatMessage::user("hi"); - m.status = Some("archived_segment".to_string()); + m.status = Some(MessageStatus::ArchivedSegment); assert!(!m.is_active(), "archived_segment 应不 active(白名单隔离)"); // "compressed" — 阶段2 待引入,白名单自动隔离 let mut m = ChatMessage::user("hi"); - m.status = Some("compressed".to_string()); + m.status = Some(MessageStatus::Compressed); assert!(!m.is_active(), "compressed 应不 active(白名单隔离)"); } diff --git a/crates/df-ai-core/src/types.rs b/crates/df-ai-core/src/types.rs index 57cf8c3..3a777f7 100644 --- a/crates/df-ai-core/src/types.rs +++ b/crates/df-ai-core/src/types.rs @@ -103,11 +103,11 @@ pub struct ChatMessage { /// 生成该消息的 model(仅 assistant 消息有,消息级 model 追溯) #[serde(default, skip_serializing_if = "Option::is_none")] pub model: Option, - /// 消息状态(UX-09 编辑重生成):None/"active" 正常可见; + /// 消息状态(UX-09 编辑重生成):None 正常可见; /// "truncated" 软删(编辑某条 user 消息后其后续消息标记,保留 DB 可追溯但不进 LLM 上下文、前端视图过滤) /// 默认 None(向前兼容老 JSON 反序列化)。落库随 messages JSON 序列化,无需独立列。 #[serde(default, skip_serializing_if = "Option::is_none")] - pub status: Option, + pub status: Option, /// DeepSeek thinking 模式的推理内容(多轮需回传) #[serde(default, skip_serializing_if = "Option::is_none")] pub reasoning_content: Option, @@ -148,11 +148,91 @@ pub enum MessageRole { Tool, } +/// 消息状态枚举(IPC 边界序列化为小写 snake_case 字符串) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum MessageStatus { + /// 正常可见 + Active, + /// 软删(编辑某条 user 消息后其后续消息标记) + Truncated, + /// 已压缩 + Compressed, + /// 已归档段 + ArchivedSegment, +} + +impl MessageStatus { + /// DB 存储用的小写 snake_case 字符串 + pub fn as_db_str(&self) -> &'static str { + match self { + MessageStatus::Active => "active", + MessageStatus::Truncated => "truncated", + MessageStatus::Compressed => "compressed", + MessageStatus::ArchivedSegment => "archived_segment", + } + } + + /// 从 DB 字符串解析 + pub fn from_db_str(s: &str) -> Option { + Some(match s { + "active" => MessageStatus::Active, + "truncated" => MessageStatus::Truncated, + "compressed" => MessageStatus::Compressed, + "archived_segment" => MessageStatus::ArchivedSegment, + _ => return None, + }) + } +} + +/// ChatMessage 结构体 +/// 工具类型(IPC 边界透明序列化为字符串,如 "function") +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(transparent)] +pub struct ToolType(String); + +impl ToolType { + /// 构造新工具类型 + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } +} + +impl std::fmt::Display for ToolType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for ToolType { + fn from(s: String) -> Self { + Self(s) + } +} + +impl From<&str> for ToolType { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl PartialEq<&str> for ToolType { + fn eq(&self, other: &&str) -> bool { + self.0 == *other + } +} + +impl PartialEq for ToolType { + fn eq(&self, other: &str) -> bool { + self.0 == other + } +} + /// 工具定义 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolDefinition { #[serde(rename = "type")] - pub tool_type: String, + pub tool_type: ToolType, pub function: ToolFunction, } @@ -169,7 +249,7 @@ pub struct ToolFunction { pub struct ToolCall { pub id: String, #[serde(rename = "type")] - pub call_type: String, + pub call_type: ToolType, pub function: ToolCallFunction, } diff --git a/crates/df-ai/src/context/manager_tests.rs b/crates/df-ai/src/context/manager_tests.rs index 9737d88..93c5d13 100644 --- a/crates/df-ai/src/context/manager_tests.rs +++ b/crates/df-ai/src/context/manager_tests.rs @@ -186,7 +186,7 @@ fn push_token_only_active() { let active_msg = ChatMessage::user("这条是 active 的"); let active_tokens = TokenEstimator::default().estimate_message(&active_msg); let mut inactive_msg = ChatMessage::assistant("这条被截断了不该计 token"); - inactive_msg.status = Some("truncated".to_string()); + inactive_msg.status = Some(MessageStatus::Truncated); let inactive_tokens = TokenEstimator::default().estimate_message(&inactive_msg); mgr.push(active_msg); @@ -210,11 +210,11 @@ fn push_token_only_active() { // 2) restore_from_messages 路径(调 push,token 同步仅 active) let mut mgr2 = ContextManager::new(cfg(100_000)); let mut a = ChatMessage::user("active 一"); - a.status = Some("active".to_string()); + a.status = Some(MessageStatus::Active); let mut b = ChatMessage::user("archived 一"); - b.status = Some("archived_segment".to_string()); + b.status = Some(MessageStatus::ArchivedSegment); let mut c = ChatMessage::user("compressed 一"); - c.status = Some("compressed".to_string()); + c.status = Some(MessageStatus::Compressed); mgr2.restore_from_messages(vec![a, b, c]); assert_eq!(mgr2.len(), 3, "restore 后全量保留三条"); @@ -330,7 +330,7 @@ fn compress_old_messages_skips_already_inactive() { // 范围内含 truncated(已 !active)的消息:跳过,不返,不重复扣 token。 let mut mgr = ContextManager::new(cfg(100_000)); let mut truncated = ChatMessage::user("被截断"); - truncated.status = Some("truncated".to_string()); + truncated.status = Some(MessageStatus::Truncated); mgr.push(truncated); mgr.push(ChatMessage::user("active 一条")); let tokens_before = mgr.history_tokens(); @@ -397,7 +397,7 @@ fn insert_at_adds_to_budget_when_active() { // 插入 !active 消息 → 不计入 token let tokens_before_inactive = mgr.history_tokens(); let mut inactive = ChatMessage::user("x"); - inactive.status = Some("truncated".to_string()); + inactive.status = Some(MessageStatus::Truncated); mgr.insert_at(0, inactive); assert_eq!( mgr.history_tokens(), diff --git a/crates/df-ai/src/context/mod.rs b/crates/df-ai/src/context/mod.rs index 184933e..c85fc05 100644 --- a/crates/df-ai/src/context/mod.rs +++ b/crates/df-ai/src/context/mod.rs @@ -31,7 +31,7 @@ pub use crate::context_helpers::{ EvictionUnit, ContextConfig, MessageGroup, TokenEstimator, TrackedMessage, }; -use crate::provider::{ChatMessage, MessageRole}; +use crate::provider::{ChatMessage, MessageRole, MessageStatus}; // ============================================================ // 上下文管理器 @@ -321,7 +321,7 @@ impl ContextManager { let mut count = 0usize; for t in self.messages[i + 1..].iter_mut() { if t.message.is_active() { - t.message.status = Some("truncated".to_string()); + t.message.status = Some(MessageStatus::Truncated); count += 1; } } @@ -513,7 +513,7 @@ impl ContextManager { for t in self.messages[..end].iter_mut() { if t.message.is_active() { newly_compressed.push(t.message.clone()); - t.message.status = Some("compressed".to_string()); + t.message.status = Some(MessageStatus::Compressed); self.history_tokens = self.history_tokens.saturating_sub(t.token_count); } } diff --git a/crates/df-types/src/types.rs b/crates/df-types/src/types.rs index 6a03ba8..2a15dfd 100644 --- a/crates/df-types/src/types.rs +++ b/crates/df-types/src/types.rs @@ -22,8 +22,68 @@ pub type ReleaseId = String; pub type BranchId = String; /// 插件 ID pub type PluginId = String; -/// 执行 ID -pub type ExecutionId = String; +/// 执行 ID(IPC 边界透明序列化为字符串) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)] +#[serde(transparent)] +pub struct ExecutionId(String); + +impl ExecutionId { + /// 构造新执行 ID + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } +} + +impl std::fmt::Display for ExecutionId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::ops::Deref for ExecutionId { + type Target = str; + fn deref(&self) -> &str { + &self.0 + } +} + +impl From for ExecutionId { + fn from(s: String) -> Self { + Self(s) + } +} + +impl From<&str> for ExecutionId { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl From for String { + fn from(id: ExecutionId) -> Self { + id.0 + } +} + +// ExecutionId 与 &str 的比较(测试/匹配中大量使用) +impl PartialEq<&str> for ExecutionId { + fn eq(&self, other: &&str) -> bool { + self.0 == *other + } +} + +impl PartialEq for ExecutionId { + fn eq(&self, other: &str) -> bool { + self.0 == other + } +} + +impl PartialEq for ExecutionId { + fn eq(&self, other: &String) -> bool { + self.0 == *other + } +} + /// 决策 ID pub type DecisionId = String; /// 节点类型(如 ai / human / ai_self_review) @@ -31,7 +91,47 @@ pub type NodeType = String; /// 任务链接类型(如 depends_on / blocks / relates_to) pub type LinkType = String; /// 工具调用类型(如 function) -pub type ToolCallType = String; +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)] +#[serde(transparent)] +pub struct ToolCallType(String); + +impl ToolCallType { + /// 构造新工具调用类型 + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } +} + +impl std::fmt::Display for ToolCallType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::ops::Deref for ToolCallType { + type Target = str; + fn deref(&self) -> &str { + &self.0 + } +} + +impl From for ToolCallType { + fn from(s: String) -> Self { + Self(s) + } +} + +impl From<&str> for ToolCallType { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl From for String { + fn from(t: ToolCallType) -> Self { + t.0 + } +} // ============================================================ // 时间工具 diff --git a/src-tauri/src/commands/ai/commands/chat.rs b/src-tauri/src/commands/ai/commands/chat.rs index 4fb99c1..08f3a24 100644 --- a/src-tauri/src/commands/ai/commands/chat.rs +++ b/src-tauri/src/commands/ai/commands/chat.rs @@ -15,7 +15,7 @@ use std::sync::atomic::Ordering; use serde::Serialize; use tauri::{AppHandle, Emitter, Manager, State}; -use df_ai::provider::{ChatMessage, ContentPart}; +use df_ai::provider::{ChatMessage, ContentPart, MessageStatus}; use df_storage::models::AiProviderRecord; use df_types::augmentation::{MentionRef, MentionSpanDto}; use df_types::types::new_id; @@ -1147,7 +1147,7 @@ pub async fn ai_chat_clear_context( // 对保护区外 active 消息标 archived_segment(messages_mut 直接改 status)。 for t in conv.messages.messages_mut()[..protect_start].iter_mut() { if t.message.is_active() { - t.message.status = Some("archived_segment".to_string()); + t.message.status = Some(MessageStatus::ArchivedSegment); } } drop(session); diff --git a/src-tauri/src/commands/ai/commands/conversation.rs b/src-tauri/src/commands/ai/commands/conversation.rs index 1e5c6fa..1db60d4 100644 --- a/src-tauri/src/commands/ai/commands/conversation.rs +++ b/src-tauri/src/commands/ai/commands/conversation.rs @@ -15,7 +15,7 @@ use std::sync::atomic::Ordering; use tauri::{AppHandle, State}; -use df_ai::provider::{ChatMessage, MessageRole}; +use df_ai::provider::{ChatMessage, MessageRole, MessageStatus}; use df_storage::models::AiMessageRecord; use df_types::types::new_id; @@ -85,10 +85,10 @@ pub fn record_to_message(rec: &AiMessageRecord) -> ChatMessage { .filter(|s| !s.is_empty()) .and_then(|s| serde_json::from_str(s).ok()); // status "active" → None(归一化,对齐 ChatMessage 默认语义 None=正常可见) - let status = if rec.status == "active" { + let status = if rec.status == "active" || rec.status.is_empty() { None } else { - Some(rec.status.clone()) + MessageStatus::from_db_str(&rec.status) }; ChatMessage { id: Some(rec.id.clone()), @@ -127,7 +127,10 @@ pub fn message_to_record( .tool_calls .as_ref() .map(|t| serde_json::to_string(t).unwrap_or_default()); - let status = msg.status.clone().unwrap_or_else(|| "active".to_string()); + let status = msg.status + .as_ref() + .map(|s| s.as_db_str().to_string()) + .unwrap_or_else(|| "active".to_string()); AiMessageRecord { id, conversation_id: conversation_id.to_string(), @@ -634,7 +637,7 @@ pub async fn ai_conversation_export( #[cfg(test)] mod tests { use super::*; - use df_ai::provider::{ChatMessage, ContentPart, MessageRole, ToolCall, ToolCallFunction}; + use df_ai::provider::{ChatMessage, ContentPart, MessageRole, MessageStatus, ToolCall, ToolCallFunction, ToolType}; fn base_msg() -> ChatMessage { ChatMessage { @@ -746,20 +749,22 @@ mod tests { assert_eq!(back.tool_call_id.as_deref(), Some("call_abc")); } - /// status 各值("truncated"/"compressed")round-trip(非 active 原样保留) + /// status 各值(Truncated/Compressed)round-trip #[test] fn roundtrip_status_non_active_preserved() { let mut msg = base_msg(); - msg.status = Some("truncated".into()); + msg.status = Some(MessageStatus::Truncated); let rec = message_to_record(&msg, "c", 0, "ts"); assert_eq!(rec.status, "truncated"); let back = record_to_message(&rec); - assert_eq!(back.status.as_deref(), Some("truncated")); + assert_eq!(back.status, Some(MessageStatus::Truncated)); // compressed - msg.status = Some("compressed".into()); + msg.status = Some(MessageStatus::Compressed); let rec = message_to_record(&msg, "c", 0, "ts"); assert_eq!(rec.status, "compressed"); + let back = record_to_message(&rec); + assert_eq!(back.status, Some(MessageStatus::Compressed)); } /// id=None 兜底:`msg_{conv}_{seq}` @@ -802,7 +807,7 @@ mod tests { }, }]); msg.model = Some("m".into()); - msg.status = Some("compressed".into()); + msg.status = Some(MessageStatus::Compressed); msg.reasoning_content = Some("r".into()); msg.timestamp = Some(123); let rec = message_to_record(&msg, "conv_full", 7, "ts_full"); diff --git a/src-tauri/src/commands/workflow.rs b/src-tauri/src/commands/workflow.rs index c5000a6..2e8a487 100644 --- a/src-tauri/src/commands/workflow.rs +++ b/src-tauri/src/commands/workflow.rs @@ -102,10 +102,10 @@ pub async fn run_workflow_inner( let runtime_dag = state.registry.build_dag(&dag).map_err(err_str)?; // 2. 写入执行记录(status=running) - let execution_id = new_id(); + let execution_id: ExecutionId = new_id().into(); let dag_json = serde_json::to_string(&dag).map_err(err_str)?; let record = WorkflowRecord { - id: execution_id.clone(), + id: execution_id.to_string(), name, dag_json, status: "running".to_string(), @@ -149,13 +149,15 @@ pub async fn run_workflow_inner( &event, WorkflowEvent::WorkflowCompleted { execution_id, .. } | WorkflowEvent::WorkflowFailed { execution_id, .. } + // execution_id: WorkflowEvent 中反序列化的 ExecutionId(#[serde(default)]), + // forward_exec_id: 本 forward 循环所属的 ExecutionId,同类型直接比较 if execution_id == &forward_exec_id ); let payload = WorkflowEventPayload { execution_id: forward_exec_id.clone(), event, }; - if let Err(e) = forward_app.emit("workflow-event", &payload) { + if let Err(e) = forward_app.emit("workflow-event", &payload) { tracing::warn!("工作流事件转发失败: {}", e); } if finished { @@ -186,21 +188,21 @@ pub async fn run_workflow_inner( match workflows.get_by_id(&forward_exec_id).await { Ok(Some(record)) => { let terminal = match record.status.as_str() { - "completed" => Some(WorkflowEvent::WorkflowCompleted { - execution_id: forward_exec_id.clone(), - total_duration_ms: 0, - }), - "failed" => Some(WorkflowEvent::WorkflowFailed { - execution_id: forward_exec_id.clone(), - error: "工作流执行失败(Lagged 兜底补发,详情见 DB)" - .to_string(), - failed_node: String::new(), - }), - "cancelled" => Some(WorkflowEvent::WorkflowFailed { - execution_id: forward_exec_id.clone(), - error: "工作流被取消(Lagged 兜底补发)".to_string(), - failed_node: String::new(), - }), + "completed" => Some(WorkflowEvent::WorkflowCompleted { + execution_id: forward_exec_id.clone(), + total_duration_ms: 0, + }), + "failed" => Some(WorkflowEvent::WorkflowFailed { + execution_id: forward_exec_id.clone(), + error: "工作流执行失败(Lagged 兜底补发,详情见 DB)" + .to_string(), + failed_node: String::new(), + }), + "cancelled" => Some(WorkflowEvent::WorkflowFailed { + execution_id: forward_exec_id.clone(), + error: "工作流被取消(Lagged 兜底补发)".to_string(), + failed_node: String::new(), + }), _ => None, }; if let Some(synth_event) = terminal { @@ -351,7 +353,7 @@ pub async fn run_workflow_inner( } }); - Ok(execution_id) + Ok(execution_id.to_string()) } /// 列出全部工作流执行记录 diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 18e8d05..e7cc646 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -48,6 +48,7 @@ use df_workflow::state::StateMachine; use crate::commands::ai::augmentation::ResolverRegistry; use crate::commands::ai::AiSession; +use df_types::types::ExecutionId; /// 应用全局状态 — 通过 `app.manage()` 注入,command 中以 `State<'_, AppState>` 取用 pub struct AppState { @@ -152,7 +153,7 @@ pub struct AppState { /// clone 共享底层 HashMap,与下沉到 NodeContext.node_status 的是同一份); /// cancel_workflow_node IPC 经 execution_id 取出后 set_cancelled, /// 直达运行中 HumanNode 的 is_cancelled 轮询。执行完成(成功/失败)后移除条目。 - pub workflow_state_registry: Arc>>, + pub workflow_state_registry: Arc>>, // ── F-260619-03 Phase A: AI 工具文件访问授权目录白名单 ── /// AI 文件工具(read/write/list/patch/...)可访问的授权目录池。 /// Phase A 仅持久化白名单(Settings KV `allowed_dirs` 加载),