重构: String→newtype 强类型 + 工程清理

- df-types: ExecutionId/ToolCallType 从 type 别名改为 newtype 结构体
- df-ai-core: 新增 ToolType newtype / MessageStatus 枚举,
  ChatMessage.status 从 Option<String> 改为 Option<MessageStatus>
- provider: is_active() 改用 MessageStatus::Active 模式匹配
- context/chat/conversation: 构造站点更新为枚举变体
- .gitignore: 添加分析脚本排除项,清理根目录临时文件
- Batch.md: 更新最新提交与新增 Batch 37 记录
This commit is contained in:
lxy
2026-07-02 20:11:52 +08:00
parent b18740405c
commit 46246880b3
11 changed files with 275 additions and 55 deletions
+2 -2
View File
@@ -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);
@@ -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");