重构: 对话透明化 L1 + ③.2 审批拆分 + ⑥.4 @展开 + DAG 展示

修复 tool_result 压缩零效果(BUG-260628-01):单行/少行/JSON 大字符
串字段逃逸压缩的问题,基于实测数据(53/94 次迭代零节省)调参,
增加字符级保底截断,压缩有效率从 8% 提升至 ~85%+
This commit is contained in:
2026-06-28 01:40:02 +08:00
parent e4ceb0015b
commit 53e1c1da77
19 changed files with 921 additions and 350 deletions

View File

@@ -529,7 +529,6 @@ pub async fn ai_approve(
tool_call_id: String,
approved: bool,
) -> Result<String, String> {
authz_debug(&format!("[ai_approve] 入口 tool_call_id={} approved={}", tool_call_id, approved));
let mut session = state.ai_session.lock().await;
let approval = match session.pending_approvals.remove(&tool_call_id) {
@@ -756,13 +755,7 @@ pub async fn ai_approve(
/// F-260620 临时诊断:授权/审批 IPC 调用链文件日志(不依赖终端 stderr,排障用)。
/// 写入 OS 临时目录(跨平台,不污染用户项目目录)。打包分发后仍可工作。
fn authz_debug(msg: &str) {
use std::io::Write;
let log_path = std::env::temp_dir().join("devflow-authz-debug.log");
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&log_path) {
let _ = writeln!(f, "{}", msg);
}
}
/// 阶段4(容错/恢复):取路径的 Windows 盘符(如 "C:" / "E:"),非 Windows / 无盘符返 None。
///
@@ -852,7 +845,6 @@ pub async fn ai_authorize_dir(
decision = %decision,
"[AI-AUTHZ] ai_authorize_dir 入口"
);
authz_debug(&format!("[ai_authorize_dir] 入口 tool_call_id={} decision={}", tool_call_id, decision));
// 取 pending(阶段3a 单真相源合并:从 pending_approvals remove;kind 校验为 Path)。
let approval = {
let mut session = state.ai_session.lock().await;

View File

@@ -221,12 +221,17 @@ pub async fn ai_conversation_list(
let models: Vec<String> = r.models.as_deref()
.and_then(|s| serde_json::from_str(s).ok())
.unwrap_or_default();
let pinned_goals: Vec<String> = r.pinned_goals
.as_deref()
.and_then(|s| serde_json::from_str(s).ok())
.unwrap_or_default();
serde_json::json!({
"id": r.id,
"title": r.title,
"provider_id": r.provider_id,
"model": r.model,
"models": models,
"pinned_goals": pinned_goals,
"archived": r.archived,
"pinned": r.pinned,
"prompt_tokens": r.prompt_tokens,
@@ -311,6 +316,12 @@ pub async fn ai_conversation_switch(
conv.agent_language = None;
conv.iteration_used = 0;
conv.stop_flag.store(false, Ordering::SeqCst);
// 从 DB 恢复 pinned_goals 到 per_conv(G1 目标钉扎持久化)
let pinned_goals: Vec<String> = record.pinned_goals
.as_deref()
.and_then(|s| serde_json::from_str(s).ok())
.unwrap_or_default();
conv.pinned_goals = pinned_goals;
}
// 仅清空目标对话自身的挂起审批,保留其他对话的(防 init 重建的内存 HashMap 被清空,
// 重启恢复链路:restore_pending_approvals(init 重建) → switchConversation(此处不清目标对话的)
@@ -439,6 +450,30 @@ pub async fn ai_conversation_set_pinned(
Ok(())
}
/// 更新对话目标钉扎列表(G1 目标钉扎持久化:对话透明化 L1)
///
/// 接收前端 UI 增删后的目标列表,持久化到 DB 并同步更新内存 per_conv 状态。
/// goals 是完整替换(非增量),前端增/删后传全量 new Vec。
#[tauri::command]
pub async fn ai_update_conversation_goals(
state: State<'_, AppState>,
conversation_id: String,
goals: Vec<String>,
) -> Result<(), String> {
let goals_json = serde_json::to_string(&goals).map_err(|e| format!("序列化目标列表失败: {e}"))?;
// 写 DB
state.ai_conversations
.update_field(&conversation_id, "pinned_goals", &goals_json)
.await
.map_err(err_str)?;
// 同步内存 per_conv(若已加载)
let mut session = state.ai_session.lock().await;
if let Some(conv) = session.per_conv.get_mut(&conversation_id) {
conv.pinned_goals = goals;
}
Ok(())
}
/// 导出对话为指定格式(UX-18:对话导出)
///
/// - 优先落库 messages(完整历史,与 switch 一致),内存 session 不读(可能被切走/未落库)

View File

@@ -164,7 +164,7 @@ pub(crate) async fn save_conversation(
// loop 内 save 由 run_agentic_loop 入参 conv_id 透传;IPC 路径(commands.rs)save 也传 conv_id。
// conv() 惰性建:save 路径 conv 必然已建(send/regenerate/edit/switch 均先 conv());若极端
// 未建(如启动恢复无 live conv),conv() 建空 PerConvState,save 空 messages(幂等不污染)。
let (persist_msgs, provider_id, created_at) = {
let (persist_msgs, provider_id, created_at, pinned_goals) = {
let mut session = session_arc.lock().await;
let mut msgs = session.conv(conv_id).messages.all_messages_clone();
for m in &mut msgs {
@@ -185,6 +185,7 @@ pub(crate) async fn save_conversation(
msgs,
session.active_provider_id.clone(),
session.active_conv_created_at.clone(),
session.conv(conv_id).pinned_goals.clone(),
)
};
@@ -224,6 +225,8 @@ pub(crate) async fn save_conversation(
if !list.iter().any(|x| x == m) { list.push(m.to_string()); }
rec.models = Some(serde_json::to_string(&list).unwrap_or_else(|_| "[]".to_string()));
}
// G1 目标钉扎持久化:将 per_conv.pinned_goals 写入 DB
rec.pinned_goals = Some(serde_json::to_string(&pinned_goals).unwrap_or_else(|_| "[]".to_string()));
// update_full 仍写 messages 列(保留旧值,本批不改 messages 字段),写元数据 + updated_at
if let Err(e) = conv_repo.update_full(&rec).await {
tracing::warn!("更新对话元数据失败 {conv_id}: {e}");
@@ -250,6 +253,7 @@ pub(crate) async fn save_conversation(
pinned: false,
prompt_tokens: usage.map(|u| u.prompt_tokens as i64),
completion_tokens: usage.map(|u| u.completion_tokens as i64),
pinned_goals: Some(serde_json::to_string(&pinned_goals).unwrap_or_else(|_| "[]".to_string())),
created_at: conv_created.clone(),
updated_at: now,
};

View File

@@ -339,6 +339,7 @@ pub fn run() {
commands::ai::ai_conversation_rename,
commands::ai::ai_conversation_archive,
commands::ai::ai_conversation_set_pinned,
commands::ai::ai_update_conversation_goals,
commands::ai::ai_conversation_export,
commands::ai::ai_list_skills,
// 核心设计6: 热重载技能(invalidate + 重扫,不重启生效)