# popiartServer 薄膜持久化设计 这份文档描述当前的轻量化持久化方案。目标不是把 `popiartServer` 做成第二个模型平台,而是让它只保存 PopiArt 自己的产品语义。 ## 设计原则 `PopiNewAPI` 已经负责: - 上游 provider key - 模型请求 - 原始用量 - 一部分任务型能力的 task 状态 所以 `popiartServer` 不再重复持有: - provider 侧原始产物数据库 - 本地 job_logs table 它当前保留五类本地状态: - `sessions` - `jobs`(更准确说是 job refs) - `artifacts` - `media_records` - `skill_routes` - `media blobs` ## 当前关系 ```text session -> jobs jobs -> result_refs_json jobs -> artifacts artifacts -> media_records project -> skill_routes media -> local files ``` 其中: - `session` 保存 PopiArt 登录态和对应的 `PopiNewAPI token` - `jobs` 保存 skill 语义、用户归属、上游引用和同步结果引用 - `result_refs_json` 仍作为 job 级兼容引用保留 - `artifacts` 保存用户可查询的 artifact 元数据与溯源快照 - `media_records` 保存稳定 URL 所需的 media 元数据 - `skill_routes` 保存项目级路由覆盖 - `media blobs` 保存真实文件内容 ## 存储位置 默认配置来自这些环境变量: - `POPIART_DATA_DIR` - `POPIART_SQLITE_PATH` - `POPIART_SESSION_SECRET` 默认值: ```text POPIART_DATA_DIR=./data POPIART_SQLITE_PATH=./data/popiart.db ``` ## Repository 接口 代码位置: - `internal/server/repository.go` 当前 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/` - SQLite 中的 `media_records` / `artifacts` 是当前主元数据存储 ## SQLite Schema ### sessions ```sql CREATE TABLE sessions ( session_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, user_json TEXT NOT NULL, token_enc BLOB NOT NULL, token_masked TEXT NOT NULL, created_at TEXT NOT NULL, expires_at TEXT NOT NULL ); ``` ### jobs ```sql CREATE TABLE jobs ( job_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, session_id TEXT, project_id TEXT, skill_id TEXT, route_key TEXT, model_id TEXT, exec_mode TEXT NOT NULL, newapi_task_id TEXT, status TEXT NOT NULL, input_json TEXT NOT NULL, result_refs_json TEXT, usage_json TEXT, error_json TEXT, idempotency_key TEXT, created_at TEXT NOT NULL, started_at TEXT, finished_at TEXT, UNIQUE(user_id, idempotency_key) ); ``` ### 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 CREATE TABLE skill_routes ( route_key TEXT NOT NULL, scope_key TEXT NOT NULL, model_id TEXT NOT NULL, options_json TEXT, updated_at TEXT NOT NULL, PRIMARY KEY(route_key, scope_key) ); ``` ## 运行时流程 ### 登录 1. CLI 把 `PopiNewAPI token` 交给 `popiartServer` 2. `popiartServer` 用 `/v1/models` 验证 token 3. server 签发本地 `sess_xxx` 4. 本地 session 持久化到 SQLite,重启后仍可继续使用 ### 创建 job 1. server 解析 `skill_id -> route_key -> model_id` 2. `jobs` 表写入一条本地 job ref 3. goroutine 开始向 `PopiNewAPI` 发请求 ### 完成同步结果 job 1. 如果 `PopiNewAPI` 直接返回同步结果,例如 `b64_json` 或 `url` 2. `popiartServer` 会先把结果 re-host 到本地 media store 3. 再把本地 `local_path + media_id + stable url` 写进 `result_refs_json` 4. 同时 upsert 到 `media_records` 和 `artifacts` 5. `GET /jobs/:id/artifacts` / `GET /v1/artifacts` 优先从 SQLite 读取 ### 拉取 artifact 1. server 从 `artifact_id` 反推出 `job_id + result index` 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 同时写 SQLite `media_records`,并保留兼容 JSON meta 3. `GET /v1/media/:id` 优先读 SQLite,JSON 作为 fallback 4. `GET /v1/media/:id/content` 从 `local_path` 读取内容 ## 当前边界 - server 重启后: - `session` 会保留 - `job ref` 会保留 - `artifact` 通过 SQLite 主路径继续可读 - 旧数据仍可通过 `result_refs_json` / JSON meta 懒回填 - 新 `media` 文件和 metadata 会继续可读 - 已存在的 `pending/running` job 仍然不会自动恢复执行 - 视频类 `upstream_task` 路径只预留了字段,后续再接 `PopiNewAPI` task 查询 - 旧数据里仍可能存在 `data_url` 或上游 `url` - 旧 `media/meta/*.json` 仍保留作为兼容层,不再是主元数据来源 - 新写入路径优先落 SQLite + 本地 media store,从而给 artifact 补出稳定 `url`