REST API 总览

QuantumAtlas server 的所有 HTTP endpoint。auth 模型详见 概念/鉴权。

交互式 API 文档(Swagger UI)

server 内嵌了一份由 swaggo 从代码注解自动生成的 OpenAPI spec,挂在 /swagger(如 https://quantum-atlas.ai/swagger/),可在浏览器里 直接浏览/点测每个 endpoint:

  • GET /swagger/index.html — Swagger UI 页面(公开,无需 auth)。

  • GET /swagger/doc.json — 原始 OpenAPI 2.0 JSON,可喂给 Postman / openapi-generator 等。

写口在 UI 里点 Authorize 填 Bearer <PAT 或 session token> 即可带鉴权调用。

历史镜像说明:这份 spec 也嵌进了文档站:API Explorer 页可在浏览器里 展开浏览全部 endpoint 的参数 / schema(静态镜像,在线点测仍用上面的 /swagger)。

上段保留的是旧版文档镜像的说明。当前 Sphinx 站点不运行旧版镜像 hook,API Explorer 改为提供原生 OpenAPI spec 下载与当前实例 /swagger/ 的普通链接;本页的静态表格 仍可直接阅读。Swagger UI 本身不是只读界面,真实 API 调用须由用户主动选择操作, 并通过服务端鉴权。

spec 怎么来的(维护者须知)

PocketBase 的路由是匿名闭包,没有可挂 doc 注释的具名 handler,所以注解集中写在 internal/routes/openapi.go 的一组 no-op stub 函数上(每个 endpoint 一个),general info 写在 cmd/qatlasd/main.go 的 func main() 之上。生成产物落在 internal/apidocs/(被 main.go blank-import,编译进二进制)。

改完注解后重新生成:

go tool swag init -g main.go -d ./cmd/qatlasd,./internal/routes \
  -o internal/apidocs --parseInternal --parseDepth 1
git diff -- internal/apidocs

swag CLI 通过 go.mod 的 tool 指令钉版本(go tool swag),生成是确定性的。CI (.github/workflows/go.yml)执行相同命令,并用 git diff --exit-code 与包含未跟踪文件的状态检查防漂移。 应提交 internal/apidocs/ 的生成源码;不能只修改注解而忘记同步 spec。

警告:注解里的 path / 参数 / 响应是手写声明,不是从真实闭包反射出来的——swaggo 在任何 非 net/http-mux 风格路由上都这样。它和真实行为的一致性靠 code review + 这份手维护的 Markdown 表交叉验证,不是自动保证。

公开端点(不需要 Authorization 头)

仅以下"无语料数据"端点保持公开:探活 / 版本 / 安装脚本 / API 文档 / scope 词表 / SPA 外壳。论文数据本身不再匿名可读——搜索、统计、论文资产等读口都已收敛到 *:read scope(见下方鉴权端点)。

Method

Path

用途

GET

/api/health

健康检查 + 依赖探活

GET

/api/server/info

版本 / 引擎信息 + 能力发现(capabilities:匿名可见布尔位 paper_access / markdown_delivery / pdf_delivery(随 paper_access 开关;已存 PDF 无需解析 token)/ agentic_search / mineru.enabled / mineru.on_demand;认证调用者额外见 mineru.daily_cap 与 mineru.converted_today,来自批处理调度器快照)

GET

/install-qatlasd.sh

qatlasd 安装脚本

GET

/swagger/index.html

交互式 API 文档(Swagger UI)

GET

/swagger/doc.json

OpenAPI 2.0 JSON spec

GET

/api/pat/scopes

列 PAT scope 词表(纯常量,无用户数据)

GET

/{path...}

SPA 前端静态外壳(数据在被门禁的 API 后面)

GET

/_/ ...

PocketBase admin UI

鉴权端点(需要 PAT 或 session token)

Papers

Method

Path

鉴权

用途

GET

/api/papers

papers:read

论文分页列表。过滤参数:has_md(默认资产已有 markdown)/ status(pending | ready | failed)/ q(标题子串,大小写不敏感)/arxiv_id / doi / paper_id(精确身份过滤——arxiv_id 自动去 vN 版本后缀,doi 容忍 https://doi.org/ URL 前缀后归一);page / per_page(默认 20,≤100)/ sort(created_at 默认 | updated_at,降序)

GET

/api/papers/{id}

papers:read

论文详情(registry 记录 + 资产行)。{id} 接受三种标识符:qa_ paper_id、arXiv ID(新式 2501.00010 / 老式 quant-ph/9508027,带或不带 vN)、DOI(含 https://doi.org/… URL 形态)。合法但未收录的标识符返回 404,detail 提示用 GET /api/papers/lookup?ids=arxiv:… 解析元数据

GET

/api/papers/lookup?ids=

papers:read

批量(≤200 条)解析 kind:id 引用(arxiv: / doi: / openalex:)。每项返回 {ref, title, authors, year, hosted, has_md, resolved}——hosted 表示是否已收录,has_md(hosted 时有意义)表示默认资产是否已有 markdown。这是批量核对「有没有 markdown」的官方入口,无需逐篇调详情

GET

/api/papers/stats

papers:read

论文资产统计(available、total、has_pdf、has_md、has_json、needs_mineru、total_images、loaded_at);paperindex 不可用时返回 {available:false}

GET

/api/papers/needs-mineru?limit=&include_claimed=

papers:read

列等待 MinerU 解析的论文

POST

/api/papers/{arxiv_id}/upload-pdf

papers:write

上传 PDF,见 Upload API

POST

/api/papers/{arxiv_id}/upload-mineru

papers:write

上传完整受支持 Middle + Markdown ZIP;所有原名/字节保留,重传新 revision;MD-only 拒收

POST

/api/v1/papers/{arxiv_id}/mineru-lease

papers:write

申请 MinerU 处理 lease(响应字段含 claim_id)

DELETE

/api/v1/papers/{arxiv_id}/mineru-lease/{claim_id}

papers:write

释放 MinerU lease

POST

/api/papers/{arxiv_id}/mineru-claim

papers:write

申请 MinerU 处理 lease

DELETE

/api/papers/{arxiv_id}/mineru-claim/{claim_id}

papers:write

释放 MinerU lease

GET

/api/papers/{id_or_doi}/markdown

papers:read

PAPER_ACCESS · 所选完整不可变 bundle 的原始 Markdown 字节;format=link 仅返回同源认证成员 locator。无 pin 未就绪时 202 →纯 read/status 轮询→带 source/revision 重取;旧 MD/JSON/images 忽略

GET

/api/papers/{id_or_doi}/markdown/status

papers:read

PAPER_ACCESS · 与 read/status 共享完整包/冻结来源 readiness 纯轮询,不启动下载/解析;顶层 source_id/revision/state/ready(兼容 flags 可附加)

GET

/api/papers/{id_or_doi}/pdf

papers:read

PAPER_ACCESS · 鉴权固定来源 PDF 字节,source_id 或语义 version=vN 互斥;完整 SHA headers,Range 同样鉴权;无需先枚举 source 或 MinerU token。format=link 仅返回同源认证 locator

GET

/api/papers/{id_or_doi}/pdf/status

papers:read

PAPER_ACCESS · PDF 来源就绪纯探测,不解析;若含 pdf_url 仅是同源固定 source 的认证 locator,不暴露对象桶

GET

/api/papers/{id_or_doi}/images/zip

papers:read

PAPER_ACCESS · 从完整 bundle 核验原名/字节后组装图片 ZIP;无 pin 未就绪可202,纯 read/status 轮询后重取固定 source/revision;无 NAS presign

| GET | /api/papers/{id}/read | papers:read | PAPER_ACCESS · Middle 派生 Markdown JSON 信封;source_id/revision/page/block/cursor/limit,1-based定位、预算1..100000默认30000;truncated/next_request.cursor 固定全部身份 | | GET | /api/papers/{id}/read/status | papers:read | PAPER_ACCESS · 纯轮询;ready须完整包+冻结PDF核验,顶层source_id/revision;pending202 + Operation-Location/Retry-After,不创建任务 | | GET | /api/papers/{id}/parses/{revision}/manifest | papers:read | PAPER_ACCESS · 已发布完整包清单,source PDF与每成员SHA/原path;不以旧Middle代替 readiness | | GET | /api/papers/{id}/parses/{revision}/files/{original_relative_path} | papers:read | PAPER_ACCESS · 所有生产者成员原名/原字节,含嵌套目录及全部JSON,不重命名/重序列化 | | GET | /api/papers/{id}/parses/{revision}/json | papers:read | PAPER_ACCESS · 原始Middle字节固定artifact SHA;历史评论仍用,不是read派生JSON,也不是完整包ready证据 |

完整身份、原件、懒迁移、上传与错误规则见论文内容契约。当前新内容不运行自动 nightly/boot/入库转换;只有显式内容访问懒解析,管理员显式 RunNow/batch 另按授权。

papers:write 隐式含 papers:read。

PAPER_ACCESS 及 raw JSON/block 原件/完整包成员/images/figures 均由 YAML paper_access.enabled 控制 (默认 OFF;关闭 404,无存储/解析副作用;qa_、别名、Range、历史 pin 不绕过)。开启等于自愿承担对外重分发 PDF / markdown 字节(或 RAG snippet 形式的派生片段)的合规义务,见 License & Attribution · 论文访问开关。 公共 quantum-atlas.ai 部署默认 OFF。

{id_or_doi} 接受qa_、明确/裸arXiv或DOI别名。规范作品ID不等于版本pin; source_id或version=vN是确切来源选择,revision/cursor固定解析。先解析明确pin, 不按旧“DOI总优先/最高版本”规则替换。默认来源歧义应显式pin;未知论文/来源、 关开关和后端不可用分别报告,不将旧MD/JSON/images当新ready。

Personal Search Keys(个人第三方搜索 key)

每个用户可存自己的第三方搜索 API key(IEEE / Scopus / Tavily 等 key 后端), POST /api/search/multi 调用时自动解密注入。存储是 AES-256-GCM 加密的 search_api_keys PocketBase collection(owner-only 规则、仅服务端写入); 加密密钥由 system PAT token 做域分离 SHA-256 派生——轮换 system PAT 会使 已存 key 全部失效(表现为解密失败,用户重新录入即可),无独立配置键。

Method

Path

鉴权

用途

GET

/api/me/search-keys

session only

当前用户的 key 清单:{enabled, keys:[{backend, hint, updated_at}]}。hint 是打码的末 4 位,永不返回 key 本体。server 未配 system PAT 时 enabled:false

PUT

/api/me/search-keys/{backend}

session only

upsert 一个 key(body {"key": "..."})。backend 必须是目录里有 user-key 槽位的后端,否则 400

DELETE

/api/me/search-keys/{backend}

session only

删除;不存在时 opaque 404

session-only(非 scopeGuard)的理由同 /api/pat:这是浏览器 dashboard 里的 凭据管理,泄露的 PAT 不应能读走或替换 owner 的第三方 key。

Robust Downloader

详见 Robust Downloader(策略阶梯 / 配置 / downloaderproxy / probe)。端点本身:

Method

Path

鉴权

用途

POST

/api/downloader/fetch

papers:write

提交一批标识符(DOI / arXiv id / 论文 URL,body {"items":[...]},≤50 条)。每条先解析 + ResolveOrMint 进 registry,再入策略阶梯队列。逐条返回 {input, kind, paper_id, created, error?} + 总 enqueued。paper_access.enabled / downloader.enabled 任一关闭时 503;registry 不可用时 503

GET

/api/downloader/jobs

papers:read

job 快照:{jobs:[...], counters:{...}}。每个 job 含 per-paper state/phase、胜出策略、完整 attempt trace(策略 id / URL / 错误 / 耗时)

SPA 页面 /$lang/downloader:粘贴标识符 → 客户端解析预览徽章 → 提交 → 逐 job 进度 + 策略轨迹渲染(挂在 downloader 插件上)。

Plugins

Plugins share one manifest/capability model with two orthogonal axes (kind × transport). The first-party theorems plugin is kind=builtin (compiled into qatlasd, in-process); third-party plugins are kind=external and connect to the RPC listener over transport=socket, or are spawned by the host over transport=stdio.

Method

Path

鉴权

用途

GET

/api/v1/plugins

plugins:read

列出发现的插件清单与状态(connected / disconnected / disabled / incompatible)

POST

/api/v1/plugins/{id}/enable

plugins:write

运行时启用已配置插件

POST

/api/v1/plugins/{id}/disable

plugins:write

运行时禁用已配置插件

配置驱动的远程微服务也会出现在插件清单里:search-remote(qatlas-search) 与 rag-remote(qatlas-rag)在各自配置段启用后注册,qatlasd 启动后探测 它们的 /healthz 并标记 connected / disconnected。

External plugins do not call host capabilities over HTTP. They connect to the WebSocket JSON-RPC service at QATLAS_RPC_WS_BIND. initialize params contain id, secret, and abi_version; after the handshake the same connection can call the host-shared capabilities papers/getMarkdown, papers/getMeta, papers/getCitedRefs, and events/publish. (The host core carries no plugin-domain methods — ADR 0003.)

Theorems(builtin 插件,read-through 一个 Lean-content git checkout)

theorems builtin 插件把上游 Lean-content 仓库(QATLAS_THEOREMS_DIR)的 artifacts/registry.json + artifacts/audit_records/certified.json + Lean 源 read-through 暴露出来。它不持有 PocketBase collection(ADR 0004:已证 Theorem 是从 git read-through,不落库)。

Method

Path

鉴权

用途

GET

/api/theorems/list

theorems:read

列已证 Theorem(支持 ?family_id=&audit_status=&kind= 过滤)

GET

/api/theorems/families

theorems:read

列 theorem family 定义(过滤下拉数据源)

GET

/api/theorems/stats

theorems:read

聚合计数(by_kind / by_family / by_audit_status / sorry_free / certified)

GET

/api/theorems/theorem/{fqn}

theorems:read

取单个 Theorem(完整 registry 条目 + audit verdict)

GET

/api/theorems/theorem-source/{fqn}

theorems:read

按需取该 Theorem 的 Lean 源文件

GET

/api/theorems/sync/status

theorems:read

theorems git 状态

POST

/api/theorems/sync/pull

theorems:write

触发服务端 theorems git fast-forward pull,随后同步 reload 内存缓存

theorems:write 隐式含 theorems:read。/api/<id>/sync/{pull,status} 是平台为 pull 类插件挂的统一形状(ADR 0002/0003)。

PAT 管理(只接受 session token,PAT auth 被拒)

Method

Path

鉴权

用途

POST

/api/pat

session only

创建 PAT,返回明文(一次)

GET

/api/pat

session only

列当前用户的 PAT(无明文)

DELETE

/api/pat/{id}

session only

撤销

OAuth Device Flow(qatlas auth login 后端)

RFC 8628 device authorization grant。CLI 没有浏览器 / session,所以由用户在浏览器里登录 + 输 user_code 授权,CLI 轮询拿 minted PAT。前两个端点匿名(CLI 调),后三个 session-only(PAT auth 被拒,理由同 /api/pat:泄露的 PAT 不应能自我繁殖)。

Method

Path

鉴权

用途

POST

/api/oauth/device/code

匿名

RFC 8628 §3.1,CLI 启动 flow。body:{name, description?, expires_in_days, scopes[]}(要 mint 的 PAT 规格)。返回 {device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval}

POST

/api/oauth/device/token

匿名

RFC 8628 §3.4 + §3.5。CLI 用 device_code 按 interval 轮询。成功返回 minted PAT plaintext(一次性);待批 / 太快 / 过期 / 拒绝 / 无效全部返 HTTP 400 + {error} ∈ authorization_pending | slow_down | expired_token | access_denied | invalid_grant

GET

/api/oauth/device/code?user_code=

session only

SPA 的 /<lang>/device 用 user_code 查待批请求;响应除了 CLI seeded 的 name / scopes / expires_in_days 外还包含 available_scopes(用户能 mint 的全部 scope)/ scope_descriptions / max_expiry_days,供浏览器渲染编辑表单

POST

/api/oauth/device/approve

session only

body {user_code} + 可选 {name, scopes, expires_in_days} 覆盖 —— 让用户在 Approve 时改 PAT 规格。下一次 /token 轮询会以最终(覆盖后的)规格 mint PAT 并绑定到当前 session 用户

POST

/api/oauth/device/deny

session only

body {user_code},拒绝。下一次 /token 轮询返回 access_denied

完整 device-flow 概念背景见 概念 · 鉴权 · OAuth Device Flow;schema 详见 /swagger/index.html。

Admin(管理端:Asset Browser + 获取失败)

Admin 端点全部走 adminGuard:session token + admin 白名单(auth.admin_logins 等,PAT 一律拒绝——管理员是人)。Asset Browser 是运维通道,独立于 paper_access.enabled(不分发义务:admin 面板不算对外重分发 lane)。

Admin Asset Browser(/api/admin/assets/*)

SPA 页面 /$lang/admin/assets:防抖搜索 → 论文卡片 → 可展开资产表,每行 Preview(PDF viewer / Markdown 与 LaTeX 渲染预览,可切换原文)、Download、Copy Presigned URL + Copy S3 Key。 {kind} 取 pdf 或 markdown(别名 md)。

Method

Path

用途

GET

/api/admin/assets/{paper_id}

资产清单:每个 asset 的 S3 object key / size / sha256 / content-type

GET

/api/admin/assets/{paper_id}/{kind}

单资产详情;?presign=true&ttl=1h 附带 presigned URL

GET

/api/admin/assets/{paper_id}/{kind}/download

代理流式下载(Content-Disposition: attachment)

GET

/api/admin/assets/{paper_id}/{kind}/inline

代理流式预览(Content-Disposition: inline;PDF 进 iframe viewer,Markdown 作为文本获取后由 SPA 安全渲染,保留原文模式)

GET

/api/admin/assets/{paper_id}/{kind}/url

presigned S3 URL JSON——浏览器直连 S3,绕过 qatlasd 代理。TTL 1 分钟–24 小时(默认 1h),超出区间截断

GET

/api/admin/assets/batch?paper_ids=a,b,c

批量资产清单(≤50 篇)

GET

/api/admin/assets/batch/download?paper_ids=a,b,c&kind=pdf

流式 ZIP 打包下载(≤20 篇)

GET

/api/admin/assets/search?q=...&kind=pdf&limit=50

按 title / DOI / arXiv ID(ILIKE)找有资产的论文

presign 仅 S3 后端支持(presign_supported 字段);dev 的 LocalStore 回落代理流。

获取失败与补救

Method

Path

用途

GET

/api/admin/acquisition/failures

持久化的 PDF 抓取失败记录:阶段、原因、重试计数(paper_acquisition_events 审计表之上的聚合视图)

管理员 SPA 的失败表对每行渲染 DOI 直达链接 + 内联 PDF 上传(人工取回 PDF 对着 DOI 直接 POST /api/papers/{doi}/upload-pdf),即时闭合获取状态并触发 MinerU——这是 Robust Downloader 的失败兜底闭环,见 Robust Downloader · 管理员补救流。

端点详解:选粹

GET /api/health

返回 PocketBase envelope 形状:

{
  "code": 200,
  "message": "API is healthy.",
  "data": {
    "status": "healthy",
    "version": "0.2.8",
    "uptime_seconds": 12345,
    "time": "2026-05-29T03:00:00Z",
    "checks": {
      "rawstore": {
        "status": "ok",
        "backend": "s3",
        "endpoint": "http://<rustfs-internal-host>:9000",
        "bucket": "qatlas-raw",
        "latency_ms": 12
      },
      "postgres": {
        "status": "ok",
        "backend": "postgres",
        "latency_ms": 8
      }
    }
  }
}
  • data.status 是聚合状态:healthy(全部 ok 或 not_configured)/ degraded(任一 error)

  • code 永远 200(即使 degraded)—— 别让上层 LB / Caddy 把整条链路 trip 成 down

  • message 在 degraded 时变 "Dependency degraded.",方便 log scraper

  • 每个 probe 5 秒超时(probeTimeout),三个并行执行

  • PostgreSQL / S3 不配置时返回 "status": "not_configured",不下拉聚合等级

POST /api/papers/{arxiv_id}/upload-pdf

完整流程详见 Upload API。要点:

  • multipart form 字段:pdf (必)

  • query 参数:expected_sha256=<hex> (强烈推荐) / overwrite=true

  • 状态码:

    • 201 Created — 写了新对象

    • 200 OK — 全部 unchanged 短路,零写入

    • 409 Conflict — 已冻结别名不同SHA,即使 overwrite=true;body含已有/新SHA,禁止替换来源

    • 400 Bad Request — sha256 mismatch / 损坏的 multipart / PDF header 不对等

  • 并发安全(S3 conditional PUT If-None-Match),多 client 同字节并发只产生 1 个 201 + 其余 200

历史兼容 LRO 示例(旧缓存状态字段)

以下图/示例保留旧接口的历史状态字段,不作为新不可变完整包/readiness或覆盖写规则。 当前PDF按gate鉴权交付,无解析token也可读;正文/图片/figures可202,纯read/status 核验完整包后state=cached/ready=true才固定source/revision重取。新信封、headers与 懒迁移/严格上传请以论文内容契约和Upload API为准。 YAML paper_access.enabled取代旧环境变量;format=link仅同源认证locator,无NAS presign。

仅在 QATLAS_PAPER_ACCESS_ENABLED=true 时注册。两类资源 (markdown 与 pdf)共享同一套异步 + 进度可拉的协议,让 agent 方可以放心并发调用同一篇论文:

  • 第一次 GET 立即返回,不阻塞 fetch / convert 完成;

  • N 个并发调用被 server-side dedupe 成 1 次 fetch + 1 次 convert,所有调用方 观察同一 Job snapshot;

  • …/status 端点是 side-effect-free 的,poll 多少次都不会再触发后端任务。

状态机

                            cached ────────────────────────────────────────────┐
GET /md ── miss ──┬→ queued ──→ fetching ──→ converting ──→ done ──────────────┤
                  │   (silent fetch from arxiv.org, then MinerU convert)         │
                  │                                                                │
                  └→ failed(retryable | fatal | quota_exhausted,                  │
                         phase ∈ {error_fetching, error_converting})              │
                  └→ not_in_arxiv (404)                                            │

注:这是历史状态图;PDF410停用阶段已被当前固定来源鉴权交付取代。
已存PDF无推理依赖,原件未就绪与完整包ready按新契约区分。

触发 GET(202 / 200 / 404)

curl -i https://<server>/api/papers/quant-ph/9508027v2/markdown \
     -H "Authorization: Bearer $QATLAS_TOKEN"

# 缓存命中
HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8
Content-Length: 76382
…markdown bytes…

# 缓存未命中(首次或仍在跑)
HTTP/1.1 202 Accepted
Operation-Location: /api/papers/quant-ph/9508027v2/markdown/status
Retry-After: 5
{
  "arxiv_id": "quant-ph/9508027v2",
  "state":   "queued",
  "phase":   "fetching_pdf",
  "pdf_ready": false,
  "md_ready":  false,
  "submitted_at": "2026-06-05T03:00:00Z",
  "detail":  "conversion in progress",
  "operation": {
    "status_url":          "/api/papers/quant-ph/9508027v2/markdown/status",
    "next_poll_after_iso": "2026-06-05T03:00:05Z"
  }
}

合规:内容访问开关(QATLAS_PAPER_ACCESS_ENABLED)

是否对外分发论文内容由这个服务端开关决定(合规落点,ADR 0011),也决定上面这些 PAPER_ACCESS 路由是否注册:

开关

pdf

markdown / json

说明

关(默认,公共 quantum-atlas.ai)

arxiv 论文 → arxiv.org 直链;非 arxiv → 不给

不给

不再分发;canonical 源自己分发。(当前实现:端点整体不注册 → 404;arxiv 直链是 ADR 0011 既定 OFF 态,跟踪 #8)

开(operator 显式接受衍生作品分发义务)

发(默认直链,见下)

发(默认字节流,见下)

详见 License & Attribution · 论文访问开关

字节流 vs 直链(?format=,ADR 0011)

开关允许发之后,用什么形式发是与合规无关的传输选择,按 kind 有默认值,并可用 ?format=link|bytes 逐请求复写:

资产

默认

复写

说明

markdown

字节流

?format=link

MinerU 派生的小文本,默认内联串出

images/zip

字节流

?format=link

MinerU 产出的图片打包 zip,显式请求才返回

后端无法 presign(dev 的 LocalStore)时,link 请求自动回落字节流。直链响应形如:

curl -i "https://<server>/api/papers/quant-ph/9508027v2/markdown?format=link" \
     -H "Authorization: ******"

HTTP/1.1 200 OK
Content-Type: application/json
{
  "arxiv_id":     "quant-ph/9508027v2",
  "format":       "link",
  "markdown_url": "https://raw.quantum-atlas.ai/qatlas-pdf/9508/9508027v2.md?X-Amz-…",
  "expires_in":   86400
}

进度查询(status;存储不可用时 503)

curl https://<server>/api/papers/quant-ph/9508027v2/markdown/status \
     -H "Authorization: Bearer $QATLAS_TOKEN"

各状态对应 body(关键字段加粗):

// 已就绪
{
  "arxiv_id": "quant-ph/9508027v2",
  "state":   "cached",
  "phase":   "ready",
  "pdf_ready": true,
  "md_ready":  true,
  "markdown_url": "/api/papers/quant-ph/9508027v2/markdown"
}

// 正在 fetch PDF
{
  "arxiv_id": "quant-ph/9508027v2",
  "state":   "running",
  "phase":   "fetching_pdf",
  "pdf_ready": false,
  "md_ready":  false,
  "submitted_at": "...",
  "started_at":   "...",
  "fetch": {
    "started_at":     "...",
    "bytes_received": 1234567,
    "bytes_total":    4503234,
    "attempts":       1
  }
}

// PDF 已保存,MinerU 在跑(pdf_ready=true 告诉 agent: PDF 没白下)
{
  "arxiv_id": "quant-ph/9508027v2",
  "state":   "running",
  "phase":   "converting_md",
  "pdf_ready": true,
  "md_ready":  false,
  "fetch":   {"started_at":"...", "completed_at":"...", "bytes_received":4503234, "sha256":"...", "attempts":1},
  "convert": {"started_at":"...", "mineru_task_id":"task-abc", "stage":"running", "polled_count":7},
  "queue":   {"running_count":4, "max_concurrent":4, "eta_basis":"observed_avg_of_15_jobs", "avg_duration_seconds":180}
}

// MinerU slot 不够,排队中(agent 看 queue.position / eta_seconds 决定退避)
{
  "arxiv_id": "2501.00010v1",
  "state":   "queued",
  "phase":   "converting_md",
  "pdf_ready": true,
  "md_ready":  false,
  "submitted_at": "...",
  "queue": {
    "position":             3,
    "ahead_of_me":          2,
    "running_count":        4,
    "max_concurrent":       4,
    "eta_seconds":          540,
    "avg_duration_seconds": 180,
    "eta_basis":            "observed_avg_of_18_jobs"
  }
}

// MinerU 配额耗尽(PDF 已抓取但不再对外交付;等配额恢复后取 markdown)
{
  "arxiv_id": "quant-ph/9508027v2",
  "state":   "cooldown",
  "phase":   "error_converting",
  "kind":    "daily_limit",
  "pdf_ready": true,
  "md_ready":  false,
  "detail":   "server quota exhausted: all 3 MinerU API keys have reached today's daily limit — service resumes at 2026-06-06T00:01:00+08:00",
  "retry_after":     1717603260,
  "retry_after_iso": "2026-06-06T00:01:00+08:00"
}

// arxiv 没这篇(fatal,agent 应放弃)
{
  "arxiv_id": "quant-ph/9999999v9",
  "state":   "failed",
  "phase":   "error_fetching",
  "kind":    "fatal",
  "pdf_ready": false,
  "md_ready":  false,
  "detail":   "fetch from arxiv: arxiv: paper not found"
}

Agent 决策三元组

任何 status 响应都带 state + pdf_ready + md_ready,从这三个字段即可 判断「该不该等」「等什么」:

state

pdf_ready

md_ready

agent 行动

cached

是

是

直接 GET 资源拿字节

queued / running

否

否

等 Retry-After,再 poll

running

是

否

等 MinerU 收尾,之后 GET /markdown

cooldown

是

否

到 retry_after_iso 之前不再 poll

failed kind=fatal

否

否

永久失败,放弃

failed kind=retryable

否 / 是

否

到 retry_after_iso 后重试

DOI 入口

{id_or_doi} 路径段头部如果匹配 ^10\.\d{4,9}/(IANA DOI 前缀),server 自动经 OpenAlex 反查 → canonical arxiv id → 走同一套 handler。

# DOI → arxiv → 同一个 LRO 流程
curl -i https://<server>/api/papers/10.1103/PhysRevLett.103.150502/markdown \
     -H "Authorization: Bearer $QATLAS_TOKEN"

自动默认值(server 应用的隐式推断)

server 接受三类"不完整"的 id 输入,并自动补齐到 canonical 形态:

输入形态

例子

server 推断

默认值出处

DOI

10.1103/PhysRevLett.103.150502

OpenAlex 反查 → arxiv canonical

OpenAlex API

无版本 arxiv id

0811.3171 / quant-ph/9508027

加 vN = latest

/abs/<id> HTML <meta property="og:url">

bare old-style(无 category)

9508027 / 9508027v2

加 category=quant-ph

paperassets.DefaultOldStyleCategory(来自 cat:quant-ph bootstrap 假设;issue #10 跟踪全量审计)

每一次应用默认值,server 都会在响应里通过两条平行通道告知 caller:

HTTP headers(永远存在,包括字节流响应):

X-QAtlas-Requested-Id: 9508027
X-QAtlas-Resolved-Id:  quant-ph/9508027v2
X-QAtlas-Defaults-Applied: version=v2 (no version specified; latest published version used); category=quant-ph (no category prefix; server default per docs/reference/arxiv-ids.md §3.1)

JSON body 字段(仅 JSON 响应;与 header 同步):

{
  "arxiv_id":       "quant-ph/9508027v2",
  "requested_id":   "9508027",
  "resolved_id":    "quant-ph/9508027v2",
  "defaults_applied": [
    "version=v2 (no version specified; latest published version used)",
    "category=quant-ph (no category prefix; server default per docs/reference/arxiv-ids.md §3.1)"
  ],
  "state": "queued",
  ...
}

输入本身就是 canonical(带 vN + 带 category)时,三个字段全部省略,body 保持精简。

Agent / client 集成建议:

  • 把 X-QAtlas-Defaults-Applied 渲染成一行 Note: … 输出,避免用户因为"我请求的是 0811.3171 但你给我 v3" 产生疑惑;

  • requested_id 跟 resolved_id 不一致时可作为缓存键的二次校验;

  • 不要硬解析 defaults_applied 的字符串内容(它是 human-readable,未来会有微调);要程序化判断"应用了什么默认"请用 resolved_id != requested_id 加上 path 前缀启发式。

DOI 路径特有错误:

状态

含义

400

DOI 格式不合规(长度 > 256 / 含控制字符 / 前缀不匹配)

404

OpenAlex 找不到该 DOI,或该 work 没有 arxiv 表现(OA ids.arxiv 缺失)

404

DOI 解出 arxiv id,但 arxiv abs 页 404(论文被撤稿 / 从未索引)

502

OpenAlex 上游错误 / arxiv version 解析上游错误(连接 / 5xx 重试用尽)

503

QATLAS_OPENALEX_MAILTO 未配置 — 服务方需补配

503

arxiv version 解析 rate-limited — 稍后重试

POST /api/pat(session only)

{
  "name": "ci-upload",
  "description": "...",
  "scopes": ["papers:write"],
  "expires_in_days": 365
}

约束:

  • name 必填,≤80 字符

  • description ≤200 字符

  • expires_in_days 必填,1–365

  • scopes 必须是 /api/pat/scopes 返回词表里的;空集合也接受(这个 PAT 啥都干不了)

响应(plaintext 只出现这一次):

{
  "id": "abc123",
  "name": "ci-upload",
  "prefix": "qat_AB",
  "plaintext": "qat_ABXXXXX...XXXXX",
  "description": "",
  "scopes": ["papers:write"],
  "expires_at": "2027-05-29 03:00:00.000Z",
  "created": "2026-05-29 03:00:00.000Z"
}

错误响应规范

绝大多数错误都是 {"detail": "<message>"},例外是:

端点

特殊形状

upload-pdf 409

{detail, existing_sha256, new_sha256, existing_path, hint}

upload-pdf 400 (sha256 mismatch)

{detail, expected_sha256, actual_sha256}

scopeGuard 403

{detail, obj, act}

graph/* 故障

200 + {error: "..."}(不是 5xx)

速率限制

PocketBase 自带 collection 级 throttle。/api/pat 还挂了自定义 rate-limit 规则(默认在启动时 pat.EnsureDefaults 装),防止暴力 mint。常见限制:

  • /api/pat POST: 10/minute per user

  • /api/health: 无限

  • 其他写口: 60/minute per user

具体规则在 PocketBase Settings → _pb_users_auth_ 等 collection 的 throttle 字段。

PocketBase 原生 collection API

除了上面 /api/* 自定义 endpoint,PocketBase 自己暴露 /api/collections/<name>/records/... 的 CRUD API。详见 PocketBase 文档。业务上几乎不用——所有暴露给用户的能力都通过自定义 /api/... endpoint 走。

例外:SPA 直接用 PocketBase JS SDK 做 OAuth 登录、读 users 自身记录等。

资产读取与存储故障

注册表的 has_md / has_pdf 及资产路径表示已登记的内容,不是实时存储健康探针。 Markdown 资产读取,以及单篇 PDF/Markdown status 的存储探测最多等待 10 秒;更短的请求 deadline 优先。存储 Stat、Get 或首字节读取失败时返回 HTTP 503、Retry-After: 5 和以下结构,详细后端错误留在服务端日志:

{"code":"asset_store_unavailable","detail":"asset storage temporarily unavailable; retry shortly","kind":"retryable","retryable":true,"state":"unavailable"}

失败不计为缓存 miss,不启动替代下载或 MinerU 转换,不清空登记资产。批量 status 保持逐项结果:受影响项的 state=unavailable 并携带 error,就绪布尔值不能单独 解释为资产缺失。流已开始后发生的失败只能中断传输,客户端应丢弃不完整内容并重试。

旧 paper_assets 不会随升级自动回填为不可变 source / parse revision;原始资产 路径保留,列出 originals 只读,可能得到空列表。需要固定版本时按贡献流程登记 实际 PDF 和解析产物,并核对 source/revision ID 与 SHA-256。