# popiartServer 薄膜持久化设计 这份文档描述当前的轻量化持久化方案。目标不是把 `popiartServer` 做成第二个模型平台,而是让它只保存 PopiArt 自己的产品语义。 ## 设计原则 `PopiNewAPI` 已经负责: - 上游 provider key - 模型请求 - 原始用量 - 一部分任务型能力的 task 状态 所以 `popiartServer` 不再重复持有: - 本地 artifact metadata table - 本地 job_logs table 它只保留三类本地状态: - `sessions` - `jobs`(更准确说是 job refs) - `skill_routes` - `media blobs + metadata` ## 当前关系 ```text session -> jobs jobs -> result_refs_json project -> skill_routes media -> local files + metadata json ``` 其中: - `session` 保存 PopiArt 登录态和对应的 `PopiNewAPI token` - `jobs` 保存 skill 语义、用户归属、上游引用和同步结果引用 - `result_refs_json` 保存同步能力的结果引用;新结果会优先 re-host 到本地 media store - `skill_routes` 保存项目级路由覆盖 - `media` 保存稳定 URL 所需的本地文件与元数据 ## 存储位置 默认配置来自这些环境变量: - `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` 本地开发版没有对象存储依赖,但现在有一个轻量 media 存储层: - blob 文件保存在 `POPIART_DATA_DIR/media/blobs/` - metadata JSON 保存在 `POPIART_DATA_DIR/media/meta/` ## 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) ); ``` ### 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. `GET /jobs/:id/artifacts` 时再从 `result_refs_json` 派生 artifact 列表 ### 拉取 artifact 1. server 从 `artifact_id` 反推出 `job_id + result index` 2. 读取 `jobs.result_refs_json` 3. 如果是本地 `local_path`,直接读取本地文件并流式返回 4. 如果是旧的 `data_url`,直接解码并流式返回 5. 如果是旧的远端 `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 的稳定内容路径 ## 当前边界 - server 重启后: - `session` 会保留 - `job ref` 会保留 - `artifact` 通过 `result_refs_json` 继续可读 - 新 `media` 文件和 metadata 会继续可读 - 已存在的 `pending/running` job 仍然不会自动恢复执行 - 视频类 `upstream_task` 路径只预留了字段,后续再接 `PopiNewAPI` task 查询 - 旧数据里仍可能存在 `data_url` 或上游 `url` - 新写入路径优先落本地 media store,从而给 artifact 补出稳定 `url`