docs: 同步 P5 备份机制重写——DEPLOY §5 / API §13 / DATA_MODEL §7 / DEVELOPMENT §4.3

- DEPLOY §5.1: 说明 SQLite 现在是「备份交换格式」,恢复走单事务整库替换,
  跨环境导入默认拒绝;§5.2 覆盖清单改为「由 metadata 派生」;§5.3 三个目录
  支持环境变量覆盖及原因
- API §13: 导出/预览/应用三个接口的字段补齐(db_backend/deployment_env/
  source_db_id/source_env/current_env、force_env_mismatch、pre_restore zip)
- DATA_MODEL §7: pre_restore 改 .zip、补目录可覆盖说明
- DEVELOPMENT §4.3: 补三个备份目录环境变量
This commit is contained in:
2026-09-13 10:35:26 +08:00
parent 4899e66c68
commit 2a3775ffd5
4 changed files with 72 additions and 24 deletions
+23 -11
View File
@@ -610,37 +610,49 @@
### POST /api/system/backup/export
**JSON body**(不是表单):`{"include_apks": false}`。响应为 **zip 附件**:
**JSON body**(不是表单):`{"include_apk": true}`(默认 true)。响应为 **zip 附件**:
```
users.db # sqlite 在线备份 API 做的一致快照
apks/*.apk # include_apks=true 时
manifest.json # {format:"auto_control_backup", version, created_at, schema_version,
# include_apk, tables:[{table,label,rows}], apks:[…], coverage_missing?}
users.db # 当前库(MySQL 或回退 SQLite)整库一致快照成的 SQLite 归档
apks/*.apk # include_apk=true 时
manifest.json # {format:"auto_control_backup", version:2, created_at, schema_version,
# db_backend, deployment_env, source_db_id, include_apk,
# tables:[{table,label,rows}], apks:[…], coverage_missing?}
```
> SQLite 在这里是**备份交换格式**而非运行时库:MySQL 下导出时把连接提到 REPEATABLE READ,
> 保证 12 张表读的是同一时刻。
### POST /api/system/backup/preview
multipart 上传 `.zip` 或 `.db`(字段 `file`)→ 校验并暂存:
```json
{"ok": true, "token": "e172dc75e2f7",
"preview": {"integrity": "ok", "schema_version": 4, "current_schema_version": 4,
"tables": [{"table":"agent_action","label":"动作库","rows":4}],
"preview": {"integrity": "ok", "schema_version": 6, "current_schema_version": 6,
"source_env": "dev", "current_env": "dev", "source_backend": "mysql",
"source_db_id": "704fbbb0…",
"tables": [{"table":"agent_action","label":"动作库","rows":5}],
"missing_optional": [], "extra_tables": [],
"warnings": ["备份为全量快照,含用户口令哈希、AI 控制台 API Key 等敏感信息…"]}}
"warnings": ["备份为全量数据,含用户口令哈希、AI 配置里的 API Key 等敏感信息…"]}}
```
- 完整性 `PRAGMA integrity_check` 必须 `ok`;缺必需表(`app_meta`/`user`/`task_job`/`device_group`)直接拒绝
- `extra_tables` 非空 = 备份含**未登记进覆盖清单**的表(提示去登记)
- `extra_tables` 非空 = 备份含**未登记的表**(提示去登记)
- `source_env` ≠ `current_env` 时 `warnings` 会多一条跨环境告警
- 暂存 **TTL 30 分钟**,过期自动清理
- 文件非法 / 未选文件 → 400
### POST /api/system/backup/apply
`{"token":"…"}` → 先自动把当前库快照到 `data/backups/pre_restore_<ts>.db`(安全网),再把暂存库落到 `data/restore_pending/`。
`{"token":"…", "force_env_mismatch": false}` → 先自动把当前库导出成
`data/backups/pre_restore_<ts>.zip`(安全网,可直接再导入回来),再把暂存归档落到
`data/restore_pending/`。
**必须重启服务才生效**(`web_server.py` 在 `init_db` 之前消费该目录)。token 无效/暂存缺失 → 400。
- **必须重启服务才生效**:`web_server.py` 在 `init_db` 之后、`TaskManager` 之前消费该目录
- 重启时**单事务整库替换**(DELETE 全表 + 分块 INSERT),失败自动回滚,当前数据不受影响
- 备份来源环境与当前库不符时**默认拒绝**(400),要跨环境须显式传 `force_env_mismatch: true`
- token 无效/暂存缺失 → 400
---
+6 -2
View File
@@ -271,9 +271,13 @@ SUMMARY_TABLES = tuple(sorted(t.name for t in db.metadata.tables.values()))
|------|------|--------|
| `data/users.db`(+`-wal`/`-shm`) | SQLite 主库(**仅回退模式用**;连 MySQL 时这些文件不被读写,可留作历史归档) | 否 |
| `data/apks/*.apk` | 上传的 APK | 否 |
| `data/backups/` | 导出临时 zip、`pre_restore_*.db`(导入前安全网)、`restore_failed_*` | 否 |
| `data/backups/` | 导出临时 zip、`pre_restore_*.zip`(导入前安全网)、`restore_failed_*` | 否 |
| `data/restore_staging/<token>/` | 导入暂存(TTL 1800s 自动清理) | 否 |
| `data/restore_pending/` | 待生效恢复任务(重启时消费) | 否 |
| `data/restore_pending/` | 待生效恢复任务(重启时单事务消费) | 否 |
> 后三个目录可用环境变量改到别处(`DATA_BACKUP_DIR` / `DATA_RESTORE_STAGING_DIR` /
> `DATA_RESTORE_PENDING_DIR`)——自动化测试必须这么做,否则测试造的待生效恢复任务
> 会被服务当成用户的操作在下次重启时消费掉。
| `data/mcp_audit.log` | MCP 调用审计(路径由 `MCP_AUDIT_FILE` 指定) | 否 |
| `data/uiauto.pid` | uiautodev 子进程 PID | 否 |
| `data/*.json.migrated` | 旧 JSON 迁移归档 | 否 |
+42 -11
View File
@@ -183,25 +183,56 @@ tail -20 logs/web.log # 无 ERROR/Traceback
**「系统 → 数据备份 / 导入恢复」**(仅管理员):
- **导出**:`POST /api/system/backup/export` → sqlite 在线备份 API 做一致快照 → 打包 zip(`users.db` + `manifest.json` + 可选 `apks/*.apk`)
- **导入**:上传 zip/`.db` → 校验预览(完整性 / 必需表 / schema 版本 / 未登记表告警)→ 确认后自动把当前库快照到 `data/backups/pre_restore_*.db`(安全网)→ 落 `data/restore_pending/` → **重启服务生效**
- **导出**:`POST /api/system/backup/export` → 把当前库(MySQL 或回退模式的 SQLite)
整库一致快照成一份 SQLite 归档 → 打包 zip(`users.db` + `manifest.json` + 可选 `apks/*.apk`)
- **导入**:上传 zip/`.db` → 校验预览(完整性 / 必需表 / schema 版本 / 未登记表 / **来源环境**)
→ 确认后自动把当前库导出一份 `data/backups/pre_restore_*.zip`(安全网,可再导入回来)
→ 落 `data/restore_pending/` → **重启服务生效**
> **SQLite 在这里是"备份交换格式",不是运行时数据库。** 所以归档里永远是一个 `users.db`,
> 无论平台连的是 MySQL 还是 SQLite —— 前端接口、校验逻辑、老备份的兼容性都不用变,
> 也不依赖 `mysqldump` 这类外部二进制(容器是 `python:slim`,没有 MySQL 客户端)。
**恢复是怎么生效的**:重启时 `consume_pending_restore()` 在**单个事务内** `DELETE` 全表 +
分块 `INSERT`,任何一步失败就回滚——当前数据保持原样,不会留下半新半旧的库。
坏归档会被挪到 `data/backups/restore_failed_*/` 且不阻塞启动。
**跨环境导入默认拒绝**:备份的 manifest 里记了来源环境(`deployment_env`),
若与当前库环境不一致(例如拿生产备份灌 dev 库),预览会弹黄框告警、直接应用会被拒绝;
确需跨环境时在预览页勾选「允许跨环境导入」。
### 5.2 备份覆盖清单(红线)
覆盖清单 = `core/system_backup.py` 的 `SUMMARY_TABLES`,当前 12 张表:`app_meta` / `user` / `device_group` / `task_job` / `custom_action` / `apk_file` / `device` / `pending_device` / `agent_conversation` / `agent_experience` / `experience_audit` / `agent_action`。完整说明见 [DATA_MODEL.md](DATA_MODEL.md) §6。
覆盖清单 = `core/system_backup.py` 的 `SUMMARY_TABLES`,**由模型元数据自动派生**:
> **新增任何持久化表,必须同步登记进该清单**——否则导出预览里看不到它,会被误判为"没有备份"(2026-09-10 动作库 `agent_action` 就踩过:数据其实在快照里,只是清单漏列)。
> 导出侧有**覆盖自检**(登记表缺失 → `manifest.coverage_missing` + 日志告警);导入侧有**反向自检**(备份含未登记表 → 预览告警)。
```python
SUMMARY_TABLES = tuple(sorted(t.name for t in db.metadata.tables.values()))
```
当前 12 张表:`app_meta` / `user` / `device_group` / `task_job` / `custom_action` /
`apk_file` / `device` / `pending_device` / `agent_conversation` / `agent_experience` /
`experience_audit` / `agent_action`。完整说明见 [DATA_MODEL.md](DATA_MODEL.md) §6。
> **新增一张 ORM 表,就自动进了覆盖清单**,不可能再漏(2026-09-10 动作库 `agent_action`
> 曾因手工维护漏登记,数据其实在快照里,只是清单没列 → 被误判为"没有备份")。
> 还需要手工做的只有:给新表补 `TABLE_LABELS` 的中文标签。
> 导出侧有**覆盖自检**(登记表缺失 → `manifest.coverage_missing` + 日志告警);
> 导入侧有**反向自检**(备份含未登记表 → 预览告警)。
### 5.3 目录与手工备份
| 目录 | 用途 |
|------|------|
| `data/backups/` | 导出临时 zip、`pre_restore_*.db`、`restore_failed_*` |
| `data/restore_staging/` | 导入暂存(TTL 30 分钟自动清理) |
| `data/restore_pending/` | 待生效恢复任务(重启时消费) |
| 目录 | 用途 | 可否用环境变量改 |
|------|------|------------------|
| `data/backups/` | 导出临时 zip、`pre_restore_*.zip`(导入前安全网)、`restore_failed_*` | `DATA_BACKUP_DIR` |
| `data/restore_staging/` | 导入暂存(TTL 30 分钟自动清理) | `DATA_RESTORE_STAGING_DIR` |
| `data/restore_pending/` | 待生效恢复任务(重启时消费) | `DATA_RESTORE_PENDING_DIR` |
手工整目录备份 `data/` 仍可作兜底,但**整库恢复建议走内置功能**(在线一致快照 + 预恢复备份 + 重启原子生效,避免手工替换被 WAL/占用文件破坏)。
> 这三个环境变量主要给**自动化测试**用:测试必须把恢复目录指到临时位置,
> 否则测试造出来的"待生效恢复任务"会在服务下次重启时被当成用户的操作消费掉。
连 MySQL 时 `data/users.db` 及其 `-wal`/`-shm` **不再被读写**(只有回退模式才用),
可以留作历史归档。手工整目录备份 `data/` 仍可作兜底,但**整库恢复建议走内置功能**
(一致快照 + 预恢复备份 + 重启时事务替换)。
> ⚠️ 备份 zip 含**用户口令哈希与 AI 控制台 API Key(明文)**,注意保管与传输。
+1
View File
@@ -145,6 +145,7 @@ MCP_ALLOW_WRITE=1 MCP_PLATFORM_USER=admin MCP_PLATFORM_PASS=<密码> \
|------|------|------|------|
| `WEB_SECRET_KEY` | 未配置则随机 | `web_server.py` | 会话密钥,**生产必须固定** |
| `DISABLE_SCHEDULER` | 未设 | `core/task_manager.py` | 设任意值则**不启动 cron 调度器**(测试用) |
| `DATA_BACKUP_DIR` / `DATA_RESTORE_STAGING_DIR` / `DATA_RESTORE_PENDING_DIR` | `data/backups` 等 | `config.py` | 备份/恢复目录改到别处;**自动化测试必须指到临时目录**(否则测试造的待生效恢复任务会在下次重启被当成用户操作消费) |
| `MCP_ENABLED` | `1` | `scripts/start.sh` | =0 则不后台拉起 MCP |
| `MCP_ALLOW_WRITE` | `0`(脚本内强制 1) | `mcp_server/config.py` | 写操作总开关 |
| `MCP_PLATFORM_URL` / `_USER` / `_PASS` | `http://127.0.0.1:18050` / `admin` / 空 | `mcp_server/config.py` | 登录平台的凭据(改过 admin 密码要同步) |