Files
work-secretfile-selfcheck/docs/plans/2026-06-08-secret-file-selfcheck.md

607 lines
25 KiB
Markdown
Raw Normal View History

# 涉密文件自检工具 实施计划
> **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`:调用 `<exe 同目录>/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 打开 | **`<exe 同目录>/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 实时) | Info14 天 |
| 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数字框默认 10 = 不限)
- 最大遍历深度数字框0=无限)
- 扩展名白名单(多选 + 自定义:默认 doc/docx/pdf
- 修改时间范围(起始/结束日期)
- 单次扫描超时分钟0=无限)
### C. 抽检
- 抽检数量(数字框)
- 抽样策略(单选:完全随机 / 分层 / 类型配额)
- 类型配额DOC / DOCX / PDF 各自数量
- **抽检执行方式:串行**(一个接一个打开,前一份处理完再开下一份,**无并发选项**
- 单文件总超时(秒,默认 120
- 同文件多少天内不重复(数字框,默认 7
- 跳过锁定/无权限文件(开关)
- 命中后是否继续后续文件(开关,默认继续;关闭则一旦命中就停)
### D. 查看器与截图
- .doc 查看器路径(默认 `<exe 同目录>/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 启动路径(默认 `<exe 同目录>/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<PathBuf>
[抽样阶段] sampler 按策略(随机 / 分层 / 类型配额)选 N 个
[抽检阶段] 串行(默认 1 并发;并发时 buffer_unordered
├─ DOCXdocx-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<AtomicUsize>` 推进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<OcrItem>, 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<OcrResponse> {
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::<OcrResponse>()?;
if resp.code != 100 { anyhow::bail!("Umi-OCR 返回 code={}", resp.code); }
Ok(resp)
}
}
```
### 6. Umi-OCR 启动 + 健康检查
- 启动时检查 `<exe 同目录>/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<bool>` 发取消信号
- 每个 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. 提交
### 任务 2egui 入口 + 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. 提交
### 任务 7DOCX 文本抽检
1. `docx-rs` 读段落
2. 命中检测
3. fixture 测试
4. 提交
### 任务 8Umi-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. 提交
### 任务 10DOC 抽检
1. `doc_inspector.rs` 拼装流程doclite.exe + 等待 + 截图 + OCR + 匹配 + 关闭
2. 提交
### 任务 11PDF 抽检
1. `pdf_inspector.rs` 拼装流程:关联程序 + 等待 + 截图 + OCR + 匹配 + 关闭
2. 提交
### 任务 12并发调度
1. `tokio` + `buffer_unordered` 调度;进度、取消、日志
2. 提交
### 任务 13报告输出
1. `report::html` + `report::png` + `report::json`
2. 提交
### 任务 14设置页 UIA-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-reslimit_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 顺序执行。