@@ -124,6 +124,37 @@ pub async fn ai_refine_start(
124124 return Err ( "未配置 API 密钥(设置页保存密钥或配置环境变量 SILICONFLOW_API_KEY)" . to_string ( ) ) ;
125125 }
126126 }
127+ // ②b F1 修复(2026-08-21):任务去重——同一会话存在进行中任务时拒绝
128+ // 重复启动(防双击/重进/多窗口重复扣费;终态任务不阻塞重试)
129+ {
130+ let tasks = st. ai_tasks . lock ( ) . map_err ( |e| format ! ( "任务注册表锁中毒: {}" , e) ) ?;
131+ let active = tasks. values ( ) . any ( |t| {
132+ matches ! ( t. state, AiTaskState :: Pending | AiTaskState :: Running { .. } )
133+ } ) ;
134+ if active {
135+ return Err ( "该会话已有进行中的 AI 任务——请等待完成或到任务中心查看进度(防重复扣费)" . to_string ( ) ) ;
136+ }
137+ }
138+ // ②c F1 修复(2026-08-21):每日配额接入——按预估片数消耗(启动前
139+ // 拦截而非失败后引导;耗尽 → 明确提示明日再试,REQ-145 配额出口)
140+ if !mock {
141+ let segments = st. db . list_segments ( session_id) . map_err ( |e| e. to_string ( ) ) ?;
142+ let ocr = st. db . list_ocr_blocks ( session_id) . map_err ( |e| e. to_string ( ) ) ?;
143+ let chars = segments. iter ( ) . map ( |s| s. text . chars ( ) . count ( ) ) . sum :: < usize > ( )
144+ + ocr. iter ( ) . map ( |b| b. text . chars ( ) . count ( ) ) . sum :: < usize > ( ) ;
145+ // 片数估算(与 ai_task::slice_note 同口径的保守上界:字符数 / 单片上限 + 1)
146+ let slices = chars. saturating_add ( crate :: ai_task:: SLICE_MAX_CHARS - 1 )
147+ / crate :: ai_task:: SLICE_MAX_CHARS
148+ + 1 ;
149+ let now = crate :: db_sessions_rows:: unix_seconds ( ) ;
150+ let mut guards = st. ai_guardrails . lock ( ) . map_err ( |e| format ! ( "护栏状态锁中毒: {}" , e) ) ?;
151+ for _ in 0 ..slices {
152+ if !guards. quota . try_consume ( now) {
153+ return Err ( "今日 AI 精修配额已用完(请明日再试或到设置页调整)" . to_string ( ) ) ;
154+ }
155+ }
156+ drop ( guards) ;
157+ }
127158 // ③ 注册任务 + 后台执行(spawn_blocking——网络/分析不阻塞异步运行时)
128159 let task_id = st. ai_task_seq . fetch_add ( 1 , Ordering :: Relaxed ) ;
129160 {
@@ -271,6 +302,9 @@ fn run_refine_task(st: AppState, task_id: u64, session_id: i64, mock: bool) {
271302 }
272303 }
273304 set_task ( & st, task_id, AiTaskState :: Succeeded ) ;
305+ // F1 修复(2026-08-21):精修调用上审计——REQ-140 轨迹可见化
306+ // (此前只有余额/测试连接/复核有记录,精修补充零审计)
307+ push_refine_audit ( & st, session_id, "ok" , Some ( & result. model ) ) ;
274308 }
275309 Err ( reason) => {
276310 // 打印具体 message——区分"未配置密钥"vs"密钥无效(401/403)"(真机排查)
@@ -281,10 +315,27 @@ fn run_refine_task(st: AppState, task_id: u64, session_id: i64, mock: bool) {
281315 reason. message( )
282316 ) ;
283317 set_task ( & st, task_id, AiTaskState :: Failed { reason } ) ;
318+ push_refine_audit ( & st, session_id, "error" , None ) ;
284319 }
285320 }
286321}
287322
323+ /// 精修任务审计记录(F1:REQ-140 轨迹可见化——summary 不含原文,隐私红线)。
324+ fn push_refine_audit ( st : & AppState , session_id : i64 , result : & str , model : Option < & str > ) {
325+ let now = crate :: db_sessions_rows:: unix_seconds ( ) ;
326+ if let Ok ( mut g) = st. ai_guardrails . lock ( ) {
327+ g. push_audit ( crate :: ai_guardrails:: AiAuditEntry {
328+ at_unix : now,
329+ upload_summary : format ! (
330+ "refine session={} model={}" ,
331+ session_id,
332+ model. unwrap_or( "?" )
333+ ) ,
334+ result : result. to_string ( ) ,
335+ } ) ;
336+ }
337+ }
338+
288339/// 精修任务主体(返回 Result;panic 由外层 catch_unwind 兜底)。
289340fn run_refine_task_inner (
290341 st : & AppState ,
0 commit comments