Skip to content

Commit d012c21

Browse files
committed
refactor(rust): 拆 commands_ai_refine 步 3 — 会话生命周期域
1 parent fcea5cc commit d012c21

3 files changed

Lines changed: 268 additions & 245 deletions

File tree

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -393,12 +393,12 @@ pub fn handle() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync
393393
crate::commands_ai_providers::ai_provider_test,
394394
// v0.8.0 M2(REQ-141/145 + REQ-143 基础版):会话→笔记 AI 精修——
395395
// 成本预估/异步任务/状态/结果/采纳落库
396-
crate::commands_ai_refine::ai_refine_estimate,
397-
crate::commands_ai_refine::ai_refine_start,
398-
crate::commands_ai_refine::ai_refine_status,
399-
crate::commands_ai_refine::ai_refine_strategy_meta,
400-
crate::commands_ai_refine::ai_refine_prompt_preview,
401-
crate::commands_ai_refine::ai_refine_result,
396+
crate::commands_ai_refine::session::ai_refine_estimate,
397+
crate::commands_ai_refine::session::ai_refine_start,
398+
crate::commands_ai_refine::session::ai_refine_status,
399+
crate::commands_ai_refine::session::ai_refine_strategy_meta,
400+
crate::commands_ai_refine::session::ai_refine_prompt_preview,
401+
crate::commands_ai_refine::session::ai_refine_result,
402402
crate::commands_ai_refine::ai_refine_apply,
403403
crate::commands_ai_refine::workbench::refine_workbench,
404404
// v0.17.0(REQ-246):笔记级 AI 精修——估计/启动/采纳(手写笔记)

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

Lines changed: 2 additions & 239 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
//! · dto.rs —— IPC 契约类型(4 个 serde 结构体;字段序 = JSON 键序)
1717
//! · gate.rs —— 成本硬拦截(免费档/本地 Provider 跳过;查询失败宽容放行)
1818
//! · registry.rs —— 任务注册表 + id 序列 + 容量守卫 + 状态写入口
19+
//! · session.rs —— 会话生命周期 6 命令(预估/启动/策略元数据/预览/状态/结果)
1920
//! · workbench.rs —— 精修工作台(三级数据源 + 章节 diff 统计 + 内联契约测试)
2021
//! @ai-context: 本外壳只留「跨域共享的命令 + 重导出门面」;外部 10 文件
2122
//! 20 处 crate::commands_ai_refine::X 引用**零改动**(逐项重导出在下方);
@@ -25,6 +26,7 @@
2526
mod dto;
2627
mod gate;
2728
mod registry;
29+
pub(crate) mod session;
2830
pub(crate) mod workbench;
2931

3032
pub use dto::{AiRefineResult, AiTaskHandle, RefineEstimateView, RefineStrategyInfo};
@@ -34,247 +36,8 @@ pub(crate) use registry::{set_task, trim_tasks};
3436

3537
use tauri::State;
3638

37-
use crate::ai_cost::estimate_for_content_model;
38-
use crate::ai_note_refine::NoteRefinePrompt;
39-
use crate::ai_strategy::{RefineStrategyMeta, StrategyOverride};
40-
use crate::ai_task::AiTaskState;
4139
use crate::commands::AppState;
42-
use crate::commands_session_note::build_rule_draft_with_analysis;
43-
use crate::note_filter::PurifyEnv;
4440
use crate::types::{NewNote, Note};
45-
use crate::video_profile::ProfileKind;
46-
47-
/// mock 模式 env 键(本地规则精修,不联网——测试/离线开发,ai_text_filter 先例)。
48-
const MOCK_ENV: &str = "AI_REFINE_MOCK";
49-
50-
/// 成本预估(REQ-143 + F1 修复:按模型映射单价、预估含输出 token——
51-
/// 切付费模型后费用不再显示 ¥0,消灭成本失真)。
52-
/// 7️⃣ 修正(2026-08-22,spec 7️⃣):字符数按剥离锚点后的规则草稿计——与
53-
/// 精修实际输入同口径(段落锚点不入模省 token),预估不再虚高。
54-
#[tauri::command]
55-
pub fn ai_refine_estimate(state: State<'_, AppState>, session_id: i64) -> Result<RefineEstimateView, String> {
56-
if session_id <= 0 {
57-
return Err("无效的会话 id".to_string());
58-
}
59-
// 构建规则草稿(与精修任务同管线:filter + 结构渲染,本地快)后剥离锚点
60-
let env = PurifyEnv {
61-
config: state.purify.clone(),
62-
symbol: state.symbol_normalize.clone(),
63-
corrections: state.ocr_corrections.clone(),
64-
};
65-
let (draft, _) = build_rule_draft_with_analysis(
66-
&state.db,
67-
&state.ui_junk,
68-
&env,
69-
&state.data_dir,
70-
session_id,
71-
None,
72-
)
73-
.map_err(|e| e.to_string())?;
74-
let chars = crate::anchor_strip::strip_anchors(&draft.markdown).chars().count();
75-
let remember = state
76-
.ai_settings
77-
.lock()
78-
.map(|s| s.remember_cost_choice)
79-
.unwrap_or(false);
80-
let model = state.ai_settings.lock().map(|s| s.model.clone()).unwrap_or_default();
81-
Ok(RefineEstimateView { estimate: estimate_for_content_model(chars, &model), remember_cost_choice: remember })
82-
}
83-
84-
/// 启动 AI 精修异步任务(授权红线 + 密钥解析 + 后台切片逐片精修)。
85-
///
86-
/// @ai-context: v0.17.0(REQ-245):strategy=任务级策略覆盖(可选——档位 +
87-
/// 逐维覆盖;缺省用设置全局默认;非法值 resolve 内部回退默认,
88-
/// 永不阻断)。dims 解析后传入任务(每片提示词一致)。
89-
#[tauri::command]
90-
pub async fn ai_refine_start(
91-
state: State<'_, AppState>,
92-
session_id: i64,
93-
authorized: bool,
94-
strategy: Option<StrategyOverride>,
95-
// REQ-284(v0.19.7):画面理解任务级覆写(None=跟随全局;前端「仅本次」勾选)
96-
vision_refine: Option<bool>,
97-
) -> Result<AiTaskHandle, String> {
98-
if session_id <= 0 {
99-
return Err("无效的会话 id".to_string());
100-
}
101-
let st: AppState = (*state).clone();
102-
// ① 授权红线:内容上传类调用 gate(enabled + authorized 双条件)
103-
let settings = st.ai_settings.lock().map_err(|e| format!("AI 设置锁中毒: {}", e))?.clone();
104-
settings.content_gate()?;
105-
if !authorized {
106-
return Err("本次上传未确认——请先阅读并同意授权说明".to_string());
107-
}
108-
let mock = std::env::var(MOCK_ENV).map(|v| v == "1").unwrap_or(false);
109-
// ② 密钥解析(env > 凭据库);非 mock 且无密钥 → 明确错误(不创建任务)
110-
if !mock {
111-
let ready = crate::commands_ai_providers::default_provider_ready(&st)?;
112-
if !ready {
113-
return Err("未配置 API 密钥(请在设置页 AI 服务提供商中配置)".to_string());
114-
}
115-
}
116-
// ②b F1 修复(2026-08-21)+ 审查修复(2026-08-21):任务去重——按
117-
// **目标会话**粒度检查进行中任务(防双击/重进/多窗口对同一会话重复
118-
// 扣费;不同会话的任务互不阻塞——原实现全表 any 会误伤其他会话)
119-
{
120-
let tasks = st.ai_tasks.lock().map_err(|e| format!("任务注册表锁中毒: {}", e))?;
121-
let active = tasks.values().any(|t| {
122-
t.target_id == session_id
123-
&& matches!(t.state, AiTaskState::Pending | AiTaskState::Running { .. })
124-
});
125-
if active {
126-
return Err("该会话已有进行中的 AI 任务——请等待完成或到任务中心查看进度(防重复扣费)".to_string());
127-
}
128-
}
129-
// ②c F1/F3-D 修复(2026-08-21):成本硬拦截 + 每日配额接入。
130-
// 顺序铁律(审查修复):先余额拦截(失败不消耗配额),后消耗配额——
131-
// 否则余额不足被拒时配额已扣(浪费每日额度)。
132-
if !mock {
133-
let segments = st.db.list_segments(session_id).map_err(|e| e.to_string())?;
134-
let ocr = st.db.list_ocr_blocks(session_id).map_err(|e| e.to_string())?;
135-
let chars = segments.iter().map(|s| s.text.chars().count()).sum::<usize>()
136-
+ ocr.iter().map(|b| b.text.chars().count()).sum::<usize>();
137-
// 成本硬拦截(免费档 ¥0 预估 → 余额 0 也放行;查询失败宽容放行)
138-
ensure_balance_for(&st, chars, &settings.model)?;
139-
// 片数估算(与 ai_task::slice_note 同口径的保守上界:向上取整,
140-
// 空内容 0 片不消耗配额——审查修复:原公式 +1 导致空会话也扣 1)
141-
let slices = if chars == 0 {
142-
0
143-
} else {
144-
chars.saturating_add(crate::ai_task::SLICE_MAX_CHARS - 1) / crate::ai_task::SLICE_MAX_CHARS
145-
};
146-
let now = crate::db_sessions_rows::unix_seconds();
147-
let mut guards = st.ai_guardrails.lock().map_err(|e| format!("护栏状态锁中毒: {}", e))?;
148-
for _ in 0..slices {
149-
if !guards.quota.try_consume(now) {
150-
return Err("今日 AI 精修配额已用完(请明日再试或到设置页调整)".to_string());
151-
}
152-
}
153-
drop(guards);
154-
}
155-
// ③ 注册任务 + 后台执行(spawn_blocking——网络/分析不阻塞异步运行时);
156-
// 任务 id 走全任务族统一认领封装(claim_task_id——见其 Why:proofread
157-
// 原 +1 偏移与各族口径冲突,相邻认领撞 id 会被 INSERT OR REPLACE 顶替)
158-
let task_id = claim_task_id(&st.ai_task_seq);
159-
{
160-
let mut tasks = st.ai_tasks.lock().map_err(|e| format!("任务注册表锁中毒: {}", e))?;
161-
tasks.insert(task_id, AiTaskEntry { state: AiTaskState::Pending, result: None, target_id: session_id });
162-
trim_tasks(&mut tasks);
163-
}
164-
// F2 任务中心(2026-08-21):任务记录落库(pending 起步;终态在
165-
// run_refine_task 回写)。L4 修复:写库失败仍不阻断 AI 调用(H2 设计不变),
166-
// 但不再静默——落库失败意味着任务中心重启后无法恢复该任务,必须可观测
167-
if let Err(e) = 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-
target_kind: Some("session".to_string()),
182-
}) {
183-
eprintln!("[AiTasks] refine 任务 {} 落库失败(不阻断 AI 调用;重启后不可恢复): {}", task_id, e);
184-
}
185-
let st2 = st.clone();
186-
// v0.17.0:策略解析(任务覆盖 > 设置全局默认 > 内置 standard——非法值
187-
// resolve 内回退,永不阻断精修主链路;标准档=现状逐字节一致)
188-
let dims = crate::ai_strategy::resolve(
189-
&NoteRefinePrompt::bundled(),
190-
&settings.refine_strategy,
191-
strategy.as_ref(),
192-
);
193-
tauri::async_runtime::spawn_blocking(move || {
194-
crate::ai_refine_task::run_refine_task(st2, task_id, session_id, mock, dims, vision_refine)
195-
});
196-
Ok(AiTaskHandle { task_id, state: AiTaskState::Pending })
197-
}
198-
199-
/// 策略声明元数据(发起对话框/设置页渲染数据源——后端声明即事实源)。
200-
#[tauri::command]
201-
pub fn ai_refine_strategy_meta() -> Result<RefineStrategyMeta, String> {
202-
let p = NoteRefinePrompt::bundled();
203-
Ok(RefineStrategyMeta {
204-
strategy_dims: p.strategy_dims,
205-
ladder_presets: p.ladder_presets,
206-
intents: p.intents,
207-
})
208-
}
209-
210-
/// 提示词组装预览(与实发精修同一 build_system 代码路径——所见即所发)。
211-
///
212-
/// @ai-context: 档案来源二选一:session_id(会话级——会话档案驱动风格模板)
213-
/// 或 profile(笔记级——handwritten/用户所选档案);全局偏好 +
214-
/// 任务级覆盖参与解析;返回完整 system 提示词(只读预览 + 复制)。
215-
#[tauri::command]
216-
pub fn ai_refine_prompt_preview(
217-
state: State<'_, AppState>,
218-
session_id: Option<i64>,
219-
profile: Option<String>,
220-
strategy: Option<StrategyOverride>,
221-
) -> Result<String, String> {
222-
let kind = match session_id.filter(|v| *v > 0) {
223-
Some(sid) => {
224-
let session = state
225-
.db
226-
.get_session(sid)
227-
.map_err(|e| format!("读取会话失败: {}", e))?
228-
.ok_or_else(|| "会话不存在".to_string())?;
229-
session
230-
.profile
231-
.as_deref()
232-
.map(ProfileKind::parse)
233-
.unwrap_or(ProfileKind::Lecture)
234-
}
235-
None => ProfileKind::parse(profile.as_deref().unwrap_or("handwritten")),
236-
};
237-
let prefs = state
238-
.ai_settings
239-
.lock()
240-
.map_err(|e| format!("AI 设置锁中毒: {}", e))?
241-
.refine_strategy
242-
.clone();
243-
Ok(crate::ai_strategy::preview_system(
244-
&NoteRefinePrompt::bundled(),
245-
kind.as_str(),
246-
&prefs,
247-
strategy.as_ref(),
248-
))
249-
}
250-
251-
/// 任务状态查询(前端轮询通道;事件通道见 "ai:task-update")。
252-
#[tauri::command]
253-
pub fn ai_refine_status(state: State<'_, AppState>, task_id: u64) -> Result<AiTaskState, String> {
254-
state
255-
.ai_tasks
256-
.lock()
257-
.map_err(|e| format!("任务注册表锁中毒: {}", e))?
258-
.get(&task_id)
259-
.map(|t| t.state.clone())
260-
.ok_or_else(|| format!("任务不存在: {}", task_id))
261-
}
262-
263-
/// 精修结果(仅成功后可取;失败/进行中返回明确错误)。
264-
#[tauri::command]
265-
pub fn ai_refine_result(state: State<'_, AppState>, task_id: u64) -> Result<AiRefineResult, String> {
266-
let tasks = state.ai_tasks.lock().map_err(|e| format!("任务注册表锁中毒: {}", e))?;
267-
let entry = tasks
268-
.get(&task_id)
269-
.ok_or_else(|| format!("任务不存在: {}", task_id))?;
270-
match (&entry.state, &entry.result) {
271-
(AiTaskState::Succeeded, Some(v)) => serde_json::from_value(v.clone())
272-
.map_err(|e| format!("精修结果反序列化失败: {}", e)),
273-
(AiTaskState::Succeeded, None) => Err("任务成功但结果缺失(内部状态异常)".to_string()),
274-
(AiTaskState::Failed { reason }, _) => Err(format!("任务失败({}): {}", reason.kind(), reason.message())),
275-
_ => Err("任务仍在进行中".to_string()),
276-
}
277-
}
27841

27942
/// 任务历史(F2 任务中心:前端面板数据源——按类型列最近任务)。
28043
#[tauri::command]

0 commit comments

Comments
 (0)