Files
popiart-server/docs/persistence.md
T
wtgoku d0318292c1 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
2026-04-18 23:06:34 +08:00

5.8 KiB
Raw Blame History

popiartServer 薄膜持久化设计

这份文档描述当前的轻量化持久化方案。目标不是把 popiartServer 做成第二个模型平台,而是让它只保存 PopiArt 自己的产品语义。

设计原则

PopiNewAPI 已经负责:

  • 上游 provider key
  • 模型请求
  • 原始用量
  • 一部分任务型能力的 task 状态

所以 popiartServer 不再重复持有:

  • provider 侧原始产物数据库
  • 本地 job_logs table

它当前保留五类本地状态:

  • sessions
  • jobs(更准确说是 job refs
  • artifacts
  • media_records
  • skill_routes
  • media blobs

当前关系

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

默认值:

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

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

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

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

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

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_jsonurl
  2. popiartServer 会先把结果 re-host 到本地 media store
  3. 再把本地 local_path + media_id + stable url 写进 result_refs_json
  4. 同时 upsert 到 media_recordsartifacts
  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. 如果是旧的远端 urlserver 代理下载并返回

读取 media

  1. POST /v1/media/upload 会把本地文件写入 media/blobs/
  2. server 同时写 SQLite media_records,并保留兼容 JSON meta
  3. GET /v1/media/:id 优先读 SQLiteJSON 作为 fallback
  4. GET /v1/media/:id/contentlocal_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