修复: 审批不超时+重启恢复 + 其他小改
- 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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user