feat: KVM-Privacy Hub initial commit

Phase 0: infra fixes (OCR dir, port conflict, systemd services)
Phase 1: video PII masking (frame_redactor, privacy API)
Phase 2: AI privacy gateway (mitmproxy addon, audit logger)
Phase 3: AI routing (Ollama backend, memory admin endpoints)
Phase 4: deployment scripts and Makefile
This commit is contained in:
2026-02-28 17:33:28 +08:00
commit 78775868d6
18 changed files with 1338 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
__pycache__/
*.pyc
*.pyo
*.db
/deploy/systemd/*.service.override
/services/privacy_gateway/__pycache__/
+49
View File
@@ -0,0 +1,49 @@
DEVICE_IP ?= 192.168.123.181
DEVICE_USER ?= pi
SSH := sshpass -p pi ssh -o StrictHostKeyChecking=no $(DEVICE_USER)@$(DEVICE_IP)
.PHONY: deploy-gateway deploy-memory deploy-all status logs help
help:
@echo "KVM-Privacy Hub 部署工具"
@echo ""
@echo " make deploy-gateway 安装 Privacy Gateway (mitmproxy)"
@echo " make deploy-memory 部署 mem-bridge 服务"
@echo " make deploy-all 完整部署 (gateway + memory + Ollama)"
@echo " make status 查看所有服务状态"
@echo " make logs SVC=<name> 查看指定服务日志"
@echo ""
@echo "变量: DEVICE_IP=$(DEVICE_IP) DEVICE_USER=$(DEVICE_USER)"
deploy-gateway:
DEVICE_IP=$(DEVICE_IP) DEVICE_USER=$(DEVICE_USER) bash scripts/setup_gateway.sh
deploy-memory:
@echo "=== 部署 mem-bridge 服务 ==="
sshpass -p pi scp -o StrictHostKeyChecking=no \
deploy/systemd/mem-bridge-memory.service \
deploy/systemd/mem-bridge-router.service \
$(DEVICE_USER)@$(DEVICE_IP):/tmp/
$(SSH) "echo '123123' | sudo -S cp /tmp/mem-bridge-*.service /etc/systemd/system/ && \
sudo systemctl daemon-reload && \
sudo systemctl enable --now mem-bridge-memory mem-bridge-router"
@echo "mem-bridge 服务已启动"
deploy-all: deploy-gateway deploy-memory
DEVICE_IP=$(DEVICE_IP) DEVICE_USER=$(DEVICE_USER) bash scripts/install_ollama.sh
@echo "=== 完整部署完成 ==="
status:
@echo "=== KVM-Privacy Hub 服务状态 ==="
$(SSH) "systemctl is-active kvm-server info-privacy mem-bridge-memory mem-bridge-router privacy-gateway 2>/dev/null | paste - - - - - | column -t" || true
@echo ""
@echo "=== 端口监听 ==="
$(SSH) "ss -tlnp | grep -E '8080|8000|8001|8002|8888' || echo '无监听端口'"
logs:
$(SSH) "journalctl -u $(SVC) -n 50 --no-pager"
clean:
@echo "清理本地临时文件"
find . -name '*.pyc' -delete
find . -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true
+22
View File
@@ -0,0 +1,22 @@
[Unit]
Description=Info-Privacy PII Detection Service
After=network.target
Wants=network.target
[Service]
Type=simple
User=pi
Group=pi
WorkingDirectory=/home/pi/Desktop/info-privacy
ExecStart=/home/pi/Desktop/info-privacy/venv/bin/uvicorn \
info_privacy.api.main:app \
--host 0.0.0.0 \
--port 8000 \
--workers 1
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+4
View File
@@ -0,0 +1,4 @@
# /etc/tmpfiles.d/kvm-ocr.conf
# 设备重启后自动重建 OCR 快照目录
# 格式: type path mode user group age
d /tmp/kvm-ocr 0755 root root -
+19
View File
@@ -0,0 +1,19 @@
[Unit]
Description=Mem-Bridge Memory Service
After=network.target
Wants=network.target
[Service]
Type=simple
User=pi
Group=pi
WorkingDirectory=/data/rockchip/embedding
EnvironmentFile=-/data/rockchip/embedding/.env
ExecStart=/data/rockchip/embedding/venv/bin/python server.py --service memory
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+20
View File
@@ -0,0 +1,20 @@
[Unit]
Description=Mem-Bridge Router Service
After=network.target mem-bridge-memory.service
Wants=network.target
Requires=mem-bridge-memory.service
[Service]
Type=simple
User=pi
Group=pi
WorkingDirectory=/data/rockchip/embedding
EnvironmentFile=-/data/rockchip/embedding/.env
ExecStart=/data/rockchip/embedding/venv/bin/python server.py --service router
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+22
View File
@@ -0,0 +1,22 @@
[Unit]
Description=KVM Privacy Gateway (mitmproxy)
After=network.target info-privacy.service
Wants=network.target info-privacy.service
[Service]
Type=simple
User=root
WorkingDirectory=/data/project/KVM-privacy/services/privacy_gateway
ExecStart=/usr/local/bin/mitmdump \
--mode regular \
--listen-host 0.0.0.0 \
--listen-port 8888 \
--ssl-insecure \
-s /data/project/KVM-privacy/services/privacy_gateway/addon.py
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+324
View File
@@ -0,0 +1,324 @@
# KVM-Privacy 项目设计文档
**日期**2026-02-27
**状态**:已批准
**硬件**NanoPC-T6RK35888GB RAM64GB eMMC
---
## 1. 项目目标
设计一台具备 PiKVM 类似能力的 RK3588 边缘设备,同时作为 AI 隐私网关,满足两类核心场景:
| 场景 | 需求 |
|------|------|
| **KVM 控制** | 通过浏览器远程控制目标机的键鼠和视频,屏幕上的敏感信息(PII)在传输前自动遮罩 |
| **AI 隐私代理** | 用户使用 Claude/GPT 等 web AI 工具时,上传的文档在离开设备前自动检测并脱敏 |
云边协同:RK3588 承载本地推理、记忆管理、复杂度路由;超出本地能力时调用云端 API。
---
## 2. 整体架构
```
┌────────────────────────────────────────────┐
[目标机/服务器] │ RK3588 KVM-Privacy Hub │
HDMI ──────────────► │ ┌─────────────────────────────────────┐ │
USB ◄────────────── │ │ KVM Engine │ │
(HID 模拟) │ │ HDMI Capture → OCR → PII Mask │ │
│ │ → WebRTC Stream │ │
│ └─────────────────────────────────────┘ │
│ ▲ │
[用户设备] │ │ │
浏览器 ─────LAN──────► │ ┌─────────────────┴───────────────────┐ │
│ │ Web UI (FastAPI + Vue3) │ │
│ │ KVM页 / 隐私仪表盘 / AI对话 / 设置 │ │
│ └─────────────────────────────────────┘ │
│ │
[用户设备流量] │ ┌─────────────────────────────────────┐ │
NIC0(LAN) ──────────► │ │ Privacy Gateway (mitmproxy) │ │
NIC1(WAN) ──────────► │ │ AI域名白名单 → 附件解析 → redact │ │
│ │ → 审计日志 → 放行脱敏后请求 │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────▼───────────────────┐ │
│ │ AI 路由层 (mem_bridge) │ │
│ │ 复杂度评分 → 本地 Qwen2-1.5B │ │
│ │ 或 → 云端 API │ │
│ └──────────┬──────────────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ Memory / embed-db │ │
│ │ 短期会话 + 文档索引 │ │
│ └─────────────────────┘ │
└────────────────────────────────────────────┘
│ WAN
┌─────▼──────┐
│ 云端 API │
│ Claude/GPT │
└────────────┘
```
**五个子系统**
1. **KVM Engine** — 视频采集、HID 模拟、视频流隐私过滤
2. **Privacy Gateway** — HTTPS 透明代理、AI 请求文档过滤
3. **AI 路由层** — 复杂度路由、记忆注入(复用 mem_bridge
4. **Memory / embed-db** — 对话记忆 + 文档语义索引(复用)
5. **Web UI** — 统一控制界面
---
## 3. 子系统详细设计
### 3.1 KVM Engine
**硬件**
- HDMI Capture Card (USB/PCIe) → V4L2 `/dev/video0`
- USB OTG → HID 模拟(键盘 + 鼠标)via USB Gadget (configfs)
**视频流水线**
```
V4L2 采集 (MJPEG/RAW)
→ RK3588 MPP 硬件 H.264 编码
→ 帧采样器 (运动检测触发 or 3s 定时兜底)
→ PaddleOCR RKNN 检测文字区域
→ PII 规则匹配 → 遮罩坐标缓存
→ 叠加遮罩(黑色矩形,<1ms)
→ WebRTC 推流 → 用户浏览器
```
**OCR 采样策略**(异步,不阻塞推流主路径):
- 画面变化 > 阈值时触发
- 最长每 3 秒兜底触发一次
- 遮罩坐标有效期至下次检测,平滑过渡
**HID 转发**:浏览器鼠标/键盘事件 → WebSocket → `/dev/usb-gadget` → 目标机
**计划工作(Phase 3+**
- 不同连接用户的视图权限控制(防心轿展示)
- KVM 会话审计日志(录屏审计,不含原始视频)
### 3.2 Privacy Gateway
**网络拓扑**
```
NIC0 eth0 ← LAN 侧 (用户设备) 192.168.2.x/24
NIC1 eth1 ← WAN 侧 (上游路由器) DHCP
iptables REDIRECT → mitmproxy 透明代理 (port 8080)
仅对 AI 域名白名单签发中间证书,其余域名直接 BYPASS
```
**AI 域名白名单**`configs/ai_domains.txt`):
```
api.anthropic.com
api.openai.com
generativelanguage.googleapis.com
dashscope.aliyuncs.com
...(可配置扩展)
```
**请求处理流水线**
```
拦截 HTTPS 请求
→ Content-Type 检查
→ 含文件附件?
是 → info-privacy.analyze() → PII 报告
→ 策略 A(自动): redact all high-level PII → 替换附件 → 放行
→ 策略 B(确认): 暂停 → Web UI 弹窗 → 用户决策 → 放行/阻断
否 → 文本 PII 快速扫描 → 通过则放行
→ 审计日志写入(无原文)
→ 转发脱敏后请求到目标 API
```
**审计日志格式**append-only SQLite):
```json
{
"ts": "2026-02-27T10:00:00Z",
"user_id": "uid_xxx",
"domain": "api.anthropic.com",
"pii_types": {"id_card": 2, "phone": 1},
"action": "auto_redact",
"doc_hash": "sha256:abc..."
}
```
**原文数据处理**:仅在内存 buffer 中处理,redact 完成后立即释放,不落盘。
**处理后文档**:会话内临时持有(TTL 30 分钟),用户可在 Web UI 预览脱敏对比,会话关闭后自动释放。
### 3.3 AI 路由层(复用 mem_bridge
直接复用 `/data/rockchip/embedding/src/mem_bridge/`,新增本地 Qwen2-1.5B 后端:
```yaml
# configs/gateway.yaml
backends:
default: qwen_local # 轻量/隐私任务 → 本地
heavy: claude_sonnet # 复杂任务 → 云端
qwen_local:
type: compatible
base_url: http://localhost:11434/v1 # Ollama
model: qwen2:1.5b
api_key: dummy
claude_sonnet:
type: anthropic
model: claude-sonnet-4-6
api_key: ${ANTHROPIC_API_KEY}
```
**路由规则**ComplexityScorer 模板):
- **本地(Qwen2-1.5B**PII 分类、文档摘要(<1k 字)、路由判断、隐私策略咨询
- **云端**:长文档分析、代码生成、深度推理、多轮复杂对话
### 3.4 Memory 服务(复用 embed-db
直接复用 `/data/rockchip/embedding/src/`
- **短期记忆**:会话对话历史(faiss + SQLiteTTL 会话级)
- **长期知识库**:用户文档索引(需用户主动导入),multilingual-e5-small 384d
- **用户隔离**session_id 命名空间隔离,不同账户索引目录独立
### 3.5 Web UI
**页面结构**
```
/ (单页应用,Vue3 + Vite)
├── /kvm KVM 控制台
│ WebRTC 画面 + 键鼠输入捕获
│ PII 遮罩开关 / 被遮内容类型提示
├── /privacy 隐私网关仪表盘
│ 今日拦截统计 / 审计日志浏览
│ 域名白名单管理 / 拦截策略配置
│ 首次使用:证书安装向导
├── /chat AI 对话
│ 文件上传 → 自动检测 → 脱敏预览 → 确认发送
│ 通过路由层,带记忆注入
└── /settings 账户 / 网络 / 模型 / 隐私策略
```
**技术选型**
- 后端:FastAPI + uvicorn(统一 API 聚合网关)
- 前端:Vue3 + Vite(轻量 SPA3588 直接 serve
- 通信:REST + WebSocketKVM 流 + 实时通知)
- 认证:JWTpython-jose+ HTTPS(设备自签证书)
---
## 4. 数据流 & 安全边界
### 4.1 安全边界
| 边界 | 措施 |
|------|------|
| Web UI 访问 | JWT 认证 + HTTPS(设备自签证书) |
| 多用户隔离 | session_id 命名空间隔离,账户间内存索引独立 |
| HTTPS 拦截证书 | 设备生成独立 CA,仅对 AI 域名白名单签发中间证书 |
| 原文不落盘 | Privacy Gateway 所有处理在内存 buffer |
| 审计日志防篡改 | append-only SQLite WAL + 定期 hash 链 |
| 本地 LLM 数据边界 | 敏感任务优先路由到本地 Qwen2-1.5B,不出设备 |
### 4.2 首次使用引导
```
1. 用户访问 http://kvm.local → 重定向到引导页
2. 下载设备根证书(一键) → 指引安装到系统/浏览器信任区
3. 选择拦截范围(推荐:仅 AI 域名白名单)
4. 验证代理连通性 → 完成(全程 < 3 分钟)
```
---
## 5. 技术选型汇总
| 层 | 技术 | 说明 |
|----|----|------|
| 视频采集 | V4L2 + RK3588 MPP | 硬件 H.264 编码,低延迟 |
| KVM 视频流 | WebRTC (aiortc) | 浏览器原生,低延迟 |
| HID 模拟 | USB Gadget (configfs) | Linux 标准方案 |
| KVM OCR | PaddleOCR RKNN | 复用现有模型 |
| 文档隐私 | info-privacy(复用) | 零开发成本 |
| HTTPS 拦截 | mitmproxy + 自定义 addon | Python 生态,易扩展 |
| AI 路由 | mem_bridge(复用) | 含复杂度路由 + 记忆注入 |
| 本地 LLM | Qwen2-1.5B via Ollama | RKNN NPU 加速 |
| 向量数据库 | embed-db(复用) | faiss + SQLite |
| 后端框架 | FastAPI + uvicorn | 统一 API 网关 |
| 前端 | Vue3 + Vite | 轻量 SPA |
| 认证 | JWT (python-jose) | 多用户支持 |
| 配置管理 | YAML + dotenv | 同现有项目约定 |
| 日志审计 | SQLite append-only | 轻量无外部依赖 |
---
## 6. 开发阶段
### Phase 1 — 隐私网关 MVP(最高优先级)
- mitmproxy addon 框架 + AI 域名白名单过滤
- 接入 info-privacyanalyze + redact
- 审计日志服务
- Web UI:隐私仪表盘 + 证书引导向导
### Phase 2 — AI 路由集成
- mem_bridge 配置 Qwen2-1.5B 本地后端(Ollama
- 多用户 session 隔离
- Web UI:AI 对话页 + 文档脱敏预览交互
### Phase 3 — KVM 核心
- V4L2 采集 + WebRTC 推流(aiortc
- HID 模拟(USB Gadget
- OCR 帧采样 + PII 遮罩渲染
- Web UIKVM 控制台
### Phase 4 — 打磨 & 集成
- 首次使用向导完善
- 多用户账户管理
- 性能调优(NPU 并发、内存优化)
- 设备出厂配置脚本
---
## 7. 项目目录结构
```
KVM-privacy/
├── services/
│ ├── privacy_gateway/ # mitmproxy addon + 审计日志服务
│ ├── kvm_engine/ # V4L2 + WebRTC + HID + OCR 遮罩
│ ├── web_ui/ # FastAPI 后端 + Vue3 前端
│ └── config_server/ # 设备配置、用户管理、CA 证书生成
├── shared/ # 符号链接或 submodule
│ ├── embed_db/ # → /data/rockchip/embedding
│ └── info_privacy/ # → /data/rockchip/info-privacy
├── scripts/
│ ├── setup_gateway.sh # iptables + 双 NIC 路由配置
│ ├── setup_usb_gadget.sh
│ └── install.sh # 一键安装脚本
├── configs/
│ ├── gateway.yaml # AI 路由 + 后端配置
│ ├── ai_domains.txt # AI 域名白名单
│ └── privacy_policy.yaml
├── tests/
├── docs/
│ └── plans/
│ └── 2026-02-27-kvm-privacy-design.md
├── README.md
├── DEVELOP.md
└── RELEASE.md
```
---
## 8. 复用现有组件
| 组件 | 来源 | 复用方式 |
|------|------|---------|
| info-privacy | `/data/rockchip/info-privacy` | Privacy Gateway 调用其 REST API |
| mem_bridge(路由+记忆) | `/data/rockchip/embedding/src/mem_bridge` | 直接 import,新增 Qwen2 本地后端 |
| embed-db(向量索引) | `/data/rockchip/embedding/src/embed_db` | Memory 服务直接复用 |
| PaddleOCR RKNN 模型 | `/data/rockchip/paddle_ocr/models/` | KVM OCR 复用 |
| MediaPipe 人脸模型 | `/data/rockchip/mediapipe/models/rknn/` | info-privacy 已封装 |
@@ -0,0 +1,321 @@
# KVM-Privacy 开发计划(基于设备调研)
**日期**2026-02-28
**调研设备**NanoPC-T6IP 192.168.123.181/home/pi/Desktop/KVM
**状态**:基于实际部署调研更新设计文档
---
## 一、设备现状调研结论
### 1.1 KVM 核心组件现状
| 组件 | 状态 | 说明 |
|------|------|------|
| kvm-server | ✅ 运行中(port 8080 | systemd 服务,运行 1.5+ 年,Go 单体二进制 |
| GStreamer 管道 | ✅ 运行中 | `/dev/video40` → RKMPP H.264 4K@30fps → RTP:5004 |
| kvm-ocr 二进制 | ✅ 已安装 | `/usr/bin/kvm-ocr`,含 RKNN NPU 支持 |
| OCR RKNN 模型 | ✅ 已存在 | `/etc/kvm/models/ocr/`PP-OCRv4 中英文) |
| RKNPU 内核模块 | ✅ 已加载 | `rknpu` 模块,`/dev/dri/renderD128` 可访问 |
| HID 设备 | ✅ 正常 | `/dev/hidg0`(键盘) + `/dev/hidg1`(鼠标) |
| HDMI 接收 | ✅ 已连接 | `/dev/video40`rk_hdmirx 驱动) |
| MariaDB | ✅ 运行中(port 3306 | GORM ORM,哈希链审计日志 |
| Web UI | ✅ 200 OKport 8080 | React 18 + TypeScript,嵌入 Go 二进制 |
| eth0 | ✅ UP | 192.168.123.181/24,当前局域网接口 |
| eth1 | ❌ DOWN(无载波) | 物理未接线,网关模式暂不可用 |
### 1.2 隐私组件现状
| 组件 | 状态 | 说明 |
|------|------|------|
| info-privacy | ✅ 运行中(port 8000 | 手动启动,uvicorn`{"status":"ok"}` |
| embed-db | ✅ 代码已在设备 | `/home/pi/Desktop/embed-db/`,未作为服务运行 |
| Ollama | ❌ 未安装 | 本地 LLM 推理不可用 |
| mitmproxy 代理 | ❌ 未配置 | 隐私网关核心组件未构建 |
| mem_bridge | ❌ 未部署 | AI 路由层未运行 |
### 1.3 已知问题
| 问题 | 影响 | 修复难度 |
|------|------|---------|
| `/tmp/kvm-ocr/` 目录不存在 | OCR 实际无法运行(GStreamer 帧写入失败) | 极低(一行命令) |
| info-privacy 非 systemd 管理 | 重启后不自动恢复 | 低(写 service 文件) |
| eth1 无物理连接 | 网关模式无法启用,当前只能做代理 | 等待硬件接线 |
| 磁盘 79% 已用(11GB 剩余) | 安装 Ollama+模型需规划 | 中(Q4量化约 1GB) |
| TODO_UNVERIFIED_FEATURES | OCR 多语言/剪贴板/OCR复制面板代码完成未测试 | 低-中 |
### 1.4 磁盘评估
```
当前:53GB 总量,40GB 已用,11GB 剩余
需要安装:
Ollama binary: ~200MB
Qwen2-1.5B (Q4_K_M): ~1.0GB
mitmproxy: ~50MB
mem_bridge venv: ~200MB
合计: ~1.5GB
剩余裕量: ~9.5GB ✅
```
---
## 二、KVM 功能全景(已实现)
KVM 服务已具备完整生产能力,无需重新开发:
```
认证层:JWT + TOTP + Casbin RBAC4角色,22权限)
视频层:GStreamer → RKMPP MPP H.264/H.265 → RTP → Pion WebRTC → Browser
HID层:WebSocket → HID Dispatcher → /dev/hidg0(键盘) + /dev/hidg1(鼠标)
录像层:fMP4 分段(5min),HLS M3U8,关键帧索引,审计回放
终端层:SSH/PTY → WebSocket → Browser xterm
AI层:kvm-ocr (RKNN NPU) → pattern_matcher → sensitive_commands → 审计告警
审计层:MariaDB SHA-256 哈希链,syslog 外发支持,30天留存
网络层:WireGuard(代码完整,未启用),TURN/STUNcoturn
```
---
## 三、KVM-Privacy 开发计划
基于调研结论,将原设计 Phase 1-4 重新映射:
### Phase 0 — 立即修复(当前 session 可完成)
**目标**:修复现有 KVM 已知问题,为 privacy 扩展打好基础
#### P0.1 修复 OCR 目录缺失
```bash
# 在设备上执行
sudo mkdir -p /tmp/kvm-ocr
sudo chmod 755 /tmp/kvm-ocr
# 持久化:创建 tmpfiles.d 规则
echo 'd /tmp/kvm-ocr 0755 root root -' | sudo tee /etc/tmpfiles.d/kvm-ocr.conf
```
**验证**:重启 kvm-server 后 OCR 快照文件出现在 `/tmp/kvm-ocr/`
#### P0.2 将 info-privacy 纳入 systemd
```ini
# /etc/systemd/system/info-privacy.service
[Unit]
Description=Info-Privacy PII Detection Service
After=network.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/Desktop/info-privacy
ExecStart=/home/pi/Desktop/info-privacy/venv/bin/uvicorn \
info_privacy.api.main:app --host 0.0.0.0 --port 8000
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
```
#### P0.3 验证 TODO_UNVERIFIED_FEATURES 关键项
`docs/TODO_UNVERIFIED_FEATURES.md` 逐一测试:
- [ ] OCR 快照功能(修复 P0.1 后验证)
- [ ] 剪贴板粘贴(ASCII 文本)
- [ ] 审计页自动刷新
---
### Phase 1 — 隐私网关 MVP(最高优先级)
**目标**:HTTPS 透明代理拦截 AI 服务请求,自动 PII 脱敏
**依赖**info-privacy 已运行(已满足)
#### 架构
```
用户浏览器 → 系统代理 → mitmproxy(:8888)
→ AI 域名白名单匹配
→ 命中 → 解密 → info-privacy analyze → redact → 放行脱敏请求
→ 未命中 → 直接 BYPASS
→ 审计日志(无原文)
```
#### 交付物
1. **`services/privacy_gateway/`**
- `addon.py` — mitmproxy 核心 addonAI 域名过滤 + info-privacy 调用)
- `audit_logger.py` — SQLite append-only 审计写入
- `cert_manager.py` — 设备 CA 证书生成与管理
- `ai_domains.txt` — AI 域名白名单
2. **KVM Web UI 扩展**(在现有 React 项目中新增页面)
- `/privacy` 路由:今日拦截统计、审计日志、域名管理
- 首次使用向导:证书下载 + 安装引导(3步,<3分钟)
3. **新增 Go API 端点**(在 `go/internal/api/` 扩展)
- `GET /api/v1/privacy/stats` — 拦截统计
- `GET /api/v1/privacy/audit` — 审计日志
- `POST /api/v1/privacy/domains` — 域名白名单管理
- `GET /api/v1/privacy/cert` — 下载 CA 证书
4. **`scripts/setup_privacy_gateway.sh`**
- 安装 mitmproxy
- 配置系统代理提示(eth1 未接线时退化为手动代理模式)
#### 注意事项
- **eth1 未接线**:当前只能使用手动代理模式(用户配置系统代理)
- **eth1 接线后**:追加 iptables REDIRECT 规则,切换为透明代理(对用户完全无感)
- **文档上传处理**info-privacy `analyze` + `redact`,处理后内容不落盘
---
### Phase 2 — AI 路由层
**目标**:本地 Qwen2-1.5B 处理轻量任务,复杂任务路由云端
#### 交付物
1. **Ollama 安装 + Qwen2-1.5B**
```bash
curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen2:1.5b # ~1GB Q4量化
```
2. **mem_bridge 服务化**(复用 `/home/pi/Desktop/embed-db/src/mem_bridge/`
```yaml
# config: 新增 qwen_local 后端
backends:
default: qwen_local # 本地 Qwen2-1.5B
heavy: claude_sonnet # 复杂任务云端
qwen_local:
type: compatible
base_url: http://localhost:11434/v1
model: qwen2:1.5b
```
3. **KVM Web UI 扩展**
- `/chat` 路由:AI 对话页,文件上传自动脱敏 → 预览 → 确认发送
4. **systemd 服务**
- `mem-bridge-memory.service`port 8001
- `mem-bridge-router.service`port 8002
---
### Phase 3 — KVM 视频 PII 遮罩
**目标**:OCR 检测屏幕 PII → 生成遮罩坐标 → 叠加到 WebRTC 视频流
**现状**:kvm-ocr 已实现敏感命令检测(告警),需扩展为 PII 实体遮罩
#### 架构变化
```
当前:kvm-ocr 输出 → pattern_matcher → audit log(告警)
新增:kvm-ocr 输出(含 bbox)→ PII matcher → mask_coords → WebRTC overlay
```
#### 交付物
1. **`go/internal/ai/pii_masker.go`**(新文件)
- 读取 kvm-ocr 输出的 bbox 坐标
- 匹配 PII 模式(复用 info-privacy 规则集)
- 维护遮罩坐标缓存(TTL = 下次 OCR 触发)
2. **WebRTC 视频叠加**`go/internal/webrtc/server.go` 扩展)
- 在向浏览器发送视频帧前叠加黑色矩形
- 叠加在服务端(防止客户端绕过)
3. **Web UI**
- KVM 控制台新增:遮罩开关 + 被遮区域类型提示
#### 计划工作(Phase 3+
- 不同远端用户的视图权限控制(防展示给无权用户)
- KVM 会话审计录像(仅存元数据和事件时间线,不存原视频)
---
### Phase 4 — 生产加固
1. **eth1 网关配置**(接线后执行)
```bash
# iptables 透明代理规则
iptables -t nat -A PREROUTING -i eth0 \
-p tcp --dport 443 -j REDIRECT --to-port 8888
```
2. **多用户账户管理**:扩展现有 RBAC,增加 privacy 权限角色
3. **性能调优**:NPU 三核并行(`NPU_CORE_ALL`),OCR 批处理
4. **设备出厂配置**`scripts/install.sh` 一键部署所有服务
---
## 四、项目目录结构(目标)
```
KVM-privacy/
├── services/
│ ├── privacy_gateway/ # Phase 1: mitmproxy addon
│ │ ├── addon.py # 核心拦截逻辑
│ │ ├── audit_logger.py # SQLite 审计
│ │ ├── cert_manager.py # CA 证书管理
│ │ └── ai_domains.txt # AI 域名白名单
│ └── config_server/ # 设备配置 API(扩展现有 KVM API
├── docs/
│ └── plans/
│ ├── 2026-02-27-kvm-privacy-design.md # 原始设计
│ └── 2026-02-28-kvm-privacy-devplan.md # 本文档
├── scripts/
│ ├── setup_privacy_gateway.sh # mitmproxy + 证书 + 代理配置
│ └── install_ollama.sh # Ollama + Qwen2-1.5B
├── README.md
├── DEVELOP.md
└── RELEASE.md
```
**复用路径(设备上)**
- info-privacy: `/home/pi/Desktop/info-privacy/`Port 8000,已运行)
- embed-db/mem_bridge: `/home/pi/Desktop/embed-db/src/mem_bridge/`
- kvm-server: `/home/pi/Desktop/KVM/`Port 8080,已运行)
---
## 五、立即可行动项(Phase 0)
```bash
# 在 192.168.123.181 上执行:
# 1. 修复 OCR 目录
sudo mkdir -p /tmp/kvm-ocr && sudo chmod 755 /tmp/kvm-ocr
echo 'd /tmp/kvm-ocr 0755 root root -' | sudo tee /etc/tmpfiles.d/kvm-ocr.conf
# 2. 重启 kvm-server 让 GStreamer 重建管道
sudo systemctl restart kvm-server
# 3. 验证 OCR 帧出现
sleep 5 && ls -la /tmp/kvm-ocr/
# 4. info-privacy 纳入 systemd
sudo tee /etc/systemd/system/info-privacy.service << 'EOF'
[Unit]
Description=Info-Privacy PII Detection Service
After=network.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/Desktop/info-privacy
ExecStart=/home/pi/Desktop/info-privacy/venv/bin/uvicorn info_privacy.api.main:app --host 0.0.0.0 --port 8000
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable info-privacy
```
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# install.sh — KVM-Privacy Hub 一键安装脚本
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "========================================"
echo " KVM-Privacy Hub 一键安装"
echo "========================================"
# 可选:跳过某些步骤
SKIP_OLLAMA="${SKIP_OLLAMA:-false}"
echo ""
echo "步骤 1/2: 安装 Privacy Gateway (mitmproxy + CA 证书 + systemd)"
bash "${SCRIPT_DIR}/setup_gateway.sh"
if [ "${SKIP_OLLAMA}" != "true" ]; then
echo ""
echo "步骤 2/2: 安装 Ollama + Qwen2-1.5B"
bash "${SCRIPT_DIR}/install_ollama.sh"
else
echo "步骤 2/2: 跳过 Ollama 安装 (SKIP_OLLAMA=true)"
fi
echo ""
echo "========================================"
echo " 安装完成!"
echo "========================================"
echo ""
echo "下一步:"
echo " 1. 在浏览器安装 CA 证书: curl http://\${DEVICE_IP}:8080/api/v1/privacy/cert"
echo " 2. 配置系统代理: \${DEVICE_IP}:8888"
echo " 3. 打开 KVM Web UI → Privacy 页面查看实时统计"
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# install_ollama.sh — 在 RK3588 设备上安装 Ollama 并拉取 Qwen2-1.5B
set -euo pipefail
DEVICE_IP="${DEVICE_IP:-192.168.123.181}"
DEVICE_USER="${DEVICE_USER:-pi}"
echo "=== [1/3] 安装 Ollama ==="
ssh "${DEVICE_USER}@${DEVICE_IP}" "curl -fsSL https://ollama.com/install.sh | sh"
echo "=== [2/3] 启动 Ollama 服务 ==="
ssh "${DEVICE_USER}@${DEVICE_IP}" "echo '123123' | sudo -S systemctl enable --now ollama"
sleep 5 # 等待 Ollama 启动
echo "=== [3/3] 拉取 Qwen2-1.5B (约 1GB Q4_K_M) ==="
ssh "${DEVICE_USER}@${DEVICE_IP}" "ollama pull qwen2:1.5b"
echo "=== 完成!验证 ==="
ssh "${DEVICE_USER}@${DEVICE_IP}" "ollama list"
echo "Ollama API 测试:"
ssh "${DEVICE_USER}@${DEVICE_IP}" "curl -s http://localhost:11434/v1/models | python3 -m json.tool 2>/dev/null | head -10"
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# setup_gateway.sh — 安装 Privacy Gateway (mitmproxy + CA 证书 + systemd)
set -euo pipefail
DEVICE_IP="${DEVICE_IP:-192.168.123.181}"
DEVICE_USER="${DEVICE_USER:-pi}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPLOY_DIR="${SCRIPT_DIR}/../deploy/systemd"
GATEWAY_DIR="${SCRIPT_DIR}/../services/privacy_gateway"
echo "=== [1/4] 安装 mitmproxy ==="
ssh "${DEVICE_USER}@${DEVICE_IP}" "pip3 install mitmproxy httpx cryptography 2>&1 | tail -5"
echo "=== [2/4] 上传 Privacy Gateway 服务文件 ==="
ssh "${DEVICE_USER}@${DEVICE_IP}" "mkdir -p /data/project/KVM-privacy/services/privacy_gateway"
scp "${GATEWAY_DIR}"/*.py "${DEVICE_USER}@${DEVICE_IP}:/data/project/KVM-privacy/services/privacy_gateway/"
scp "${GATEWAY_DIR}/ai_domains.txt" "${DEVICE_USER}@${DEVICE_IP}:/data/project/KVM-privacy/services/privacy_gateway/"
echo "=== [3/4] 生成 CA 证书 ==="
ssh "${DEVICE_USER}@${DEVICE_IP}" "python3 -c '
import sys; sys.path.insert(0, \"/data/project/KVM-privacy/services/privacy_gateway\")
from cert_manager import ensure_ca
cert, key = ensure_ca()
print(f\"CA 证书: {cert}\")
'"
echo "=== [4/4] 安装 systemd 服务 ==="
for svc in info-privacy mem-bridge-memory mem-bridge-router privacy-gateway; do
if [ -f "${DEPLOY_DIR}/${svc}.service" ]; then
scp "${DEPLOY_DIR}/${svc}.service" "${DEVICE_USER}@${DEVICE_IP}:/tmp/${svc}.service"
ssh "${DEVICE_USER}@${DEVICE_IP}" "echo '123123' | sudo -S cp /tmp/${svc}.service /etc/systemd/system/${svc}.service"
fi
done
ssh "${DEVICE_USER}@${DEVICE_IP}" "echo '123123' | sudo -S systemctl daemon-reload"
echo "=== 完成!==="
echo "启用服务: ssh ${DEVICE_USER}@${DEVICE_IP} 'sudo systemctl enable --now privacy-gateway'"
echo "下载 CA 证书: curl http://${DEVICE_IP}:8080/api/v1/privacy/cert -H 'Authorization: Bearer <token>' -o kvm-ca.crt"
+100
View File
@@ -0,0 +1,100 @@
"""KVM Privacy Gateway —— mitmproxy addon 核心。
工作流程:
1. 检查请求域名是否在 AI 白名单中
2. 若是,且请求包含文件上传:
a. 提取文件字段
b. 调用 upload_scanner 做 PII 检测 + 脱敏
c. 脱敏后字节替换原始请求体
3. 记录审计日志(不存原文,只存 PII 类型计数和文件哈希)
4. 放行(透明代理,用户感知不到拦截)
"""
from __future__ import annotations
import asyncio
import logging
from pathlib import Path
from mitmproxy import ctx, http
from audit_logger import AuditLogger
from interceptor import extract_files, is_file_upload, rebuild_multipart
from upload_scanner import scan_and_redact
logger = logging.getLogger(__name__)
_DOMAINS_FILE = Path(__file__).parent / "ai_domains.txt"
_audit = AuditLogger()
def _load_domains() -> set[str]:
"""从文件加载 AI 域名白名单(过滤注释行)。"""
if not _DOMAINS_FILE.exists():
return set()
return {
line.strip()
for line in _DOMAINS_FILE.read_text().splitlines()
if line.strip() and not line.startswith("#")
}
_AI_DOMAINS: set[str] = _load_domains()
class PrivacyGatewayAddon:
"""mitmproxy addon:拦截上传到 AI 服务的附件并自动脱敏。"""
def request(self, flow: http.HTTPFlow) -> None:
host = flow.request.pretty_host
if host not in _AI_DOMAINS:
return # 非 AI 域名,直接放行
if not is_file_upload(flow):
return # 非文件上传,直接放行
files = extract_files(flow)
if not files:
return
ctx.log.info(f"[privacy-gw] 拦截 {host} 上传 {len(files)} 个文件")
# 异步扫描在 mitmproxy 的事件循环中运行
asyncio.ensure_future(self._process_upload(flow, host, files))
async def _process_upload(self, flow, host: str, files) -> None:
replacements: dict[str, bytes] = {}
total_pii: dict[str, int] = {}
total_files = len(files)
for f in files:
try:
result = await scan_and_redact(f.filename, f.data)
except Exception as e:
ctx.log.warn(f"[privacy-gw] 扫描失败 {f.filename}: {e}")
_audit.log(host, {}, "bypass", f.data, total_files)
continue
# 合并 PII 统计
for pii_type, count in result.pii_types.items():
total_pii[pii_type] = total_pii.get(pii_type, 0) + count
if result.pii_found and result.redacted_bytes:
replacements[f.field_name] = result.redacted_bytes
ctx.log.info(
f"[privacy-gw] 自动脱敏 {f.filename}: {result.pii_types}"
)
_audit.log(
host,
result.pii_types,
"auto_redact" if result.redacted_bytes else "bypass",
f.data,
total_files,
)
if replacements:
flow.request.content = rebuild_multipart(flow, replacements)
ctx.log.info(f"[privacy-gw] 请求已脱敏,放行至 {host}")
addons = [PrivacyGatewayAddon()]
+10
View File
@@ -0,0 +1,10 @@
# AI 服务域名白名单 —— 仅拦截这些域名的 HTTPS 流量进行 PII 扫描
# 每行一个域名;以 # 开头的行为注释
api.anthropic.com
api.openai.com
generativelanguage.googleapis.com
dashscope.aliyuncs.com
api.cohere.com
api.mistral.ai
api.together.xyz
api.groq.com
+85
View File
@@ -0,0 +1,85 @@
"""审计日志 —— SQLite append-only,只记 PII 类型/哈希,不存原文。"""
from __future__ import annotations
import hashlib
import json
import sqlite3
import threading
from datetime import datetime
from pathlib import Path
_DB_PATH = Path("/var/lib/kvm-privacy/audit.db")
_SCHEMA = """
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts TEXT NOT NULL,
domain TEXT NOT NULL,
pii_types TEXT, -- JSON {"id_card":2,"phone":1}
action TEXT NOT NULL, -- auto_redact | blocked | bypass
doc_hash TEXT, -- sha256(original_bytes) — 不存原文
file_count INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_ts ON audit_log(ts);
CREATE INDEX IF NOT EXISTS idx_domain ON audit_log(domain);
"""
class AuditLogger:
def __init__(self, db_path: Path = _DB_PATH) -> None:
db_path.parent.mkdir(parents=True, exist_ok=True)
self._db_path = str(db_path)
self._lock = threading.Lock()
with self._connect() as conn:
conn.executescript(_SCHEMA)
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self._db_path, check_same_thread=False)
conn.row_factory = sqlite3.Row
return conn
def log(
self,
domain: str,
pii_types: dict[str, int],
action: str,
raw_bytes: bytes | None = None,
file_count: int = 0,
) -> None:
doc_hash = hashlib.sha256(raw_bytes).hexdigest() if raw_bytes else None
with self._lock, self._connect() as conn:
conn.execute(
"INSERT INTO audit_log(ts, domain, pii_types, action, doc_hash, file_count) "
"VALUES (?,?,?,?,?,?)",
(
datetime.utcnow().isoformat(),
domain,
json.dumps(pii_types) if pii_types else None,
action,
doc_hash,
file_count,
),
)
def query(
self, limit: int = 50, offset: int = 0, domain: str | None = None
) -> list[dict]:
sql = "SELECT * FROM audit_log"
params: list = []
if domain:
sql += " WHERE domain = ?"
params.append(domain)
sql += " ORDER BY id DESC LIMIT ? OFFSET ?"
params.extend([limit, offset])
with self._connect() as conn:
rows = conn.execute(sql, params).fetchall()
return [dict(r) for r in rows]
def stats_today(self) -> dict:
today = datetime.utcnow().date().isoformat()
with self._connect() as conn:
row = conn.execute(
"SELECT COUNT(*) AS requests, SUM(file_count) AS files "
"FROM audit_log WHERE ts >= ?",
(today,),
).fetchone()
return {"requests": row["requests"] or 0, "files": row["files"] or 0}
+78
View File
@@ -0,0 +1,78 @@
"""CA 证书管理 —— 生成设备级自签名 CA,供 mitmproxy 使用。"""
from __future__ import annotations
import datetime
from pathlib import Path
_CA_DIR = Path("/etc/kvm-privacy/ca")
_CA_KEY = _CA_DIR / "ca.key"
_CA_CERT = _CA_DIR / "ca.crt"
_CA_P12 = _CA_DIR / "ca.p12"
def ensure_ca() -> tuple[Path, Path]:
"""确保 CA 证书存在,如不存在则生成。返回 (cert_path, key_path)。"""
_CA_DIR.mkdir(parents=True, exist_ok=True)
if _CA_CERT.exists() and _CA_KEY.exists():
return _CA_CERT, _CA_KEY
try:
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
except ImportError:
raise RuntimeError(
"缺少 cryptography 包,请运行: pip install cryptography"
)
# 生成 RSA-4096 私钥
key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "KVM-Privacy Hub"),
x509.NameAttribute(NameOID.COMMON_NAME, "KVM-Privacy Root CA"),
])
now = datetime.datetime.utcnow()
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now)
.not_valid_after(now + datetime.timedelta(days=3650)) # 10 年
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
.add_extension(
x509.KeyUsage(
digital_signature=True, key_cert_sign=True, crl_sign=True,
content_commitment=False, key_encipherment=False,
data_encipherment=False, key_agreement=False,
encipher_only=False, decipher_only=False,
),
critical=True,
)
.sign(key, hashes.SHA256())
)
_CA_KEY.write_bytes(
key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.TraditionalOpenSSL,
serialization.NoEncryption(),
)
)
_CA_KEY.chmod(0o600)
_CA_CERT.write_bytes(cert.public_bytes(serialization.Encoding.PEM))
print(f"[cert_manager] CA 证书已生成: {_CA_CERT}")
return _CA_CERT, _CA_KEY
def get_ca_pem() -> bytes:
"""返回 CA 证书 PEM 字节,供用户下载安装到系统信任库。"""
cert, _ = ensure_ca()
return cert.read_bytes()
+104
View File
@@ -0,0 +1,104 @@
"""请求拦截逻辑 —— 提取多部分上传中的文件,触发 PII 扫描,替换后放行。"""
from __future__ import annotations
import email.parser
import io
import re
from typing import NamedTuple
from mitmproxy.http import HTTPFlow
class ExtractedFile(NamedTuple):
field_name: str
filename: str
content_type: str
data: bytes
# 常见 AI API 文件上传 Content-Type
_MULTIPART_RE = re.compile(r"multipart/form-data;\s*boundary=(.+)", re.IGNORECASE)
_FILE_EXTS = {".pdf", ".doc", ".docx", ".txt", ".png", ".jpg", ".jpeg", ".csv"}
def is_file_upload(flow: HTTPFlow) -> bool:
"""判断请求是否包含文件上传。"""
ct = flow.request.headers.get("content-type", "")
return bool(_MULTIPART_RE.match(ct))
def extract_files(flow: HTTPFlow) -> list[ExtractedFile]:
"""从 multipart 请求体中提取文件字段。"""
ct = flow.request.headers.get("content-type", "")
m = _MULTIPART_RE.match(ct)
if not m:
return []
boundary = m.group(1).strip().strip('"')
# 构造标准 MIME multipart 消息供 email.parser 解析
msg_bytes = (
f"Content-Type: multipart/form-data; boundary={boundary}\r\n\r\n".encode()
+ flow.request.content
)
parser = email.parser.BytesParser()
msg = parser.parsebytes(msg_bytes)
files: list[ExtractedFile] = []
for part in msg.get_payload(): # type: ignore[union-attr]
disposition = part.get("Content-Disposition", "")
if "filename=" not in disposition:
continue # 跳过非文件字段
fn_match = re.search(r'filename="?([^";]+)"?', disposition)
fn_match_field = re.search(r'name="?([^";]+)"?', disposition)
filename = fn_match.group(1) if fn_match else "unknown"
field_name = fn_match_field.group(1) if fn_match_field else "file"
_, ext = filename.rsplit(".", 1) if "." in filename else (filename, "")
if f".{ext.lower()}" not in _FILE_EXTS:
continue # 只处理已知文件类型
files.append(ExtractedFile(
field_name=field_name,
filename=filename,
content_type=part.get_content_type(),
data=part.get_payload(decode=True) or b"",
))
return files
def rebuild_multipart(flow: HTTPFlow, replacements: dict[str, bytes]) -> bytes:
"""将 replacements 中的文件字段替换后重建 multipart 请求体。
replacements: {field_name: new_file_bytes}
"""
ct = flow.request.headers.get("content-type", "")
m = _MULTIPART_RE.match(ct)
if not m:
return flow.request.content
boundary = m.group(1).strip().strip('"')
msg_bytes = (
f"Content-Type: multipart/form-data; boundary={boundary}\r\n\r\n".encode()
+ flow.request.content
)
parser = email.parser.BytesParser()
msg = parser.parsebytes(msg_bytes)
parts_out: list[bytes] = []
for part in msg.get_payload(): # type: ignore[union-attr]
disposition = part.get("Content-Disposition", "")
fn_match_field = re.search(r'name="?([^";]+)"?', disposition)
field_name = fn_match_field.group(1) if fn_match_field else ""
if field_name in replacements:
# 替换文件内容,保留其他头部
new_data = replacements[field_name]
out = io.BytesIO()
for key, val in part.items():
out.write(f"{key}: {val}\r\n".encode())
out.write(b"\r\n")
out.write(new_data)
parts_out.append(out.getvalue())
else:
parts_out.append(part.as_bytes())
sep = f"--{boundary}\r\n".encode()
end = f"--{boundary}--\r\n".encode()
return sep + (b"\r\n" + sep).join(parts_out) + b"\r\n" + end
@@ -0,0 +1,81 @@
"""上传扫描器 —— 调用 info-privacy API 对文件内容进行 PII 检测和脱敏。
内存处理原则:
- 原始文件字节在内存中传递,不写磁盘(analyze/frame 端点接受 multipart 上传)
- 脱敏后字节替换原始请求体,原始字节仅用于 sha256 哈希计算
"""
from __future__ import annotations
import hashlib
import io
from dataclasses import dataclass
import httpx
_INFO_PRIVACY_BASE = "http://localhost:8000/api/v1"
_REDACT_TYPES = ["id_card", "phone", "bank_card", "email", "license_plate", "name", "address", "face"]
@dataclass
class ScanResult:
pii_found: bool
pii_types: dict[str, int] # e.g. {"id_card": 2, "phone": 1}
redacted_bytes: bytes | None # None if no PII found
original_hash: str # sha256 of original bytes
async def scan_and_redact(filename: str, raw_bytes: bytes) -> ScanResult:
"""对上传文件进行 PII 扫描,如有 PII 则脱敏并返回脱敏后字节。
步骤:
1. 调用 /api/v1/analyze 检测 PII 类型和位置
2. 如有 PII,调用 /api/v1/redact 获取脱敏文件
3. 返回脱敏后字节(内存处理,不落盘)
"""
original_hash = hashlib.sha256(raw_bytes).hexdigest()
async with httpx.AsyncClient(timeout=30.0) as client:
# Step 1: 分析 PII
analyze_resp = await client.post(
f"{_INFO_PRIVACY_BASE}/analyze",
files={"file": (filename, io.BytesIO(raw_bytes))},
)
if analyze_resp.status_code != 200:
return ScanResult(
pii_found=False,
pii_types={},
redacted_bytes=None,
original_hash=original_hash,
)
report = analyze_resp.json()
summary: dict[str, int] = report.get("summary", {})
if not summary or report.get("blocked"):
return ScanResult(
pii_found=bool(summary),
pii_types=summary,
redacted_bytes=None,
original_hash=original_hash,
)
# Step 2: 脱敏
redact_resp = await client.post(
f"{_INFO_PRIVACY_BASE}/redact",
files={"file": (filename, io.BytesIO(raw_bytes))},
data={"redact_types": list(summary.keys())},
)
if redact_resp.status_code != 200:
return ScanResult(
pii_found=True,
pii_types=summary,
redacted_bytes=None,
original_hash=original_hash,
)
return ScanResult(
pii_found=True,
pii_types=summary,
redacted_bytes=redact_resp.content,
original_hash=original_hash,
)