|
1 | | -//! AI 精修任务终态骨架(拆分自 ai_refine_task.rs;AGENTS.md §3 单文件 ≤300 行)。 |
2 | | -//! |
3 | | -//! @ai-context: 会话级/笔记级精修任务共用的收尾骨架:catch_unwind 把 panic 归一为 |
4 | | -//! Failed(spawn_blocking 的 JoinHandle 未被 await——否则任务状态永久 |
5 | | -//! 停 Pending、前端永久"排队中");成功路径:轨迹落库 → 结果写注册表 → |
6 | | -//! set_task → 终态帧 → 审计 → finish_ai_task + trim_ai_tasks。 |
7 | | -//! @ai-context: 锁序红线:`st.ai_tasks` 的写入用**显式块**(块尾释放守卫),set_task |
8 | | -//! 在其后**重新加同一把 std::Mutex**——两者合并 = 自死锁(std Mutex 不可 |
9 | | -//! 重入),任务永久 Running。该序列不可分割。 |
10 | | -//! @ai-context: 审计写入 push_refine_audit 的守卫活到 `if let` 块尾(窗口 = push_audit |
11 | | -//! 调用)——不得把 now/format! 挪进或挪出该块。 |
| 1 | +//! AI 精修任务终态骨架(拆分自 ai_refine_task.rs;AGENTS.md §3 单文件 ≤300 行)。 |
| 2 | +//! |
| 3 | +//! @ai-context: 会话级/笔记级精修任务共用的收尾骨架:catch_unwind 把 panic 归一为 |
| 4 | +//! Failed(spawn_blocking 的 JoinHandle 未被 await——否则任务状态永久 |
| 5 | +//! 停 Pending、前端永久"排队中");成功路径:轨迹落库 → 结果写注册表 → |
| 6 | +//! set_task → 终态帧 → 审计 → finish_ai_task + trim_ai_tasks。 |
| 7 | +//! @ai-context: 锁序红线:`st.ai_tasks` 的写入用**显式块**(块尾释放守卫),set_task |
| 8 | +//! 在其后**重新加同一把 std::Mutex**——两者合并 = 自死锁(std Mutex 不可 |
| 9 | +//! 重入),任务永久 Running。该序列不可分割。 |
| 10 | +//! @ai-context: 审计写入 push_refine_audit 的守卫活到 `if let` 块尾(窗口 = push_audit |
| 11 | +//! 调用)——不得把 now/format! 挪进或挪出该块。 |
12 | 12 |
|
13 | | -use crate::ai_chat::{AiTurn, trajectory_to_json}; |
14 | | -use crate::ai_task::{AiTaskFailure, AiTaskState}; |
15 | | -use crate::commands::AppState; |
16 | | -use crate::commands_ai_refine::{set_task, AiRefineResult}; |
| 13 | +use crate::ai_chat::{AiTurn, trajectory_to_json}; |
| 14 | +use crate::ai_task::{AiTaskFailure, AiTaskState}; |
| 15 | +use crate::commands::AppState; |
| 16 | +use crate::commands_ai_refine::{set_task, AiRefineResult}; |
| 17 | + |
| 18 | +use super::RefineStreamFrame;use super::stream::emit_refine_stream; |
17 | 19 |
|
18 | | -use super::{emit_refine_stream, RefineStreamFrame}; |
19 | | - |
20 | | -/// 精修任务审计记录(F1:REQ-140 轨迹可见化——summary 不含原文,隐私红线)。 |
21 | | -/// v0.17.0:summary_ctx 泛化("session=1"/"note=3"——会话级/笔记级共用)。 |
22 | | -fn push_refine_audit(st: &AppState, summary_ctx: &str, result: &str, model: Option<&str>) { |
23 | | - let now = crate::db_sessions_rows::unix_seconds(); |
24 | | - if let Ok(mut g) = st.ai_guardrails.lock() { |
25 | | - g.push_audit(crate::ai_guardrails::AiAuditEntry { |
26 | | - at_unix: now, |
27 | | - upload_summary: format!("refine {} model={}", summary_ctx, model.unwrap_or("?")), |
28 | | - result: result.to_string(), |
29 | | - }); |
30 | | - } |
31 | | -} |
32 | | - |
33 | | -/// 任务收尾公共骨架(会话级/笔记级共用——v0.17.0 REQ-246 提取)。 |
34 | | -/// |
35 | | -/// @ai-context: 彻底检测加固(2026-08-21):spawn_blocking 的 JoinHandle 未被 |
36 | | -/// await——闭包内 panic 会被 tokio 吞掉,任务状态永久停在 |
37 | | -/// Pending(前端永久显示"任务排队中",无失败可重试)。 |
38 | | -/// catch_unwind 把 panic 归一为 Failed 状态,状态流转永不失联。 |
39 | | -/// @ai-context: F2-B4:单片失败重试后仍失败 → 保留已成功片(partial_failed |
40 | | -/// 语义:failed_slices > 0,前端显示"部分成功 x/y 片")。 |
41 | | -pub(crate) fn run_refine_task_skeleton( |
42 | | - st: AppState, |
43 | | - task_id: u64, |
44 | | - target_summary: String, |
45 | | - work: impl FnOnce() -> Result<(AiRefineResult, Vec<AiTurn>), AiTaskFailure>, |
46 | | -) { |
47 | | - let started = std::time::Instant::now(); |
48 | | - let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(work)).unwrap_or_else(|_| { |
49 | | - Err(AiTaskFailure::Other( |
50 | | - "精修任务内部错误(panic)——请重试;若复现请反馈".to_string(), |
51 | | - )) |
52 | | - }); |
53 | | - let elapsed_ms = started.elapsed().as_millis() as i64; |
54 | | - match outcome { |
55 | | - Ok((result, turns)) => { |
56 | | - eprintln!( |
57 | | - "[refine-task] task={} succeeded slices={} failed={} diff={}", |
58 | | - task_id, |
59 | | - result.slices, |
60 | | - result.failed_slices, |
61 | | - result.diff.len() |
62 | | - ); |
63 | | - // v0.16.0(REQ-230):轨迹落库(提示词/回答全文——任务对话视图数据源) |
64 | | - if let Some(json) = trajectory_to_json(&turns) { |
65 | | - if let Err(e) = st.db.update_ai_task_trajectory(task_id, &json) { |
66 | | - eprintln!("[refine-task] task={} 轨迹落库失败(不阻断): {}", task_id, e); |
67 | | - } |
68 | | - } |
69 | | - { |
70 | | - let mut tasks = st.ai_tasks.lock().unwrap_or_else(|e| e.into_inner()); |
71 | | - if let Some(entry) = tasks.get_mut(&task_id) { |
72 | | - entry.result = serde_json::to_value(&result).ok(); |
73 | | - } |
74 | | - } |
75 | | - set_task(&st, task_id, AiTaskState::Succeeded); |
76 | | - // REQ-247:终态帧(前端消息定格为最终摘要+双入口) |
77 | | - emit_refine_stream(&st, task_id, RefineStreamFrame::Done { |
78 | | - slices: result.slices, |
79 | | - failed_slices: result.failed_slices, |
80 | | - }); |
81 | | - // F1 修复(2026-08-21):精修调用上审计——REQ-140 轨迹可见化 |
82 | | - push_refine_audit(&st, &target_summary, "ok", Some(&result.model)); |
83 | | - // F2 任务中心:终态落库(写库失败不阻断——H2 设计)+ 保留策略 |
84 | | - // 裁剪(审查修复:trim 原只在启动时跑,运行期终态任务会累积—— |
85 | | - // 每次终态后清理超限旧终态,防表膨胀) |
86 | | - let result_json = serde_json::to_string(&result).ok(); |
87 | | - let _ = st.db.finish_ai_task( |
88 | | - task_id, |
89 | | - "succeeded", |
90 | | - result_json.as_deref(), |
91 | | - None, |
92 | | - elapsed_ms, |
93 | | - ); |
94 | | - let _ = st.db.trim_ai_tasks(); |
95 | | - } |
96 | | - Err(reason) => { |
97 | | - // 打印具体 message——区分"未配置密钥"vs"密钥无效(401/403)"(真机排查) |
98 | | - eprintln!( |
99 | | - "[refine-task] task={} failed kind={} msg={}", |
100 | | - task_id, |
101 | | - reason.kind(), |
102 | | - reason.message() |
103 | | - ); |
104 | | - set_task(&st, task_id, AiTaskState::Failed { reason: reason.clone() }); |
105 | | - push_refine_audit(&st, &target_summary, "error", None); |
106 | | - let _ = st.db.finish_ai_task( |
107 | | - task_id, |
108 | | - "failed", |
109 | | - None, |
110 | | - Some(&format!("{}: {}", reason.kind(), reason.message())), |
111 | | - elapsed_ms, |
112 | | - ); |
113 | | - let _ = st.db.trim_ai_tasks(); |
114 | | - } |
115 | | - } |
116 | | -} |
| 20 | +/// 精修任务审计记录(F1:REQ-140 轨迹可见化——summary 不含原文,隐私红线)。 |
| 21 | +/// v0.17.0:summary_ctx 泛化("session=1"/"note=3"——会话级/笔记级共用)。 |
| 22 | +fn push_refine_audit(st: &AppState, summary_ctx: &str, result: &str, model: Option<&str>) { |
| 23 | + let now = crate::db_sessions_rows::unix_seconds(); |
| 24 | + if let Ok(mut g) = st.ai_guardrails.lock() { |
| 25 | + g.push_audit(crate::ai_guardrails::AiAuditEntry { |
| 26 | + at_unix: now, |
| 27 | + upload_summary: format!("refine {} model={}", summary_ctx, model.unwrap_or("?")), |
| 28 | + result: result.to_string(), |
| 29 | + }); |
| 30 | + } |
| 31 | +} |
| 32 | + |
| 33 | +/// 任务收尾公共骨架(会话级/笔记级共用——v0.17.0 REQ-246 提取)。 |
| 34 | +/// |
| 35 | +/// @ai-context: 彻底检测加固(2026-08-21):spawn_blocking 的 JoinHandle 未被 |
| 36 | +/// await——闭包内 panic 会被 tokio 吞掉,任务状态永久停在 |
| 37 | +/// Pending(前端永久显示"任务排队中",无失败可重试)。 |
| 38 | +/// catch_unwind 把 panic 归一为 Failed 状态,状态流转永不失联。 |
| 39 | +/// @ai-context: F2-B4:单片失败重试后仍失败 → 保留已成功片(partial_failed |
| 40 | +/// 语义:failed_slices > 0,前端显示"部分成功 x/y 片")。 |
| 41 | +pub(crate) fn run_refine_task_skeleton( |
| 42 | + st: AppState, |
| 43 | + task_id: u64, |
| 44 | + target_summary: String, |
| 45 | + work: impl FnOnce() -> Result<(AiRefineResult, Vec<AiTurn>), AiTaskFailure>, |
| 46 | +) { |
| 47 | + let started = std::time::Instant::now(); |
| 48 | + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(work)).unwrap_or_else(|_| { |
| 49 | + Err(AiTaskFailure::Other( |
| 50 | + "精修任务内部错误(panic)——请重试;若复现请反馈".to_string(), |
| 51 | + )) |
| 52 | + }); |
| 53 | + let elapsed_ms = started.elapsed().as_millis() as i64; |
| 54 | + match outcome { |
| 55 | + Ok((result, turns)) => { |
| 56 | + eprintln!( |
| 57 | + "[refine-task] task={} succeeded slices={} failed={} diff={}", |
| 58 | + task_id, |
| 59 | + result.slices, |
| 60 | + result.failed_slices, |
| 61 | + result.diff.len() |
| 62 | + ); |
| 63 | + // v0.16.0(REQ-230):轨迹落库(提示词/回答全文——任务对话视图数据源) |
| 64 | + if let Some(json) = trajectory_to_json(&turns) { |
| 65 | + if let Err(e) = st.db.update_ai_task_trajectory(task_id, &json) { |
| 66 | + eprintln!("[refine-task] task={} 轨迹落库失败(不阻断): {}", task_id, e); |
| 67 | + } |
| 68 | + } |
| 69 | + { |
| 70 | + let mut tasks = st.ai_tasks.lock().unwrap_or_else(|e| e.into_inner()); |
| 71 | + if let Some(entry) = tasks.get_mut(&task_id) { |
| 72 | + entry.result = serde_json::to_value(&result).ok(); |
| 73 | + } |
| 74 | + } |
| 75 | + set_task(&st, task_id, AiTaskState::Succeeded); |
| 76 | + // REQ-247:终态帧(前端消息定格为最终摘要+双入口) |
| 77 | + emit_refine_stream(&st, task_id, RefineStreamFrame::Done { |
| 78 | + slices: result.slices, |
| 79 | + failed_slices: result.failed_slices, |
| 80 | + }); |
| 81 | + // F1 修复(2026-08-21):精修调用上审计——REQ-140 轨迹可见化 |
| 82 | + push_refine_audit(&st, &target_summary, "ok", Some(&result.model)); |
| 83 | + // F2 任务中心:终态落库(写库失败不阻断——H2 设计)+ 保留策略 |
| 84 | + // 裁剪(审查修复:trim 原只在启动时跑,运行期终态任务会累积—— |
| 85 | + // 每次终态后清理超限旧终态,防表膨胀) |
| 86 | + let result_json = serde_json::to_string(&result).ok(); |
| 87 | + let _ = st.db.finish_ai_task( |
| 88 | + task_id, |
| 89 | + "succeeded", |
| 90 | + result_json.as_deref(), |
| 91 | + None, |
| 92 | + elapsed_ms, |
| 93 | + ); |
| 94 | + let _ = st.db.trim_ai_tasks(); |
| 95 | + } |
| 96 | + Err(reason) => { |
| 97 | + // 打印具体 message——区分"未配置密钥"vs"密钥无效(401/403)"(真机排查) |
| 98 | + eprintln!( |
| 99 | + "[refine-task] task={} failed kind={} msg={}", |
| 100 | + task_id, |
| 101 | + reason.kind(), |
| 102 | + reason.message() |
| 103 | + ); |
| 104 | + set_task(&st, task_id, AiTaskState::Failed { reason: reason.clone() }); |
| 105 | + push_refine_audit(&st, &target_summary, "error", None); |
| 106 | + let _ = st.db.finish_ai_task( |
| 107 | + task_id, |
| 108 | + "failed", |
| 109 | + None, |
| 110 | + Some(&format!("{}: {}", reason.kind(), reason.message())), |
| 111 | + elapsed_ms, |
| 112 | + ); |
| 113 | + let _ = st.db.trim_ai_tasks(); |
| 114 | + } |
| 115 | + } |
| 116 | +} |
|
0 commit comments