Skip to content

Commit 14d4843

Browse files
committed
feat(notes): v0.7.5 会话转笔记净化优化(REQ-162~173)
- REQ-162:verbal_normalize(保守档)+symbol_normalize 接入 filter_note 段循环—— 口语净化进笔记管线(原料层不动,可逆);净化统计落 FilterStats - REQ-163:口头禅短段规则级删除(≤8 字全口头禅词,免 AI)——段 1018「对不对?」 类不再进笔记;'对'单字回应语义不误杀(碎片规则既有口径,测试注释说明) - REQ-164:stutter_fold.rs 结巴折叠(≥3 连同字→1,白名单保护慢慢/常常) +术语替换(项目班→项目班子/可行行研究→可行性研究,后随守卫防二次误伤); 顺序契约:折叠先于书面化(防 verbal compress 把 3 连压成 2 连导致折叠失效) - REQ-165:段落 [MM:SS] 时间戳锚点(段首 start_ms,可回跳原视频;可开关) - REQ-166:ui_junk VideoPageUi 类别(简介/评论/点赞/收藏/投币/观看/标签/展开/ 二维码/推荐…)+数字量词短串(1.3万/48/451781112;3.14/2024年不误杀)+QR/群号 (qh202522);screens 帧级共现判定(同帧 ≥3 视频页信号→丢弃标签形非讲述短块 ——清晖加油站/若凡娃娃 类作者名,讲述共现保护识别重大风险 类真标签) - REQ-167:单字符碎片丢弃(非表格/公式/代码上下文)+MIN_BLOCK_SCORE 0.5→0.7 - REQ-168:ocr_correction.rs 种子纠错+转写共现校验(項灣→项目;无共现不猜) - REQ-169:零跨度屏并入相邻屏(截断子集包含率≥0.5+180s 时间窗);重复图只留 首个屏引用(569515.webp 三屏重复→一屏) - REQ-170:失败/异常会话转笔记警示行(> ⚠️ 会话异常(status),内容可能不完整; 预览/落库/AI 复核三出口同口径) - REQ-171:notes 加 rule_version/purify_stats 列(ensure_column 幂等迁移; 旧笔记 NULL 诚实降级);purify_stats 与预览统计同源 - REQ-172:黄金语料回归集 note_filter_golden_tests.rs(会话31 口头禅/结巴/视频页 垃圾/单字符/纠错/锚点/警示 + 会话29 直播 UI 回归) - REQ-173:purify_config.rs 阈值配置化(120字/60s/0.5→0.7/0.6 集中 + JSON 校准) - 结构工程:note_filter_ai.rs 拆分(line-limit 登记计划)+lib.rs setup 拆至 app_setup.rs(>600 硬拆);PurifyEnv 依赖注入聚合(clippy too_many_arguments) - 前端:NotePreviewView 口头禅/净化统计卡+警示行渲染;types.ts 新字段 - 验证:1103 测试全绿(+43);clippy 新代码零警告;前端构建通过
1 parent 37ce5c1 commit 14d4843

33 files changed

Lines changed: 2845 additions & 671 deletions

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

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
//! Tauri setup 装配(line-limit-exemptions.md 登记计划落地:lib.rs >600 行硬拆)。
2+
//!
3+
//! @ai-context: 本模块承载 setup 的 AppState 初始化(数据目录/DB/引擎池/可校准
4+
//! 配置/内存态存储)与装配期辅助函数(模型路径构造/捆绑同步/ORT
5+
//! 运行时目录注入)——lib.rs 只保留声明与 command 注册,职责单一。
6+
//! @ai-context: 无业务逻辑(业务自底向上分布:types → concat/db → asr/ocr →
7+
//! engine → commands);全部装配失败均显式返回错误串,不静默。
8+
9+
use std::path::Path;
10+
11+
use tauri::Manager;
12+
13+
use crate::asr::AsrModels;
14+
use crate::commands::AppState;
15+
use crate::db::Db;
16+
use crate::device_config::{decide, OcrDeviceConfig, OcrDeviceStatus};
17+
use crate::engine::EnginePool;
18+
#[cfg(target_os = "windows")]
19+
use crate::live_session::LiveSessionManager;
20+
use crate::model_downloader::ModelDownloader;
21+
use crate::ocr::{OcrModels, OcrParams};
22+
use crate::streaming_asr::StreamingAsrModels;
23+
24+
/// 装配 AppState(setup 调用):数据目录/DB/引擎池/可校准配置/内存态存储。
25+
///
26+
/// @ai-context: 顺序敏感:① ORT 运行时目录注入(引擎线程加载 DLL 前)→
27+
/// ② 数据/模型目录创建 → ③ 捆绑模型同步 → ④ DB 打开 + 崩溃恢复
28+
/// → ⑤ OCR 设备决策(DXGI 探测/折叠模式)→ ⑥ 引擎池(后台加载)
29+
/// → ⑦ 可校准配置与内存态存储 → ⑧ app.manage 注入。
30+
pub fn setup_app_state(app: &mut tauri::App) -> Result<(), String> {
31+
// ADR-009:引擎线程加载 ORT 之前注入运行时目录(CUDA 多 DLL 依赖)
32+
ensure_ort_runtime_search_dir();
33+
let data_dir = app
34+
.path()
35+
.app_data_dir()
36+
.map_err(|e| format!("无法获取应用数据目录: {}", e))?;
37+
std::fs::create_dir_all(&data_dir).map_err(|e| format!("创建数据目录失败: {}", e))?;
38+
39+
// 模型根目录(ASR 模型由下载/捆绑放入;OCR 模型经 ModelScope 自动缓存)
40+
let model_dir = data_dir.join("models");
41+
std::fs::create_dir_all(&model_dir).map_err(|e| format!("创建模型目录失败: {}", e))?;
42+
43+
// 同步随安装包捆绑的模型(方式①;开发环境缺 resources 时自动跳过)
44+
sync_bundled_models(app.handle(), &model_dir);
45+
46+
// SQLite 数据库(本地优先,数据不出本机)
47+
let db_path = data_dir.join("entropy.db");
48+
let db = Db::open(&db_path.to_string_lossy()).map_err(|e| format!("初始化数据库失败: {}", e))?;
49+
50+
// 崩溃恢复:上次异常退出残留的 recording 会话标记为 failed(ADR-004)
51+
// 日志可观测:用户反馈会话"异常中断"时据此判断是否来自重启打断
52+
let interrupted = db.mark_interrupted_sessions().map_err(|e| e.to_string())?;
53+
if interrupted > 0 {
54+
eprintln!("[Db] 启动恢复:{} 个中断会话标记为 failed", interrupted);
55+
}
56+
57+
// 常驻引擎池(后台线程加载模型,不阻塞启动)
58+
// ADR-009(v0.4.0 M1):OCR 推理后端启动期决策——
59+
// 三层检测:DXGI 硬件门槛(select_best)→ decide 折叠模式/校准 → ORT 原生回退(引擎内)
60+
let ocr_device_config_path = data_dir.join("ocr_device.json");
61+
// TD-044 同批修复:配置内存态单点(命令层锁内 read-modify-write,防 TOCTOU)
62+
let ocr_device_config = std::sync::Arc::new(std::sync::Mutex::new(OcrDeviceConfig::load(
63+
&ocr_device_config_path,
64+
)));
65+
#[cfg(target_os = "windows")]
66+
let adapters = crate::device_probe::probe_adapters();
67+
#[cfg(not(target_os = "windows"))]
68+
let adapters = Vec::new();
69+
let best_device = crate::device_probe::select_best(&adapters);
70+
let nvidia_count = crate::device_probe::nvidia_candidate_count(&adapters);
71+
let ocr_cfg_snapshot = ocr_device_config
72+
.lock()
73+
.map(|c| c.clone())
74+
.unwrap_or_default();
75+
let ocr_backend = decide(ocr_cfg_snapshot.mode, best_device, nvidia_count, ocr_cfg_snapshot.bench);
76+
eprintln!(
77+
"[Engine] OCR 设备决策: 模式 {:?} → 后端 {:?}{}",
78+
ocr_cfg_snapshot.mode,
79+
ocr_backend,
80+
best_device
81+
.map(|id| format!("(候选设备 index={},NVIDIA 候选数 {})", id, nvidia_count))
82+
.unwrap_or_else(|| format!("(无候选,NVIDIA 候选数 {})", nvidia_count))
83+
);
84+
let ocr_device_status =
85+
std::sync::Arc::new(std::sync::Mutex::new(OcrDeviceStatus::new(
86+
ocr_cfg_snapshot.mode,
87+
ocr_backend,
88+
ocr_cfg_snapshot.bench,
89+
)));
90+
let ocr_models = ocr_models();
91+
let ocr_params = OcrParams::default();
92+
// M5/REQ-040:共享词表(热词注入 ASR 流、替换词纠错 OCR;JSON 持久化)
93+
let vocab_path = data_dir.join("vocab.json");
94+
let vocab =
95+
std::sync::Arc::new(std::sync::Mutex::new(crate::vocab::VocabStore::load(&vocab_path)));
96+
// v0.5.0 M1(REQ-043):档案记忆偏好(混合检测的用户确认记忆;JSON 持久化)
97+
let profile_memory_path = data_dir.join("video_profile_memory.json");
98+
let profile_memory = std::sync::Arc::new(std::sync::Mutex::new(
99+
crate::video_profile::ProfileMemory::load(&profile_memory_path),
100+
));
101+
// v0.5.0 M8(REQ-055):补缝式 AI 护栏骨架(配额/缓存/审计;云端 V1.0 生效)
102+
let ai_guardrails =
103+
std::sync::Arc::new(std::sync::Mutex::new(crate::ai_guardrails::AiGuardrails::default()));
104+
// v0.6.0 M1(REQ-083/060):可校准配置——ui_junk.json 黑名单与
105+
// symbol_map.json 映射表(缺失走内置默认;损坏回退默认,不阻断启动)
106+
let ui_junk = crate::ui_junk::UiJunkList::load(&data_dir.join("ui_junk.json"));
107+
let symbol_normalize =
108+
crate::symbol_normalize::SymbolNormalizeConfig::load(&data_dir.join("symbol_map.json"));
109+
// v0.7.5(REQ-173/168):净化阈值与 OCR 纠错表可校准配置——
110+
// purify_config.json / ocr_correction.json(同 ui_junk 先例:
111+
// 缺失走内置默认;损坏回退默认,不阻断启动)
112+
let purify = crate::purify_config::PurifyConfig::load(&data_dir.join("purify_config.json"));
113+
let ocr_corrections =
114+
crate::ocr_correction::OcrCorrectionTable::load(&data_dir.join("ocr_correction.json"));
115+
// v0.5.0 模型版(REQ-047/049/050):结构模型下载器(按需下载,独立状态机)
116+
let structure_downloader = crate::structure_models::StructureModelDownloader::new();
117+
// v0.5.0 模型版(REQ-050):结构档位配置(公式档位持久化,审查 H3 修复)
118+
let structure_tier_path = data_dir.join("structure_tier.json");
119+
// 结构模型装配目录(models/structure;下载器/引擎共用)
120+
let _ = std::fs::create_dir_all(model_dir.join("structure"));
121+
let engines = EnginePool::start(
122+
asr_models(&model_dir),
123+
ocr_models.clone(),
124+
ocr_params.clone(),
125+
ocr_backend,
126+
ocr_device_status,
127+
Some(vocab.clone()),
128+
)
129+
.map_err(|e| format!("启动引擎池失败: {}", e))?;
130+
131+
let streaming_models = streaming_asr_models(&model_dir);
132+
// v0.7.0 M1(REQ-104/132):剪贴板信号内存态存储 + 监听句柄占位
133+
// (start_live_session 成功后启动监听线程,stop 时置位停止)
134+
let clipboard = std::sync::Arc::new(crate::clipboard_signal::ClipboardSignalStore::new());
135+
let clipboard_monitor = std::sync::Arc::new(std::sync::Mutex::new(
136+
None::<crate::clipboard_signal::ClipboardMonitorHandle>,
137+
));
138+
// v0.7.0 M2(REQ-115):VAD 阈值共享槽(会话线程发布、诊断面板读取)
139+
let vad_slot =
140+
std::sync::Arc::new(crate::vad_threshold_slot::VadThresholdSlot::default());
141+
app.manage(AppState {
142+
db,
143+
engines,
144+
streaming_models,
145+
#[cfg(target_os = "windows")]
146+
live_session: LiveSessionManager::new(),
147+
model_downloader: ModelDownloader::new(),
148+
app: app.handle().clone(),
149+
ocr_device_config_path,
150+
ocr_device_config,
151+
ocr_models,
152+
ocr_params,
153+
vocab,
154+
vocab_path,
155+
model_dir,
156+
profile_memory_path,
157+
profile_memory,
158+
data_dir,
159+
ai_guardrails,
160+
structure_downloader,
161+
structure_tier_path,
162+
// v0.6.0 M1:可校准配置(UI 垃圾黑名单 / 口语符号映射表)
163+
ui_junk,
164+
symbol_normalize,
165+
// v0.7.5:净化阈值配置 / OCR 错字纠错表(purify_config.json /
166+
// ocr_correction.json 可校准)
167+
purify,
168+
ocr_corrections,
169+
// v0.7.0 M1(REQ-104/132):剪贴板信号(内存态存储 + 监听句柄占位;
170+
// start_live_session 启动监听 / stop_live_session 置位停止)
171+
clipboard,
172+
clipboard_monitor,
173+
// v0.7.0 M2(REQ-115):VAD 阈值共享槽
174+
vad_slot,
175+
});
176+
Ok(())
177+
}
178+
179+
/// 由模型根目录构造 ASR 模型路径(约定布局,禁止硬编码绝对路径)。
180+
///
181+
/// @ai-context: 正式版将随安装包捆绑模型(bundle.resources)并在首启复制到该目录,
182+
/// 路径约定保持不变即可无缝切换。
183+
fn asr_models(model_dir: &Path) -> AsrModels {
184+
AsrModels {
185+
model: model_dir.join("asr/sensevoice/model.int8.onnx").to_string_lossy().into_owned(),
186+
tokens: model_dir.join("asr/sensevoice/tokens.txt").to_string_lossy().into_owned(),
187+
}
188+
}
189+
190+
/// 构造流式 ASR 模型路径(ADR-003:models/streaming-zipformer/ 四件套)。
191+
///
192+
/// @ai-context: 2026-08 升级为 2025-06-30 新版中文 zipformer(fp16)——替代 2023-02-20
193+
/// 旧双语包(性能与准确性显著提升,用户决策);文件名与官方仓库
194+
/// csukuangfj/sherpa-onnx-streaming-zipformer-zh-fp16-2025-06-30 一致。
195+
fn streaming_asr_models(model_dir: &Path) -> StreamingAsrModels {
196+
let dir = model_dir.join("streaming-zipformer");
197+
StreamingAsrModels {
198+
encoder: dir.join("encoder.fp16.onnx").to_string_lossy().into_owned(),
199+
decoder: dir.join("decoder.fp16.onnx").to_string_lossy().into_owned(),
200+
joiner: dir.join("joiner.fp16.onnx").to_string_lossy().into_owned(),
201+
tokens: dir.join("tokens.txt").to_string_lossy().into_owned(),
202+
}
203+
}
204+
205+
/// 构造 OCR 模型标识(oar-ocr 注册名,首次使用从 ModelScope 国内源自动下载缓存)。
206+
fn ocr_models() -> OcrModels {
207+
OcrModels {
208+
det: "pp-ocrv6_tiny_det.onnx".to_string(),
209+
rec: "pp-ocrv6_tiny_rec.onnx".to_string(),
210+
dict: "ppocrv6_tiny_dict.txt".to_string(),
211+
}
212+
}
213+
214+
/// 同步随安装包捆绑的模型到数据目录(模型集成方式①)。
215+
///
216+
/// @ai-context: bundle.resources 把 models/ 打进安装包,首启解压到数据目录供引擎加载;
217+
/// 开发环境无 resources 目录或 models 为空时自动跳过(走按需下载路径)。
218+
/// 已存在的目标文件不覆盖(避免回退用户已更新的新版模型)。
219+
fn sync_bundled_models(app: &tauri::AppHandle, model_dir: &Path) {
220+
let Ok(resource_dir) = app.path().resource_dir() else { return };
221+
let bundled_root = resource_dir.join("models");
222+
if !bundled_root.is_dir() {
223+
return;
224+
}
225+
if let Err(e) = copy_dir_skip_existing(&bundled_root, model_dir) {
226+
eprintln!("同步捆绑模型失败: {}", e);
227+
}
228+
}
229+
230+
/// 递归复制目录,跳过已存在的文件。
231+
fn copy_dir_skip_existing(src: &Path, dst: &Path) -> std::io::Result<()> {
232+
std::fs::create_dir_all(dst)?;
233+
for entry in std::fs::read_dir(src)? {
234+
let entry = entry?;
235+
let from = entry.path();
236+
let to = dst.join(entry.file_name());
237+
if from.is_dir() {
238+
copy_dir_skip_existing(&from, &to)?;
239+
} else if !to.exists() {
240+
std::fs::copy(&from, &to)?;
241+
}
242+
}
243+
Ok(())
244+
}
245+
246+
/// 把 ORT 运行时目录加入 DLL 搜索路径(ADR-009:CUDA 运行时含 cudart/cublas/cudnn
247+
/// 等数十个 DLL,不复制进 target;AddDllDirectory 让 onnxruntime.dll 按需找到它们)。
248+
///
249+
/// @ai-context: 必须在引擎线程首次加载 onnxruntime.dll 之前调用(setup 最早期);
250+
/// 失败仅告警——CPU-only 运行时无需额外 DLL,不阻断启动。
251+
/// @ai-context: SetDefaultDllDirectories(DEFAULT_DIRS) 是 AddDllDirectory 生效的前提;
252+
/// 搜索序 = 应用目录(build.rs 已复制 onnxruntime.dll,压过 system32 旧版)
253+
/// + 系统目录 + 本目录。
254+
fn ensure_ort_runtime_search_dir() {
255+
let Ok(lib_dir) = std::env::var("ORT_LIB_LOCATION") else { return };
256+
let path = Path::new(&lib_dir);
257+
if !path.join("onnxruntime.dll").exists() {
258+
return;
259+
}
260+
unsafe {
261+
// 注意:本 crate 存在本地 `mod windows`(窗口枚举模块),
262+
// 外部 windows crate 必须以 ::windows:: 全路径引用(遮蔽问题)
263+
use ::windows::Win32::System::LibraryLoader::{
264+
AddDllDirectory, SetDefaultDllDirectories, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS,
265+
};
266+
let _ = SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
267+
let wide = ::windows::core::HSTRING::from(path.to_string_lossy().as_ref());
268+
// AddDllDirectory 返回句柄(null=失败,windows 0.61 无 Result 包装)
269+
if AddDllDirectory(::windows::core::PCWSTR(wide.as_ptr())).is_null() {
270+
eprintln!(
271+
"[Setup] AddDllDirectory 失败: {}(CUDA 运行时 DLL 可能无法加载)",
272+
path.display()
273+
);
274+
}
275+
}
276+
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ pub struct AppState {
9999
/// v0.6.0 M1(REQ-060):口语符号映射表(内置默认 + symbol_map.json 校准合并;
100100
/// 产物层书面化管线消费)
101101
pub symbol_normalize: crate::symbol_normalize::SymbolNormalizeConfig,
102+
/// v0.7.5(REQ-173):笔记净化阈值配置(内置默认 + purify_config.json 校准合并;
103+
/// 120字/60s/0.5/0.6 等集中常量,现场调参无需改码)
104+
pub purify: crate::purify_config::PurifyConfig,
105+
/// v0.7.5(REQ-168):OCR 错字纠错表(内置种子 + ocr_correction.json 校准合并;
106+
/// 画面词与讲述词互证,无映射不猜)
107+
pub ocr_corrections: crate::ocr_correction::OcrCorrectionTable,
102108
/// v0.7.0 M1(REQ-104/132):剪贴板信号存储(内存态;课中复制=高置信信号,
103109
/// 只存前 30 字符预览——隐私红线:原始剪贴板内容不持久化)
104110
pub clipboard: std::sync::Arc<crate::clipboard_signal::ClipboardSignalStore>,
@@ -170,6 +176,8 @@ pub async fn save_draft_as_note(state: State<'_, AppState>, draft: NoteDraft) ->
170176
content: truncate_chars(draft.markdown, CONTENT_MAX_CHARS),
171177
source: "classroom".to_string(),
172178
session_id: None,
179+
rule_version: None,
180+
purify_stats: None,
173181
};
174182
state.db.create_note(&new).map_err(|e| e.to_string())
175183
}
@@ -235,6 +243,8 @@ pub async fn process_to_note(
235243
content: draft.markdown.clone(),
236244
source: "classroom".to_string(),
237245
session_id: None,
246+
rule_version: None,
247+
purify_stats: None,
238248
})
239249
.map_err(|e| e.to_string())
240250
})
@@ -251,6 +261,9 @@ pub async fn create_note(state: State<'_, AppState>, new: NewNote) -> Result<Not
251261
source: if new.source == "classroom" { "classroom" } else { "manual" }.to_string(),
252262
// 手动新建笔记无来源会话(session_id 仅由会话→笔记链路写入)
253263
session_id: None,
264+
// 手动笔记无净化规则版本/统计(None 诚实降级——REQ-171 口径)
265+
rule_version: None,
266+
purify_stats: None,
254267
};
255268
state.db.create_note(&new).map_err(|e| e.to_string())
256269
}

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ pub async fn review_text_filter(
193193
// v0.7.3 审查修复:AI 复核预览与 preview_session_note/session_to_note 同口径——
194194
// 画面要点屏 attach 归档图 + 重建配图行(否则复核后预览配图消失,双出口不一致)
195195
let data_dir = state.data_dir.clone();
196+
// v0.7.5:净化配置/符号映射/纠错表与预览/落库同源(REQ-173 阈值口径一致)
197+
let purify_env = crate::note_filter::PurifyEnv {
198+
config: state.purify.clone(),
199+
symbol: state.symbol_normalize.clone(),
200+
corrections: state.ocr_corrections.clone(),
201+
};
196202
let mock = std::env::var("AI_TEXT_FILTER_MOCK").map(|v| v == "1").unwrap_or(false);
197203
tauri::async_runtime::spawn_blocking(move || {
198204
let session = db
@@ -206,12 +212,21 @@ pub async fn review_text_filter(
206212
}
207213
let segments = db.list_segments(session_id).map_err(|e| e.to_string())?;
208214
let ocr_blocks = db.list_ocr_blocks(session_id).map_err(|e| e.to_string())?;
209-
// ① 纯规则过滤(与 preview_session_note 同一管线)
210-
let mut result = filter_note(&session.title, &segments, &ocr_blocks, &ui_junk);
215+
// ① 纯规则过滤(与 preview_session_note 同一管线——v0.7.5 起净化配置/
216+
// 符号映射同源,阈值/锚点口径一致)
217+
let mut result = filter_note(
218+
&session.title,
219+
&segments,
220+
&ocr_blocks,
221+
&ui_junk,
222+
&purify_env,
223+
);
211224
// ①b v0.7.3 审查修复:画面要点配图(与 preview_session_note 同口径——
212225
// 归档图匹配 + 重建含图段落;目录缺失/无图 → 纯文本降级)
213226
let images_dir = data_dir.join("session-images").join(session_id.to_string());
214227
crate::screens::attach_images(&mut result.ocr_screens, &images_dir);
228+
// ①c v0.7.5(REQ-170):异常会话警示行(与预览/落库同口径)
229+
crate::note_filter::apply_session_warning(&mut result, &session.status);
215230
crate::note_filter::refresh_screen_points(&mut result);
216231
// ② 边界候选(规则层判不了的段)
217232
let boundary = boundary_candidates(&result.kept);

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ pub async fn artifact_to_note(
9696
source: "classroom".to_string(),
9797
// v0.7.1:产物→笔记同样建立会话关联(列表 has_note 标记口径统一)
9898
session_id: Some(session_id),
99+
// v0.7.5(REQ-171):产物→笔记非净化管线产物——无规则版本(None 诚实)
100+
rule_version: None,
101+
purify_stats: None,
99102
})
100103
.map_err(|e| e.to_string())
101104
}

0 commit comments

Comments
 (0)