Files
model-platform/HANDOVER.md
T
2026-08-14 19:30:25 +08:00

242 lines
11 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.
# Handover — 近期变更与待办
面向接手运维 / 二次开发的人。记录最近几次 commit 的动机、改动范围、未尽事项,
以及「线上有旧数据时怎么过渡」。详细架构见 `ARCHITECTURE.md`;本仓库的开发规约
`DEVELOP.md`
---
## 1. 最近 5 个 commit(按时间倒序)
### `139f2c0` — fix: delete errortrash 路径统一 + 删除后 DB 字段更新)
**背景**:删除文件统一改为移动到 trash 桶后,存在三个连锁 bug:
1. **local 模式 trash 路径双重 `data`**`trash_key = f"{item.bucket_name}/{item.object_key}"`
local 模式下 `bucket_name` 是绝对路径(如 `/data/workspace`),最终落到
`data/trash/data/workspace/{ws_id}/{user_id}/...`
2. **s3 模式桶名 / 路径名错位**`.env` 之前是 `S3_WORKSPACE_BUCKET=workspaces`(复数),
代码 default / `PURPOSE_BUCKETS` / `USAGE_TYPE_TO_PURPOSE` 全部是单数 `workspace`
跨环境部署产生 `KeyError: 'workspaces'`(软删时 `object_stores[item.bucket_name]` 抛错)。
3. **软删后 `storage_objects` 路径字段未更新**:只翻 `trash_key` / `object_status` /
`deleted_at` / `is_deleted``bucket_name` / `object_key` / `storage_uri` 仍指向源,
与物理位置不一致,`purge_trash_object` 必须读旧 `trash_key` 才能定位 trash 文件。
**改动**5 个文件,44+/24-):
| 文件 | 改动 |
|---|---|
| `backend/src/backend/services/storage.py` | `trash_key = f"{source_purpose}/{item.object_key}"`;软删成功后更新 `bucket_name` / `object_key` / `storage_uri` 指向 trash 位置 |
| `backend/src/backend/storage_api.py` | `restore_object``item.object_key` 直接定位 trash,按 `/``source_purpose`,写回源桶,更新 DB 字段指回 source;`purge_trash_object``(actual_bucket_name("trash"), item.object_key)` 删除,加 `bucket_name != trash` 的 409 防御 |
| `common/src/common/storage/factory.py` | 引入 `USAGE_TYPE_TO_PURPOSE` 字典(替代 `storage_api.py` 的本地定义) |
| `common/src/common/storage/__init__.py` | 导出 `USAGE_TYPE_TO_PURPOSE` |
| `.env.example` | `S3_WORKSPACE_BUCKET=workspaces``workspace``S3_VERSION_BUCKET=versions``version``S3_RUN_LOG_BUCKET=run-logs``run-log`(统一单数) |
**注意点**
- `workspaces_root()` 函数名 / `WORKSPACES_ROOT` 环境变量名不动(这是 path 概念,不是 bucket 名)。
- `Workspaces` 数据库表名 / `auth.py``"workspaces"` JSON key(API 响应)不动(这些不是桶名)。
- `visibility="workspace"` 是 ACL Literal,不动。
### `2e070fa` — fix: pre check(前端 script 创建/上传前置校验)
`CreateScriptModal.tsx`+25 行)— 文件名 / workspace_id / 类型前置校验。
`ScriptsPage.tsx`+1/ `scriptWorkspaceStore.ts`+19/-10)— store action 增加
`precheck` 步骤,提前拦截无效请求。
### `5c5dec9` — fix: allows_same_name_different_parent
允许同名 script 出现在不同父目录下。`scripts.py`+32/-?)、`services/storage.py`
`storage_api.py` 微调;`test_scripts.py` 新增 57 行回归测试。
### `5d49ff5` — fix: file upload error(大重写)
修复 file upload 路径上一连串 schema / API / 上传流程的不一致。改动面最大
14 个文件,872+/604-):
- 新增索引 / 调整唯一约束(去掉了 scripts 和 data_resources 的旧 unique index
- 把多个小 migration 合并进 `e1f2a3b4c5d6_rebuild_baseline.py` 一份大 baseline
- `resources.py` / `scripts.py` 上传流程对齐
- `test_resources.py` / `test_scripts.py` 大量回归
- `API.md` 增加 3 行
### `c65d6dc` — fix: build error
`backend/Dockerfile` 单行修复(镜像构建报错)。
---
## 2. Trash 修复详解(commit `139f2c0`
### 2.1 修复后的语义
- **trash_key**`f"{source_purpose}/{item.object_key}"``source_purpose` 来自
`USAGE_TYPE_TO_PURPOSE[item.usage_type]`,与 `item.bucket_name` 解耦 —
避免 local 模式绝对路径泄漏 + 命名错位风险。
- **软删后 `storage_objects` 行的 7 个字段同步**
| 字段 | 软删前 | 软删后 |
|---|---|---|
| `bucket_name` | 源桶 | trash 桶 |
| `object_key` | 源 key | trash key |
| `trash_key` | NULL | trash key |
| `storage_uri` | 源 URI | trash URI`build_storage_uri` 重建) |
| `object_status` | `available` | `deleted` |
| `is_deleted` | `0` | `1` |
| `deleted_at` | NULL | `<utcnow>` |
- **物理文件落点**
| 模式 | 源文件(被删) | trash 文件(新落) |
|---|---|---|
| S3 | `s3://<source_bucket>/<key>` | `s3://trash/<source_purpose>/<key>` |
| local | `<LOCAL_STORAGE_BASE_DIR>/<source_purpose>/<key>` | `<LOCAL_STORAGE_BASE_DIR>/trash/<source_purpose>/<key>` |
`source_purpose``{workspace, version, run_log}`
### 2.2 `restore_object` 反向流程
```python
data = await object_stores[trash_bucket].get(item.object_key) # 从 trash 读
source_purpose, _, source_key = item.object_key.partition("/") # 拆 source_purpose
target_bucket = actual_bucket_name(source_purpose) # 还原目标桶
await object_stores[target_bucket].put(source_key, data) # 写回源
item.bucket_name = target_bucket
item.object_key = source_key
item.storage_uri = build_storage_uri(target_bucket, source_key)
# trash_key 保留不动 → 未来 reaper 可据此清理 trash 里的 orphan 副本
item.object_status = "available"
item.deleted_at = None
```
**trash 文件不会被 restore 主动删除**(保留现有安全语义:restore 失败时
数据仍在 trash 里有兜底)。未实现的 reaper 未来可扫
`object_status='available' AND trash_key IS NOT NULL` 清理 orphan。
### 2.3 `purge_trash_object` 防御
加 409 防御:`if item.bucket_name != actual_bucket_name("trash"): raise 409`
避免误删源数据(用 `item.object_key` 直接删 trash 文件,不再读旧 `trash_key`)。
### 2.4 USAGE_TYPE_TO_PURPOSE 提升到 common 层
`backend/storage_api.py``backend/services/storage.py` 都依赖这个映射。
提到 `common/storage/factory.py` 后两边都 `from common.storage import USAGE_TYPE_TO_PURPOSE`
避免互相 import 循环。
---
## 3. 线上数据兼容(重要)
修复后**新行为对 DB 里已有的旧行不感知**。如果部署环境之前是
`S3_WORKSPACE_BUCKET=workspaces`(复数)且已上传过文件,
`storage_objects.bucket_name` 列里会有复数残留。新代码 default 是单数,
`object_stores` dict key 也是单数,访问旧行仍会 `KeyError: 'workspaces'`
**部署新代码前必须先跑一次数据修复 SQL**(先 SELECT 看数量,确认是同一个 deployment):
```sql
-- 1. 先看现状(按 backend 区分)
SELECT storage_backend, bucket_name, COUNT(*)
FROM storage_objects
WHERE object_status = 'available'
GROUP BY storage_backend, bucket_name
ORDER BY storage_backend, bucket_name;
-- 2a. S3 部署:把复数残留修正成单数
UPDATE storage_objects
SET bucket_name = 'workspace'
WHERE bucket_name = 'workspaces'
AND object_status = 'available';
-- 2b. local 部署:把绝对路径里的复数残留修正成单数
UPDATE storage_objects
SET bucket_name = '/data/workspace'
WHERE bucket_name = '/data/workspaces'
AND object_status = 'available';
```
> **只改 `available` 行**`deleted` 行已经在 trash 里、bucket_name 会被新代码改写,无需处理。
> 如果同时改 `version` / `run_log` 桶残留,按相同模式跑。
---
## 4. 本地验收清单
```bash
# 1. 语法
uv run python -m compileall common/src backend/src runtime/src schedule/src
# 2. 测试
uv run --package backend pytest backend/tests -q
# 期望:30 passed
# 3. 命名一致性
grep -rn '"workspaces"\|"versions"\|"run-logs"' \
--include='*.py' --include='*.yml' --include='*.env*' \
backend/src common/src docker-compose.yml .env .env.example 2>/dev/null
# 期望:除 `__tablename__ = "workspaces"`DB 表名)和 `auth.py` 的 JSON key 外,零结果
# 4. 单复数对齐
grep -n 'S3_WORKSPACE_BUCKET\|S3_VERSION_BUCKET\|S3_RUN_LOG_BUCKET\|S3_TRASH_BUCKET' \
.env.example
# 期望:全部 = 单数(workspace / version / run-log / trash
```
---
## 5. 回归测试覆盖
| 场景 | 触发方式 | 期望 |
|---|---|---|
| 同名 script 不同父目录 | `test_soft_delete_object_sets_is_deleted` 旁边的新测试 | 两条 row 同名共存 |
| 软删 → 物理文件落到 trash 桶的 `source_purpose/{key}` | 手工:上传 → 删 → `mc ls s3/trash/` | 文件在;源桶里已删 |
| 软删后 `storage_objects` 7 字段同步 | 软删后 `SELECT * FROM storage_objects WHERE storage_object_id=...` | 见 §2.1 表格 |
| `restore_object` 把 trash 文件写回源桶 | 软删 → restore → 查源桶 | 源桶恢复;trash 文件保留(orphan |
| `purge_trash_object` 拒绝误删源数据 | 手工:把 `bucket_name` 改成源桶后调 purge | 409 |
---
## 6. 监控项(建议加)
1. `storage_objects``bucket_name``actual_bucket_name(purpose)` 不一致的比例
(按 `usage_type` 分组 COUNT)。生产环境应长期趋近于 0。
2. `object_status='deleted'``bucket_name != actual_bucket_name('trash')` 的行数 —
应为 0(软删失败回滚残留)。
3. trash 桶目录大小 vs `object_status='deleted'` 行数(孤儿文件比例)。
没实现 reaper 前这个会单调上升。
---
## 7. 已知未实现项
- **Trash reaper**`s3_trash_retention_days` 已声明,但代码没有扫表清理。
`restore_object` 故意保留 trash 文件作 orphan,等 reaper 兜底。
第一次实现建议在 `schedule/src/schedule/` 加一个 tick job
```sql
SELECT storage_object_id FROM storage_objects
WHERE deleted_at < UTC_TIMESTAMP() - INTERVAL :retention_days DAY
AND object_status = 'deleted'
LIMIT 100;
```
然后调 `/internal/v1/admin/trash/purge`。
- **跨 backend trash 兼容**:当前只在 `item.storage_backend == settings.storage_backend`
的行上做物理移动。异构 backend 的旧行(少)软删时只翻 DB 状态,trash 文件实际没移动。
远程 8.153.151.51:8890 应该都是同一 backend,目前无影响。
- **历史 trash_key 兼容读**:新代码用 `item.object_key` 读 trash**不**回退到
`item.trash_key`。如果迁移前有按旧 `trash_key = f"{bucket_name}/{object_key}"`
写入的已删行,`object_key` 还是源 keyrestore 会找不到 trash 文件(404)。
处理:在 `restore_object` 加 fallback — 先按 `item.object_key` 试,失败再按 `item.trash_key`
试一次(兼容窗口期)。建议在 reaper 上线后删除 fallback。
---
## 8. 仍未合并到 develop 的本地改动
```text
M frontend/vite.config.ts (4+/2-)
?? data/ (untracked — 本地 docker volume 残留,可忽略)
```
`frontend/vite.config.ts` 的改动不在本次 trash 修复范围内,等下次 commit 一起提。