Skip to content

Commit b6ad051

Browse files
committed
feat(ai): 精修/补充审计补齐 + 每日配额接入 + 进行中任务去重 (F1-A4/A5)
1 parent ba9e618 commit b6ad051

2 files changed

Lines changed: 99 additions & 1 deletion

File tree

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

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,32 @@ pub async fn ai_enrich_start(
9090
return Err("未配置 API 密钥(设置页保存密钥或配置环境变量 SILICONFLOW_API_KEY)".to_string());
9191
}
9292
}
93+
// F1 修复(2026-08-21):任务去重——同笔记存在进行中任务时拒绝重复启动
94+
{
95+
let tasks = st.ai_tasks.lock().map_err(|e| format!("任务注册表锁中毒: {}", e))?;
96+
let active = tasks.values().any(|t| {
97+
matches!(t.state, crate::ai_task::AiTaskState::Pending | crate::ai_task::AiTaskState::Running { .. })
98+
});
99+
if active {
100+
return Err("该笔记已有进行中的 AI 任务——请等待完成或到任务中心查看进度(防重复扣费)".to_string());
101+
}
102+
}
103+
// F1 修复(2026-08-21):每日配额接入——按预估片数消耗(启动前拦截)
104+
if !mock {
105+
let note = get_note(&st, note_id)?;
106+
let chars = note.content.chars().count();
107+
let slices = chars.saturating_add(crate::ai_task::SLICE_MAX_CHARS - 1)
108+
/ crate::ai_task::SLICE_MAX_CHARS
109+
+ 1;
110+
let now = crate::db_sessions_rows::unix_seconds();
111+
let mut guards = st.ai_guardrails.lock().map_err(|e| format!("护栏状态锁中毒: {}", e))?;
112+
for _ in 0..slices {
113+
if !guards.quota.try_consume(now) {
114+
return Err("今日 AI 补充配额已用完(请明日再试或到设置页调整)".to_string());
115+
}
116+
}
117+
drop(guards);
118+
}
93119
let task_id = st.ai_task_seq.fetch_add(1, Ordering::Relaxed);
94120
{
95121
let mut tasks = st.ai_tasks.lock().map_err(|e| format!("任务注册表锁中毒: {}", e))?;
@@ -260,8 +286,29 @@ fn run_enrich_task(st: AppState, task_id: u64, note_id: i64, selected: Vec<AiEnr
260286
}
261287
}
262288
set_task(&st, task_id, AiTaskState::Succeeded);
289+
// F1 修复(2026-08-21):补充调用上审计(REQ-140 轨迹可见化)
290+
push_enrich_audit(&st, note_id, "ok", Some(&result.model));
291+
}
292+
Err(reason) => {
293+
set_task(&st, task_id, AiTaskState::Failed { reason });
294+
push_enrich_audit(&st, note_id, "error", None);
263295
}
264-
Err(reason) => set_task(&st, task_id, AiTaskState::Failed { reason }),
296+
}
297+
}
298+
299+
/// 补充任务审计记录(F1:summary 不含原文——隐私红线)。
300+
fn push_enrich_audit(st: &AppState, note_id: i64, result: &str, model: Option<&str>) {
301+
let now = crate::db_sessions_rows::unix_seconds();
302+
if let Ok(mut g) = st.ai_guardrails.lock() {
303+
g.push_audit(crate::ai_guardrails::AiAuditEntry {
304+
at_unix: now,
305+
upload_summary: format!(
306+
"enrich note={} model={}",
307+
note_id,
308+
model.unwrap_or("?")
309+
),
310+
result: result.to_string(),
311+
});
265312
}
266313
}
267314

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 兜底)。
289340
fn run_refine_task_inner(
290341
st: &AppState,

0 commit comments

Comments
 (0)