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

607 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 涉密文件自检工具 实施计划
> **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 顺序执行。