Files
auto_control/doc/DATA_MODEL.md
T
butubb 24d57d3b96 docs: doc/ 全量重整——按现状重写并建立文档索引;项目统一更名 auto_control
背景:文档长期落后于代码(Tab 数、任务类型、接口示例等多处与现状不符),
且信息分散重复。这次按当前代码状态逐篇重写,并建立统一的文档体系。

新增
- doc/README.md:文档总索引(文档地图 / 推荐阅读路径 / **文档维护约定**)
- doc/DATA_MODEL.md:数据模型(7 张模型表 + 5 张非模型表、迁移机制、app_meta 键、
  数据目录、备份覆盖清单与双向自检)
- doc/AI_CONSOLE.md:AI 控制台机制(会话与 SSE、经验库/动作库蒸馏与召回、巡检、
  Markdown 渲染、推理链、token 统计、故障排查)

重写(按现状,去掉过时与重复)
- README.md:7 个 Tab、18 种步骤、设备生命周期、调度/窗口语义、常见问题;修掉
  「6 个 Tab / 分组为顶级 Tab」等过时内容与损坏的目录树
- doc/ARCHITECTURE.md:补启动装配顺序(import 期副作用、A~G 七阶段)、线程与锁清单、
  设备状态机、调度全链路、前端结构与实时通道、设计决策、**已知缺陷与踩坑清单**、扩展点
- doc/API.md:按蓝图重建「接口总索引」(107 条路由含鉴权)+ 分域详细说明 +
  非 JSON 响应汇总 + 错误分支速查
- doc/TASK_DEV.md:18 种步骤全表(参数/默认值/语义)、容器与公共参数、
  选择器与 XPath 序号语义、抓取器建议规则、新增任务类型骨架
- doc/DEPLOY.md:容器入口 start.sh 三件事、发布流程与检查清单、备份覆盖红线、
  按现象分类的故障排查
- doc/DEVELOPMENT.md:流程/红线/本地开发/**测试与写测试的约定**/配置速查/文档同步
- doc/MCP.md:19 个工具的参数级清单、坐标空间、写门控三连、安全与审计
- doc/MCP_DESIGN.md、doc/AI_TASK_GEN.md:标注设计 vs 实现现状,补交叉链接
- doc/backlog/TODO.md:新增「已知缺陷」小节(含复现与影响)+ 已完成留档
- .env.example:按代码实际读取的键重写(补 USB/DISCOVERY/MCP/AGENT,删死配置)

其它
- 项目名统一 auto_control:README/文档/scripts/pack.py 产物名;代码内的
  doc 章节引用(templates/admin/monitor.html)同步更新
- 校验:16 篇文档 156 条相对链接全部可解析;文档中的关键数字与代码核对一致
  (19 个 MCP 工具 / 18 种步骤 / 12 张备份表 / 1 种任务类型)
2026-09-10 22:19:18 +08:00

247 lines
12 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.
# 数据模型(DATA_MODEL)
> 适用读者:改后端 / 排数据问题的开发者与运维。
> 相关文档:[ARCHITECTURE.md](ARCHITECTURE.md)(运行时架构)、[DEPLOY.md](DEPLOY.md) §数据备份(备份覆盖红线)、[API.md](API.md)(消费这些数据的接口)。
> **表结构以 `core/models.py` 与各模块的建表 SQL 为准**,本文是它们的映射说明。
---
## 1. 总览
| 项 | 值 |
|----|----|
| 引擎 | SQLite(单文件 `data/users.db`) |
| driver | Flask-SQLAlchemy(SQLAlchemy 2.x) |
| 连接 PRAGMA | `journal_mode=WAL`、`busy_timeout=5000`、`synchronous=NORMAL` |
| 建表方式 | ① 模型表:`db.create_all()`;② 版本化迁移 `SCHEMA_MIGRATIONS`;③ 幂等原生 `CREATE TABLE IF NOT EXISTS` |
| 主库文件 | `data/users.db`(运行时另有 `-wal` / `-shm`) |
| 当前 schema 版本 | `app_meta.schema_version = 4` |
**表清单(12 张业务表 + 1 张 SQLite 内部表)**
| # | 表 | 来源 | 用途 |
|---|---|------|------|
| 1 | `user` | 模型 | 登录用户与权限 |
| 2 | `device_group` | 模型 | 设备分组(JSON 存 serial 列表) |
| 3 | `task_job` | 模型 | 任务计划 |
| 4 | `custom_action` | 模型 | 自定义动作(可复用步骤包) |
| 5 | `apk_file` | 模型 | APK 记录 |
| 6 | `device` | 模型 + 迁移 v2/v3 | 设备池 |
| 7 | `pending_device` | 模型 + 迁移 v4 | 待确认的发现设备 |
| 8 | `app_meta` | 原生(迁移前建) | KV 配置(schema 版本、AI 配置、发现配置) |
| 9 | `agent_conversation` | 原生(`web/agent_api.py`) | AI 控制台会话 |
| 10 | `agent_experience` | 原生(`web/agent_api.py`) | 经验库(任务级配方) |
| 11 | `experience_audit` | 原生(`web/agent_api.py`) | 经验巡检结论 |
| 12 | `agent_action` | 原生(`web/agent_api.py`) | 动作库(命名动作) |
| — | `sqlite_sequence` | SQLite 内部 | `AUTOINCREMENT` 的附带产物(备份自检时按 `sqlite_` 前缀排除) |
> **没有外键、没有关系(relationship)、没有索引**:全部靠应用层维护一致性。分组 ↔ 设备是多对多的 **JSON 列表**(`device_group.serials`),删除设备不会级联清理分组里的 serial。
---
## 2. 模型表(`core/models.py`)
### 2.1 `user` — 登录用户
| 列 | 类型 | 默认 | 说明 |
|----|------|------|------|
| `id` | Integer | — | 主键 |
| `username` | String(80) | — | **唯一**,非空 |
| `password_hash` | String(255) | — | werkzeug 加盐哈希;兼容旧裸 SHA-256(校验通过后自动升级) |
| `is_admin` | Boolean | `True` | 管理员不受权限位限制 |
| `perms` | Text | `"[]"` | JSON 数组:`tasks` / `devices` / `apks` / `logs` |
方法:`get_perms` / `set_perms` / `has_perm` / `set_password` / `check_password`。
### 2.2 `device_group` — 设备分组
| 列 | 类型 | 默认 | 说明 |
|----|------|------|------|
| `id` | Integer | — | 主键 |
| `name` | String(80) | — | **唯一**,非空 |
| `serials` | Text | `"[]"` | 组内设备 serial 的 JSON 数组 |
| `description` | Text | `""` | 备注 |
### 2.3 `task_job` — 任务计划
| 列 | 类型 | 默认 | 说明 |
|----|------|------|------|
| `id` | String(32) | — | 主键,uuid 前 8 位 |
| `name` | String(120) | — | 非空 |
| `task_type` | String(60) | `"generic_steps"` | 必须已注册 |
| `target` | Text | `'{"mode":"all"}'` | JSON |
| `params` | Text | `"{}"` | JSON(与任务类默认值合并后使用) |
| `schedule` | Text | `'{"mode":"once"}'` | JSON |
| `retry` | Text | `'{"max_attempts":1,"delay":60}'` | JSON |
| `enabled` | Boolean | `True` | 是否参与调度 |
### 2.4 `custom_action` — 自定义动作
| 列 | 类型 | 默认 | 说明 |
|----|------|------|------|
| `id` | String(32) | — | 主键 |
| `name` | String(120) | — | 非空 |
| `icon` | String(4) | `"📦"` | 展示图标 |
| `steps` | Text | `"[]"` | JSON,schema 与 `generic_steps` 的 `params.steps` 一致 |
| `created_at` | String(20) | `""` | 时间串 |
### 2.5 `apk_file` — APK 记录
| 列 | 类型 | 默认 | 说明 |
|----|------|------|------|
| `id` | String(32) | — | 主键;磁盘文件名为 `<id>.apk` |
| `filename` | String(255) | — | 原始文件名 |
| `display_name` / `package_name` / `version_name` | String | `""` | 解析结果 |
| `version_code` / `size` | Integer | `0` | |
| `upload_time` | String(20) | `""` | |
### 2.6 `device` — 设备池
| 列 | 类型 | 默认 | 说明 |
|----|------|------|------|
| `serial` | String(120) | — | 主键:`IP:5555` 或 USB 序列号 |
| `name` | String(80) | `""` | 备注名 |
| `model` | String(120) | `""` | 型号(迁移 v3 追加) |
| `enabled` | Boolean | `True` | 停用则不参与调度 |
| `note` | Text | `""` | |
| `created_at` | String(20) | `""` | |
### 2.7 `pending_device` — 待确认的发现设备
| 列 | 类型 | 默认 | 说明 |
|----|------|------|------|
| `serial` | String(120) | — | 主键 |
| `source` | String(20) | `""` | `lan` / `tailscale` |
| `first_seen` / `last_seen` | String(20) | `""` | 时间串 |
> ⚠️ SQLAlchemy 模型的 `default=` 是 **Python 侧默认值**,SQLite 建表语句里没有 `DEFAULT` 子句;只有原生建表的表才有真正的 SQL DEFAULT。
---
## 3. 非模型表
### 3.1 `app_meta` — KV 配置
| 列 | 类型 |
|----|------|
| `key` | TEXT PRIMARY KEY |
| `value` | TEXT |
由 `_migrate_schema()` 建表(启动必执行)。**所有键见 §5**。
### 3.2 AI 相关四张表(`web/agent_api.py`,原生建表)
| 表 | 列 |
|----|----|
| `agent_conversation` | `id`(PK) · `title` · `messages`(JSON) · `created_at` · `updated_at` |
| `agent_experience` | `id`(PK AUTOINCREMENT) · `task_prompt` · `recipe` · `tool_seq` · `hits` · `created_at` |
| `experience_audit` | `id`(PK) · `exp_id` · `verdict`(keep/delete) · `score`(REAL) · `reason` · `hits` · `action`(pending/kept/deleted) · `audited_at` |
| `agent_action` | `id`(PK) · `name` · `app` · `aliases`(JSON) · `params`(JSON) · `steps`(JSON) · `preconditions` · `hits` · `source_prompt` · `created_at` · `updated_at` |
> ⚠️ 这四张表**不走 `SCHEMA_MIGRATIONS`,无版本管理**,由各模块首次使用时 `CREATE TABLE IF NOT EXISTS` 幂等创建;建表失败会被 `except: pass` 吞掉(后续 SQL 才会报错)。新增此类表时,务必同时登记进备份覆盖清单(§6)。
---
## 4. 迁移机制
### 4.1 版本化迁移 `SCHEMA_MIGRATIONS`
| 版本 | 内容 | SQL 幂等性 |
|------|------|-----------|
| 1 | `user` 增加 `perms` | `ALTER TABLE`(非幂等,靠 duplicate column 兜底) |
| 2 | 建 `device` 表(**无 `model` 列**) | 幂等(`IF NOT EXISTS`) |
| 3 | `device` 增加 `model` | `ALTER TABLE`(同上兜底) |
| 4 | 建 `pending_device` 表 | 幂等 |
执行器 `_migrate_schema()`:
1. 先 `CREATE TABLE IF NOT EXISTS app_meta(...)`(幂等)
2. 读 `app_meta.schema_version`(缺省 0)
3. 只执行 `version > current` 的迁移;**SQL 报 `duplicate column name` 时回滚该语句但仍记版本号**(自愈:`create_all` 已建列而版本未记录时不再卡住),其它异常才抛出
4. 每条迁移后 `INSERT OR REPLACE app_meta('schema_version', v)` 并提交
5. 整体包 try:失败只记 error 日志,**不阻塞启动**
### 4.2 旧 JSON 迁移(一次性)
启动时若存在 `data/groups.json` / `data/jobs.json`:对应表为空则导入,随后把文件重命名为 `<name>.json.migrated` 归档。**库非空但 JSON 仍在 → 直接归档**,防止"用户删空数据后重启又复原"。
`init_db(app)` 顺序:`db.init_app` → `create_all()` → `_migrate_schema()` → `_ensure_default_admin()`(首次创建 `admin/admin123`)→ `_migrate_old_json()`。
---
## 5. `app_meta` 键清单
| key | 用途 | 写入方 |
|-----|------|--------|
| `schema_version` | 迁移版本游标 | `core/models.py` |
| `agent_api_base` | AI 接口地址 | AI 控制台配置页 |
| `agent_model` | 模型名 | 同上 |
| `agent_api_key` | API Key(**明文存库**) | 同上 |
| `agent_default_serial` | 默认目标设备 | 同上 |
| `agent_max_steps` | 最大步数(钳制 1-200,默认 40) | 同上 |
| `discovery_enabled` | 自动发现开关(`"1"`/`"0"`) | 工具页「设备池管理」 |
| `discovery_subnets` | 扫描网段 JSON 数组 | 同上 |
| `discovery_interval` | 扫描周期秒(10-3600) | 同上 |
| `discovery_port` | adb 探测端口(1-65535) | 同上 |
> ⚠️ `agent_api_key` 是**明文存储**,导出备份的 zip 里也含它——备份预览会固定给出"含敏感信息"告警。
---
## 6. 备份覆盖清单(红线)
`core/system_backup.py` 的 `SUMMARY_TABLES`(12 张,与库中实际业务表一一对应):
| 表 | 中文标签 |
|----|---------|
| `app_meta` | 系统配置(app_meta) |
| `user` | 用户 |
| `device_group` | 设备分组 |
| `task_job` | 任务计划 |
| `custom_action` | 自定义动作 |
| `apk_file` | APK 记录 |
| `device` | 设备池 |
| `pending_device` | 待连接设备 |
| `agent_conversation` | AI 会话 |
| `agent_experience` | 经验库 |
| `experience_audit` | 经验巡检 |
| `agent_action` | 动作库 |
**双向自检**:
- **导出侧**:登记在 `SUMMARY_TABLES` 但快照里缺失 → 写入 `manifest.coverage_missing` + 日志告警
- **导入侧**:备份里出现未登记的表(排除 `sqlite_` 前缀)→ 预览告警"请登记进 `core/system_backup.py` 的覆盖清单"(字段 `extra_tables`)
- `REQUIRED_TABLES = (app_meta, user, task_job, device_group)`:缺任一直接拒绝导入
> **红线**:**新增任何持久化表(含原生建表的 agent 类表),必须同时登记进 `SUMMARY_TABLES` 与 `TABLE_LABELS`**,并更新 [DEPLOY.md](DEPLOY.md) §数据备份。历史教训:`agent_action` 曾漏登记,导致"动作库看起来没备份"(数据其实在快照里,只是清单没列)。
---
## 7. 数据目录
| 路径 | 内容 | 进 git |
|------|------|--------|
| `data/users.db`(+`-wal`/`-shm`) | SQLite 主库 | 否 |
| `data/apks/*.apk` | 上传的 APK | 否 |
| `data/backups/` | 导出临时 zip、`pre_restore_*.db`(导入前安全网)、`restore_failed_*` | 否 |
| `data/restore_staging/<token>/` | 导入暂存(TTL 1800s 自动清理) | 否 |
| `data/restore_pending/` | 待生效恢复任务(重启时消费) | 否 |
| `data/mcp_audit.log` | MCP 调用审计(路径由 `MCP_AUDIT_FILE` 指定) | 否 |
| `data/uiauto.pid` | uiautodev 子进程 PID | 否 |
| `data/*.json.migrated` | 旧 JSON 迁移归档 | 否 |
| `logs/*.log` | 运行日志 | 否 |
> `data/` 与 `logs/` 全部是运行时产物,**任何文件都不入 git**。生产机上的库由「系统 → 数据备份导出/导入」或整目录手工备份。
---
## 8. 约定与注意事项
1. **主键用 uuid 前 8 位字符串**(`task_job` / `custom_action` / `apk_file` / `agent_conversation`):可读性好,理论上有碰撞概率(8 位 hex = 32 bit)。
2. **JSON 字段一律 Text 存储**,读写通过 `get_*/set_*` 方法;解析失败回退默认值。
3. **时间统一为字符串**(`"YYYY-MM-DD HH:MM"` 或 `"YYYY-MM-DD HH:MM:SS"`),排序依赖字符串序。
4. **删除必须显式删行**(`delete_job` / `delete_group`):只 upsert 会导致重启后数据"复活"。
5. **跨线程访问 DB 必须自推 app context**(后台线程里用 `with app.app_context()`)。
6. **不要手工改库结构**:走 `SCHEMA_MIGRATIONS`(模型表)或幂等建表(原生表),否则 `create_all` 与版本号会不一致。
7. **改表结构后记得**:① 更新本文;② 若是新表,登记备份覆盖清单(§6 红线)。