优化: token消息级持久化 + 技能注入修复 + TopBar减法/UI调
token持久化(方案A,治压缩/切会话后历史token不显):ChatMessage/AiMessageRecord 加 prompt_tokens/completion_tokens(serde + DB V38 迁移 + message_repo 映射);agentic push_assistant_message 设本轮 token + provider/title 构造默认 None;前端 AiMessage 加字段 + switchConversation reload 映射 tokenUsage(双轨:消息级新+会话级旧累计保留) 技能注入修复:read_skill_content_stripped 改 skills_cached 扫盘(防御 SKILLS None 致不注入)+ 细化诊断(缓存/path/fs 各步) TopBar减法/UI:删铅笔新建(与侧栏+重复)/删垃圾桶clear-chat(危险,clear-context归档替代)/删系统就绪装饰占位;更多菜单popout CSS补全(修样式错乱);provider绿点有信息化(绿/红/灰基于AI请求成败)+垂直居中;goals面板补top:100%(修位置飘)+dot/check/remove CSS+goals/history item统一+history index边距;4面板互斥(点一个收其他) MessageList token v-if 去 !streaming(修发新消息历史token消失)
This commit is contained in:
@@ -42,25 +42,25 @@ impl ContentPart {
|
||||
|
||||
impl ChatMessage {
|
||||
pub fn system(content: impl Into<String>) -> Self {
|
||||
Self { id: Some(new_message_id()), role: MessageRole::System, content: content.into(), parts: None, tool_call_id: None, tool_calls: None, model: None, status: None, reasoning_content: None, timestamp: Some(now_millis_i64()) }
|
||||
Self { id: Some(new_message_id()), role: MessageRole::System, content: content.into(), parts: None, tool_call_id: None, tool_calls: None, model: None, status: None, reasoning_content: None, prompt_tokens: None, completion_tokens: None, timestamp: Some(now_millis_i64()) }
|
||||
}
|
||||
pub fn user(content: impl Into<String>) -> Self {
|
||||
Self { id: Some(new_message_id()), role: MessageRole::User, content: content.into(), parts: None, tool_call_id: None, tool_calls: None, model: None, status: None, reasoning_content: None, timestamp: Some(now_millis_i64()) }
|
||||
Self { id: Some(new_message_id()), role: MessageRole::User, content: content.into(), parts: None, tool_call_id: None, tool_calls: None, model: None, status: None, reasoning_content: None, prompt_tokens: None, completion_tokens: None, timestamp: Some(now_millis_i64()) }
|
||||
}
|
||||
pub fn assistant(content: impl Into<String>) -> Self {
|
||||
Self { id: Some(new_message_id()), role: MessageRole::Assistant, content: content.into(), parts: None, tool_call_id: None, tool_calls: None, model: None, status: None, reasoning_content: None, timestamp: Some(now_millis_i64()) }
|
||||
Self { id: Some(new_message_id()), role: MessageRole::Assistant, content: content.into(), parts: None, tool_call_id: None, tool_calls: None, model: None, status: None, reasoning_content: None, prompt_tokens: None, completion_tokens: None, timestamp: Some(now_millis_i64()) }
|
||||
}
|
||||
pub fn assistant_with_tools(content: impl Into<String>, tool_calls: Vec<ToolCall>) -> Self {
|
||||
Self { id: Some(new_message_id()), role: MessageRole::Assistant, content: content.into(), parts: None, tool_call_id: None, tool_calls: Some(tool_calls), model: None, status: None, reasoning_content: None, timestamp: Some(now_millis_i64()) }
|
||||
Self { id: Some(new_message_id()), role: MessageRole::Assistant, content: content.into(), parts: None, tool_call_id: None, tool_calls: Some(tool_calls), model: None, status: None, reasoning_content: None, prompt_tokens: None, completion_tokens: None, timestamp: Some(now_millis_i64()) }
|
||||
}
|
||||
pub fn tool_result(call_id: impl Into<String>, content: impl Into<String>) -> Self {
|
||||
Self { id: Some(new_message_id()), role: MessageRole::Tool, content: content.into(), parts: None, tool_call_id: Some(call_id.into()), tool_calls: None, model: None, status: None, reasoning_content: None, timestamp: Some(now_millis_i64()) }
|
||||
Self { id: Some(new_message_id()), role: MessageRole::Tool, content: content.into(), parts: None, tool_call_id: Some(call_id.into()), tool_calls: None, model: None, status: None, reasoning_content: None, prompt_tokens: None, completion_tokens: None, timestamp: Some(now_millis_i64()) }
|
||||
}
|
||||
|
||||
/// 多模态 user 消息:content 文本 + parts(含 Image 片)。
|
||||
/// content 作为人类可读文本(也作非 vision 端点降级载荷);parts 透传给 vision 端点。
|
||||
pub fn user_parts(content: impl Into<String>, parts: Vec<ContentPart>) -> Self {
|
||||
Self { id: Some(new_message_id()), role: MessageRole::User, content: content.into(), parts: Some(parts), tool_call_id: None, tool_calls: None, model: None, status: None, reasoning_content: None, timestamp: Some(now_millis_i64()) }
|
||||
Self { id: Some(new_message_id()), role: MessageRole::User, content: content.into(), parts: Some(parts), tool_call_id: None, tool_calls: None, model: None, status: None, reasoning_content: None, prompt_tokens: None, completion_tokens: None, timestamp: Some(now_millis_i64()) }
|
||||
}
|
||||
|
||||
/// 是否含图片片(供 provider 判定走多模态分支)。
|
||||
|
||||
@@ -115,6 +115,15 @@ pub struct ChatMessage {
|
||||
/// provider 请求映射不读此字段(构造器打戳→映射忽略,不进 LLM 请求),老数据反序列化为 None。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub timestamp: Option<i64>,
|
||||
/// 本轮 LLM 调用的输入 token 用量(仅 assistant 消息,消息级 token 持久化)。
|
||||
/// 双轨:消息级(本字段,新)+ 会话级累计(ai_conversations.prompt_tokens,旧,保留)。
|
||||
/// 用于 reload/压缩/切会话后历史 assistant 消息 token 显示;老 JSON 反序列化为 None(向前兼容)。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub prompt_tokens: Option<u32>,
|
||||
/// 本轮 LLM 调用的输出 token 用量(仅 assistant 消息,消息级 token 持久化)。
|
||||
/// 语义同 prompt_tokens;provider 流式 usage 缺失时(GLM 等)可能为 0。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub completion_tokens: Option<u32>,
|
||||
}
|
||||
|
||||
/// 当前 Unix 毫秒(ChatMessage 打戳用;df-ai-core 不依赖 df-types,内联避免新增依赖)。
|
||||
|
||||
@@ -41,6 +41,8 @@ fn ai_message_from_row(row: &Row<'_>) -> std::result::Result<AiMessageRecord, ru
|
||||
reasoning_content: row.get("reasoning_content")?,
|
||||
timestamp: row.get("timestamp")?,
|
||||
created_at: row.get("created_at")?,
|
||||
prompt_tokens: row.get("prompt_tokens")?,
|
||||
completion_tokens: row.get("completion_tokens")?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -79,15 +81,17 @@ impl AiMessageRepo {
|
||||
let mut stmt = tx.prepare(
|
||||
"INSERT OR IGNORE INTO ai_messages
|
||||
(id, conversation_id, seq, role, content, parts, tool_call_id,
|
||||
tool_calls, model, status, reasoning_content, timestamp, created_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
|
||||
tool_calls, model, status, reasoning_content, timestamp, created_at,
|
||||
prompt_tokens, completion_tokens)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
|
||||
)
|
||||
.map_err(storage_err)?;
|
||||
for rec in &records {
|
||||
stmt.execute(params![
|
||||
rec.id, rec.conversation_id, rec.seq, rec.role, rec.content,
|
||||
rec.parts, rec.tool_call_id, rec.tool_calls, rec.model, rec.status,
|
||||
rec.reasoning_content, rec.timestamp, rec.created_at
|
||||
rec.reasoning_content, rec.timestamp, rec.created_at,
|
||||
rec.prompt_tokens, rec.completion_tokens
|
||||
])
|
||||
.map_err(storage_err)?;
|
||||
}
|
||||
@@ -111,7 +115,8 @@ impl AiMessageRepo {
|
||||
let mut stmt = guard
|
||||
.prepare(
|
||||
"SELECT id, conversation_id, seq, role, content, parts, tool_call_id,
|
||||
tool_calls, model, status, reasoning_content, timestamp, created_at
|
||||
tool_calls, model, status, reasoning_content, timestamp, created_at,
|
||||
prompt_tokens, completion_tokens
|
||||
FROM ai_messages WHERE conversation_id = ?1 ORDER BY seq ASC",
|
||||
)
|
||||
.map_err(storage_err)?;
|
||||
@@ -151,11 +156,13 @@ impl AiMessageRepo {
|
||||
let limit = limit.max(1) as i64;
|
||||
let sql = if before_seq.is_some() {
|
||||
"SELECT id, conversation_id, seq, role, content, parts, tool_call_id,
|
||||
tool_calls, model, status, reasoning_content, timestamp, created_at
|
||||
tool_calls, model, status, reasoning_content, timestamp, created_at,
|
||||
prompt_tokens, completion_tokens
|
||||
FROM ai_messages WHERE conversation_id = ?1 AND seq < ?2 ORDER BY seq DESC LIMIT ?3"
|
||||
} else {
|
||||
"SELECT id, conversation_id, seq, role, content, parts, tool_call_id,
|
||||
tool_calls, model, status, reasoning_content, timestamp, created_at
|
||||
tool_calls, model, status, reasoning_content, timestamp, created_at,
|
||||
prompt_tokens, completion_tokens
|
||||
FROM ai_messages WHERE conversation_id = ?1 ORDER BY seq DESC LIMIT ?2"
|
||||
};
|
||||
let mut stmt = guard.prepare(sql).map_err(storage_err)?;
|
||||
@@ -347,6 +354,8 @@ mod tests {
|
||||
reasoning_content: None,
|
||||
timestamp: None,
|
||||
created_at: now_millis_str(),
|
||||
prompt_tokens: None,
|
||||
completion_tokens: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,6 +513,8 @@ mod tests {
|
||||
reasoning_content: Some("思考".into()),
|
||||
timestamp: Some(1700000000000),
|
||||
created_at: now.clone(),
|
||||
prompt_tokens: None,
|
||||
completion_tokens: None,
|
||||
},
|
||||
AiMessageRecord {
|
||||
id: "new_1".into(),
|
||||
@@ -519,6 +530,8 @@ mod tests {
|
||||
reasoning_content: None,
|
||||
timestamp: None,
|
||||
created_at: now,
|
||||
prompt_tokens: None,
|
||||
completion_tokens: None,
|
||||
},
|
||||
];
|
||||
repo.replace_conversation("conv", records).await.expect("replace");
|
||||
@@ -586,6 +599,8 @@ mod tests {
|
||||
reasoning_content: None,
|
||||
timestamp: None,
|
||||
created_at: now,
|
||||
prompt_tokens: None,
|
||||
completion_tokens: None,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
@@ -622,6 +637,8 @@ mod tests {
|
||||
reasoning_content: None,
|
||||
timestamp: None,
|
||||
created_at: now.clone(),
|
||||
prompt_tokens: None,
|
||||
completion_tokens: None,
|
||||
};
|
||||
repo.replace_conversation("c", vec![rec()]).await.expect("1st");
|
||||
repo.replace_conversation("c", vec![rec()]).await.expect("2nd");
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
// 什么数据库、Redis 在哪、有没有 MQ"的基础设施上下文。
|
||||
// V33 = 审批重启恢复:ai_conversations 加 pending_approvals TEXT 列,持久化挂起审批快照,
|
||||
// 重启后从 DB 恢复 pending_approvals 内存态,使待审批不丢。
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 37] = [
|
||||
let steps: [(i32, fn(&Connection) -> Result<()>); 38] = [
|
||||
(1, migrate_v1),
|
||||
(2, migrate_v2),
|
||||
(3, migrate_v3),
|
||||
@@ -83,6 +83,7 @@ pub fn run(conn: &Connection) -> Result<()> {
|
||||
(35, migrate_v35),
|
||||
(36, migrate_v36),
|
||||
(37, migrate_v37),
|
||||
(38, migrate_v38),
|
||||
];
|
||||
|
||||
for (version, migrate_fn) in steps {
|
||||
@@ -1135,6 +1136,27 @@ fn migrate_v37(conn: &Connection) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V38: ai_messages 加 prompt_tokens / completion_tokens 列(消息级 token 持久化)
|
||||
///
|
||||
/// 解「压缩/切会话后历史 assistant 消息 token 不显」:原 token 仅前端内存态
|
||||
/// (useAiEvents AiCompleted 设 tokenUsage),DB 仅会话级累计
|
||||
/// (ai_conversations.prompt_tokens/completion_tokens)。本迁移加消息级两列,
|
||||
/// 让 push_assistant_message 设的本轮 token 经 save_conversation → AiMessageRecord
|
||||
/// 落库,前端 reload 时映射回 tokenUsage。NULL(老消息)→ 前端 tokenUsage=undefined(向前兼容)。
|
||||
fn migrate_v38(conn: &Connection) -> Result<()> {
|
||||
if !column_exists(conn, "ai_messages", "prompt_tokens") {
|
||||
conn.execute("ALTER TABLE ai_messages ADD COLUMN prompt_tokens INTEGER", [])?;
|
||||
tracing::info!("v38: ai_messages 加 prompt_tokens 列");
|
||||
}
|
||||
if !column_exists(conn, "ai_messages", "completion_tokens") {
|
||||
conn.execute("ALTER TABLE ai_messages ADD COLUMN completion_tokens INTEGER", [])?;
|
||||
tracing::info!("v38: ai_messages 加 completion_tokens 列");
|
||||
}
|
||||
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [38])?;
|
||||
tracing::info!("迁移 v38 完成: ai_messages 加消息级 token 列");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// V21 建表 SQL — 消息拆分存储 ai_messages 表
|
||||
///
|
||||
/// 与 V9_SQL 中的 ai_messages 镜像(V9 给新库,此 const 给老库 V21 迁移用 IF NOT EXISTS)。
|
||||
@@ -1154,6 +1176,8 @@ CREATE TABLE IF NOT EXISTS ai_messages (
|
||||
reasoning_content TEXT,
|
||||
timestamp INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
prompt_tokens INTEGER,
|
||||
completion_tokens INTEGER,
|
||||
UNIQUE(conversation_id, seq)
|
||||
);
|
||||
|
||||
@@ -1412,6 +1436,8 @@ CREATE TABLE IF NOT EXISTS ai_messages (
|
||||
reasoning_content TEXT,
|
||||
timestamp INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
prompt_tokens INTEGER,
|
||||
completion_tokens INTEGER,
|
||||
UNIQUE(conversation_id, seq)
|
||||
);
|
||||
|
||||
|
||||
@@ -421,6 +421,11 @@ pub struct AiMessageRecord {
|
||||
pub timestamp: Option<i64>,
|
||||
/// 落库时间字符串(迁移期 fallback 到对话 created_at)
|
||||
pub created_at: String,
|
||||
/// 本轮 LLM 调用输入 token 用量(仅 assistant,消息级 token 持久化)。
|
||||
/// 老/NULL → ChatMessage.prompt_tokens=None(向前兼容)。
|
||||
pub prompt_tokens: Option<u32>,
|
||||
/// 本轮 LLM 调用输出 token 用量(仅 assistant,消息级 token 持久化)。
|
||||
pub completion_tokens: Option<u32>,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user