Make artifact and media metadata first-class server records

Artifact and media metadata were previously reconstructed from job result
refs and JSON sidecar files. This change regularizes metadata into SQLite,
keeps filesystem blobs in place, and preserves backward compatibility via
lazy fallback and backfill from existing job refs and JSON metadata.

Constraint: Blob storage remains on the local filesystem in this phase
Rejected: Migrate blobs into SQLite | larger scope and worse operational profile for current media sizes
Rejected: Hard cutover without fallback | unsafe for historical data already on the test server
Confidence: medium
Scope-risk: moderate
Directive: Treat SQLite as the metadata source of truth; JSON sidecars are compatibility fallback only
Tested: go test ./...; deployed to test server 101.42.99.35; verified /v1/artifacts, /v1/artifacts/:id, signed media URL 200, unsigned content 401
Not-tested: Full historical backfill sweep over all existing artifact rows under production-sized data volume
This commit is contained in:
wtgoku
2026-04-18 23:06:34 +08:00
parent 07fd1ad0a7
commit d0318292c1
12 changed files with 1303 additions and 75 deletions
+78 -18
View File
@@ -14,32 +14,38 @@
所以 `popiartServer` 不再重复持有:
- 本地 artifact metadata table
- provider 侧原始产物数据库
- 本地 job_logs table
保留类本地状态:
当前保留类本地状态:
- `sessions`
- `jobs`(更准确说是 job refs
- `artifacts`
- `media_records`
- `skill_routes`
- `media blobs + metadata`
- `media blobs`
## 当前关系
```text
session -> jobs
jobs -> result_refs_json
jobs -> artifacts
artifacts -> media_records
project -> skill_routes
media -> local files + metadata json
media -> local files
```
其中:
- `session` 保存 PopiArt 登录态和对应的 `PopiNewAPI token`
- `jobs` 保存 skill 语义、用户归属、上游引用和同步结果引用
- `result_refs_json` 保存同步能力的结果引用;新结果会优先 re-host 到本地 media store
- `result_refs_json` 仍作为 job 级兼容引用保留
- `artifacts` 保存用户可查询的 artifact 元数据与溯源快照
- `media_records` 保存稳定 URL 所需的 media 元数据
- `skill_routes` 保存项目级路由覆盖
- `media` 保存稳定 URL 所需的本地文件与元数据
- `media blobs` 保存真实文件内容
## 存储位置
@@ -62,16 +68,19 @@ POPIART_SQLITE_PATH=./data/popiart.db
- `internal/server/repository.go`
当前只保留三层 repository
当前 repository 分成五层
1. `SessionRepository`
2. `JobRepository`
3. `RouteRepository`
4. `ArtifactRepository`
5. `MediaRepository`
本地开发版没有对象存储依赖,但现在有一个轻量 media 存储层:
- blob 文件保存在 `POPIART_DATA_DIR/media/blobs/`
- metadata JSON 保存在 `POPIART_DATA_DIR/media/meta/`
- 兼容期 metadata JSON 保存在 `POPIART_DATA_DIR/media/meta/`
- SQLite 中的 `media_records` / `artifacts` 是当前主元数据存储
## SQLite Schema
@@ -115,6 +124,53 @@ CREATE TABLE jobs (
);
```
### media_records
```sql
CREATE TABLE media_records (
media_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
artifact_id TEXT,
project_id TEXT,
filename TEXT NOT NULL,
content_type TEXT NOT NULL,
size_bytes INTEGER NOT NULL,
created_at TEXT NOT NULL,
url TEXT NOT NULL,
visibility TEXT,
sha256 TEXT,
local_path TEXT NOT NULL
);
```
### artifacts
```sql
CREATE TABLE artifacts (
artifact_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
job_id TEXT NOT NULL,
project_id TEXT,
result_index INTEGER NOT NULL,
media_id TEXT,
filename TEXT NOT NULL,
content_type TEXT NOT NULL,
size_bytes INTEGER NOT NULL,
created_at TEXT NOT NULL,
expires_at TEXT,
visibility TEXT,
sha256 TEXT,
storage_status TEXT,
source_skill_id TEXT,
source_model_id TEXT,
source_route_key TEXT,
source_input_json TEXT,
prompt_text TEXT,
usage_json TEXT,
ref_json TEXT NOT NULL
);
```
### skill_routes
```sql
@@ -148,31 +204,35 @@ CREATE TABLE skill_routes (
1. 如果 `PopiNewAPI` 直接返回同步结果,例如 `b64_json``url`
2. `popiartServer` 会先把结果 re-host 到本地 media store
3. 再把本地 `local_path + media_id + stable url` 写进 `result_refs_json`
4. `GET /jobs/:id/artifacts` 时再从 `result_refs_json` 派生 artifact 列表
4. 同时 upsert 到 `media_records``artifacts`
5. `GET /jobs/:id/artifacts` / `GET /v1/artifacts` 优先从 SQLite 读取
### 拉取 artifact
1. server 从 `artifact_id` 反推出 `job_id + result index`
2. 读取 `jobs.result_refs_json`
3. 如果是本地 `local_path`,直接读取本地文件并流式返回
4. 如果是旧的 `data_url`,直接解码并流式返回
5. 如果是旧的远端 `url`server 代理下载并返回
2. 优先读取 `artifacts` 表中的 `ref_json`
3. 如果 SQLite 尚未命中,则从 `jobs.result_refs_json` 回填并重试
4. 如果是本地 `local_path`,直接读取本地文件并流式返回
5. 如果是旧的 `data_url`直接解码并流式返回
6. 如果是旧的远端 `url`server 代理下载并返回
### 读取 media
1. `POST /v1/media/upload` 会把本地文件写入 `media/blobs/`
2. server 同时写一份 metadata JSON 到 `media/meta/`
3. `GET /v1/media/:id` 返回 media 元数据
4. `GET /v1/media/:id/content` 返回可供模型或客户端直接 fetch 的稳定内容路径
2. server 同时写 SQLite `media_records`,并保留兼容 JSON meta
3. `GET /v1/media/:id` 优先读 SQLiteJSON 作为 fallback
4. `GET /v1/media/:id/content` `local_path` 读取内容
## 当前边界
- server 重启后:
- `session` 会保留
- `job ref` 会保留
- `artifact` 通过 `result_refs_json` 继续可读
- `artifact` 通过 SQLite 主路径继续可读
- 旧数据仍可通过 `result_refs_json` / JSON meta 懒回填
-`media` 文件和 metadata 会继续可读
- 已存在的 `pending/running` job 仍然不会自动恢复执行
- 视频类 `upstream_task` 路径只预留了字段,后续再接 `PopiNewAPI` task 查询
- 旧数据里仍可能存在 `data_url` 或上游 `url`
- 新写入路径优先落本地 media store,从而给 artifact 补出稳定 `url`
- `media/meta/*.json` 仍保留作为兼容层,不再是主元数据来源
- 新写入路径优先落 SQLite + 本地 media store,从而给 artifact 补出稳定 `url`