新增: 审批超时后端兜底,覆盖重启恢复场景

前端已有审批超时定时器(设置页可选 0/5/15/30/60 分钟),负责会话
内运行的审批超时检测。但应用重启后前端定时器丢失,从数据库恢复
的待审批无人计时,会永久卡死会话。

本次补充后端兜底机制:

- 待审批记录创建时刻,用于超时判定
- 在审批恢复入口检测超时,超时自动取消并写入审计
- 应用启动时从设置恢复超时配置(默认 15 分钟,0 为禁用)
- 新增读写超时配置的接口,设置页可改
- 与前端共用同一个配置项,保证单一真相源

同时更新待办文档,把已实现项标记为完成。
This commit is contained in:
2026-06-28 18:16:34 +08:00
parent 3fac0a618d
commit 55542abf41
11 changed files with 279 additions and 57 deletions

View File

@@ -3,7 +3,7 @@
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use anyhow::Result;
use serde::{Deserialize, Serialize};
@@ -37,10 +37,18 @@ pub enum ExtractTrigger {
ManualOnly,
}
/// 知识库配置持久化 KV key(P0 设置走查-2026-06-21:原纯内存 Arc<Mutex> 启动 default 覆盖
/// 致 8 项配置重启全丢;save_config 落此 KV,init reload_knowledge_config 恢复)。
/// 知识库配置持久化 KV key(P0 设置走查-2026-06-21:原纯内存 Arc<Mutex> 启动 default 覆盖
/// 致 8 项配置重启全丢;save_config 落此 KV,init reload_knowledge_config 恢复)。
pub const KNOWLEDGE_CONFIG_KEY: &str = "df-knowledge-config";
/// 审批超时配置持久化 KV key(前端 AdvancedSection.vue 也在用同一个 key,保持单一真相源)。
/// 前端值单位为毫秒(0/300000/900000/1800000/3600000),后端同步使用此 key 读取并转换。
/// 前端在运行期负责会话内的审批超时(定时器+ai_approve(false)),后端在启动恢复阶段
/// 清理重启前遗留的 pending(避免重启后无人调用 try_continue 致审批永不超时)。
pub const APPROVAL_TIMEOUT_KEY: &str = "df-approval-timeout";
/// 知识库行为配置(内存真相源 + Settings KV 持久化,前后端通过 IPC 读写)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeConfig {
@@ -410,6 +418,11 @@ pub struct AppState {
/// 任何 token流中途失败 MidStream Partial 保文不重试。退避复用 retry::backoff_delay +
/// is_status_retryable Fatal 分类 + 30s 总预算,详见 agentic.rs 重试循环。默认 3
pub agent_max_retries: Arc<AtomicUsize>,
// ── 审批超时配置 ──
/// 审批超时分钟数(前端 Settings 配置 → AppState 字段 → try_continue 入口读取)。
/// 待审批超过此时长后自动取消,避免用户离开后会话永久卡死。
/// 默认 15 分钟0 表示禁用超时(不推荐,会导致卡死无自愈)。
pub approval_timeout_minutes: Arc<AtomicU64>,
// ── 工作流执行状态 ──
/// 工作流执行 → 节点状态机注册表
///
@@ -718,6 +731,7 @@ impl AppState {
agent_max_retries: Arc::new(AtomicUsize::new(
crate::commands::ai::agentic::DEFAULT_MAX_AGENT_RETRIES,
)),
approval_timeout_minutes: Arc::new(AtomicU64::new(15)),
workflow_state_registry: Arc::new(Mutex::new(HashMap::new())),
// F-260619-03 Phase A: 与 ai_tools registry 共享同一 Arc(构建时注入同一句柄)
allowed_dirs: allowed_dirs.clone(),
@@ -742,6 +756,8 @@ impl AppState {
state.reload_allowed_dirs().await;
// P0(设置走查):从 Settings KV 恢复持久化知识库配置覆盖 default(防 8 项重启全丢)。
state.reload_knowledge_config().await;
// 从 Settings KV 恢复审批超时配置(默认 15 分钟,0=禁用超时)。
state.reload_approval_timeout().await;
// 迁移旧 .trash(编译期 workspace_root → 运行期 data_dir),仅一次,幂等。
let old_trash = workspace_root_path().join(".trash");
@@ -799,6 +815,32 @@ impl AppState {
}
}
/// 从 Settings KV 恢复审批超时分钟数(默认 15 分钟,0=禁用)。
///
/// KV key `df-approval-timeout` 与前端 AdvancedSection.vue 共用,值为毫秒数(0/300000/900000/...)。
/// 启动时读取用户配置,失败或首次启动无配置时保持默认 15 分钟。
pub async fn reload_approval_timeout(&self) {
match self.settings.get(APPROVAL_TIMEOUT_KEY).await {
Ok(Some(json)) => {
// 兼容两种格式:JSON 数字或字符串(纯数字字串)
let ms: u64 = if let Ok(v) = serde_json::from_str::<u64>(&json) {
v
} else if let Ok(s) = serde_json::from_str::<String>(&json) {
s.parse().unwrap_or(900_000)
} else {
tracing::warn!("[APPROVAL-TIMEOUT] KV 格式非法,保持 default: {}", json);
900_000
};
// ms → minutes(向上取整,0 保留为 0 表示禁用)
let mins = if ms == 0 { 0 } else { (ms + 59_999) / 60_000 };
self.approval_timeout_minutes.store(mins, Ordering::SeqCst);
tracing::info!("[APPROVAL-TIMEOUT] 启动恢复: {} 分钟 (KV ms={})", mins, ms);
}
Ok(None) => {} // 首次启动保持默认 15
Err(e) => tracing::warn!("[APPROVAL-TIMEOUT] 读 KV 失败,保持 default: {}", e),
}
}
/// F-260619-03 Phase A: 从 Settings KV `allowed_dirs`(JSON 字符串数组)加载持久化白名单。
///
/// 启动 + Settings IPC `ai_set_allowed_dirs` 写入后调用。解析失败/缺失 → 保持