修复: 审批不超时+重启恢复 + 其他小改

- APPROVAL_TIMEOUT_MS=Infinity(已决策:审批不做超时)
- V33 迁移: ai_conversations 加 pending_approvals 列
- save_conversation 持久化 pending_approvals
- ai_conversation_switch 从 DB 恢复 pending_approvals
- switchConversation 加 try/catch(对话不存在→建新)
- @项目 关联添加取消按钮(×)
- TopBar 底部无标题时图标右对齐
- TaskDetail.vue 删除重复 case
This commit is contained in:
2026-06-28 13:09:55 +08:00
parent c4b02b5370
commit ad1821bc14
11 changed files with 140 additions and 19 deletions

View File

@@ -332,6 +332,29 @@ pub async fn ai_conversation_switch(
super::chat::finalize_pending_placeholders(&mut *session, &conversation_id, "会话已切换");
// 阶段3a 单真相源合并:单表 retain(kind 不区分,清本 conv 保留其他)。
session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&conversation_id));
// 从 DB 恢复本对话的挂起审批快照(重启/切回时,之前 save_conversation 持久化的 pending_approvals)
// 反序列化后插入 session.pending_approvals,使 ai_pending_tool_calls 可查回、审批卡片恢复。
if let Some(pending_json) = &record.pending_approvals {
if pending_json != "{}" && !pending_json.is_empty() {
if let Ok(restored) = serde_json::from_str::<
std::collections::HashMap<String, crate::commands::ai::PendingApproval>
>(pending_json) {
let restored_count = restored.len();
// 仅恢复本 conv 的条目(已按 conv_id 过滤写入,DB 快照天然是本 conv 的)
session.pending_approvals.extend(restored);
tracing::info!(
"切换对话 {} 恢复 {} 条挂起审批(来自 DB pending_approvals 列)",
conversation_id, restored_count
);
} else {
tracing::warn!(
"切换对话 {} 反序列化 pending_approvals 失败,原始内容前 200 字: {:?}",
conversation_id, &pending_json.chars().take(200).collect::<String>()
);
}
}
}
// 释放 session lock 再做 async provider 查询 + spawn(避免持锁 await DB)
drop(session);

View File

@@ -189,6 +189,18 @@ pub(crate) async fn save_conversation(
)
};
// 序列化当前 conv 的挂起审批快照:从 session.pending_approvals 筛选本 conv 条目,
// 序列化为 JSON 对象(tool_call_id → PendingApproval),落库供重启/切回时恢复。
let pending_approvals_json = {
let session = session_arc.lock().await;
let conv_pending: std::collections::HashMap<&String, &crate::commands::ai::PendingApproval> = session.pending_approvals
.iter()
.filter(|(_, a)| a.conversation_id.as_deref() == Some(conv_id))
.collect();
serde_json::to_string(&conv_pending).unwrap_or_else(|_| "{}".to_string())
};
// F-260619-03 批次 B:映射 Vec<ChatMessage> → Vec<AiMessageRecord>(带 seq 索引 + conv_id)
// 全量重写 ai_messages(单事务 DELETE + INSERT OR IGNORE,原子无中间空窗)。
// created_at 用对话级 created_at(老对话 None 时 now 兜底),保证消息创建时间与对话一致。
@@ -227,6 +239,8 @@ pub(crate) async fn save_conversation(
}
// G1 目标钉扎持久化:将 per_conv.pinned_goals 写入 DB
rec.pinned_goals = Some(serde_json::to_string(&pinned_goals).unwrap_or_else(|_| "[]".to_string()));
// 挂起审批快照持久化:每轮 save 同步当前 pending_approvals 快照
rec.pending_approvals = Some(pending_approvals_json.clone());
// update_full 仍写 messages 列(保留旧值,本批不改 messages 字段),写元数据 + updated_at
if let Err(e) = conv_repo.update_full(&rec).await {
tracing::warn!("更新对话元数据失败 {conv_id}: {e}");
@@ -254,6 +268,7 @@ pub(crate) async fn save_conversation(
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())),
pending_approvals: Some(pending_approvals_json.clone()),
created_at: conv_created.clone(),
updated_at: now,
};

View File

@@ -847,7 +847,7 @@ impl Default for PerConvState {
/// 原顶层 `diff` / `path_auth` 字段下沉进 `ApprovalKind` enum 变体(决策1:状态层合 +
/// 决策层分 —— kind 在 PendingApproval 内携带语义,但 ai_approve/ai_authorize_dir
/// 各只消费自己 kind,入口校验防误调)。
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PendingApproval {
pub tool_call_id: String,
pub tool_name: String,
@@ -880,7 +880,7 @@ pub struct PendingApproval {
/// - `Risk { diff }`:普通 Med/High RiskLevel 审批,由 `ai_approve` 消费(一次性 approve/reject)。
/// `diff`:AE-2025-03(路径 B)write_file 行级 unified diff,供前端审批卡预览;
/// 旧文件不存在(新建)或非 write_file / recovered 审批为 None。
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ApprovalKind {
/// F-260619-03 Phase B: 路径授权挂起(携带待授权目录列表)。
Path(PathAuthRequest),
@@ -891,7 +891,7 @@ pub enum ApprovalKind {
}
/// F-260619-03 Phase B: 路径授权挂起请求(ApprovalKind::Path 标记)。
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathAuthRequest {
/// 待授权目录列表(规范化父目录,对齐 session_trust 目录粒度)。
/// L1 补丁(rename_file 双路径漏校):收集所有未授权父目录,rename_file 的 from+to