新增: 无人值守安全系列(H1-H3) + P5 审计日志与部署运维 + #14 异步委托
- H3 外部干预感知: run_agentic_loop 增 h3_watch_dir 参数, loop 启动取 git HEAD 基线, 每 5 轮探测, HEAD 被外部改写(切分支/reset/他话提交)即 emit_task_parked(stop_kind=external_change) + teardown 停靠; 4 调用方(发送/编辑/强制发送/审批续跑)从 allowed_dirs.persistent 排序取首目录, 修复初版 current_dir 恒空缺陷 - P5 审计日志: df-im-gateway audit.rs append-only SQLite(tunnel_audit 表, payload 4KB 截断, recent/by_request 查询), main.rs 8 打点点位(inbound/command/outbound), 写失败不阻断主链路, 17 tests passed - P5 部署运维: publish-im-gateway.ps1(musl 交叉编译→scp .new→systemd 原子切换→9191 验证→自动回滚) + systemd unit + nginx im.1216.top 反代 + 运维手册 - #14 异步委托: 受理/完成两段式 + 桌面任务卡(后台进行中徽章/轮次/切换) + 飞书双端同构, 关联缺陷 #8-#12 速清 验收: cargo check workspace EXIT 0, cargo test workspace 全绿(711+560+33+12...), vue-tsc EXIT 0
This commit is contained in:
@@ -98,6 +98,16 @@ pub const PROGRESS_EVERY_N_ROUNDS: usize = 5;
|
||||
pub const ROUND_REENTRY_MAX: usize = 3;
|
||||
pub const ROUND_REENTRY_BACKOFF_SECS: [u64; ROUND_REENTRY_MAX] = [60, 300, 900];
|
||||
|
||||
/// H2 token 预算三闸(无人值守可靠性专项 B2,抄 sub_agent round_budget_stop 语义):
|
||||
/// 无人值守长任务(max_iterations=0 不限轮次)缺 token/挂钟兜底——模型死循环级输出
|
||||
/// 白烧预算无人踩闸。三闸在每轮头检查(轮间粒度,单轮内不中断):
|
||||
/// ①挂钟:超 UNATTENDED_WALL_SECS(默认 30min,对齐委托任务超时底座)停靠 wall_clock;
|
||||
/// ②累计 token:超 UNATTENDED_MAX_TOKENS(默认 5M)停靠 token_budget;
|
||||
/// ③轮次:max_iterations>0 既有 for 上界 + 达上限停靠分支,不重复设闸。
|
||||
/// 超限走既有 emit_task_parked——前端词表已预留,零新契约。
|
||||
pub const UNATTENDED_WALL_SECS: u64 = 30 * 60;
|
||||
pub const UNATTENDED_MAX_TOKENS: u64 = 5_000_000;
|
||||
|
||||
/// 改进3 B 常量开关 KEYWORD_FALLBACK_ENABLED 已随压缩逻辑迁至 context_lifecycle.rs
|
||||
/// (在该模块内部为私有 const,文档见 context_lifecycle.rs 顶部)。
|
||||
///
|
||||
@@ -439,6 +449,9 @@ pub(crate) async fn run_agentic_loop(
|
||||
// F1 并发 epoch(2026-08-05):调用方 spawn 前 `per_conv.loop_epoch.fetch_add(1)` 后传入,
|
||||
// loop 捕获自己的 epoch。退出 / Guard 复位 / emit 只认 owner(epoch 匹配),否则是旧 loop。
|
||||
loop_epoch: u64,
|
||||
// H3 外部干预感知:被监视的工作区目录(调用方从 allowed_dirs.persistent 取,None=无授权
|
||||
// 目录,探测静默跳过)。传目录而非 HEAD:loop 启动时自行取基线,续跑循环自动重置基线。
|
||||
h3_watch_dir: Option<String>,
|
||||
) {
|
||||
// F1 并发 epoch + F2 存活心跳:一次锁内取 per_conv 关键 Arc + 建 guard。
|
||||
// guard.new 同步(transition 经 ConvStateStore 无 await),可在此锁内调。
|
||||
@@ -1102,6 +1115,22 @@ pub(crate) async fn run_agentic_loop(
|
||||
// 0 = 不限:effective_max=usize::MAX,for 到不了上界,靠 stop_flag/收敛/审批退出(下方达上限暂停分支不触发)
|
||||
let effective_max = if max_iterations == 0 { usize::MAX } else { max_iterations };
|
||||
|
||||
// H2 token 三闸基点:loop 启动挂钟起点(轮间粒度检查,单轮不中断)
|
||||
let loop_started = tokio::time::Instant::now();
|
||||
|
||||
// H3 外部干预感知基点:loop 启动时在被监视工作区取 git HEAD 快照(轮间粒度比对)。
|
||||
// 设计口径:loop_epoch 管「引擎内接管」,H3 管「引擎外干预」——用户/另一 AI 会话在 loop
|
||||
// 运行期改写了工作区 HEAD(切分支/commit/reset),当前任务的工作前提已失效,继续跑有破坏面。
|
||||
// 无监视目录/非 git 仓/命令失败=无法感知,静默跳过(探测是增益非阻断)。
|
||||
let h3_baseline_head = if let Some(dir) = h3_watch_dir.as_deref() {
|
||||
let (out, ok) = crate::commands::ai::tools::git::git_exec(
|
||||
"rev-parse", &["HEAD"], dir,
|
||||
).await;
|
||||
if ok { out.trim().to_string() } else { String::new() }
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// panic 防护:loop 主体(for + 达上限收尾 + 正常完成收尾)整体包 catch_unwind。
|
||||
// 背景:本函数由 IPC 层 tauri::async_runtime::spawn 托管,主体 panic 时任务静默死亡,
|
||||
// conv 卡 Generating(无终态事件,前端无法恢复)。捕获后经 unwind_to_result 把 panic 转
|
||||
@@ -1113,6 +1142,73 @@ pub(crate) async fn run_agentic_loop(
|
||||
// 依赖 panic=unwind(tokio 默认),panic=abort 构建下 catch_unwind 不生效(尽力兜底)。
|
||||
let outcome = unwind_to_result(async {
|
||||
for iteration in start_iteration..effective_max {
|
||||
// ── H2 三闸(B2):轮间粒度检查,超限安全停靠(轮内不中断,下一轮头生效) ──
|
||||
{
|
||||
let elapsed = loop_started.elapsed().as_secs();
|
||||
let total_tokens = tokens.total() as u64;
|
||||
let over = if elapsed >= UNATTENDED_WALL_SECS {
|
||||
Some((
|
||||
"wall_clock",
|
||||
format!("任务运行超 {} 分钟,安全停靠:已完成进度已保存,点继续或重发消息可续跑", UNATTENDED_WALL_SECS / 60),
|
||||
))
|
||||
} else if total_tokens >= UNATTENDED_MAX_TOKENS {
|
||||
Some((
|
||||
"token_budget",
|
||||
format!("任务累计消耗达 {} tokens 上限,安全停靠:防无人值守预算失控,点继续或重发消息可追加授权续跑", UNATTENDED_MAX_TOKENS),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((reason, message)) = over { tracing::warn!(conv_id = %conv_id, elapsed, total_tokens, "[ai] H2 闸触发({reason}),任务安全停靠");
|
||||
crate::commands::ai::emit_task_parked(&app_handle, &conv_id, &message, reason);
|
||||
// 走 teardown 正典收尾(save + guard 复位 + AiCompleted(incomplete))再退
|
||||
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
||||
let emit_usage = tokens_snapshot(&tokens);
|
||||
teardown_round(
|
||||
&session_arc, &db, &conv_id,
|
||||
Some(&save_usage), Some(&resolved_model),
|
||||
false, &provider_config, &llm_concurrency, &mut guard,
|
||||
&emit_usage, last_round_estimated,
|
||||
Some(true), None, Some(reason), true,
|
||||
&pinned_goals_snapshot, &app_handle, &loop_epoch_arc, my_epoch, false,
|
||||
).await;
|
||||
return;
|
||||
}
|
||||
// H3 外部干预感知(B4):每 5 轮探测一次 git HEAD(轮间粒度,非 git 仓跳过)。
|
||||
// HEAD≠基线=有人动了工作区(切分支/reset/外部 commit),任务工作前提失效,
|
||||
// 安全停靠交还用户决策。轻量 rev-parse ~10ms,5 轮一次可忽略。
|
||||
if !h3_baseline_head.is_empty()
|
||||
&& iteration % PROGRESS_EVERY_N_ROUNDS == 0
|
||||
{
|
||||
let head_now = crate::commands::ai::tools::git::git_exec(
|
||||
"rev-parse", &["HEAD"], h3_watch_dir.as_deref().unwrap_or("."),
|
||||
).await;
|
||||
if head_now.1 && head_now.0.trim() != h3_baseline_head {
|
||||
tracing::warn!(
|
||||
conv_id = %conv_id,
|
||||
baseline = %h3_baseline_head,
|
||||
current = %head_now.0.trim(),
|
||||
"[ai] H3 检测到 git HEAD 被外部改写,任务安全停靠"
|
||||
);
|
||||
crate::commands::ai::emit_task_parked(
|
||||
&app_handle, &conv_id,
|
||||
"检测到代码仓库 HEAD 在任务运行期间被外部修改(可能是切分支/回滚/其他会话提交),任务工作前提已变化,已安全停靠:请确认仓库状态后决定继续或重新安排任务",
|
||||
"external_change",
|
||||
); let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
||||
let emit_usage = tokens_snapshot(&tokens);
|
||||
teardown_round(
|
||||
&session_arc, &db, &conv_id,
|
||||
Some(&save_usage), Some(&resolved_model),
|
||||
false, &provider_config, &llm_concurrency, &mut guard,
|
||||
&emit_usage, last_round_estimated,
|
||||
Some(true), None, Some("external_change"), true,
|
||||
&pinned_goals_snapshot, &app_handle, &loop_epoch_arc, my_epoch, false,
|
||||
).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 进度可观测段(设计 §4.3):每 N 轮 emit AiTaskProgress ──
|
||||
// round 1 起算对齐 AiAgentRound 口径;max_rounds=0=不限轮次(前端按无上限处理)。
|
||||
// owner 守卫:被接管的旧 loop 不发(防串会话进度)。事件即逝不落库,终局不补发。
|
||||
|
||||
Reference in New Issue
Block a user