Skip to content

Commit f06fd7f

Browse files
committed
feat(ai): 任务中心持久化——ai_tasks 表/启动恢复/采纳标记/历史查询 (F2-B1/B2)
1 parent b6ad051 commit f06fd7f

9 files changed

Lines changed: 486 additions & 4 deletions

File tree

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,40 @@ pub fn setup_app_state(app: &mut tauri::App) -> Result<(), String> {
153153
// 执行,前端轮询/事件双通道——禁止同步阻塞 30s+ 长会话精修)
154154
let ai_tasks = crate::commands_ai_refine::task_registry();
155155
let ai_task_seq = crate::commands_ai_refine::task_seq();
156+
// v0.8.0 F2(2026-08-21):任务中心——启动恢复未采纳的成功结果
157+
// (重启不丢;注册表 + id 序列以恢复结果为基准,防 id 冲突覆盖)
158+
{
159+
let restored = db
160+
.list_restorable_succeeded(100)
161+
.unwrap_or_else(|e| {
162+
eprintln!("[ai-tasks] 恢复失败(注册表空启动): {}", e);
163+
Vec::new()
164+
});
165+
if let Ok(mut tasks) = ai_tasks.lock() {
166+
let mut max_id = 0u64;
167+
for rec in &restored {
168+
let result = rec
169+
.result_json
170+
.as_deref()
171+
.and_then(|s| serde_json::from_str(s).ok());
172+
tasks.insert(
173+
rec.task_id,
174+
crate::commands_ai_refine::AiTaskEntry {
175+
state: crate::ai_task::AiTaskState::Succeeded,
176+
result,
177+
},
178+
);
179+
max_id = max_id.max(rec.task_id);
180+
}
181+
// id 序列越过恢复的最大 id——新任务不复用旧 id(防覆盖已恢复结果)
182+
let _ = ai_task_seq.fetch_update(
183+
std::sync::atomic::Ordering::Relaxed,
184+
std::sync::atomic::Ordering::Relaxed,
185+
|cur| Some(cur.max(max_id + 1)),
186+
);
187+
eprintln!("[ai-tasks] 启动恢复 {} 条未采纳任务", restored.len());
188+
}
189+
}
156190
app.manage(AppState {
157191
db,
158192
engines,

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,22 @@ pub async fn ai_enrich_start(
122122
tasks.insert(task_id, AiTaskEntry { state: AiTaskState::Pending, result: None });
123123
trim_tasks(&mut tasks);
124124
}
125+
// F2 任务中心(2026-08-21):任务记录落库(写库失败不阻断 AI 调用)
126+
let _ = st.db.insert_ai_task(&crate::db_ai_tasks::AiTaskRecord {
127+
task_id,
128+
op_type: "enrich".to_string(),
129+
ref_id: note_id,
130+
state: "pending".to_string(),
131+
result_json: None,
132+
cost_yuan: None,
133+
elapsed_ms: None,
134+
model: None,
135+
error: None,
136+
slices: None,
137+
created_at: crate::db_sessions_rows::unix_seconds(),
138+
finished_at: None,
139+
adopted: false,
140+
});
125141
let st2 = st.clone();
126142
tauri::async_runtime::spawn_blocking(move || run_enrich_task(st2, task_id, note_id, kinds, mock));
127143
Ok(AiTaskHandle { task_id, state: AiTaskState::Pending })
@@ -143,11 +159,14 @@ pub fn ai_enrich_result(state: State<'_, AppState>, task_id: u64) -> Result<AiEn
143159

144160
/// 采纳落库(v0.8.0 M4 版本化写路径:新版本 ai-enrich + 成本 meta +
145161
/// note_ai_usage 落库——"重新生成"从覆盖变为新版本)。
162+
/// @ai-context: F2(2026-08-21):task_id 可选——传入时标记任务已采纳
163+
/// (防重启后从任务中心重复采纳)。
146164
#[tauri::command]
147165
pub fn ai_enrich_apply(
148166
state: State<'_, AppState>,
149167
note_id: i64,
150168
result: AiEnrichResult,
169+
task_id: Option<u64>,
151170
) -> Result<Note, String> {
152171
// 存在性校验(versioned_save 内部也会校验——提前失败给明确错误)
153172
get_note(state.inner(), note_id)?;
@@ -184,6 +203,11 @@ pub fn ai_enrich_apply(
184203
},
185204
)
186205
.map_err(|e| e.to_string())?;
206+
// F2 任务中心:标记采纳 + 成本回填(task_id 可选;防重启后重复采纳)
207+
if let Some(tid) = task_id {
208+
let _ = state.db.mark_ai_task_adopted(tid);
209+
let _ = state.db.update_ai_task_cost(tid, cost);
210+
}
187211
get_note(state.inner(), note_id)
188212
}
189213

@@ -213,6 +237,7 @@ pub fn ai_enrich_revert(
213237

214238
/// 后台补充任务:读笔记 → 切片 → 逐片批量补充(mock/云端)→ 合并 → 混合落位。
215239
fn run_enrich_task(st: AppState, task_id: u64, note_id: i64, selected: Vec<AiEnrichKind>, mock: bool) {
240+
let started = std::time::Instant::now();
216241
let outcome: Result<AiEnrichResult, AiTaskFailure> = (|| {
217242
let note = st
218243
.db
@@ -277,6 +302,7 @@ fn run_enrich_task(st: AppState, task_id: u64, note_id: i64, selected: Vec<AiEnr
277302
model: client.config.model,
278303
})
279304
})();
305+
let elapsed_ms = started.elapsed().as_millis() as i64;
280306
match outcome {
281307
Ok(result) => {
282308
{
@@ -288,10 +314,26 @@ fn run_enrich_task(st: AppState, task_id: u64, note_id: i64, selected: Vec<AiEnr
288314
set_task(&st, task_id, AiTaskState::Succeeded);
289315
// F1 修复(2026-08-21):补充调用上审计(REQ-140 轨迹可见化)
290316
push_enrich_audit(&st, note_id, "ok", Some(&result.model));
317+
// F2 任务中心:终态落库(写库失败不阻断——H2 设计)
318+
let result_json = serde_json::to_string(&result).ok();
319+
let _ = st.db.finish_ai_task(
320+
task_id,
321+
"succeeded",
322+
result_json.as_deref(),
323+
None,
324+
elapsed_ms,
325+
);
291326
}
292327
Err(reason) => {
293-
set_task(&st, task_id, AiTaskState::Failed { reason });
328+
set_task(&st, task_id, AiTaskState::Failed { reason: reason.clone() });
294329
push_enrich_audit(&st, note_id, "error", None);
330+
let _ = st.db.finish_ai_task(
331+
task_id,
332+
"failed",
333+
None,
334+
Some(&format!("{}: {}", reason.kind(), reason.message())),
335+
elapsed_ms,
336+
);
295337
}
296338
}
297339
}

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,23 @@ pub async fn ai_refine_start(
162162
tasks.insert(task_id, AiTaskEntry { state: AiTaskState::Pending, result: None });
163163
trim_tasks(&mut tasks);
164164
}
165+
// F2 任务中心(2026-08-21):任务记录落库(pending 起步;终态在
166+
// run_refine_task 回写——写库失败不阻断 AI 调用,H2 设计)
167+
let _ = st.db.insert_ai_task(&crate::db_ai_tasks::AiTaskRecord {
168+
task_id,
169+
op_type: "refine".to_string(),
170+
ref_id: session_id,
171+
state: "pending".to_string(),
172+
result_json: None,
173+
cost_yuan: None,
174+
elapsed_ms: None,
175+
model: None,
176+
error: None,
177+
slices: None,
178+
created_at: crate::db_sessions_rows::unix_seconds(),
179+
finished_at: None,
180+
adopted: false,
181+
});
165182
let st2 = st.clone();
166183
tauri::async_runtime::spawn_blocking(move || run_refine_task(st2, task_id, session_id, mock));
167184
Ok(AiTaskHandle { task_id, state: AiTaskState::Pending })
@@ -195,14 +212,33 @@ pub fn ai_refine_result(state: State<'_, AppState>, task_id: u64) -> Result<AiRe
195212
}
196213
}
197214

215+
/// 任务历史(F2 任务中心:前端面板数据源——按类型列最近任务)。
216+
#[tauri::command]
217+
pub fn ai_task_history(
218+
state: State<'_, AppState>,
219+
op_type: String,
220+
limit: Option<usize>,
221+
) -> Result<Vec<crate::db_ai_tasks::AiTaskRecord>, String> {
222+
if op_type != "refine" && op_type != "enrich" {
223+
return Err("无效的任务类型(refine|enrich)".to_string());
224+
}
225+
state
226+
.db
227+
.list_ai_tasks(&op_type, limit.unwrap_or(50).min(200))
228+
.map_err(|e| e.to_string())
229+
}
230+
198231
/// 采纳落库(REQ-141:diff 预览后用户采纳;v0.8.0 M4 版本化写路径——
199232
/// ① 以规则基线建笔记(首快照)→ ② 精修版 = 新版本(ai-refine,含成本
200233
/// meta)→ ③ 成本落库 note_ai_usage)。
234+
/// @ai-context: F2(2026-08-21):task_id 可选——传入时标记任务已采纳
235+
/// (防重启后从任务中心重复采纳产生重复笔记)。
201236
#[tauri::command]
202237
pub fn ai_refine_apply(
203238
state: State<'_, AppState>,
204239
session_id: i64,
205240
result: AiRefineResult,
241+
task_id: Option<u64>,
206242
) -> Result<Note, String> {
207243
if session_id <= 0 {
208244
return Err("无效的会话 id".to_string());
@@ -259,6 +295,12 @@ pub fn ai_refine_apply(
259295
},
260296
)
261297
.map_err(|e| e.to_string())?;
298+
// F2 任务中心:标记采纳 + 成本回填(task_id 可选——旧前端调用不传则跳过;
299+
// 防重启后从任务中心重复采纳产生重复笔记)
300+
if let Some(tid) = task_id {
301+
let _ = state.db.mark_ai_task_adopted(tid);
302+
let _ = state.db.update_ai_task_cost(tid, cost);
303+
}
262304
state
263305
.db
264306
.get_note(note.id)
@@ -279,6 +321,7 @@ pub fn ai_refine_apply(
279321
fn run_refine_task(st: AppState, task_id: u64, session_id: i64, mock: bool) {
280322
// 诊断日志(2026-08-21 真机"排队中"排查):tauri dev 终端可见各阶段进度
281323
eprintln!("[refine-task] task={} start session={} mock={}", task_id, session_id, mock);
324+
let started = std::time::Instant::now();
282325
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
283326
run_refine_task_inner(&st, task_id, session_id, mock)
284327
}))
@@ -287,6 +330,7 @@ fn run_refine_task(st: AppState, task_id: u64, session_id: i64, mock: bool) {
287330
"精修任务内部错误(panic)——请重试;若复现请反馈".to_string(),
288331
))
289332
});
333+
let elapsed_ms = started.elapsed().as_millis() as i64;
290334
match outcome {
291335
Ok(result) => {
292336
eprintln!(
@@ -305,6 +349,15 @@ fn run_refine_task(st: AppState, task_id: u64, session_id: i64, mock: bool) {
305349
// F1 修复(2026-08-21):精修调用上审计——REQ-140 轨迹可见化
306350
// (此前只有余额/测试连接/复核有记录,精修补充零审计)
307351
push_refine_audit(&st, session_id, "ok", Some(&result.model));
352+
// F2 任务中心:终态落库(写库失败不阻断——H2 设计)
353+
let result_json = serde_json::to_string(&result).ok();
354+
let _ = st.db.finish_ai_task(
355+
task_id,
356+
"succeeded",
357+
result_json.as_deref(),
358+
None,
359+
elapsed_ms,
360+
);
308361
}
309362
Err(reason) => {
310363
// 打印具体 message——区分"未配置密钥"vs"密钥无效(401/403)"(真机排查)
@@ -314,8 +367,15 @@ fn run_refine_task(st: AppState, task_id: u64, session_id: i64, mock: bool) {
314367
reason.kind(),
315368
reason.message()
316369
);
317-
set_task(&st, task_id, AiTaskState::Failed { reason });
370+
set_task(&st, task_id, AiTaskState::Failed { reason: reason.clone() });
318371
push_refine_audit(&st, session_id, "error", None);
372+
let _ = st.db.finish_ai_task(
373+
task_id,
374+
"failed",
375+
None,
376+
Some(&format!("{}: {}", reason.kind(), reason.message())),
377+
elapsed_ms,
378+
);
319379
}
320380
}
321381
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,10 @@ impl Db {
178178
// v0.8.0 M4(REQ-144):笔记版本快照链 + AI 成本记录(幂等建表)
179179
crate::db_notes_versions::init(&conn)?;
180180
crate::db_ai_usage::init(&conn)?;
181-
Ok(Self { conn: Arc::new(Mutex::new(conn)) })
181+
// v0.8.0 F2(2026-08-21):AI 任务中心持久化(任务记录/恢复/保留)
182+
let db = Self { conn: Arc::new(Mutex::new(conn)) };
183+
db.init_ai_tasks()?;
184+
Ok(db)
182185
}
183186

184187
/// 新建笔记,返回含 id 与时间戳的完整记录。

0 commit comments

Comments
 (0)