|
3 | 3 | //! @ai-context: 本层只做参数校验、调用数据层、错误映射(AGENTS.md §6); |
4 | 4 | //! DB 读写为快速操作直接调用,不额外 spawn_blocking(连接内 Mutex 保护)。 |
5 | 5 | //! @ai-context: 入参校验(TD-005 修复口径):title 非空且 ≤100 字;分页参数有界。 |
6 | | -//! @ai-context: REQ-081/082(v0.6.0 M1):session_to_note(落库)与 |
7 | | -//! preview_session_note(只读预览)共用 note_filter 单一管线—— |
8 | | -//! 输出一致性由构造保证(同一过滤链/同一 Markdown 组装)。 |
9 | | -
|
10 | | -use std::collections::HashSet; |
| 6 | +//! @ai-context: 笔记转换管线(session_to_note/preview_session_note/批量编排)已 |
| 7 | +//! 于 v0.7.6 审查拆至 commands_session_note.rs(>600 行硬拆, |
| 8 | +//! AGENTS.md §3);lib.rs 命令注册按定义模块引用。 |
11 | 9 |
|
12 | 10 | use tauri::State; |
13 | 11 |
|
14 | | -use crate::ai_protocol::TextFilterDecision; |
15 | | -use crate::analysis::analyze_session_opt; |
16 | 12 | use crate::commands::{normalize_title, AppState, TITLE_MAX_CHARS}; |
17 | | -use crate::db::Db; |
18 | | -use crate::db_sessions::SESSION_STATUS_RECORDING; |
19 | | -use crate::note_filter::{apply_ai_decisions, filter_note, NoteFilterResult, PurifyEnv, RULE_VERSION}; |
20 | | -use crate::outline::{detect_outline_smart, OutlineConfig}; |
21 | | -use crate::structure_note::render_note_structure; |
22 | | -use crate::types::{ |
23 | | - BatchNoteResult, ConvertedNote, NewNote, NewSession, NewSessionOcrBlock, NewSessionSegment, |
24 | | - Note, Session, SessionDetail, SessionListItem, SessionOcrBlock, SessionSegment, SkippedNote, |
25 | | -}; |
26 | | -use crate::ui_junk::UiJunkList; |
27 | | -use crate::video_profile::ProfileKind; |
| 13 | +use crate::types::{NewSession, NewSessionOcrBlock, NewSessionSegment, Session, SessionDetail, SessionListItem}; |
28 | 14 |
|
29 | 15 | /// 会话列表单页上限。 |
30 | 16 | const LIST_LIMIT_MAX: u64 = 200; |
@@ -170,251 +156,11 @@ pub async fn add_session_ocr_block( |
170 | 156 | state.db.add_ocr_block(&new).map(|b| b.id).map_err(|e| e.to_string()) |
171 | 157 | } |
172 | 158 |
|
173 | | -/// 加载会话笔记原料(会话 + 转写段 + OCR 块;单一管线双出口共用)。 |
174 | | -/// |
175 | | -/// @ai-context: REQ-081:预览与转笔记从同一原料装载开始,保证口径一致; |
176 | | -/// v0.7.1 起注入 &Db(不再依赖 AppState)——批量编排可单测。 |
177 | | -fn load_note_material( |
178 | | - db: &Db, |
179 | | - id: i64, |
180 | | -) -> Result<(Session, Vec<SessionSegment>, Vec<SessionOcrBlock>), String> { |
181 | | - if id <= 0 { |
182 | | - return Err("无效的会话 id".to_string()); |
183 | | - } |
184 | | - let session = db |
185 | | - .get_session(id) |
186 | | - .map_err(|e| e.to_string())? |
187 | | - .ok_or_else(|| format!("会话不存在: {}", id))?; |
188 | | - if session.status == SESSION_STATUS_RECORDING { |
189 | | - return Err("进行中的会话不能生成笔记,请先结束会话".to_string()); |
190 | | - } |
191 | | - let segments = db.list_segments(id).map_err(|e| e.to_string())?; |
192 | | - let ocr_blocks = db.list_ocr_blocks(id).map_err(|e| e.to_string())?; |
193 | | - Ok((session, segments, ocr_blocks)) |
194 | | -} |
195 | | - |
196 | | -/// 结构渲染接线(v0.7.6 REQ-177/178,双出口共用)。 |
197 | | -/// |
198 | | -/// @ai-context: 在 refresh_screen_points 之后调用——净化/配图/警示行先落定, |
199 | | -/// 结构层在其上叠加章节标题与词汇表块;预览与落库同函数同口径 |
200 | | -/// (REQ-081 单一管线双出口契约)。 |
201 | | -/// @ai-context: 输入为会话原料(segments/ocr_blocks)——章节检测/大纲标题/ |
202 | | -/// 术语表均为纯本地规则(analysis/outline/glossary 复用)—— |
203 | | -/// 不依赖云端 AI(本地优先铁律);失败不阻断转笔记主链路 |
204 | | -/// (分析失败按空结构处理,不抛错)。 |
205 | | -fn apply_note_structure( |
206 | | - result: &mut NoteFilterResult, |
207 | | - db: &Db, |
208 | | - session: &Session, |
209 | | - segments: &[SessionSegment], |
210 | | - ocr_blocks: &[SessionOcrBlock], |
211 | | - env: &PurifyEnv, |
212 | | -) { |
213 | | - // 档案驱动:章节检测/术语表按档案开关(网课开、口播关——analysis 内部门控) |
214 | | - let kind = session |
215 | | - .profile |
216 | | - .as_deref() |
217 | | - .map(ProfileKind::parse) |
218 | | - .unwrap_or(ProfileKind::Lecture); |
219 | | - let detail = SessionDetail { |
220 | | - session: session.clone(), |
221 | | - segments: segments.to_vec(), |
222 | | - ocr_blocks: ocr_blocks.to_vec(), |
223 | | - events: db.list_events(session.id).unwrap_or_default(), |
224 | | - screens: Vec::new(), |
225 | | - }; |
226 | | - let analysis = analyze_session_opt(&detail, kind, &env.symbol); |
227 | | - let outline = detect_outline_smart(ocr_blocks, &result.ocr_screens, &OutlineConfig::default()); |
228 | | - let _ = render_note_structure( |
229 | | - result, |
230 | | - &analysis.chapters, |
231 | | - &outline, |
232 | | - &analysis.glossary, |
233 | | - &env.config.structure, |
234 | | - ); |
235 | | -} |
236 | | - |
237 | | -/// 会话 → 笔记核心(v0.7.1 提取:单条与批量共用同一管线)。 |
238 | | -/// |
239 | | -/// @ai-context: REQ-082:过滤链(UI 垃圾/重复合并/碎片/低置信/口语净化/口头禅 |
240 | | -/// 删除——v0.7.5 净化接线)与预览共用;ai_decisions(REQ-085) |
241 | | -/// 可选叠加——前端把预览中已确认的 AI 判定结果回传,落库与预览 |
242 | | -/// 输出保持一致(默认 None=纯规则)。 |
243 | | -/// @ai-context: 只允许 finished/failed 会话转换;source 沿用 classroom; |
244 | | -/// v0.7.1 起落库携带 session_id(列表 has_note/查看笔记跳转的数据源); |
245 | | -/// v0.7.3(REQ-160):data_dir 供画面要点屏 attach 归档图 |
246 | | -/// (配图行随 image_ref 进入笔记 markdown)。 |
247 | | -/// @ai-context: v0.7.5:净化配置/符号映射注入(REQ-173 JSON 可校准);失败/ |
248 | | -/// 异常会话追加警示行(REQ-170 诚实降级);落库携带 rule_version |
249 | | -/// + purify_stats 元数据(REQ-171——旧笔记 NULL 诚实降级)。 |
250 | | -fn convert_to_note( |
251 | | - db: &Db, |
252 | | - ui_junk: &UiJunkList, |
253 | | - env: &PurifyEnv, |
254 | | - data_dir: &std::path::Path, |
255 | | - id: i64, |
256 | | - title: Option<String>, |
257 | | - ai_decisions: Option<Vec<TextFilterDecision>>, |
258 | | -) -> Result<Note, String> { |
259 | | - let (session, segments, ocr_blocks) = load_note_material(db, id)?; |
260 | | - let fallback = format!("{}(会话)", session.title); |
261 | | - let title = normalize_title(title.unwrap_or_default(), &fallback); |
262 | | - let mut result = filter_note(&title, &segments, &ocr_blocks, ui_junk, env); |
263 | | - if let Some(decisions) = ai_decisions { |
264 | | - result = apply_ai_decisions(result, &decisions); |
265 | | - } |
266 | | - // v0.7.3(REQ-160):画面要点配图(归档 full 图匹配;目录缺失/无图 → 纯文本降级) |
267 | | - let images_dir = data_dir.join("session-images").join(id.to_string()); |
268 | | - crate::screens::attach_images(&mut result.ocr_screens, &images_dir); |
269 | | - // v0.7.5(REQ-170):失败/异常会话 → 警示行(refresh 前写入——markdown |
270 | | - // 重建口径一致;警示为正文行,用户可手动删除) |
271 | | - crate::note_filter::apply_session_warning(&mut result, &session.status); |
272 | | - crate::note_filter::refresh_screen_points(&mut result); |
273 | | - // v0.7.6(REQ-177/178):结构渲染——章节标题 + 词汇表块(纯本地增强层; |
274 | | - // 无结构数据/分析失败 → 原样输出不阻断,见 apply_note_structure) |
275 | | - apply_note_structure(&mut result, db, &session, &segments, &ocr_blocks, env); |
276 | | - let new = NewNote { |
277 | | - title: result.title.clone(), |
278 | | - content: result.markdown.clone(), |
279 | | - source: "classroom".to_string(), |
280 | | - session_id: Some(id), |
281 | | - // REQ-171:规则版本 + 净化统计落库(可追溯"用哪版规则生成") |
282 | | - rule_version: Some(RULE_VERSION.to_string()), |
283 | | - purify_stats: Some(serde_json::to_string(&result.stats).unwrap_or_default()), |
284 | | - }; |
285 | | - db.create_note(&new).map_err(|e| e.to_string()) |
286 | | -} |
287 | | - |
288 | | -/// 会话 → 笔记:转写段 + OCR 块经 note_filter 净化管线,一键落库(REQ-010 闭环)。 |
289 | | -#[tauri::command] |
290 | | -pub async fn session_to_note( |
291 | | - state: State<'_, AppState>, |
292 | | - id: i64, |
293 | | - title: Option<String>, |
294 | | - ai_decisions: Option<Vec<TextFilterDecision>>, |
295 | | -) -> Result<Note, String> { |
296 | | - convert_to_note( |
297 | | - &state.db, |
298 | | - &state.ui_junk, |
299 | | - &PurifyEnv { |
300 | | - config: state.purify.clone(), |
301 | | - symbol: state.symbol_normalize.clone(), |
302 | | - corrections: state.ocr_corrections.clone(), |
303 | | - }, |
304 | | - &state.data_dir, |
305 | | - id, |
306 | | - title, |
307 | | - ai_decisions, |
308 | | - ) |
309 | | -} |
310 | | - |
311 | | -/// 批量转笔记核心编排(v0.7.1:部分成功语义——单条失败不阻塞其他)。 |
312 | | -/// |
313 | | -/// @ai-context: 纯编排纯函数(注入 Db + UiJunkList,无 Tauri 依赖,可单测); |
314 | | -/// 跳过规则显式回传原因(不静默):无效 id / 进行中 / 已转 / 会话不存在; |
315 | | -/// DB 读错误视为硬失败中止(库损坏时继续处理无意义)。 |
316 | | -pub fn run_batch_conversion( |
317 | | - db: &Db, |
318 | | - ui_junk: &UiJunkList, |
319 | | - env: &PurifyEnv, |
320 | | - data_dir: &std::path::Path, |
321 | | - ids: Vec<i64>, |
322 | | -) -> Result<BatchNoteResult, String> { |
323 | | - if ids.len() > 50 { |
324 | | - return Err("批量转换上限 50 条".to_string()); |
325 | | - } |
326 | | - let mut converted = Vec::new(); |
327 | | - let mut skipped = Vec::new(); |
328 | | - let mut seen = HashSet::new(); |
329 | | - for id in ids { |
330 | | - if id <= 0 { |
331 | | - skipped.push(SkippedNote { session_id: id, reason: "无效的会话 id".to_string() }); |
332 | | - continue; |
333 | | - } |
334 | | - if !seen.insert(id) { |
335 | | - continue; // 重复 id 静默去重(同一会话只处理一次) |
336 | | - } |
337 | | - match db.get_session(id) { |
338 | | - Ok(Some(s)) if s.status == SESSION_STATUS_RECORDING => { |
339 | | - skipped.push(SkippedNote { |
340 | | - session_id: id, |
341 | | - reason: "进行中的会话不能生成笔记".to_string(), |
342 | | - }); |
343 | | - } |
344 | | - Ok(Some(_)) => { |
345 | | - // 已转跳过(防重复笔记);详情页单条转换保留"有意重新生成"路径 |
346 | | - if db.find_note_by_session(id).map_err(|e| e.to_string())?.is_some() { |
347 | | - skipped.push(SkippedNote { |
348 | | - session_id: id, |
349 | | - reason: "已转笔记(批量转跳过,避免重复)".to_string(), |
350 | | - }); |
351 | | - continue; |
352 | | - } |
353 | | - match convert_to_note(db, ui_junk, env, data_dir, id, None, None) { |
354 | | - Ok(note) => converted.push(ConvertedNote { session_id: id, note_id: note.id }), |
355 | | - Err(reason) => skipped.push(SkippedNote { session_id: id, reason }), |
356 | | - } |
357 | | - } |
358 | | - Ok(None) => { |
359 | | - skipped.push(SkippedNote { session_id: id, reason: "会话不存在".to_string() }); |
360 | | - } |
361 | | - Err(e) => { |
362 | | - skipped.push(SkippedNote { session_id: id, reason: e.to_string() }); |
363 | | - } |
364 | | - } |
365 | | - } |
366 | | - Ok(BatchNoteResult { converted, skipped }) |
367 | | -} |
368 | | - |
369 | | -/// 批量转笔记(v0.7.1 会话体验:列表勾选批量转化)。 |
370 | | -#[tauri::command] |
371 | | -pub async fn batch_session_to_note( |
372 | | - state: State<'_, AppState>, |
373 | | - ids: Vec<i64>, |
374 | | -) -> Result<BatchNoteResult, String> { |
375 | | - run_batch_conversion( |
376 | | - &state.db, |
377 | | - &state.ui_junk, |
378 | | - &PurifyEnv { |
379 | | - config: state.purify.clone(), |
380 | | - symbol: state.symbol_normalize.clone(), |
381 | | - corrections: state.ocr_corrections.clone(), |
382 | | - }, |
383 | | - &state.data_dir, |
384 | | - ids, |
385 | | - ) |
386 | | -} |
387 | | - |
388 | | -/// 会话笔记预览(REQ-081):过滤后只读预览——不落库、不改库。 |
389 | | -/// |
390 | | -/// @ai-context: 与 session_to_note 同一过滤管线(输出一致性由构造保证); |
391 | | -/// 返回 NoteFilterResult(markdown + 过滤统计 + 被过滤对照 + |
392 | | -/// kept 段),前端展示过滤统计卡/对照复查/一键落库。 |
393 | | -/// @ai-context: v0.7.3(REQ-160):ocr_screens attach 归档图(预览屏卡配图; |
394 | | -/// 目录缺失/无图 → 纯文本降级)。 |
395 | | -/// @ai-context: v0.7.6(REQ-177/178):预览与落库同口径——结构渲染(章节 |
396 | | -/// 标题 + 词汇表块)在预览即生效。 |
397 | | -#[tauri::command] |
398 | | -pub async fn preview_session_note( |
399 | | - state: State<'_, AppState>, |
400 | | - id: i64, |
401 | | -) -> Result<NoteFilterResult, String> { |
402 | | - let (session, segments, ocr_blocks) = load_note_material(&state.db, id)?; |
403 | | - let env = PurifyEnv { |
404 | | - config: state.purify.clone(), |
405 | | - symbol: state.symbol_normalize.clone(), |
406 | | - corrections: state.ocr_corrections.clone(), |
407 | | - }; |
408 | | - let mut result = filter_note(&session.title, &segments, &ocr_blocks, &state.ui_junk, &env); |
409 | | - let images_dir = state.data_dir.join("session-images").join(id.to_string()); |
410 | | - crate::screens::attach_images(&mut result.ocr_screens, &images_dir); |
411 | | - // v0.7.5(REQ-170):预览与落库同口径——异常会话预览即带警示行 |
412 | | - crate::note_filter::apply_session_warning(&mut result, &session.status); |
413 | | - crate::note_filter::refresh_screen_points(&mut result); |
414 | | - // v0.7.6(REQ-177/178):结构渲染(与 convert_to_note 同函数同口径) |
415 | | - apply_note_structure(&mut result, &state.db, &session, &segments, &ocr_blocks, &env); |
416 | | - Ok(result) |
417 | | -} |
| 159 | +// ──────────────────────────────────────────────────────────── |
| 160 | +// 笔记转换管线(v0.7.6 审查硬拆):commands_session_note.rs——原料装载/ |
| 161 | +// 结构渲染接线/单条转换/批量编排/只读预览;lib.rs 按定义模块注册命令。 |
| 162 | +// 批量转笔记单测(commands_session_batch_tests.rs)引用新模块 run_batch_conversion。 |
| 163 | +// ──────────────────────────────────────────────────────────── |
418 | 164 |
|
419 | 165 | /// 归一化转写段来源标识(asr | subtitle | fused,其余回退 asr)。 |
420 | 166 | fn normalize_source(source: &str) -> String { |
|
0 commit comments