OpenMAIC v1.2.1 API 使用详解
OpenMAIC v1.2.1 API 使用详解:77 条 HTTP 路由的分层说明、访问口令换令牌与两种错误信封、课程库/资料/课程/生成任务/模型配置接口的逐条 curl 实测回显、发布撤销与端到端脚本,以及常见错误码排障。
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.js16.3.3,Node.jsv22.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}/(status | manifest | scenes |
| 生成任务 | /api/generate-classroom、/api/generation-runs、`/api/generation-runs/{id}/(confirm-outline | hold-outline | retry |
| 模型与能力 | /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都只取必要字段,避免响应结构变化导致整段解析失败。
十、错误码与排障
| 现象 | HTTP | errorCode | 原因 | 处理 |
|---|---|---|---|---|
| 未授权 | 401 | INVALID_REQUEST / Access code required | 没带令牌或令牌过期 | 重新 verify 换令牌 |
| 口令错误 | 401 | INVALID_REQUEST / Invalid access code | 口令不匹配 | 核对 ACCESS_CODE |
| 方法不对 | 405 | 无 | 路径存在但不支持该方法 | 查源码路由或改用正确方法 |
| 路由不存在 | 404 | 纯 JSON not_found | 路径拼错或无该路由 | 对照第二章分组核对 |
| 缺字段 | 400 | MISSING_REQUIRED_FIELD | 少 requirement、model 等 | 按错误文案补字段 |
| 未配置模型 | 400 | MISSING_MODEL | 槽位未分配 | 配 openmaic.yml 后重启 |
| 任务超限 | 429 | ACTIVE_RUN_LIMIT | 活跃任务达上限 | 等任务结束或调大上限 |
| 私网被拦 | 400 | INVALID_REQUEST | SSRF 防护 | 自建内网模型时开 ALLOW_LOCAL_NETWORKS |
| 上游不可达 | 500 / 502 | INTERNAL_ERROR / UPSTREAM_ERROR | 出网失败或地址错 | 先测网络连通性 |
| 认领失败 | 409 | NO_PENDING_CLAIM | 无待认领的匿名身份 | 先以匿名身份访问一次再认领 |
几条实测排障经验:
- 401 与 403 的区分:401 是「没进门」,403 是「进门但无权」——源码中当
allowUserKeys=false时,用自带密钥调用探测接口会返回PROVIDER_DISABLED。先把认证修好,再排查权限。 SecureCookie 与纯 HTTP:浏览器场景下,除 localhost 外都不会保存带Secure的 Cookie;命令行curl不受影响,所以「curl 通、浏览器不通」的第一嫌疑就是它。- 两种错误信封:写客户端时不要只判断
success字段,文件夹类路由根本没有该字段。 - 匿名身份需要文档导航头:
curl不带Accept: text/html与Sec-Fetch-Dest: document时,首页不会下发anonymous_id,每次请求都换一个新所有者,表现为「刚创建的数据立刻消失」。
十一、参考链接
- OpenMAIC 官方仓库(
app/api路由源码):https://github.com/THU-MAIC/OpenMAIC (查询日期 2026-10-25) - 官方中文部署文档:https://github.com/THU-MAIC/OpenMAIC/blob/main/packages/docs/content/docs/deployment.zh-cn.mdx (查询日期 2026-10-25)
- 官方中文配置文档:https://github.com/THU-MAIC/OpenMAIC/blob/main/packages/docs/content/docs/configuration.zh-cn.mdx (查询日期 2026-10-25)