# 涉密文件自检工具 实施计划 > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 在 Windows 上做一个单文件 GUI 程序,对磁盘中文档(doc/docx/pdf)按可配置数量随机抽检,借助本地 Umi-OCR HTTP 服务识别是否含有"机密"等敏感关键词,并产出截图+结论报告。 **Architecture:** - `eframe + egui` Material 风格单窗口 GUI(暗/亮/跟随系统三主题),左侧设置、右侧主面板、底部状态栏;按钮 "开始检测" 启动任务。 - 抽检分三层: - `docx`:`docx-rs` 解析段落 → 关键词匹配 - `pdf`:调用 Windows 默认 PDF 关联程序 → 等待 → 模拟 `Win+Shift+S` 触发系统截图 → 用户在屏幕上手动框选 → 程序轮询剪贴板读取位图 → 调 Umi-OCR HTTP → 关键词匹配 - `doc`:调用 `/doclite.exe` 打开 → 同上截图链路 - OCR 客户端:`reqwest` 把 base64 图像 POST 到 `http://127.0.0.1:1224/api/ocr`,解析返回 `data[].text` 与 `box`。 - 默认全盘扫描;UI 中加"白名单目录"列表(不扫的目录);启动时单次申请 UAC,整会话复用。 - 配置 `serde + toml` 持久化到 `%APPDATA%\secret-file-selfcheck\config.toml`。 - 资源(中文字体、图标)通过 `include_bytes!` 嵌入二进制;`doclite.exe` / `Umi-OCR.exe` 由程序在 exe 同目录自动发现并按需启动。 - `#![windows_subsystem = "windows"]` 隐藏控制台,单 exe 交付。 **Tech Stack:** - 语言/工具链:Rust `stable-x86_64-pc-windows-gnu`(MSYS2 + MinGW) - GUI:`eframe = "0.27"`、`egui = "0.27"`、`egui_extras`,手写 Material token - 文本:`docx-rs`、`encoding_rs`、`chardetng` - OCR 客户端:`reqwest`(阻塞 + JSON)→ Umi-OCR - 截屏链路:`windows` crate(`keybd_event` 触发 `Win+Shift+S`、`OpenClipboard` / `GetClipboardData` 读位图) - 进程/窗口:`sysinfo`、`windows` crate(`CreateProcessW`、`EnumWindows`、`WM_CLOSE`、`taskkill`) - 异步:`tokio`(轻量使用) - 权限:`windows` crate(`ShellExecuteExW` + `runas` 提升 UAC) - 配置/日志:`serde`、`toml`、`tracing` + `tracing-appender` - 打包:`embed-resource` 嵌入图标 + `#![windows_subsystem = "windows"]` 隐藏控制台 - 构建:`cargo build --release --target x86_64-pc-windows-gnu` > 用户规则 2 规定 Python 才用 loguru;本项目是 Rust,统一使用 `tracing`。 --- ## 一、需求与决策记录 ### 1.1 用户已确认的设计决策 | # | 项 | 决策 | |---|----|------| | 1 | 扫描范围 | **默认全盘扫描**;UI 维护"白名单目录"列表(不扫的目录) | | 2 | 排除目录语义 | "白名单" = 不扫描的目录;模板按钮:系统目录、Program Files、回收站、临时目录 | | 3 | 文件大小 | **不设上限**(可加最小 KB 过滤以跳过空文件) | | 4 | .doc 打开 | **`/doclite.exe`** 自动探测,配置中可改 | | 5 | .pdf 打开 | Windows 默认 PDF 关联程序(SumatraPDF / Edge / Adobe),可设置覆盖 | | 6 | 启动后等待 | 设置项"截图前等待时间"毫秒(默认 1500ms) | | 7 | 截图方式 | **模拟 `Win+Shift+S`** 触发系统截图工具;UI 提示用户在屏幕上手动框选;轮询剪贴板读位图 | | 8 | OCR | **Umi-OCR HTTP**(`http://127.0.0.1:1224/api/ocr`),`Umi-OCR.exe` 放 exe 同目录自动启动 | | 9 | 管理员权限 | **启动时单次申请 UAC**(未以管理员运行时 `ShellExecuteExW` + `runas` 重启一次) | | 10 | 报告 | **本地留存**(HTML/JSON/PNG 组图) | | 11 | 关键词来源 | **不从网络拉取** | | 12 | UI 风格 | **Material**(圆角、阴影、规范化间距、Material 配色) | | 13 | 触发方式 | **"开始检测"按钮**,无全局热键 | | 14 | 关键词网络拉取 | 不做 | ### 1.2 进入设置页的可配置项 | # | 项 | 默认 | |---|----|------| | 15 | 关键词列表 / 导入导出 / 大小写 / 正则 / 整词 / 按文件类型覆盖 | 默认"机密/秘密/绝密/内部/Confidential/Secret" | | 16 | 抽检数量 | 10 | | 17 | 抽样策略(随机 / 分层 / 类型配额) | 完全随机 | | 18 | 并发度 | 1(人工参与截图,串行更稳) | | 19 | 单文件超时 | 120s | | 20 | 报告格式(HTML/JSON/PNG 组图) | HTML + JSON | | 21 | 首次启动向导 | 引导 Umi-OCR/doclite 路径、关键词、等待时间 | | 22 | 中文字体(嵌入或自选) | 嵌入 SourceHanSansSC | | 23 | 主题(Light / Dark / Follow) | Follow | | 24 | 国际化(中/英) | 中文 | | 25 | 日志(级别、保留天数、UI 实时) | Info,14 天 | | 26 | 静默/托盘/开机自启 | 默认关闭 | | 27 | 历史去重(同文件 N 天不重复) | 7 | --- ## 二、目录结构 ``` secret-file-selfcheck/ ├── Cargo.toml ├── build.rs # embed-resource / winres ├── app.ico # 图标 ├── app.rc # 资源脚本 ├── README.md # 部署说明:与 doclite.exe / Umi-OCR.exe 同目录 ├── assets/ │ ├── fonts/SourceHanSansSC-Regular.otf │ └── templates/report.html ├── src/ │ ├── main.rs # eframe 入口 + UAC 提升 + 启动 Umi-OCR │ ├── app.rs # 顶层 App struct + 页面路由 │ ├── privilege.rs # 单次 UAC 提升(ShellExecuteExW runas) │ ├── config/ │ │ ├── mod.rs │ │ ├── model.rs # AppConfig 强类型 │ │ └── persist.rs # 读写 %APPDATA%/.../config.toml │ ├── ui/ │ │ ├── settings.rs # 设置页(多分组) │ │ ├── home.rs # 主面板(开始/进度/日志/截图引导) │ │ ├── report.rs # 报告查看 │ │ ├── material.rs # Material 风格 token & 主题 │ │ └── widgets.rs # 字体加载、托盘 │ ├── scan/ │ │ ├── walker.rs # 全盘遍历 + 白名单(不扫的目录) │ │ ├── sampler.rs # 抽检算法 │ │ └── filter.rs # 扩展名/隐藏/最小大小过滤 │ ├── inspect/ │ │ ├── mod.rs # Inspector trait + Finding │ │ ├── docx_inspector.rs # 文本解析(docx-rs) │ │ ├── pdf_inspector.rs # 外部查看器 + 截图 + OCR │ │ ├── doc_inspector.rs # doclite.exe + 截图 + OCR │ │ ├── external.rs # 启进程 / 关闭窗口 / 强杀 │ │ ├── screenshot.rs # 触发 Win+Shift+S + 读剪贴板 │ │ └── umi_ocr.rs # Umi-OCR HTTP 客户端 │ ├── matcher/ │ │ ├── keywords.rs # 关键词匹配(正则、大小写、整词、按文件类型) │ │ └── hash.rs # 文件指纹 + 去重 │ ├── report/ │ │ ├── model.rs # Report / Finding 结构 │ │ ├── html.rs # 渲染 HTML │ │ └── png.rs # 合并截图 │ └── utils/ │ ├── paths.rs # APPDATA / exe 同目录 / 临时目录 │ └── logger.rs # tracing 初始化 └── tests/ ├── matcher_test.rs ├── sampler_test.rs └── fixtures/ ├── sample.docx └── sample.pdf ``` --- ## 三、关键 Cargo.toml 节选 ```toml [package] name = "secret-file-selfcheck" version = "0.1.0" edition = "2021" [dependencies] eframe = { version = "0.27", default-features = false, features = ["default_fonts", "glow"] } egui = "0.27" egui_extras = { version = "0.27", features = ["all_loaders"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "fs"] } serde = { version = "1", features = ["derive"] } serde_json = "1" toml = "0.8" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-appender = "0.2" docx-rs = "0.4" encoding_rs = "0.8" chardetng = "0.1" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } base64 = "0.22" sysinfo = "0.31" windows = { version = "0.58", features = [ "Win32_UI_WindowsAndMessaging", "Win32_System_Threading", "Win32_Graphics_Gdi", "Win32_System_Console", "Win32_Foundation", ] } sys-locale = "0.3" dirs = "5" walkdir = "2" regex = "1" aho-corasick = "1" sha2 = "0.10" anyhow = "1" thiserror = "1" chrono = { version = "0.4", features = ["serde"] } once_cell = "1" log = "0.4" url = "2" image = { version = "0.25", default-features = false, features = ["png"] } [build-dependencies] embed-resource = "2" [profile.release] opt-level = 3 lto = "thin" codegen-units = 1 strip = true panic = "abort" ``` --- ## 四、设置页设计(6 个分组,60+ 控件) 设置页用 `egui::ScrollArea` + `CollapsingHeader` 分组,所有变更即时写入 `config.toml`。 ### A. 常规 - 启动时自动检测(开关) - 启动时最小化到托盘(开关) - 开机自启(开关 + 写入 `HKCU\...\Run`) - 主题:Light / Dark / Follow - 语言:中文 / English - 日志级别:Error / Warn / Info / Debug / Trace - 日志保留天数(数字框) - 日志文件路径(只读 + 打开按钮) - 单实例锁(开关,避免多开) - 检查前清空临时目录(开关) - 检查完自动退出(开关 + 倒计时秒数) ### B. 扫描范围 - 扫描根目录:默认空(=全盘) - **白名单目录**(不扫的目录,多行列表 + 增删 + 拖拽) - 模板按钮:系统目录、Windows、Program Files、回收站、临时目录 - 包含隐藏文件(开关) - 包含系统文件(开关) - 跟随符号链接(开关) - 文件最小大小 KB(数字框,默认 1;0 = 不限) - 最大遍历深度(数字框,0=无限) - 扩展名白名单(多选 + 自定义:默认 doc/docx/pdf) - 修改时间范围(起始/结束日期) - 单次扫描超时(分钟,0=无限) ### C. 抽检 - 抽检数量(数字框) - 抽样策略(单选:完全随机 / 分层 / 类型配额) - 类型配额:DOC / DOCX / PDF 各自数量 - **抽检执行方式:串行**(一个接一个打开,前一份处理完再开下一份,**无并发选项**) - 单文件总超时(秒,默认 120) - 同文件多少天内不重复(数字框,默认 7) - 跳过锁定/无权限文件(开关) - 命中后是否继续后续文件(开关,默认继续;关闭则一旦命中就停) ### D. 查看器与截图 - .doc 查看器路径(默认 `/doclite.exe`,文件选择 + 探测) - .doc 启动参数模板(默认 `{path}` 占位) - .pdf 查看器路径(默认 = Windows 关联程序,文件选择 + "用默认"按钮) - .pdf 启动参数模板(默认 `{path}`) - 截图前等待时间 ms(默认 1500) - 用户截图超时秒(默认 60,超时跳过当前文件) - 截图后是否自动关闭查看器(开关 + 关闭等待 ms + 强杀超时 ms) - Umi-OCR HTTP 地址(默认 `http://127.0.0.1:1224/api/ocr`) - Umi-OCR 启动路径(默认 `/Umi-OCR.exe`) - Umi-OCR 启动后等待秒(默认 3) - Umi-OCR 调用超时秒(默认 30) - OCR 选项语言配置(默认 `models/config_chinese.txt`) - OCR 启用文本方向校正(开关) - OCR 限制边长(960/2880/4320/不压缩) ### E. 关键词 - 全局关键词列表(多行编辑 + 增删 + 一键清空) - 导入 / 导出 txt - 区分大小写(开关) - 使用正则(开关) - 整词匹配(开关) - 按文件类型追加(DOC / DOCX / PDF 各自多行列表) - 误报白名单(列表:指纹 → 关键词,可清除) - 命中最低置信度(数字框 0-100) - 高亮颜色(颜色选择器) ### F. 报告 - 输出目录(路径选择) - 输出格式(多选:HTML / JSON / PNG 组图) - 报告文件名前缀(默认 `selfcheck-{date}`) - 包含截图(开关) - 敏感词高亮(开关) - 截图最大边长 px(数字框,默认 1600) - 历史保留条数(数字框,默认 30) - 检查完自动打开报告(开关) - 同时复制报告到剪贴板摘要(开关) 底部按钮:**恢复默认** / **另存为模板** / **立即保存** / **打开配置文件**。 --- ## 五、工作流程 ``` [启动] ├─ IsUserAnAdmin? 否 → ShellExecuteExW "runas" 重启 → 退出当前进程 ├─ 加载 config.toml(不存在则首次启动向导) ├─ 嵌入字体 set_fonts ├─ 启动 Umi-OCR.exe(同目录 / 配置路径)→ 轮询 HTTP /api/ocr 是否可达 └─ 启动单实例锁 [主面板] 用户点击 "开始检测" │ ▼ [扫描阶段] walker 遍历全盘 → 白名单剔除 → 扩展名/大小/隐藏过滤 → 候选列表 Vec │ ▼ [抽样阶段] sampler 按策略(随机 / 分层 / 类型配额)选 N 个 │ ▼ [抽检阶段] 串行(默认 1 并发;并发时 buffer_unordered) ├─ DOCX:docx-rs 读段落 → keywords 匹配 ├─ DOC: spawn(doclite.exe "{path}") → 等待 wait_ms │ → keybd_event LWIN+LSHIFT+'S' 模拟 Win+Shift+S │ → UI 弹层 "请框选 doclite 窗口区域" + 倒计时 │ → 轮询剪贴板(OpenClipboard/GetClipboardData CF_BITMAP)拿位图 │ → base64 编码 → POST /api/ocr │ → 匹配关键词 → WM_CLOSE 关闭窗口 → 等 close_wait_ms → 强杀 └─ PDF: spawn(pdf_viewer "{path}") → 等待 → 同上截图 → OCR → 匹配 → 关闭 [汇总] Finding 列表 → 写 HTML/JSON/PNG → 通知系统 → 弹窗(命中/未命中) ``` 并发:用 `tokio::sync::watch` 通道发取消信号;进度用 `Arc` 推进;UI 在 `update()` 里非阻塞地拉取状态。 --- ## 六、报告格式 **HTML 报告(默认)**: - 顶部:检查时间、机器名、用户、抽检数量、命中数量、关键词列表 - 表格:文件名、路径、类型、命中关键词、置信度、截图缩略图、状态 - 截图:原图 + 敏感词高亮(红框) - 底部:原始 JSON 数据链接 **JSON 报告**: ```json { "scan_id": "2026-06-08-153012-1234", "machine": "DESKTOP-XXX", "user": "alice", "started_at": "...", "finished_at": "...", "total": 20, "hit": 1, "findings": [ { "path": "...", "type": "doc", "matched": ["机密"], "score": 0.92, "screenshot": "screenshots/001.png", "boxes": [[x1,y1,x2,y2]] } ] } ``` **PNG 组图**:把每张截图拼成 A4 大图,便于一键分享。 --- ## 七、关键技术点 ### 1. MSYS2 + MinGW 编译要点 ```bash pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake rustup target add x86_64-pc-windows-gnu ``` `.cargo/config.toml`: ```toml [target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc" ar = "x86_64-w64-mingw32-ar" ``` ### 2. 隐藏控制台 + UAC 单次提升 - `src/main.rs` 顶部:`#![windows_subsystem = "windows"]` - `privilege.rs`: - `IsUserAnAdmin()` → `false` 时构造 `ShellExecuteExW { lpVerb: "runas", lpFile: current_exe, nShow: SW_SHOWNORMAL }` - 当前进程 `ExitProcess(0)`;新进程以管理员启动 - 第二次进入已经管理员,跳过 - 入口处保证 `IsUserAnAdmin() == true` 后再 `run()` ### 3. 中文字体 - `assets/fonts/SourceHanSansSC-Regular.otf`(~16MB)通过 `include_bytes!` 嵌入 - `eframe::NativeOptions` 启动后 `egui::Context::set_fonts`: ```rust let mut fonts = FontDefinitions::default(); fonts.font_data.insert("cn".into(), FontData::from_static(include_bytes!("../assets/fonts/SourceHanSansSC-Regular.otf"))); fonts.families.get_mut(&FontFamily::Proportional).unwrap().insert(0, "cn".into()); ctx.set_fonts(fonts); ``` ### 4. .doc / .pdf 截图流程(最复杂) 1. `CreateProcessW` 启动 `doclite.exe "{path}"`(或 PDF 关联程序) 2. 轮询 `EnumWindows` + `GetWindowThreadProcessId` 匹配 PID 获取 HWND 3. `SetForegroundWindow` + `ShowWindow(SW_RESTORE)` 强制前台 4. `Sleep(wait_ms)` 5. `keybd_event(VK_LWIN, 0, 0, 0); keybd_event(VK_LSHIFT, 0, 0, 0); keybd_event(0x53, 0, 0, 0)` 等 - 释放:`keybd_event(..., KEYEVENTF_KEYUP, 0)` 6. UI 弹层 "请用系统截图工具框选窗口区域"(带倒计时) 7. 启动 `tokio::time::interval(200ms)` 轮询剪贴板:`OpenClipboard` → `GetClipboardData(CF_BITMAP)` → 转 `DynamicImage` 8. 拿到位图 → `base64::encode` → `reqwest::Client::new().post(URL).json(...)` 9. 解析 `{"code":100, "data":[{"text":"...","box":[[x1,y1],...]}]}` 10. `keywords` 匹配 → 写 `Finding` 11. 关闭:先 `PostMessage(hwnd, WM_CLOSE, 0, 0)` → `Sleep(close_wait_ms)` → `taskkill /F /PID xxx /T` ### 5. Umi-OCR HTTP 客户端(关键代码片段) ```rust // src/inspect/umi_ocr.rs pub struct UmiOcrClient { base_url: String, client: reqwest::blocking::Client } #[derive(Serialize)] struct OcrRequest<'a> { base64: &'a str, options: OcrOptions } #[derive(Serialize)] struct OcrOptions { #[serde(rename = "ocr.language")] language: String, #[serde(rename = "ocr.cls")] cls: bool, #[serde(rename = "ocr.limit_side_len")] limit_side_len: u32, #[serde(rename = "tbpu.parser")] parser: String } #[derive(Deserialize)] pub struct OcrResponse { pub code: u32, pub data: Vec, pub time: f64 } pub struct OcrItem { pub text: String, pub score: f32, pub box: Vec<[f64;2]> } impl UmiOcrClient { pub fn recognize_png(&self, png: &[u8], lang: &str) -> anyhow::Result { let b64 = base64::engine::general_purpose::STANDARD.encode(png); let req = OcrRequest { base64: &b64, options: OcrOptions { language: lang.into(), cls: false, limit_side_len: 2880, parser: "multi_para".into() } }; let resp = self.client.post(&self.base_url).json(&req).send()?.json::()?; if resp.code != 100 { anyhow::bail!("Umi-OCR 返回 code={}", resp.code); } Ok(resp) } } ``` ### 6. Umi-OCR 启动 + 健康检查 - 启动时检查 `/Umi-OCR.exe` 存在 → 不存在则提示"请将 Umi-OCR.exe 放在 exe 同目录" - `CreateProcessW` 启动后,循环 `GET http://127.0.0.1:1224/` 直到成功或超时(默认 3s) - 失败时给出修复指引(端口被占、Umi-OCR 未运行) ### 7. 关键词匹配 - 关键词少:用 `regex::RegexSet` - 关键词多:用 `aho_corasick::AhoCorasick`(多模式 AC 自动机) - 命中位置记录 `(text_index, char_offset, length)` → 写回 `box` 坐标做红框高亮 ### 8. 取消与超时 - 全局 `tokio::sync::watch::Sender` 发取消信号 - 每个 Inspector 用 `tokio::time::timeout(总超时, ...)` 包裹 - UI 顶栏 "取消" 按钮 → 发送 `true` ### 9. 性能与稳定 - 截图位图大于 `screenshot_max_side` 时先 `image::imageops::resize` - 临时文件清理:每次启动清 `temp/secret-scan-*` - 关闭查看器失败 → 二次 `taskkill /F /T` - doclite 进程残留 → 主程序退出前强制清 --- ## 八、构建与发布 ```bash # 一次性 pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake rustup target add x86_64-pc-windows-gnu # 编译 cargo build --release --target x86_64-pc-windows-gnu # 产物 ls target/x86_64-pc-windows-gnu/release/secret-file-selfcheck.exe ``` 发布物(同级目录): ``` secret-file-selfcheck.exe Umi-OCR.exe Umi-OCR-data/ # Umi-OCR 自带数据目录 doclite.exe README.md ``` README 章节:构建步骤、运行方式、首次配置(放 Umi-OCR 与 doclite)、常见问题(OCR 不返回、doclite 启动失败、UAC 弹窗被禁用、AV 误报)。 --- ## 九、测试策略 - **单元测试**: - `matcher::keywords`:大小写、正则、整词、误报白名单 - `sampler`:随机 / 分层 / 配额 - `filter`:隐藏文件、扩展名白名单、最小大小 - `walker`:白名单目录剔除、最大深度 - **集成测试**: - `inspect::umi_ocr` 用一个 mock HTTP server(`httpmock` / `wiremock`)验证请求体/响应解析 - fixture:`sample.docx`(含"机密")和"普通"两份 → 走完 Inspector - **手动测试**: - 在 doclite / SumatraPDF / Edge 下验证窗口截图与 OCR 准确度 - 验证 Umi-OCR 中文识别率 - 验证 UAC 弹窗仅出现一次 - **不**做 GUI 自动化测试(egui 暂无稳定 e2e 方案) --- ## 十、MVP 任务拆分 ### 任务 1:脚手架 + 隐藏控制台 + UAC 1. `Cargo.toml` + 最小 `main.rs`,`cargo build` 通过 2. `build.rs` + `#![windows_subsystem = "windows"]` 隐藏控制台 3. `privilege.rs` 实现 `ensure_admin()`,未提权时 `runas` 重启 4. 提交 ### 任务 2:egui 入口 + Material 主题 + 中文字体 1. `app.rs` 骨架:左侧设置、右侧主面板、底部状态栏 2. `ui/material.rs`:颜色 token + 圆角控件 3. `ui/widgets.rs`:嵌入 `SourceHanSansSC-Regular.otf` 4. UI 显示"涉密文件自检工具"中文正常 5. 提交 ### 任务 3:配置模型与持久化 1. `config/model.rs` 强类型 `AppConfig`(serde + toml) 2. 启动加载、退出保存到 `%APPDATA%\secret-file-selfcheck\config.toml` 3. 首次启动向导:检测 Umi-OCR/doclite 路径、关键词、等待时间 4. 单元测试:序列化往返 5. 提交 ### 任务 4:目录遍历 walker 1. `walkdir` + 白名单剔除 + 扩展名白名单 + 隐藏/系统过滤 + 最小大小 2. 单元测试:白名单、扩展名、深度 3. 提交 ### 任务 5:抽检 sampler 1. 完全随机 / 分层 / 类型配额 三种策略 2. 单元测试 3. 提交 ### 任务 6:关键词匹配 1. `keywords` 模块:大小写、正则、整词、白名单 2. `aho-corasick` 多模式 3. 单元测试 4. 提交 ### 任务 7:DOCX 文本抽检 1. `docx-rs` 读段落 2. 命中检测 3. fixture 测试 4. 提交 ### 任务 8:Umi-OCR HTTP 客户端 1. `umi_ocr.rs` 阻塞 POST /api/ocr,解析返回 2. Umi-OCR 启动 + 健康检查 3. 集成测试(mock server) 4. 提交 ### 任务 9:截图链路 1. `external.rs` 启进程 / 关闭窗口 / 强杀 2. `screenshot.rs`:`keybd_event` 模拟 `Win+Shift+S` + 剪贴板轮询读位图 3. 单元测试 + 手动测试 4. 提交 ### 任务 10:DOC 抽检 1. `doc_inspector.rs` 拼装流程:doclite.exe + 等待 + 截图 + OCR + 匹配 + 关闭 2. 提交 ### 任务 11:PDF 抽检 1. `pdf_inspector.rs` 拼装流程:关联程序 + 等待 + 截图 + OCR + 匹配 + 关闭 2. 提交 ### 任务 12:并发调度 1. `tokio` + `buffer_unordered` 调度;进度、取消、日志 2. 提交 ### 任务 13:报告输出 1. `report::html` + `report::png` + `report::json` 2. 提交 ### 任务 14:设置页 UI(A-F 六分组) 1. 所有控件 2. 即时写 config 3. 提交 ### 任务 15:主面板 UI 1. "开始检测" 按钮、进度条、实时日志 2. 截图引导弹层 3. 提交 ### 任务 16:打包 1. release 构建 2. 验证无控制台、UI 中文、抽检 10 份报告正确、UAC 仅一次 3. README(构建/部署/FAQ) 4. 提交 --- ## 十一、风险与对策 | 风险 | 影响 | 对策 | |------|------|------| | Umi-OCR 启动失败/端口占用 | OCR 全失败 | 启动时健康检查 + 清晰错误提示;允许用户在设置中改端口 | | Win+Shift+S 触发失败(被其它程序抢占) | 截不到图 | 增加"手动粘贴"兜底:UI 提供"从剪贴板读取"按钮;再不行就跳过 | | doclite 启动后窗口未出现 | 截图空白 | wait_ms 可调;增加"窗口标题探测"再截图 | | OCR 识别率低(中文/复杂排版) | 漏报 | 提供 hi-res(limit_side_len=4320);截图后预处理(二值化)作为后续 | | 截图位图被 Word 起始页覆盖 | 漏报 | OCR 后再次截图(用户配合二次框选),或启用 `PrintWindow` 抓窗口(兜底) | | 单 exe 体积 | 20-40MB | LTO + strip + release 优化;提供"瘦身版"不内嵌字体 | | Anti-Virus 误报 | 启动被拦 | README 提示加白名单;可选代码签名 | | 全盘扫描耗时 | 几分钟到几十分钟 | UI 实时进度 + 可取消;默认全盘但允许"白名单"缩小范围 | | UAC 弹窗被用户禁用 | 后续权限不足 | 首次启动检测 UAC 状态;告知用户 | | 剪贴板被其它程序占用 | 读不到图 | `OpenClipboard` 重试 + 退避;超过 N 次失败则放弃当前文件 | --- ## 十二、可后续追加(不在 MVP 范围) - 哈希去重在 SQLite 中持久化 - 关键词 + 文件指纹的"已确认误报"列表 - 隔离区(命中后复制到独立目录) - 开机自启 + 托盘 - 报告 PDF 格式 - 关键词网络下发 - 抽检历史对比 / 趋势图 --- ## 十三、最终确认(已通过) 1. ✅ 截图三模式(manual / auto_printwindow / auto_with_manual_fallback)已纳入设计与实现。 2. ✅ 抽检固定串行,无并发选项。 3. ✅ 所有核心设计决策(1.1 节 15 项)已锁定。 可进入实现阶段,按任务 1-16 顺序执行。