优化: 工具调用标识类型安全加固
This commit is contained in:
@@ -98,7 +98,9 @@ fn ai_tool_execution_from_row(row: &Row<'_>) -> std::result::Result<AiToolExecut
|
|||||||
// message_id 列老库经 v21 迁移补建。unwrap_or(None) 兜底:
|
// message_id 列老库经 v21 迁移补建。unwrap_or(None) 兜底:
|
||||||
// 新库空表直接有列;老库行 ALTER 后 NULL;极端情况(迁移未跑/手工删列)防御。
|
// 新库空表直接有列;老库行 ALTER 后 NULL;极端情况(迁移未跑/手工删列)防御。
|
||||||
message_id: row.get("message_id").unwrap_or(None),
|
message_id: row.get("message_id").unwrap_or(None),
|
||||||
tool_call_id: row.get("tool_call_id")?,
|
// row.get::<String> 后 From<String> → ToolCallId(newtype 不直接 impl rusqlite::FromSql,
|
||||||
|
// 避免 df-types 反向依赖 rusqlite;Deref<Target=str> 不自动给 ToSql)
|
||||||
|
tool_call_id: row.get::<_, String>("tool_call_id")?.into(),
|
||||||
tool_name: row.get("tool_name")?,
|
tool_name: row.get("tool_name")?,
|
||||||
arguments: row.get("arguments")?,
|
arguments: row.get("arguments")?,
|
||||||
result: row.get("result")?,
|
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)
|
"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)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
|
||||||
params![
|
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.arguments, rec.result, rec.status, rec.risk_level,
|
||||||
rec.requested_at, rec.executed_at, rec.decided_by
|
rec.requested_at, rec.executed_at, rec.decided_by
|
||||||
],
|
],
|
||||||
@@ -231,7 +233,7 @@ impl_repo!(
|
|||||||
conn.execute(
|
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",
|
"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![
|
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.arguments, rec.result, rec.status, rec.risk_level,
|
||||||
rec.requested_at, rec.executed_at, rec.decided_by, rec.id
|
rec.requested_at, rec.executed_at, rec.decided_by, rec.id
|
||||||
],
|
],
|
||||||
@@ -269,7 +271,7 @@ impl AiToolExecutionRepo {
|
|||||||
)?;
|
)?;
|
||||||
for rec in &records {
|
for rec in &records {
|
||||||
stmt.execute(params![
|
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.arguments, rec.result, rec.status, rec.risk_level,
|
||||||
rec.requested_at, rec.executed_at, rec.decided_by
|
rec.requested_at, rec.executed_at, rec.decided_by
|
||||||
])?;
|
])?;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! 数据模型定义 — 与数据库表对应的 Rust 结构体
|
//! 数据模型定义 — 与数据库表对应的 Rust 结构体
|
||||||
|
|
||||||
use df_ai_core::model::{deserialize_model_configs, ModelConfig};
|
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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -383,7 +383,9 @@ pub struct AiToolExecutionRecord {
|
|||||||
/// 升级后,audit 写入从 ContextManager 取当前 assistant 消息 id 填入。
|
/// 升级后,audit 写入从 ContextManager 取当前 assistant 消息 id 填入。
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub message_id: Option<String>,
|
pub message_id: Option<String>,
|
||||||
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 tool_name: String,
|
||||||
pub arguments: String,
|
pub arguments: String,
|
||||||
pub result: Option<String>,
|
pub result: Option<String>,
|
||||||
|
|||||||
@@ -84,6 +84,73 @@ impl PartialEq<String> 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<String>) -> 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<String> 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<ToolCallId> 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<str> for ToolCallId {
|
||||||
|
fn eq(&self, other: &str) -> bool {
|
||||||
|
self.0 == other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<String> for ToolCallId {
|
||||||
|
fn eq(&self, other: &String) -> bool {
|
||||||
|
self.0 == *other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 决策 ID
|
/// 决策 ID
|
||||||
pub type DecisionId = String;
|
pub type DecisionId = String;
|
||||||
/// 节点类型(如 ai / human / ai_self_review)
|
/// 节点类型(如 ai / human / ai_self_review)
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ pub(crate) fn build_audit_record(
|
|||||||
// assistant 消息已 push 到 per_conv.messages,入口取末条 assistant id)。
|
// assistant 消息已 push 到 per_conv.messages,入口取末条 assistant id)。
|
||||||
// None 表示无 assistant 消息(异常路径/老数据无 id),展示侧兼容。
|
// None 表示无 assistant 消息(异常路径/老数据无 id),展示侧兼容。
|
||||||
message_id: message_id.map(|s| s.to_string()),
|
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(),
|
tool_name: tool_name.to_string(),
|
||||||
arguments: arguments.to_string(),
|
arguments: arguments.to_string(),
|
||||||
result,
|
result,
|
||||||
@@ -228,7 +229,8 @@ pub async fn list_tool_executions(
|
|||||||
.map(|r| ToolExecutionDto {
|
.map(|r| ToolExecutionDto {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
conversation_id: r.conversation_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,
|
tool_name: r.tool_name,
|
||||||
arguments_brief: super::truncate_chars(&r.arguments, 120),
|
arguments_brief: super::truncate_chars(&r.arguments, 120),
|
||||||
result_brief: r.result.map(|s| super::truncate_chars(&s, 160)),
|
result_brief: r.result.map(|s| super::truncate_chars(&s, 160)),
|
||||||
@@ -362,7 +364,7 @@ mod tests {
|
|||||||
id: new_id(),
|
id: new_id(),
|
||||||
conversation_id: None,
|
conversation_id: None,
|
||||||
message_id: None,
|
message_id: None,
|
||||||
tool_call_id: new_id(),
|
tool_call_id: new_id().into(),
|
||||||
tool_name: tool.to_string(),
|
tool_name: tool.to_string(),
|
||||||
arguments: "{}".to_string(),
|
arguments: "{}".to_string(),
|
||||||
result: None,
|
result: None,
|
||||||
|
|||||||
@@ -75,9 +75,11 @@ pub async fn restore_pending_approvals(state: &AppState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
session.pending_approvals.insert(
|
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 {
|
PendingApproval {
|
||||||
tool_call_id: rec.tool_call_id,
|
tool_call_id: rec.tool_call_id.to_string(),
|
||||||
tool_name: rec.tool_name,
|
tool_name: rec.tool_name,
|
||||||
arguments: args,
|
arguments: args,
|
||||||
conversation_id: rec.conversation_id,
|
conversation_id: rec.conversation_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user