Skip to content

Commit a0c5e62

Browse files
committed
refactor(rust): 拆 note_filter 第 1 步 — 渲染层
1 parent 79ba797 commit a0c5e62

3 files changed

Lines changed: 170 additions & 131 deletions

File tree

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

Lines changed: 16 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,25 @@
3131
use crate::note_body_source::{detect_body_source, BodySource};
3232
use crate::purify_config::PurifyConfig;
3333
use crate::symbol_normalize::SymbolNormalizeConfig;
34-
use crate::types::{SessionOcrBlock, SessionScreen, SessionSegment, TranscriptSegment};
34+
use crate::types::{SessionOcrBlock, SessionScreen, SessionSegment};
3535
use crate::ui_junk::UiJunkList;
3636
use crate::verbal_normalize::{NormalizeConfig, NormalizeStrength};
3737

38+
// ────────────────────────────────────────────────────────────
39+
// 拆分子模块(AGENTS.md §3:单文件 ≤300 行)——父模块**自己**用 #[path] 声明,
40+
// 不动 lib.rs 的 mod 块;三个文件与 note_filter.rs 平铺于 src/(#[path] 相对
41+
// 「源文件所在目录」解析)。模块私有 ⇒ 外部经下方再导出访问,调用点零改动。
42+
// ────────────────────────────────────────────────────────────
43+
44+
/// 渲染层(屏卡 → 画面要点行 / markdown 组装 / 异常警示行)。
45+
#[path = "note_filter_render.rs"]
46+
mod render;
47+
48+
/// 再导出:可见性与项 **1:1**(`pub` 项用 `pub use`、`pub(crate)` 项用
49+
/// `pub(crate) use`——后者写成 `pub use` 会触发 E0365,项本身不允许外泄)。
50+
pub use self::render::{refresh_screen_points, render_screen_points};
51+
pub(crate) use self::render::{apply_session_warning, rebuild_markdown};
52+
3853
/// 净化环境(依赖注入聚合——净化配置 + 符号映射 + OCR 纠错表)。
3954
///
4055
/// @ai-context: filter_note/convert_to_note 参数收敛(clippy too_many_arguments
@@ -490,135 +505,6 @@ fn is_fragment(seg: &SessionSegment, config: &PurifyConfig) -> bool {
490505
|| text.chars().all(|c| !c.is_alphanumeric() && !is_cjk(c))
491506
}
492507

493-
/// 画面要点屏段落渲染(纯函数):屏卡 → 笔记画面要点行(Markdown 列表项)。
494-
///
495-
/// @ai-context: 每屏一段:`- **[MM:SS–MM:SS] 标题**` + 正文行(二级列表)+
496-
/// 标签行 + 配图行(image_ref 有值时;src 为相对 data_dir 路径
497-
/// `session-images/{sid}/full/{ts}.webp`——前端渲染拼 baseUrl)。
498-
/// @ai-context: 无标题屏(旧数据无 bbox 降级)用"画面 N"占位——不丢内容。
499-
pub fn render_screen_points(screens: &[SessionScreen]) -> Vec<String> {
500-
let mut lines = Vec::new();
501-
for (i, s) in screens.iter().enumerate() {
502-
let range = format!(
503-
"[{}–{}]",
504-
crate::concat::format_timestamp(s.first_seen_ms),
505-
crate::concat::format_timestamp(s.last_seen_ms)
506-
);
507-
let title = s.title.clone().unwrap_or_else(|| format!("画面 {}", i + 1));
508-
lines.push(format!("- **{} {}**", range, title));
509-
for b in &s.body {
510-
lines.push(format!(" - {}", b));
511-
}
512-
if !s.labels.is_empty() {
513-
lines.push(format!(" - 标签:{}", s.labels.join(" · ")));
514-
}
515-
if let Some(rel) = &s.image_ref {
516-
// 修复(2026-08-21 审查):缺闭合 `)`——前端渲染正则
517-
// `^\s*-\s*!\[([^\]]*)\]\(([^)]*)\)$` 要求 `)$` 结尾,缺括号时
518-
// 配图行永远匹配不上 → 渲染为纯文本而非图片(丢图真因之一)
519-
lines.push(format!(" - ![画面 {}](session-images/{}/{})", i + 1, s.session_id, rel));
520-
}
521-
}
522-
lines
523-
}
524-
525-
/// 会话异常警示行(REQ-170,纯函数):status != finished → 追加警示。
526-
///
527-
/// @ai-context: 会话31 实证:status=failed 但内容完整(停止链路异常翻案)——
528-
/// 照常转笔记但诚实标注"内容可能不完整",用户自行判断;
529-
/// 警示行是普通 Markdown 引用行(用户可手动删除)。
530-
/// @ai-context: 预览/落库/AI 复核三出口共用(REQ-081 单一管线)——命令层在
531-
/// refresh_screen_points 前调用,markdown 重建口径一致。
532-
pub(crate) fn apply_session_warning(result: &mut NoteFilterResult, status: &str) {
533-
if status == crate::db_sessions::SESSION_STATUS_FINISHED {
534-
result.warning = None;
535-
} else {
536-
result.warning = Some(format!("> ⚠️ 会话异常({}),内容可能不完整", status));
537-
}
538-
}
539-
540-
/// 刷新画面要点数据(纯函数):ocr_screens 重新渲染 + 重建 markdown。
541-
///
542-
/// @ai-context: 命令层 attach_images 填充 image_ref 后调用——原料视图
543-
/// 屏卡配图随 image_ref 出现/消失;markdown 重建不含画面要点
544-
/// (v0.11.5 移出笔记,配图行仅存于 AI 精修 image 块);
545-
/// 净化配置随 result 透传(段落阈值/锚点与预览口径一致)。
546-
pub fn refresh_screen_points(result: &mut NoteFilterResult) {
547-
result.ocr_points = render_screen_points(&result.ocr_screens);
548-
// v0.12.0(ADR-021):按正文源分派重建——OcrDirect 走 OCR 正文重建
549-
// (kept 为空,走 rebuild_markdown 会把 OCR 正文覆盖成标题仅)
550-
result.markdown = match result.body_source {
551-
BodySource::OcrDirect => crate::note_filter_ocr::rebuild_ocr_markdown(
552-
&result.title,
553-
&result.ocr_body,
554-
&result.purify,
555-
result.warning.as_deref(),
556-
),
557-
_ => rebuild_markdown(
558-
&result.title,
559-
&result.kept,
560-
&[],
561-
&result.purify,
562-
result.warning.as_deref(),
563-
),
564-
};
565-
}
566-
567-
/// 组装 Markdown(标题 + 讲述内容;段落切分复用 concat 口径;
568-
/// v0.7.5 REQ-165/170/173:段首 [MM:SS] 时间戳锚点(可回跳原视频位置,可开关)
569-
/// + 段落阈值走净化配置 + 会话异常警示行(None=无警示)。
570-
/// v0.11.5:画面要点段移出笔记(_ocr_points 签名保留兼容调用方,忽略)。
571-
pub(crate) fn rebuild_markdown(
572-
title: &str,
573-
kept: &[SessionSegment],
574-
_ocr_points: &[String],
575-
config: &PurifyConfig,
576-
warning: Option<&str>,
577-
) -> String {
578-
let mut md = assemble_purified_markdown(title, kept, _ocr_points, config);
579-
if let Some(w) = warning {
580-
md = format!("{}\n\n{}", w, md);
581-
}
582-
md
583-
}
584-
585-
/// 净化组装(无警示行版——供 rebuild_markdown 内部与警示拼接)。
586-
fn assemble_purified_markdown(
587-
title: &str,
588-
kept: &[SessionSegment],
589-
_ocr_points: &[String],
590-
config: &PurifyConfig,
591-
) -> String {
592-
let transcript: Vec<TranscriptSegment> = kept
593-
.iter()
594-
.map(|s| TranscriptSegment {
595-
start_ms: s.start_ms,
596-
end_ms: s.end_ms,
597-
text: s.text.clone(),
598-
word_timestamps: None,
599-
// 段落切分不消费置信度/音量(None 占位)
600-
confidence: None,
601-
volume: None,
602-
})
603-
.collect();
604-
let paragraphs = crate::concat::split_transcript_paragraphs_with(
605-
&transcript,
606-
config.paragraph_max_chars,
607-
config.paragraph_max_span_ms,
608-
);
609-
let anchored: Vec<String> = paragraphs
610-
.into_iter()
611-
.map(|(start_ms, text)| {
612-
if config.anchor_timestamps {
613-
format!("{} {}", crate::concat::format_timestamp(start_ms), text)
614-
} else {
615-
text
616-
}
617-
})
618-
.collect();
619-
crate::concat::assemble_markdown(title, &anchored, &[])
620-
}
621-
622508
/// CJK 统一表意文字区段(含扩展 A)。
623509
fn is_cjk(c: char) -> bool {
624510
let u = c as u32;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
//! @ai-context: 笔记过滤域的**渲染层**(批 0-C3 Task 8 S1 从 note_filter.rs 平铺拆出,
2+
//! AGENTS.md §3 单文件 ≤300 行)——屏卡 → markdown 画面要点行、标题+讲述
3+
//! 内容 markdown 组装、会话异常警示行前缀;由过滤链(filter_note_transcript)
4+
//! 与命令层(commands_ai / commands_session_note)共用。
5+
//! @ai-context: 副作用:无 IO / 无 DB / 无锁 —— 全部是纯字符串构造,唯一写入是
6+
//! `apply_session_warning`/`refresh_screen_points` 对 &mut
7+
//! NoteFilterResult 的字段赋值。
8+
//! @ai-context: 边界(**既有行为,逐字搬运不改,仅登记**):`refresh_screen_points`
9+
//! 按 `body_source` 分派重建 markdown,`BodySource::Web` 落入 `_` 分支、
10+
//! 以**空 kept** 调 rebuild_markdown ⇒ 退化为标题仅(filter_note 注释声明
11+
//! web 正文不走过滤链,此处是"防御未来误入")。
12+
//! @ai-context: 可见性契约:`render_screen_points`/`refresh_screen_points` 保持 `pub`、
13+
//! `apply_session_warning`/`rebuild_markdown` 保持 `pub(crate)`、
14+
//! `assemble_purified_markdown` 保持私有(唯一消费者 rebuild_markdown 同在本
15+
//! 模块)——由 note_filter.rs 按 1:1 可见性再导出,外部 17 文件 37 处
16+
//! `crate::note_filter::…` 调用点零改动。
17+
//! @ai-context: 未覆盖路径:25 例域内测试**无一例带 image_ref** ⇒ 配图行格式串
18+
//! (` - ![画面 N](session-images/{sid}/{rel})`)无测试拦网,靠逐字搬运 +
19+
//! 人工 diff 证明未改;OCR 文本未经转义直拼 markdown 的注入面同样**只记录不改**。
20+
21+
use crate::note_body_source::BodySource;
22+
use crate::purify_config::PurifyConfig;
23+
use crate::types::{SessionScreen, SessionSegment, TranscriptSegment};
24+
25+
use super::NoteFilterResult;
26+
27+
/// 画面要点屏段落渲染(纯函数):屏卡 → 笔记画面要点行(Markdown 列表项)。
28+
///
29+
/// @ai-context: 每屏一段:`- **[MM:SS–MM:SS] 标题**` + 正文行(二级列表)+
30+
/// 标签行 + 配图行(image_ref 有值时;src 为相对 data_dir 路径
31+
/// `session-images/{sid}/full/{ts}.webp`——前端渲染拼 baseUrl)。
32+
/// @ai-context: 无标题屏(旧数据无 bbox 降级)用"画面 N"占位——不丢内容。
33+
pub fn render_screen_points(screens: &[SessionScreen]) -> Vec<String> {
34+
let mut lines = Vec::new();
35+
for (i, s) in screens.iter().enumerate() {
36+
let range = format!(
37+
"[{}–{}]",
38+
crate::concat::format_timestamp(s.first_seen_ms),
39+
crate::concat::format_timestamp(s.last_seen_ms)
40+
);
41+
let title = s.title.clone().unwrap_or_else(|| format!("画面 {}", i + 1));
42+
lines.push(format!("- **{} {}**", range, title));
43+
for b in &s.body {
44+
lines.push(format!(" - {}", b));
45+
}
46+
if !s.labels.is_empty() {
47+
lines.push(format!(" - 标签:{}", s.labels.join(" · ")));
48+
}
49+
if let Some(rel) = &s.image_ref {
50+
// 修复(2026-08-21 审查):缺闭合 `)`——前端渲染正则
51+
// `^\s*-\s*!\[([^\]]*)\]\(([^)]*)\)$` 要求 `)$` 结尾,缺括号时
52+
// 配图行永远匹配不上 → 渲染为纯文本而非图片(丢图真因之一)
53+
lines.push(format!(" - ![画面 {}](session-images/{}/{})", i + 1, s.session_id, rel));
54+
}
55+
}
56+
lines
57+
}
58+
59+
/// 会话异常警示行(REQ-170,纯函数):status != finished → 追加警示。
60+
///
61+
/// @ai-context: 会话31 实证:status=failed 但内容完整(停止链路异常翻案)——
62+
/// 照常转笔记但诚实标注"内容可能不完整",用户自行判断;
63+
/// 警示行是普通 Markdown 引用行(用户可手动删除)。
64+
/// @ai-context: 预览/落库/AI 复核三出口共用(REQ-081 单一管线)——命令层在
65+
/// refresh_screen_points 前调用,markdown 重建口径一致。
66+
pub(crate) fn apply_session_warning(result: &mut NoteFilterResult, status: &str) {
67+
if status == crate::db_sessions::SESSION_STATUS_FINISHED {
68+
result.warning = None;
69+
} else {
70+
result.warning = Some(format!("> ⚠️ 会话异常({}),内容可能不完整", status));
71+
}
72+
}
73+
74+
/// 刷新画面要点数据(纯函数):ocr_screens 重新渲染 + 重建 markdown。
75+
///
76+
/// @ai-context: 命令层 attach_images 填充 image_ref 后调用——原料视图
77+
/// 屏卡配图随 image_ref 出现/消失;markdown 重建不含画面要点
78+
/// (v0.11.5 移出笔记,配图行仅存于 AI 精修 image 块);
79+
/// 净化配置随 result 透传(段落阈值/锚点与预览口径一致)。
80+
pub fn refresh_screen_points(result: &mut NoteFilterResult) {
81+
result.ocr_points = render_screen_points(&result.ocr_screens);
82+
// v0.12.0(ADR-021):按正文源分派重建——OcrDirect 走 OCR 正文重建
83+
// (kept 为空,走 rebuild_markdown 会把 OCR 正文覆盖成标题仅)
84+
result.markdown = match result.body_source {
85+
BodySource::OcrDirect => crate::note_filter_ocr::rebuild_ocr_markdown(
86+
&result.title,
87+
&result.ocr_body,
88+
&result.purify,
89+
result.warning.as_deref(),
90+
),
91+
_ => rebuild_markdown(
92+
&result.title,
93+
&result.kept,
94+
&[],
95+
&result.purify,
96+
result.warning.as_deref(),
97+
),
98+
};
99+
}
100+
101+
/// 组装 Markdown(标题 + 讲述内容;段落切分复用 concat 口径;
102+
/// v0.7.5 REQ-165/170/173:段首 [MM:SS] 时间戳锚点(可回跳原视频位置,可开关)
103+
/// + 段落阈值走净化配置 + 会话异常警示行(None=无警示)。
104+
/// v0.11.5:画面要点段移出笔记(_ocr_points 签名保留兼容调用方,忽略)。
105+
pub(crate) fn rebuild_markdown(
106+
title: &str,
107+
kept: &[SessionSegment],
108+
_ocr_points: &[String],
109+
config: &PurifyConfig,
110+
warning: Option<&str>,
111+
) -> String {
112+
let mut md = assemble_purified_markdown(title, kept, _ocr_points, config);
113+
if let Some(w) = warning {
114+
md = format!("{}\n\n{}", w, md);
115+
}
116+
md
117+
}
118+
119+
/// 净化组装(无警示行版——供 rebuild_markdown 内部与警示拼接)。
120+
fn assemble_purified_markdown(
121+
title: &str,
122+
kept: &[SessionSegment],
123+
_ocr_points: &[String],
124+
config: &PurifyConfig,
125+
) -> String {
126+
let transcript: Vec<TranscriptSegment> = kept
127+
.iter()
128+
.map(|s| TranscriptSegment {
129+
start_ms: s.start_ms,
130+
end_ms: s.end_ms,
131+
text: s.text.clone(),
132+
word_timestamps: None,
133+
// 段落切分不消费置信度/音量(None 占位)
134+
confidence: None,
135+
volume: None,
136+
})
137+
.collect();
138+
let paragraphs = crate::concat::split_transcript_paragraphs_with(
139+
&transcript,
140+
config.paragraph_max_chars,
141+
config.paragraph_max_span_ms,
142+
);
143+
let anchored: Vec<String> = paragraphs
144+
.into_iter()
145+
.map(|(start_ms, text)| {
146+
if config.anchor_timestamps {
147+
format!("{} {}", crate::concat::format_timestamp(start_ms), text)
148+
} else {
149+
text
150+
}
151+
})
152+
.collect();
153+
crate::concat::assemble_markdown(title, &anchored, &[])
154+
}

‎scripts/line-limits.mjs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export const SOFT_LIMIT = 300;
3030
* 每完成一个拆分(0-C2 / 0-C3),就从这里删掉对应一行。
3131
*/
3232
export const FROZEN_OVER_LIMIT = [
33-
'app/src-tauri/src/commands_ai_refine.rs',
3433
];
3534

3635
const toPosix = (p) => p.split(sep).join('/');

0 commit comments

Comments
 (0)