优化: 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:
@@ -1104,27 +1104,31 @@ pub(crate) async fn run_agentic_loop(
|
||||
let merge_result = coord.merge(&results);
|
||||
|
||||
// 将合并产出推回主对话(单条可展开 assistant 消息)
|
||||
// 消息级 token:Coordinator 路径无本轮 LLM(子任务已各自计入其会话/或未计入),
|
||||
// 此处为合成汇总非直接 LLM 输出,与下方 emit coord_usage(空 tokens 快照)对齐 → 0。
|
||||
{
|
||||
let mut session = session_arc.lock().await;
|
||||
session.conv(&conv_id).messages.push(
|
||||
ChatMessage::assistant(&format!(
|
||||
"## 多 Agent 执行完成\n\n共执行 {} 个子任务,成功 {} 个。\n\n{}",
|
||||
results.len(),
|
||||
results.iter().filter(|r| r.success).count(),
|
||||
merge_result.merged_output
|
||||
))
|
||||
);
|
||||
let mut merge_msg = ChatMessage::assistant(&format!(
|
||||
"## 多 Agent 执行完成\n\n共执行 {} 个子任务,成功 {} 个。\n\n{}",
|
||||
results.len(),
|
||||
results.iter().filter(|r| r.success).count(),
|
||||
merge_result.merged_output
|
||||
));
|
||||
merge_msg.prompt_tokens = Some(0);
|
||||
merge_msg.completion_tokens = Some(0);
|
||||
session.conv(&conv_id).messages.push(merge_msg);
|
||||
if !merge_result.conflicts.is_empty() {
|
||||
session.conv(&conv_id).messages.push(
|
||||
ChatMessage::assistant(&format!(
|
||||
"### 冲突检测\n\n检测到 {} 个文件冲突:\n{}",
|
||||
merge_result.conflicts.len(),
|
||||
merge_result.conflicts.iter()
|
||||
.map(|c| format!("- {}: {}", c.file, c.description))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
))
|
||||
);
|
||||
let mut conflict_msg = ChatMessage::assistant(&format!(
|
||||
"### 冲突检测\n\n检测到 {} 个文件冲突:\n{}",
|
||||
merge_result.conflicts.len(),
|
||||
merge_result.conflicts.iter()
|
||||
.map(|c| format!("- {}: {}", c.file, c.description))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
));
|
||||
conflict_msg.prompt_tokens = Some(0);
|
||||
conflict_msg.completion_tokens = Some(0);
|
||||
session.conv(&conv_id).messages.push(conflict_msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1565,6 +1569,10 @@ pub(crate) async fn run_agentic_loop(
|
||||
msg.model = Some(resolved_model.clone());
|
||||
// MidStream 保文也回填 reasoning_content
|
||||
msg.reasoning_content = round_reasoning_content.clone();
|
||||
// 消息级 token(对齐 push_assistant_message 双轨持久化):本轮 partial usage
|
||||
// (prompt=round 或 estimated 兜底,completion=round)。系统提示消息无 token,不设。
|
||||
msg.prompt_tokens = Some(usage.prompt_tokens);
|
||||
msg.completion_tokens = Some(usage.completion_tokens);
|
||||
conv.messages.push(msg);
|
||||
// 追加系统提示消息:响应因网络中断不完整(对齐决策 a1 系统提示机制)
|
||||
let mut notice = ChatMessage::system("⚠ 响应因网络中断不完整,以上为已接收的部分内容。可重新发送以获取完整回复。");
|
||||
@@ -1618,6 +1626,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
push_assistant_message(
|
||||
&mut session, &conv_id, has_tool_calls, &tool_calls_acc,
|
||||
&full_text, &resolved_model, &last_reasoning_content,
|
||||
round_prompt, round_usage.completion_tokens,
|
||||
);
|
||||
if GOAL_PIN_ENABLED {
|
||||
update_pinned_goals(&mut session, &conv_id, &tool_calls_acc);
|
||||
@@ -2324,6 +2333,8 @@ async fn finish_round_exit(
|
||||
// has_tool_calls=true: assistant_with_tools(文本+工具调用占位), false 且文本非空: assistant(纯文本)。
|
||||
// 两分支都不为空且都不需 push 时(no tool + empty text),返回(no-op)。
|
||||
// 入参 session 需外层调用方持锁;本函数只做纯内存 mutate,无 await/emit,不会死锁。
|
||||
// prompt_tokens/completion_tokens: 本轮 LLM 调用 token 用量(消息级持久化,解 reload/压缩/切会话后
|
||||
// 历史 assistant 消息 token 不显)。两构造分支都设。
|
||||
fn push_assistant_message(
|
||||
session: &mut AiSession,
|
||||
conv_id: &str,
|
||||
@@ -2332,6 +2343,8 @@ fn push_assistant_message(
|
||||
full_text: &str,
|
||||
resolved_model: &str,
|
||||
last_reasoning_content: &Option<String>,
|
||||
prompt_tokens: u32,
|
||||
completion_tokens: u32,
|
||||
) {
|
||||
if has_tool_calls {
|
||||
let mut order: Vec<u32> = tool_calls_acc.keys().copied().collect();
|
||||
@@ -2353,11 +2366,15 @@ fn push_assistant_message(
|
||||
let mut msg = ChatMessage::assistant_with_tools(full_text, ai_tool_calls);
|
||||
msg.model = Some(resolved_model.to_string());
|
||||
msg.reasoning_content = last_reasoning_content.clone();
|
||||
msg.prompt_tokens = Some(prompt_tokens);
|
||||
msg.completion_tokens = Some(completion_tokens);
|
||||
session.conv(conv_id).messages.push(msg);
|
||||
} else if !full_text.is_empty() {
|
||||
let mut msg = ChatMessage::assistant(full_text);
|
||||
msg.model = Some(resolved_model.to_string());
|
||||
msg.reasoning_content = last_reasoning_content.clone();
|
||||
msg.prompt_tokens = Some(prompt_tokens);
|
||||
msg.completion_tokens = Some(completion_tokens);
|
||||
session.conv(conv_id).messages.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,8 @@ pub fn record_to_message(rec: &AiMessageRecord) -> ChatMessage {
|
||||
status,
|
||||
reasoning_content: rec.reasoning_content.clone(),
|
||||
timestamp: rec.timestamp,
|
||||
prompt_tokens: rec.prompt_tokens,
|
||||
completion_tokens: rec.completion_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +147,8 @@ pub fn message_to_record(
|
||||
reasoning_content: msg.reasoning_content.clone(),
|
||||
timestamp: msg.timestamp,
|
||||
created_at: created_at.to_string(),
|
||||
prompt_tokens: msg.prompt_tokens,
|
||||
completion_tokens: msg.completion_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -381,20 +381,33 @@ pub(crate) fn invalidate_skills() {
|
||||
/// 注:guard(std::sync::RwLockReadGuard 非 Send)必须在 spawn_blocking 的 .await 前 drop,
|
||||
/// 否则非 Send 跨 await 点致 future 不 Send(MentionResolver 要求 Send)。guard 用 { } 限作用域。
|
||||
pub(crate) async fn read_skill_content_stripped(name: String) -> Option<String> {
|
||||
// 在作用域内取 path 后立即 drop guard,避免非 Send guard 跨 spawn_blocking await 点
|
||||
// 锁中毒 → skills_lock_async 返 None → ? 早返 None(对齐 P1-260617-3 中毒降级返空 desc)。
|
||||
let path: String = {
|
||||
let g = skills_lock_async().await?;
|
||||
let skills = g.as_ref()?;
|
||||
let info = skills.iter().find(|s| s.name == name)?;
|
||||
info.path.clone()
|
||||
// 用 skills_cached(扫盘填充 + clone)替代 skills_lock_async(只读):防御 SKILLS 为 None
|
||||
// (首次/invalidate 后)时 ? 早返 None 致技能不注入。skills_cached 触发 get_or_init 扫盘确保填充。
|
||||
let path: Option<String> = {
|
||||
let skills = skills_cached().await;
|
||||
skills.iter().find(|s| s.name == name).map(|s| s.path.clone())
|
||||
};
|
||||
let path = match path {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
tracing::warn!(skill = %name, "[read_skill_content_stripped] 缓存中未找到该技能(name 不匹配/未扫到/user_invocable:false)");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// 单文件读(spawn_blocking 防 Windows fs 慢),细化诊断各失败步便于定位真断点。
|
||||
// path move 进 spawn_blocking,先 clone 留诊断用(tracing 借用 move 后值会 E0382)。
|
||||
let path_for_log = path.clone();
|
||||
let md = match tokio::task::spawn_blocking(move || fs::read_to_string(&path)).await {
|
||||
Ok(Ok(content)) => content,
|
||||
Ok(Err(e)) => {
|
||||
tracing::warn!(skill = %name, path = %path_for_log, error = %e, "[read_skill_content_stripped] 读 SKILL.md 失败(路径/权限/编码)");
|
||||
return None;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(skill = %name, error = %e, "[read_skill_content_stripped] spawn_blocking join 失败");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// 单文件读取仍是同步 fs,但只一次小读;持续阻塞风险远低于扫盘递归。
|
||||
// 仍包 spawn_blocking 与慢路径一致(对齐 detect_stack:防 Windows fs 调度慢)。
|
||||
let md = tokio::task::spawn_blocking(move || fs::read_to_string(&path))
|
||||
.await
|
||||
.ok()?
|
||||
.ok()?;
|
||||
Some(strip_frontmatter(&md))
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ pub(crate) async fn ensure_conversation_title(
|
||||
model: None,
|
||||
status: None,
|
||||
reasoning_content: m.reasoning_content.clone(),
|
||||
prompt_tokens: None,
|
||||
completion_tokens: None,
|
||||
timestamp: m.timestamp,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Reference in New Issue
Block a user