Skip to content

Commit ee02012

Browse files
committed
refactor(rust): 拆 ai_refine_task 步 4 — 流式帧推送
1 parent ab51556 commit ee02012

4 files changed

Lines changed: 420 additions & 405 deletions

File tree

‎app/src-tauri/src/ai_refine_task.rs‎

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
//! 线程内再开 worker——网络调用阻塞 worker 线程,互不干扰;
1010
//! AiClient 为 Clone+Send,跨线程共享安全)。
1111
12-
use tauri::Emitter;
13-
1412
use crate::ai_chat::AiTurn;
1513
use crate::ai_client::AiClient;
1614
use crate::ai_mock::AiMockAdapter;
@@ -42,6 +40,10 @@ pub(crate) use self::skeleton::run_refine_task_skeleton;
4240
mod vision;
4341
use self::vision::load_session_vision_images;
4442

43+
/// 流式帧推送子模块(唯一 emit 点与静默口径见 stream.rs 模块头)
44+
#[path = "ai_refine_task/stream.rs"]
45+
mod stream;
46+
4547
/// 切片并发上限(REQ-145:并发 2-3——配额并发安全由 command 层启动前按
4648
/// 预估片数一次性消耗保证,此处 worker 数不超切片数)。v0.17.0:pub(crate)
4749
/// ——笔记级精修任务共用同一上限。
@@ -65,19 +67,6 @@ pub enum RefineStreamFrame {
6567
Done { slices: usize, failed_slices: usize },
6668
}
6769

68-
/// 流式事件载荷(taskId 过滤——多任务并行时各订阅只收自己的帧)。
69-
#[derive(Debug, Clone, serde::Serialize)]
70-
#[serde(rename_all = "camelCase")]
71-
struct RefineStreamPayload {
72-
task_id: u64,
73-
frame: RefineStreamFrame,
74-
}
75-
76-
/// 推送精修流帧(失败静默——流式是呈现增强,不得影响任务主链路)。
77-
fn emit_refine_stream(st: &AppState, task_id: u64, frame: RefineStreamFrame) {
78-
let _ = st.app.emit("ai:refine-stream", RefineStreamPayload { task_id, frame });
79-
}
80-
8170
/// 后台精修任务:规则草稿 → 切片 → 逐片精修(mock/云端)→ 合并 → diff。
8271
///
8372
/// @ai-context: v0.17.0(REQ-245):dims=策略解析结果(command 层 resolve 后

‎app/src-tauri/src/ai_refine_task/skeleton.rs‎

Lines changed: 114 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,116 @@
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! 挪进或挪出该块。
1212
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;
1719

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+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//! AI 精修流式帧事件推送(拆分自 ai_refine_task.rs;AGENTS.md §3 单文件 ≤300 行)。
2+
//!
3+
//! @ai-context: 唯一 emit 点(事件名 "ai:refine-stream" 逐字)——5 种帧类型
4+
//! (Progress/BlockDone/Delta/SliceFailed/Done)定义仍在父模块,
5+
//! 本模块只承载载荷与推送。
6+
//! @ai-context: 失败静默(let _ =):流式是呈现增强,不得影响任务主链路;调用方在
7+
//! worker 线程(Delta)与主线程(Done/Progress 收集段)两侧。
8+
9+
use tauri::Emitter;
10+
11+
use crate::commands::AppState;
12+
13+
use super::RefineStreamFrame;
14+
15+
/// 流式事件载荷(taskId 过滤——多任务并行时各订阅只收自己的帧)。
16+
#[derive(Debug, Clone, serde::Serialize)]
17+
#[serde(rename_all = "camelCase")]
18+
struct RefineStreamPayload {
19+
task_id: u64,
20+
frame: RefineStreamFrame,
21+
}
22+
23+
/// 推送精修流帧(失败静默——流式是呈现增强,不得影响任务主链路)。
24+
pub(super) fn emit_refine_stream(st: &AppState, task_id: u64, frame: RefineStreamFrame) {
25+
let _ = st.app.emit("ai:refine-stream", RefineStreamPayload { task_id, frame });
26+
}

0 commit comments

Comments
 (0)