Skip to content

Commit 4dbafdf

Browse files
committed
refactor(session): 审查硬拆——commands_session 611行超600阈值,笔记管线拆至 commands_session_note.rs
1 parent 843bfce commit 4dbafdf

4 files changed

Lines changed: 299 additions & 268 deletions

File tree

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

Lines changed: 9 additions & 263 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,14 @@
33
//! @ai-context: 本层只做参数校验、调用数据层、错误映射(AGENTS.md §6);
44
//! DB 读写为快速操作直接调用,不额外 spawn_blocking(连接内 Mutex 保护)。
55
//! @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 命令注册按定义模块引用。
119
1210
use tauri::State;
1311

14-
use crate::ai_protocol::TextFilterDecision;
15-
use crate::analysis::analyze_session_opt;
1612
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};
2814

2915
/// 会话列表单页上限。
3016
const LIST_LIMIT_MAX: u64 = 200;
@@ -170,251 +156,11 @@ pub async fn add_session_ocr_block(
170156
state.db.add_ocr_block(&new).map(|b| b.id).map_err(|e| e.to_string())
171157
}
172158

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+
// ────────────────────────────────────────────────────────────
418164

419165
/// 归一化转写段来源标识(asr | subtitle | fused,其余回退 asr)。
420166
fn normalize_source(source: &str) -> String {

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
//! @ai-context: 由 commands_session.rs 以 #[cfg(test)] #[path] 引入;
44
//! 被测对象 run_batch_conversion 为纯编排(注入 Db + UiJunkList),
55
//! 全部走 :memory: 隔离库(环境隔离——不触碰真实数据)。
6+
//! @ai-context: v0.7.6 审查硬拆:run_batch_conversion 随笔记管线移入
7+
//! commands_session_note.rs——本测试引用随之更新。
68
7-
use crate::commands_session::run_batch_conversion;
9+
use crate::commands_session_note::run_batch_conversion;
810
use crate::db::Db;
911
use crate::types::{NewNote, NewSession, NewSessionSegment};
1012
use crate::ui_junk::UiJunkList;

0 commit comments

Comments
 (0)