OpenMAIC v1.2.1 API 使用详解

OpenMAIC v1.2.1 API 使用详解:77 条 HTTP 路由的分层说明、访问口令换令牌与两种错误信封、课程库/资料/课程/生成任务/模型配置接口的逐条 curl 实测回显、发布撤销与端到端脚本,以及常见错误码排障。

OpenMAICOpenMAICREST APIAPILLM2026/10/25

OpenMAIC v1.2.1 API 使用详解

目标:把 OpenMAIC v1.2.1 的 HTTP 接口讲清楚并能直接用起来。本文覆盖 77 条路由的分层结构、访问口令换令牌的认证流程、两种错误信封、课程库/资料/课程/生成任务/模型配置五组接口的逐条实测,以及可直接复用的端到端脚本。所有请求与回显均在本机实测(OpenMAIC v1.2.1、PostgreSQL 16.15、Ubuntu 24.04.5、Docker 29.9.2),采集日期 2026-10-25。部署流程见《OpenMAIC v1.2.1 Docker Compose 部署指南》,参数含义见《OpenMAIC v1.2.1 配置参数详解》。

一、环境信息

  • 应用:OpenMAIC v1.2.1,Next.js 16.3.3,Node.js v22.23.4
  • 数据库:PostgreSQL 16.15,实测 25 张业务表
  • 基地址:http://127.0.0.1:8144(服务内 3000)
  • 身份模式:单用户(OWNER_SINGLE_USER=true),所有者标识固定为 local
  • 访问口令:示例口令 q1w2e3r4,登录后 7 天有效
  • 模型:未配置任何模型,因此涉及大模型调用的端点只验证到「前置校验层」

为什么用 API 文档而不是纯 Web 操作文档:OpenMAIC 的前端本身完全构建在这些 HTTP 接口上,课程库、资料、课程、生成任务、模型配置都有稳定路由,可以脱离浏览器被脚本、CI 或自有门户直接调用。掌握这些接口等于掌握了平台的自动化入口。

二、API 分层与认证

2.1 路由分类

源码中 app/api 下一共有 77 条 route.ts,按职责可以分成七组:

分组代表路由是否需要访问口令说明
健康与门禁GET /api/health、GET /api/access-code/status、POST /api/access-code/verify否三个路由在口令校验之前执行
课程库/api/folders、/api/folders/{id}、/api/folders/members是文件夹的增删改查
资料库/api/materials、/api/materials/{id}、/api/materials/{id}/extraction是上传、解析与删除
课程/api/stages、/api/stages/{id}、`/api/stages/{id}/(statusmanifestscenes
生成任务/api/generate-classroom、/api/generation-runs、`/api/generation-runs/{id}/(confirm-outlinehold-outlineretry
模型与能力/api/model-config、/api/server-providers、/api/provider/probe-models、/api/verify-model、/api/usage、/api/generate-classroom/capabilities是服务商、槽位、用量、能力探测
智能体与其他/api/agents、/api/agent/sessions、/api/pbl/v2/*、/api/export-video/*、/api/quiz-grade 等是课堂运行、PBL、视频导出

2.2 认证流程

访问口令是站点级门禁:先 POST /api/access-code/verify 换取一个 7 天有效的签名令牌,后续请求带上 openmaic_access Cookie。浏览器会自动完成,脚本需要手工处理。

先确认门禁状态:

curl -s http://127.0.0.1:8144/api/access-code/status

实测回显:

{"success":true,"enabled":true,"authenticated":false}

未认证就访问业务接口:

curl -s -o /tmp/out.json -w 'HTTP %{http_code}\n' http://127.0.0.1:8144/api/folders && cat /tmp/out.json

实测回显:

HTTP 401
{"success":false,"errorCode":"INVALID_REQUEST","error":"Access code required"}

换取令牌(错误口令先验证失败路径):

curl -s -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8144/api/access-code/verify \
  -H 'Content-Type: application/json' --data '{"code":"wrong-code"}'

实测回显:

{"success":false,"errorCode":"INVALID_REQUEST","error":"Invalid access code"}
[HTTP 401]

正确口令换取令牌并写入环境变量(后续命令统一使用 $TOK):

TOK=$(curl -s -D - -o /dev/null -X POST http://127.0.0.1:8144/api/access-code/verify \
  -H 'Content-Type: application/json' --data '{"code":"q1w2e3r4"}' \
  | sed -n 's/.*openmaic_access=\([^;]*\).*/\1/p')
AUTH="Cookie: openmaic_access=$TOK"
echo "${#TOK} 字符"

实测回显:

78 字符

验证令牌有效:

curl -s -H "$AUTH" http://127.0.0.1:8144/api/access-code/status

实测回显:

{"success":true,"enabled":true,"authenticated":true}

⚠️ 纯 HTTP 部署的坑:生产构建给该 Cookie 带 Secure,浏览器通过 http://服务器IP:8144 访问时不会保存它,表现为每次刷新都要重输口令。必须在编排中设 COOKIE_SECURE=0;curl 不受影响,但代理链路与浏览器要一起考虑。

2.3 两种错误信封

实测发现错误响应有两种形状,写客户端时必须都处理:

统一信封(多数业务路由,如 401、生成任务):

{"success":false,"errorCode":"INVALID_REQUEST","error":"Access code required"}

路由局部信封(文件夹、身份认领等):

{"error":{"code":"NO_PENDING_CLAIM","message":"this request presents no anonymous identity to claim"}}

兜底 404(未匹配的路由,返回纯文本或最简 JSON):

curl -s -w '\n[HTTP %{http_code}]\n' -H "$AUTH" http://127.0.0.1:8144/api/stage-meta/nope

实测回显:

{"error":"not_found"}
[HTTP 404]

405(路径存在但方法不对):

[HTTP 405]

实测 GET /api/materials/{id}/extraction 返回 405,该方法只支持 POST(用于主动触发解析)。

判定建议:优先看 HTTP 状态码,再按 errorCode 或 error.code 取业务码;两者都缺时按「网关/路由层错误」处理。

2.4 所有者分区

所有业务数据按所有者隔离。单用户模式下所有者恒为 local:

curl -s -H "$AUTH" http://127.0.0.1:8144/api/persistence/learner-key

实测回显:

{"learnerKey":"local"}

关闭单用户模式后,同一口令的两台浏览器会各自得到一个匿名所有者(详见配置篇),数据互不可见。这意味着:访问口令不等于用户账号,它只决定「能不能进来」。

三、课程库接口(folders)

3.1 创建文件夹

curl -s -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8144/api/folders \
  -H "$AUTH" -H 'Content-Type: application/json' --data '{"name":"API 实测课程组"}'

实测回显:

{"folder":{"id":"folder-wsnM0Eo5c7EE","name":"API 实测课程组","order":0,"createdAt":1792915280594,"updatedAt":1792915280594,"userKey":"local"}}
[HTTP 200]

要点:请求体只需 name;名称按显示宽度校验(全角算 2,半角算 1,上限 40),重名按不区分大小写拒绝;order 由服务端分配。

3.2 列出与改名

curl -s -w '\n[HTTP %{http_code}]\n' -H "$AUTH" http://127.0.0.1:8144/api/folders

实测回显:

{"folders":[{"id":"folder-wsnM0Eo5c7EE","name":"API 实测课程组","order":0,"createdAt":1792915280594,"updatedAt":1792915280594,"userKey":"local"}]}
[HTTP 200]
curl -s -w '\n[HTTP %{http_code}]\n' -X PATCH http://127.0.0.1:8144/api/folders/folder-wsnM0Eo5c7EE \
  -H "$AUTH" -H 'Content-Type: application/json' --data '{"name":"API 实测课程组(改名)"}'

实测回显:

{"folder":{"id":"folder-wsnM0Eo5c7EE","name":"API 实测课程组(改名)","order":0,"createdAt":1792915280594,"updatedAt":1792915286594,"userKey":"local"}}
[HTTP 200]

createdAt 不变、updatedAt 前移,说明是原地改名而不是重建。

3.3 删除文件夹

curl -s -X DELETE "http://127.0.0.1:8144/api/folders/folder-wsnM0Eo5c7EE?mode=ungroup" -H "$AUTH"

实测回显:

{"ok":true,"removedStageIds":[]}

mode 有两个取值:

  • ungroup(默认):只删文件夹,里面课程变为「未归档」,课程本身保留。
  • remove:删文件夹并返回被捕获的课程 ID 列表,由调用方自行级联删除。

四、资料接口(materials)

4.1 上传

上传不是 JSON,而是「原始字节 + 元数据头」:

  • Content-Type:资料的真实 MIME,服务端会归一化并校验,不支持的类型返回 415。
  • x-material-filename:展示文件名,必填。
  • 体积上限:媒体类与文档类分别限流,超限返回 413,并在响应体给出 maxBytes。
printf 'OpenMAIC API probe material: TCP three-way handshake.\n' > probe-material.txt
curl -s -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8144/api/materials \
  -H "$AUTH" -H 'Content-Type: text/plain' -H 'x-material-filename: probe-material.txt' \
  --data-binary @probe-material.txt

实测回显:

{"materialId":"mat_qpxkw7jw0cevtpad9bk4tjpv7w","originalName":"probe-material.txt","bytes":54,"mime":"text/plain","mediaKind":"document","extraction":{"status":"extracting","updatedAt":1792915286613}}
[HTTP 201]

上传即触发后台解析,所以 201 时状态是 extracting。按源码约定还支持 ?extract=false 跳过立即解析,此时状态为 idle,任务引用该资料时再触发。

4.2 查询与解析状态

curl -s -w '\n[HTTP %{http_code}]\n' -H "$AUTH" http://127.0.0.1:8144/api/materials/mat_qpxkw7jw0cevtpad9bk4tjpv7w

实测回显:

{"material":{"materialId":"mat_qpxkw7jw0cevtpad9bk4tjpv7w","kind":"source","mime":"text/plain","bytes":54,"originalName":"probe-material.txt","mediaKind":"document","extraction":{"status":"ready","extractor":"plain-text","pageCount":0,"textChars":54,"updatedAt":1792915286633,"imageCount":0,"resultBytes":104},"createdAt":"2026-10-25T08:01:26.609Z"}}
[HTTP 200]

解析完成后状态为 ready,并给出提取器名称、字符数与结果字节数。列表接口返回每个资料的精简视图:

curl -s -H "$AUTH" http://127.0.0.1:8144/api/materials | python3 -c 'import sys,json; d=json.load(sys.stdin); print("count=",len(d["materials"])); [print(m["materialId"],m["originalName"],m["mediaKind"],m["extraction"]["status"]) for m in d["materials"]]'

实测回显:

count= 2
mat_qpxkw7jw0cevtpad9bk4tjpv7w probe-material.txt document ready
mat_nfg8sft697vwmc2dpw7mbad1hc probe-material.txt document ready

第二条是早前探测留下的同解析结果,说明资料按所有者累积、不会因同名覆盖。

4.3 删除

curl -s -w '\n[HTTP %{http_code}]\n' -X DELETE http://127.0.0.1:8144/api/materials/mat_qpxkw7jw0cevtpad9bk4tjpv7w -H "$AUTH"

实测回显:

{"materialId":"mat_qpxkw7jw0cevtpad9bk4tjpv7w","deleted":true}
[HTTP 200]

要点:删除会同时回收字节对象与配额;未被任何任务引用且超过 TTL 的资料也会被后台清理。

五、课程接口(stages)

5.1 创建课程壳

curl -s -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8144/api/stages \
  -H "$AUTH" -H 'Content-Type: application/json' \
  --data '{"name":"API 实测课程","description":"HTTP API 示例"}'

实测回显:

{"stage":{"id":"stage-secv9eLy-fh7","name":"API 实测课程","description":"HTTP API 示例","createdAt":1792915293471,"updatedAt":1792915293471,"sceneCount":0}}
[HTTP 201]

创建出来的是课程壳:有名称、描述与空大纲,场景数为 0。真正的场景要由生成任务填充,或用 PUT /api/stages/{id} 整体写入文档。

5.2 读取完整文档

curl -s -w '\n[HTTP %{http_code}]\n' -H "$AUTH" http://127.0.0.1:8144/api/stages/stage-secv9eLy-fh7

实测回显:

{"stage":{"id":"stage-secv9eLy-fh7","name":"API 实测课程","createdAt":1792915293471,"updatedAt":1792915293471,"description":"HTTP API 示例"},"scenes":[],"dslVersion":"0.3.0","outline":{"outlines":[],"createdAt":1792915293471,"updatedAt":1792915293471,"requirement":"API 实测课程","generationComplete":false}}
[HTTP 200]

dslVersion: "0.3.0" 是课程文档格式版本,导入导出时应校验;outline.generationComplete: false 表示尚未生成。

5.3 状态、清单与场景

curl -s -H "$AUTH" http://127.0.0.1:8144/api/stages/stage-secv9eLy-fh7/status

实测回显:

{"isPublic":false,"publishedAt":null}
curl -s -H "$AUTH" http://127.0.0.1:8144/api/stages/stage-secv9eLy-fh7/manifest

实测回显:

{"rev":1,"scenes":[]}

manifest 是给播放端用的轻量清单,rev 随文档每次修改递增,可用于判断是否需要重新拉取。

场景接口按 ID 批量取,必须给 ids 参数,否则拒绝:

curl -s -w '\n[HTTP %{http_code}]\n' -H "$AUTH" http://127.0.0.1:8144/api/stages/stage-secv9eLy-fh7/scenes

实测回显:

{"success":false,"errorCode":"INVALID_REQUEST","error":"empty_scene_ids"}
[HTTP 400]

正确用法是 ?ids=<场景ID>,<场景ID>;本次课程场景为空,故只验证了参数校验层。

5.4 发布与撤销

curl -s -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8144/api/stages/stage-secv9eLy-fh7/publish \
  -H "$AUTH" -H 'Content-Type: application/json' --data '{}'

实测回显:

{"success":true,"publishedAt":1792915305526,"name":"API 实测课程"}
[HTTP 200]

再查状态:

{"isPublic":true,"publishedAt":1792915305526}

撤销发布:

curl -s -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8144/api/stages/stage-secv9eLy-fh7/unpublish \
  -H "$AUTH" -H 'Content-Type: application/json' --data '{}'

实测回显:

{"success":true}
[HTTP 200]

publishedAt 是毫秒时间戳,发布后课程可被公开访问;撤销后 isPublic 回到 false,但课程内容不删。

5.5 删除课程

curl -s -w '\n[HTTP %{http_code}]\n' -X DELETE http://127.0.0.1:8144/api/stages/stage-secv9eLy-fh7 -H "$AUTH"

实测回显:

{"ok":true}
[HTTP 200]

删除会连同场景与大纲一起移除;此操作不可恢复,脚本里应先确认课程 ID。

六、生成任务接口

6.1 启动生成

curl -s -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8144/api/generate-classroom \
  -H "$AUTH" -H 'Content-Type: application/json' --data '{}'

实测回显(缺必填字段):

{"success":false,"errorCode":"MISSING_REQUIRED_FIELD","error":"Missing required field: requirement"}
[HTTP 400]

补上需求描述,但本环境没有配置模型:

curl -s -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8144/api/generate-classroom \
  -H "$AUTH" -H 'Content-Type: application/json' \
  --data '{"requirement":"用 10 分钟讲清 TCP 三次握手"}'

实测回显:

{"success":false,"errorCode":"MISSING_MODEL","error":"No model is configured for course.outline. Set one in the model settings, or assign the slot (or an ancestor) in openmaic.yml."}
[HTTP 400]

请求体字段与服务端行为:

  • requirement:必填,自然语言需求;
  • materialIds:可选,引用已上传资料;
  • 按源码约定,成功时返回 202 与任务对象(不是 200),因为生成是异步任务;本环境无模型,未取到成功回显;
  • 大纲会被自动确认;同一所有者活跃任务超限时返回 429 ACTIVE_RUN_LIMIT。

6.2 任务列表与限制

curl -s -w '\n[HTTP %{http_code}]\n' -H "$AUTH" http://127.0.0.1:8144/api/generation-runs

实测回显:

{"success":false,"errorCode":"INVALID_REQUEST","error":"Only the active runs are listed: pass active=1"}
[HTTP 400]

必须显式带 active=1:

curl -s -w '\n[HTTP %{http_code}]\n' -H "$AUTH" 'http://127.0.0.1:8144/api/generation-runs?active=1'

实测回显:

{"success":true,"runs":[],"limits":{"maxActive":2,"maxWaiting":10}}
[HTTP 200]

limits 直接来自服务端配置,可用来在客户端提前拦截超额请求,而不是等 429。

6.3 任务级操作

方法与路径作用关键语义
GET /api/generation-runs/{id}查看任务详情含阶段、进度与错误
GET /api/generation-runs/{id}/events拉事件流用于断线重连
POST /api/generation-runs/{id}/confirm-outline确认大纲转人工确认流程时使用
POST /api/generation-runs/{id}/hold-outline暂缓确认保持大纲待审
POST /api/generation-runs/{id}/retry重试失败任务从检查点续跑

任务在数据库中保存检查点,服务进程重启不会丢任务;租约 10 秒过期后由其他进程接管(见启动日志 leaseTtl=10000ms)。

💡 技术拓展:异步任务的三种轮询方式。

方式实现优点缺点
固定间隔轮询每 2–5 秒 GET /api/generation-runs/{id}简单、无状态有延迟与空请求
长连接事件流GET /api/generation-runs/{id}/events实时、可按事件续传需要处理断线与重连
Webhook 回调监听任务完成事件转发解耦需自建接收端与验签

OpenMAIC 提供前两种;对绝大多数脚本,按 2 秒轮询 GET /api/generation-runs/{id} 直到 finished 或用 retry 兜底已足够。

七、模型配置与能力接口

7.1 模型配置视图

GET /api/model-config 返回完整配置(服务商预设、已保存服务商、槽位状态)。直接输出内容很长,实测用脚本汇总:

curl -s -H "$AUTH" http://127.0.0.1:8144/api/model-config \
  | python3 -c 'import sys,json; d=json.load(sys.stdin); print("presets=",len(d["presets"]),"providers=",[(p["id"],p["preset"]) for p in d["providers"]],"slots=",len(d["slots"])); print("assigned:",[s["slot"] for s in d["slots"] if s["effective"]["status"]=="assigned"])'

实测回显:

presets= 62 providers= [('builtin', 'builtin')] slots= 20
assigned: ['webFetch']

解读:

  • 62 个服务商预设:开箱可选的服务商模板数量。
  • builtin 服务商:平台内置能力,不需要密钥。
  • 20 个槽位:llm、course.research、course.outline、course.agents、course.content 及其四个子槽位、course.actions、classroom、agent、agent.title、tts、asr、image、video、webFetch、webSearch、document。
  • 仅 webFetch 已分配,其余 19 个为 unassigned:这正是生成课程报 MISSING_MODEL 的根因。webFetch 走内置抓取,不消耗模型。

7.2 服务端能力汇总

curl -s -w '\n[HTTP %{http_code}]\n' -H "$AUTH" http://127.0.0.1:8144/api/server-providers

实测回显:

{"success":true,"providers":{},"tts":{},"asr":{},"pdf":{},"image":{},"video":{},"webSearch":{},"generation":{"parallelSceneConcurrency":0}}
[HTTP 200]

各项为空对象表示该类未配置服务商;parallelSceneConcurrency: 0 与健康接口一致。

7.3 生成能力与资料限制

curl -s -w '\n[HTTP %{http_code}]\n' -H "$AUTH" http://127.0.0.1:8144/api/generate-classroom/capabilities

实测回显:

{"success":true,"capabilities":{"webSearch":false,"imageGeneration":false,"videoGeneration":false,"tts":false},"materials":{"formats":[{"id":"pdf","mime":"application/pdf","extensions":[".pdf"]},{"id":"txt","mime":"text/plain","extensions":[".txt"]},{"id":"markdown","mime":"text/markdown","extensions":[".md",".markdown"]}],"maxCount":5,"maxTotalBytes":157286400,"maxDocumentBytes":52428800,"maxMediaBytes":52428800}}
[HTTP 200]

这张表可以直接驱动前端:资料格式(PDF/TXT/Markdown)、单次最多 5 份、总量 150 MiB、单文档 50 MiB、单媒体 50 MiB。

7.4 用量统计

curl -s -w '\n[HTTP %{http_code}]\n' -H "$AUTH" http://127.0.0.1:8144/api/usage

实测回显:

{"success":true,"totals":{"requests":0,"llmTokens":0},"byModel":[],"byDay":[],"byKind":[]}
[HTTP 200]

四个维度(总量、按模型、按天、按类型)都返回,便于直接做成本看板。

7.5 模型探测与验证

两个端点用途不同:probe-models 拉取「这个端点有哪些聊天模型」,verify-model 验证「指定模型能否正常工作」。

私网地址被拦截(SSRF 防护):

curl -s -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8144/api/provider/probe-models \
  -H "$AUTH" -H 'Content-Type: application/json' \
  --data '{"baseUrl":"https://127.0.0.1/v1","apiKey":"sk-test"}'

实测回显:

{"success":false,"errorCode":"INVALID_REQUEST","error":"Local/private network URLs are not allowed. If this is a self-hosted deployment or internal gateway (including split-horizon DNS), set ALLOW_LOCAL_NETWORKS=true to allow local network targets."}
[HTTP 400]

缺模型名:

curl -s -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8144/api/verify-model \
  -H "$AUTH" -H 'Content-Type: application/json' \
  --data '{"baseUrl":"https://api.openai.com/v1","apiKey":"sk-test"}'

实测回显:

{"success":false,"errorCode":"MISSING_REQUIRED_FIELD","error":"Model name is required"}
[HTTP 400]

外部端点不可达(本机到公网模型服务超时):

time curl -s -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8144/api/verify-model \
  -H "$AUTH" -H 'Content-Type: application/json' \
  --data '{"model":"gpt-5.5","baseUrl":"https://api.openai.com/v1","apiKey":"sk-test"}'

实测回显:

{"success":false,"errorCode":"INTERNAL_ERROR","error":"Cannot connect to API server, please check the Base URL"}
[HTTP 500]

real	0m6.843s
curl -s -w '\n[HTTP %{http_code}]\n' -X POST http://127.0.0.1:8144/api/provider/probe-models \
  -H "$AUTH" -H 'Content-Type: application/json' \
  --data '{"baseUrl":"https://api.openai.com/v1","apiKey":"sk-test"}'

实测回显:

{"success":false,"errorCode":"UPSTREAM_ERROR","error":"Cannot connect to the provider, please check the Base URL"}
[HTTP 502]

排障含义:500 与 502 的文案都在提示检查 Base URL,但真实原因是应用所在网络出不去。验证顺序应为「服务内 DNS 解析 → TCP 连通 → 再做模型验证」,否则会误改配置。

7.6 其他能力接口

curl -s -w '\n[HTTP %{http_code}]\n' -H "$AUTH" http://127.0.0.1:8144/api/export-video/capability

实测回显:

{"success":true,"enabled":false}
[HTTP 200]

enabled: false 表示未部署渲染服务,MP4 导出不可用;PPTX 与离线 HTML 导出不受影响。

八、智能体接口(agents)

curl -s -H "$AUTH" http://127.0.0.1:8144/api/agents \
  | python3 -c 'import sys,json; d=json.load(sys.stdin); [print(a["id"],a["name"],a["role"],len(a["persona"])) for a in d["agents"]]'

实测回显:

default-1 AI teacher teacher 882
default-2 AI助教 assistant 751
default-3 显眼包 student 949
default-4 好奇宝宝 student 929
default-5 笔记员 student 856
default-6 思考者 student 963

平台内置 6 个默认智能体:1 名教师、1 名助教、4 名学生(人设长度在 750–1000 字符之间)。课堂中的角色分工由此而来:教师讲解、助教补充、学生提问与讨论。这些是课程内容资产,可通过 POST /api/agents、POST /api/agents/import 新建或导入。

九、端到端示例

把前几节的调用串成一个可复用脚本,覆盖「认证 → 建文件夹 → 上传资料 → 建课 → 发布 → 撤销 → 清理」。以下脚本与实测输出一一对应:

#!/bin/bash
set -e
BASE=http://127.0.0.1:8144
TOK=$(curl -s -D - -o /dev/null -X POST $BASE/api/access-code/verify \
  -H 'Content-Type: application/json' --data '{"code":"q1w2e3r4"}' \
  | sed -n 's/.*openmaic_access=\([^;]*\).*/\1/p')
AUTH="Cookie: openmaic_access=$TOK"

FID=$(curl -s -X POST $BASE/api/folders -H "$AUTH" -H 'Content-Type: application/json' \
  --data '{"name":"端到端示例"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["folder"]["id"])')
printf 'TCP three-way handshake: SYN, SYN-ACK, ACK.\n' > e2e-material.txt
MID=$(curl -s -X POST $BASE/api/materials -H "$AUTH" \
  -H 'Content-Type: text/plain' -H 'x-material-filename: e2e-material.txt' \
  --data-binary @e2e-material.txt | python3 -c 'import sys,json;print(json.load(sys.stdin)["materialId"])')
SID=$(curl -s -X POST $BASE/api/stages -H "$AUTH" -H 'Content-Type: application/json' \
  --data '{"name":"端到端示例课程","description":"随 API 文档创建"}' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["stage"]["id"])')

curl -s -X POST $BASE/api/stages/$SID/publish -H "$AUTH" -H 'Content-Type: application/json' --data '{}'; echo
curl -s $BASE/api/stages/$SID/status -H "$AUTH"; echo
curl -s -X POST $BASE/api/stages/$SID/unpublish -H "$AUTH" -H 'Content-Type: application/json' --data '{}'; echo

curl -s -X DELETE $BASE/api/stages/$SID -H "$AUTH"; echo
curl -s -X DELETE "$BASE/api/folders/$FID?mode=ungroup" -H "$AUTH"; echo
curl -s -X DELETE $BASE/api/materials/$MID -H "$AUTH"; echo
curl -s $BASE/api/stages -H "$AUTH"; echo
curl -s $BASE/api/folders -H "$AUTH"; echo
curl -s $BASE/api/materials -H "$AUTH"; echo

实测输出:

FID=folder-PtSXtch4HExQ
MID=mat_ayq7hr9etbm0q2mcbgmkwzb2pw
SID=stage-tjI9dD4yj8M9
{"success":true,"publishedAt":1792915453326,"name":"端到端示例课程"}
{"isPublic":true,"publishedAt":1792915453326}
{"success":true}
{"ok":true}
{"ok":true,"removedStageIds":[]}
{"materialId":"mat_ayq7hr9etbm0q2mcbgmkwzb2pw","deleted":true}
{"stages":[]}
{"folders":[]}
{"materials":[]}

脚本要点:

  • set -e 之外还要检查 HTTP 状态码,因为 curl 默认对 4xx/5xx 不失败;生产脚本应加 -f 或 -w '%{http_code}' 判断。
  • 令牌只活 7 天,长期任务要缓存并在 401 时重新换取。
  • 每个 python3 -c 都只取必要字段,避免响应结构变化导致整段解析失败。

十、错误码与排障

现象HTTPerrorCode原因处理
未授权401INVALID_REQUEST / Access code required没带令牌或令牌过期重新 verify 换令牌
口令错误401INVALID_REQUEST / Invalid access code口令不匹配核对 ACCESS_CODE
方法不对405无路径存在但不支持该方法查源码路由或改用正确方法
路由不存在404纯 JSON not_found路径拼错或无该路由对照第二章分组核对
缺字段400MISSING_REQUIRED_FIELD少 requirement、model 等按错误文案补字段
未配置模型400MISSING_MODEL槽位未分配配 openmaic.yml 后重启
任务超限429ACTIVE_RUN_LIMIT活跃任务达上限等任务结束或调大上限
私网被拦400INVALID_REQUESTSSRF 防护自建内网模型时开 ALLOW_LOCAL_NETWORKS
上游不可达500 / 502INTERNAL_ERROR / UPSTREAM_ERROR出网失败或地址错先测网络连通性
认领失败409NO_PENDING_CLAIM无待认领的匿名身份先以匿名身份访问一次再认领

几条实测排障经验:

  1. 401 与 403 的区分:401 是「没进门」,403 是「进门但无权」——源码中当 allowUserKeys=false 时,用自带密钥调用探测接口会返回 PROVIDER_DISABLED。先把认证修好,再排查权限。
  2. Secure Cookie 与纯 HTTP:浏览器场景下,除 localhost 外都不会保存带 Secure 的 Cookie;命令行 curl 不受影响,所以「curl 通、浏览器不通」的第一嫌疑就是它。
  3. 两种错误信封:写客户端时不要只判断 success 字段,文件夹类路由根本没有该字段。
  4. 匿名身份需要文档导航头:curl 不带 Accept: text/html 与 Sec-Fetch-Dest: document 时,首页不会下发 anonymous_id,每次请求都换一个新所有者,表现为「刚创建的数据立刻消失」。

十一、参考链接