docs: 添加涉密文件自检工具实施计划
This commit is contained in:
606
docs/plans/2026-06-08-secret-file-selfcheck.md
Normal file
606
docs/plans/2026-06-08-secret-file-selfcheck.md
Normal file
@@ -0,0 +1,606 @@
|
||||
# 涉密文件自检工具 实施计划
|
||||
|
||||
> **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 实时) | 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 查看器路径(默认 `<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)
|
||||
├─ 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<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. 提交
|
||||
|
||||
### 任务 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 顺序执行。
|
||||
Reference in New Issue
Block a user