From 256af4e73657fd2604edab6ab07646e21ed6ba4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Mon, 10 Aug 2026 08:04:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96:=20=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E6=A0=87=E8=AF=86=E7=B1=BB=E5=9E=8B=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E5=8A=A0=E5=9B=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../df-storage/src/crud/conversation_repo.rs | 10 +-- crates/df-storage/src/models.rs | 6 +- crates/df-types/src/types.rs | 67 +++++++++++++++++++ src-tauri/src/commands/ai/audit/record.rs | 8 ++- src-tauri/src/commands/ai/audit/restore.rs | 6 +- 5 files changed, 86 insertions(+), 11 deletions(-) diff --git a/crates/df-storage/src/crud/conversation_repo.rs b/crates/df-storage/src/crud/conversation_repo.rs index 637b2a4..4a4967b 100644 --- a/crates/df-storage/src/crud/conversation_repo.rs +++ b/crates/df-storage/src/crud/conversation_repo.rs @@ -98,7 +98,9 @@ fn ai_tool_execution_from_row(row: &Row<'_>) -> std::result::Result 后 From → ToolCallId(newtype 不直接 impl rusqlite::FromSql, + // 避免 df-types 反向依赖 rusqlite;Deref 不自动给 ToSql) + tool_call_id: row.get::<_, String>("tool_call_id")?.into(), tool_name: row.get("tool_name")?, arguments: row.get("arguments")?, result: row.get("result")?, @@ -221,7 +223,7 @@ impl_repo!( "INSERT INTO ai_tool_executions (id, conversation_id, message_id, tool_call_id, tool_name, arguments, result, status, risk_level, requested_at, executed_at, decided_by) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", params![ - rec.id, rec.conversation_id, rec.message_id, rec.tool_call_id, rec.tool_name, + rec.id, rec.conversation_id, rec.message_id, &*rec.tool_call_id, rec.tool_name, rec.arguments, rec.result, rec.status, rec.risk_level, rec.requested_at, rec.executed_at, rec.decided_by ], @@ -231,7 +233,7 @@ impl_repo!( conn.execute( "UPDATE ai_tool_executions SET conversation_id = ?1, message_id = ?2, tool_call_id = ?3, tool_name = ?4, arguments = ?5, result = ?6, status = ?7, risk_level = ?8, requested_at = ?9, executed_at = ?10, decided_by = ?11 WHERE id = ?12", params![ - rec.conversation_id, rec.message_id, rec.tool_call_id, rec.tool_name, + rec.conversation_id, rec.message_id, &*rec.tool_call_id, rec.tool_name, rec.arguments, rec.result, rec.status, rec.risk_level, rec.requested_at, rec.executed_at, rec.decided_by, rec.id ], @@ -269,7 +271,7 @@ impl AiToolExecutionRepo { )?; for rec in &records { stmt.execute(params![ - rec.id, rec.conversation_id, rec.message_id, rec.tool_call_id, rec.tool_name, + rec.id, rec.conversation_id, rec.message_id, &*rec.tool_call_id, rec.tool_name, rec.arguments, rec.result, rec.status, rec.risk_level, rec.requested_at, rec.executed_at, rec.decided_by ])?; diff --git a/crates/df-storage/src/models.rs b/crates/df-storage/src/models.rs index 8fb956d..0a0b722 100644 --- a/crates/df-storage/src/models.rs +++ b/crates/df-storage/src/models.rs @@ -1,7 +1,7 @@ //! 数据模型定义 — 与数据库表对应的 Rust 结构体 use df_ai_core::model::{deserialize_model_configs, ModelConfig}; -use df_types::types::{IdeaStatus, LinkType, NodeType, ProjectStatus, TaskStatus}; +use df_types::types::{IdeaStatus, LinkType, NodeType, ProjectStatus, TaskStatus, ToolCallId}; use serde::{Deserialize, Serialize}; // ============================================================ @@ -383,7 +383,9 @@ pub struct AiToolExecutionRecord { /// 升级后,audit 写入从 ContextManager 取当前 assistant 消息 id 填入。 #[serde(default, skip_serializing_if = "Option::is_none")] pub message_id: Option, - pub tool_call_id: String, + /// 工具调用关联键(跨 tool_calls / pending_approvals / ai_tool_executions)。 + /// branded newtype 防把 execution_id / conversation_id 等同形 String 误传。 + pub tool_call_id: ToolCallId, pub tool_name: String, pub arguments: String, pub result: Option, diff --git a/crates/df-types/src/types.rs b/crates/df-types/src/types.rs index d6ba3ad..5dec18d 100644 --- a/crates/df-types/src/types.rs +++ b/crates/df-types/src/types.rs @@ -84,6 +84,73 @@ impl PartialEq for ExecutionId { } } +/// 工具调用 ID(跨 ai_tool_executions / tool_calls 关联键,IPC 边界透明序列化为字符串) +/// +/// branded newtype:编译器拒绝把任意 String 当 ToolCallId 传入,防误传(如把 +/// execution_id 当 tool_call_id 落库)。serde `transparent` 保证 JSON 形态为纯字符串, +/// 前端零感知。设计同 [`ExecutionId`] / [`ToolCallType`],手写不抽宏(全仓仅 3 个 +/// 字符串 newtype,抽 macro_rules 增心智成本无收益)。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)] +#[serde(transparent)] +pub struct ToolCallId(String); + +impl ToolCallId { + /// 构造新工具调用 ID + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } +} + +impl std::fmt::Display for ToolCallId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::ops::Deref for ToolCallId { + type Target = str; + fn deref(&self) -> &str { + &self.0 + } +} + +impl From for ToolCallId { + fn from(s: String) -> Self { + Self(s) + } +} + +impl From<&str> for ToolCallId { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl From for String { + fn from(id: ToolCallId) -> Self { + id.0 + } +} + +// ToolCallId 与 &str/String 的比较(测试/匹配中大量使用,与 ExecutionId 对齐) +impl PartialEq<&str> for ToolCallId { + fn eq(&self, other: &&str) -> bool { + self.0 == *other + } +} + +impl PartialEq for ToolCallId { + fn eq(&self, other: &str) -> bool { + self.0 == other + } +} + +impl PartialEq for ToolCallId { + fn eq(&self, other: &String) -> bool { + self.0 == *other + } +} + /// 决策 ID pub type DecisionId = String; /// 节点类型(如 ai / human / ai_self_review) diff --git a/src-tauri/src/commands/ai/audit/record.rs b/src-tauri/src/commands/ai/audit/record.rs index 85f53ef..7b10a00 100644 --- a/src-tauri/src/commands/ai/audit/record.rs +++ b/src-tauri/src/commands/ai/audit/record.rs @@ -48,7 +48,8 @@ pub(crate) fn build_audit_record( // assistant 消息已 push 到 per_conv.messages,入口取末条 assistant id)。 // None 表示无 assistant 消息(异常路径/老数据无 id),展示侧兼容。 message_id: message_id.map(|s| s.to_string()), - tool_call_id: tool_call_id.to_string(), + // &str → ToolCallId(From<&str>),branded newtype 防外部 String 误传 + tool_call_id: tool_call_id.into(), tool_name: tool_name.to_string(), arguments: arguments.to_string(), result, @@ -228,7 +229,8 @@ pub async fn list_tool_executions( .map(|r| ToolExecutionDto { id: r.id, conversation_id: r.conversation_id, - tool_call_id: r.tool_call_id, + // ToolCallId → String(DTO 边界,前端按字符串消费) + tool_call_id: r.tool_call_id.into(), tool_name: r.tool_name, arguments_brief: super::truncate_chars(&r.arguments, 120), result_brief: r.result.map(|s| super::truncate_chars(&s, 160)), @@ -362,7 +364,7 @@ mod tests { id: new_id(), conversation_id: None, message_id: None, - tool_call_id: new_id(), + tool_call_id: new_id().into(), tool_name: tool.to_string(), arguments: "{}".to_string(), result: None, diff --git a/src-tauri/src/commands/ai/audit/restore.rs b/src-tauri/src/commands/ai/audit/restore.rs index 003b7d2..aa3719c 100644 --- a/src-tauri/src/commands/ai/audit/restore.rs +++ b/src-tauri/src/commands/ai/audit/restore.rs @@ -75,9 +75,11 @@ pub async fn restore_pending_approvals(state: &AppState) { } } session.pending_approvals.insert( - rec.tool_call_id.clone(), + // ToolCallId → String(AiSession pending_approvals 是 String HashMap, + // 与 LLM provider draft.id 同形直接比较;DB 边界已 newtype 防误传) + rec.tool_call_id.to_string(), PendingApproval { - tool_call_id: rec.tool_call_id, + tool_call_id: rec.tool_call_id.to_string(), tool_name: rec.tool_name, arguments: args, conversation_id: rec.conversation_id,