Coral 9.11.8 API 使用详解
Coral 9.11.8 GraphQL API 全链路实测详解:会话登录与 API 令牌两条认证路径、coral-cli 0.2.0 令牌工作流(含无 TTY 与 --json 不支持等实测坑)、createStory/createComment/sites/story 计数等核心域逐条请求与响应、Webhook 端点创建与 HMAC 验签端到端实测、分页约定与错误码(USER_NOT_ENTITLED/RAW_QUERY_NOT_AUTHORIZED),回显逐字取自 2026-11-02。
Coral 9.11.8 API 使用详解
目标:给出 Coral 9.11.8 GraphQL API 的端到端可用手册:认证(会话令牌 / API 持久令牌 / coral-cli 工作流)、故事与评论的创建与查询、站点查询、Webhook 端点创建与 HMAC 验签、分页约定与全部实测错误码。本文每条请求与响应均为 2026-11-02 在已安装实例上的逐字实测。实例部署见 《Coral Docker Compose 部署指南》,参数背景见 《Coral 9.11.8 配置参数详解》。
一、环境信息
- 操作系统:Ubuntu 22.04 LTS(x86_64),客户端工具 curl + Node.js v24.22.1(coral-cli 宿主运行)
- Coral 实例:9.11.8,production,已安装租户(
/api/version实测{"version":"9.11.8","revision":""}) - API 端点:
http://127.0.0.1:8138/api/graphql(GraphQL,POST) - 辅助端点:
/api/version、/api/health、/api/auth/local(登录)、/metrics(Prometheus,Basic Auth) - 管理员账号:
root/q1w2e3r4(root@example.com,角色 ADMIN) - 站点:
demo-site,Permitted Domainhttp://127.0.0.1:8138 - 调试台:本实例设
ENABLE_GRAPHIQL=true(生产建议关闭,影响见 3.3) - Webhook 端到端验证使用同机另起的
NODE_ENV=development第二实例与监听在10.89.64.1:4242的接收端(原因见 5.10/八章)
二、前置条件
2.1 服务与端点可用
curl -s -w '\n-> %{http_code}\n' http://127.0.0.1:8138/api/version
curl -s -w '\n-> %{http_code}\n' http://127.0.0.1:8138/api/health
实测回显(逐字):
{"version":"9.11.8","revision":""}
-> 200
OK
-> 200
2.2 管理员账号就绪
Coral API 的硬约束(官方 API 文档原文):"Only administrative users can integrate with the API directly"——API 集成必须用管理员身份签发的令牌,普通读者/版主账号不能直连。账号 root 由安装流程创建(见 B2 篇 4.7)。
2.3 客户端工具
curl(本文全部请求形态);- 可选
coral-cli(官方配套令牌管理工具,npm 包@coralproject/coral-cli,实测版本 0.2.0),安装与坑见第四章。
三、API 总览
3.1 单一 GraphQL 端点
Coral 没有 REST 管理 API(/api/* 下只有登录、安装、版本、健康等辅助端点),全部数据操作走 POST /api/graphql 一个端点,请求体为 JSON 对象:query 字段承载 GraphQL 查询字符串,variables 字段承载变量对象(见 5.1 实测形态)。
3.2 能力面:113 个 mutation、26 个 query(introspection 实测)
curl -s -X POST http://127.0.0.1:8138/api/graphql -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"query":"{ __schema { mutationType { fields { name } } } }"}'
实测统计:mutations 113 个、queries 26 个。queries 全量清单(逐字):
["activeStories", "comment", "comments", "debugScrapeStoryMetadata", "discoverOIDCConfiguration", "dsaReport", "dsaReports", "emailDomain", "externalModerationPhase", "flags", "moderationQueues", "notificationCount", "notifications", "persistedQueries", "queues", "sections", "settings", "site", "sites", "stories", "story", "stream", "user", "users", "viewer", "webhookEndpoint"]
mutations 清单(输出节选,实测取与集成最相关部分):
[
"approveComment", "banUser", "closeStory", "createComment", "createCommentReply",
"createSite", "createStory", "createToken", "deactivateToken", "editComment",
"featureComment", "invalidateCachedStory", "openStory", "rejectComment",
"removeStory", "requestCommentsDownload", "reviewCommentFlag",
"rotateWebhookEndpointSigningSecret", "scrapeStory", "suspendUser",
"updateSite", "updateStory", "updateStoryMode", "updateStorySettings",
"updateUserRole", "updateUserUsername", "warnUser"
]
(完整 113 项含审核队列、用户警告/封禁、DSA 报告、外部审核阶段等治理动作,introspection 一次可取全。)
3.3 持久化查询白名单与安全模式
服务端启动加载 236 条持久化查询(前端专用白名单)。生产默认(ENABLE_GRAPHIQL=false)下,外部自写的原始查询被拒(逐字):
{"error":{"id":"b57dc060-d66e-11f1-adad-d3d6510464fa","traceID":"b57d9950-d66e-11f1-adad-d3d6510464fa","code":"RAW_QUERY_NOT_AUTHORIZED","type":"INVALID_REQUEST_ERROR","message":"You are not authorized to execute this query."},"report":null}
带管理员 API 令牌的原始查询不受此限制(官方口径:白名单只约束无特权调用);ENABLE_GRAPHIQL=true 时原始查询直接放行(实测对照见 C1 篇 5.5)。两个易混错误码的区分见第十一章。
3.4 限流约定
内置按 IP 限流覆盖登录、安装等端点(安装 10 秒 1 次、check 10 秒 2 次,实测源码口径);GraphQL 侧无额外全局配额,但高频发帖受评论限速约束。批量脚本请自行节流。
四、认证与首次调用
4.1 会话登录(浏览器态,临时验证用)
curl -s -X POST http://127.0.0.1:8138/api/auth/local \
-H 'Content-Type: application/json' \
-d '{"usernameOrEmail":"root","password":"q1w2e3r4"}'
实测回显(HTTP 200,令牌节选——完整值含签名段,不外泄):
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3OTM1OTAwMjcsImV4cCI6MTgwMTM2NjAyNywiaXNzIjoiMmQ4ZGQ2ZGMt…"}
解码可见:iss 为租户 ID(2d8dd6dc-…)、sub 为用户 ID、exp 约 90 天。会话令牌适合人工验证,不适合程序集成——集成用下面的持久 API 令牌。
4.2 安装 coral-cli 并绕开交互登录
官方推荐用 coral-cli 签发 API 令牌。宿主安装:
npm install -g --prefix /root/.npm-global @coralproject/coral-cli
coral-cli --version
实测回显(逐字):
added 215 packages in 7s
@coralproject/coral-cli/0.2.0 linux-x64 node-v24.22.1
⚠️ 实测坑一:直接
npm install -g会因全局前缀权限报npm error permissions of the file and its containing directories,需--prefix指到可写前缀(或将前缀目录 chown 给当前用户)。 ⚠️ 实测坑二:coral-cli login -d <域名>是交互式命令(提示Email:/Password:),在无人值守/无 TTY 环境会陷入Password:无限重打(实测刷屏后被终止)。官方 0.2.0 支持用环境变量CORAL_API_TOKEN直接注入 4.1 拿到的会话令牌,免交互完成后续所有命令:
export CORAL_API_TOKEN="$TOKEN" # 4.1 的会话令牌
4.3 创建 / 列出 / 吊销 API 令牌(实测全流程)
coral-cli token:create -d http://127.0.0.1:8138 --name api-demo-host
实测回显(逐字,令牌节选):
Created Token 7b769b0e-7dbb-4bcc-a2a4-ae607f72a309: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXQiOnRydWUsImlhdCI6MTc5MzU5MDUzNCwibmJmIjoxNzkzNTkwNTM0LCJpc3MiOiIyZDhkZDZkYy1iYzJmLTQ5NzMtYmNhNC1jMjQ2ZjkwNTEyMzkiLCJzdWIiOiI5ZDcyZjlhNy04ZThlLTRjZTgtYjBkYy03MzExYzVkZjRkZGYiLCJqdGkiOiI3Yjc2OWIwZS03ZGJiLTRiY2MtYTJhNC1hZTYwN2Y3MmEzMDkifQ.1scM…(签名段节选)
注意 payload 中 "pat":true——持久 API 令牌(Persistent Access Token)的标记位,官方文档明示此类令牌当前永不过期,泄露即长期有效,务必按密钥管理。
coral-cli token:list -d http://127.0.0.1:8138
实测回显(逐字):
ID Name Created At
──────────────────────────────────── ───────────── ────────────────────────
a20cb1d6-2843-4ea3-be25-41a8beec57db api-demo 2026-11-02T03:34:40.901Z
7b769b0e-7dbb-4bcc-a2a4-ae607f72a309 api-demo-host 2026-11-02T03:35:33.833Z
吊销指定令牌:
coral-cli token:revoke -d http://127.0.0.1:8138 --id a20cb1d6-2843-4ea3-be25-41a8beec57db
coral-cli token:list -d http://127.0.0.1:8138
实测回显(逐字):
Revoked Token a20cb1d6-2843-4ea3-be25-41a8beec57db
ID Name Created At
──────────────────────────────────── ───────────── ────────────────────────
7b769b0e-7dbb-4bcc-a2a4-ae607f72a309 api-demo-host 2026-11-02T03:35:33.833Z
⚠️ 实测坑三:官方 API 文档示例给
coral-cli token:create加了--json标志,该标志在 0.2.0 上不支持(逐字):› Error: Unexpected argument: --json。令牌值以Created Token <id>: <jwt>文本行输出,脚本需自行解析。
4.4 首次 API 调用(Bearer 令牌)
curl -s -X POST http://127.0.0.1:8138/api/graphql \
-H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
-d '{"query":"{ sites { nodes { name allowedOrigins } } }"}'
实测回显(逐字):
{"data":{"sites":{"nodes":[{"name":"demo-site","allowedOrigins":["http://127.0.0.1:8138"]}]}}}
对照实验——不带令牌调同一查询(本实例 ENABLE_GRAPHIQL=true):
{"errors":[{"message":"You are not authorized to access that resource.","locations":[{"line":1,"column":3}],"path":["sites"],"extensions":{"code":"USER_NOT_ENTITLED","id":"7d18f410-d66e-11f1-8984-e9eacc17974c","traceID":"7d187ee0-d66e-11f1-8984-e9eacc17974c","type":"INVALID_REQUEST_ERROR","message":"You are not authorized to access that resource."}}],"data":null}
已吊销的令牌则报 INTERNAL_ERROR(逐字):
{"error":{"id":"89e6b1a0-d66e-11f1-8984-e9eacc17974c","traceID":"89e63c70-d66e-11f1-8984-e9eacc17974c","code":"INTERNAL_ERROR","type":"INVALID_REQUEST_ERROR","message":"Internal Error"},"report":null}
三种失败形态的判别表见第十一章。
五、核心 API 域详解
以下 mutation 均带 Authorization: Bearer $API_TOKEN 调 /api/graphql,请求与响应逐字实测。
5.1 故事域:createStory
输入结构(introspection 实测):createStory(input: CreateStoryInput!),input.story 为 CreateStory!,字段:
[('id', 'ID'), ('url', 'String'), ('mode', 'ENUM'), ('metadata', 'INPUT_OBJECT'), ('closedAt', 'SCALAR')]
外加必填 clientMutationId: String!。
实测坑:story.id 是必填——首次尝试省略 id 并查询不存在的字段,报错逐字:
{"errors":[{"message":"Field CreateStory.id of required type ID! was not provided.","locations":[{"line":1,"column":40}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"Cannot query field \"title\" on type \"Story\". Did you mean \"site\"?","locations":[{"line":1,"column":178}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"Cannot query field \"publishedAt\" on type \"Story\". Did you mean \"closedAt\"?","locations":[{"line":1,"column":191}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
要点:① 调用方自带 UUID 作为故事 ID(便于与 CMS 主键对齐,官方 Lazy Story Creation 语义的 API 版);② 标题不是 Story 顶层字段,在 metadata.title 里;③ 无 publishedAt,对应字段是 closedAt。修正后的成功请求:
curl -s -X POST http://127.0.0.1:8138/api/graphql \
-H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
-d '{"query":"mutation($input: CreateStoryInput!){ createStory(input:$input){ story { id url status metadata { title } } } }","variables":{"input":{"story":{"id":"14cb4c15-54bc-42fe-a7f7-e63bff2b0f14","url":"http://127.0.0.1:8138/news/demo-article-1","metadata":{"title":"Demo Article One"}},"clientMutationId":"m-create-story-1"}}}'
实测响应(逐字):
{"data":{"createStory":{"story":{"id":"14cb4c15-54bc-42fe-a7f7-e63bff2b0f14","url":"http://127.0.0.1:8138/news/demo-article-1","status":"OPEN","metadata":{"title":"Demo Article One"}}}}}
Story 可查询字段实测全量(introspection,逐字):
["id", "status", "url", "metadata", "scrapedAt", "featuredComments", "comments", "commentActionCounts", "moderationQueues", "closedAt", "isClosed", "isArchiving", "isArchived", "isUnarchiving", "commentCounts", "createdAt", "settings", "lastCommentedAt", "canModerate", "site", "viewerCount", "ratings", "viewerRating", "cached"]
故事生命周期操作对应 mutations:closeStory / openStory / updateStory(改 URL 用)/ removeStory / scrapeStory / invalidateCachedStory。
5.2 评论域:createComment
输入 CreateCommentInput 实测字段(introspection,逐字归并):
nudge: Boolean, storyID: ID!, body: String!, clientMutationId: String!, media: CreateCommentMediaInput, rating: Int
实测坑:payload 没有 comment 字段——首版查询把 createComment 的返回选择集写成 comment { id },服务端报(逐字):
{"errors":[{"message":"Cannot query field \"comment\" on type \"CreateCommentPayload\".","locations":[{"line":1,"column":160}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
payload 实际结构是 Relay 风格 edge { node }(introspection 逐字:edge: CommentEdge!、clientMutationId: String!)。成功请求与响应:
curl -s -X POST http://127.0.0.1:8138/api/graphql \
-H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
-d '{"query":"mutation($input: CreateCommentInput!){ createComment(input:$input){ edge { node { id body status author { username } } } } }","variables":{"input":{"storyID":"14cb4c15-54bc-42fe-a7f7-e63bff2b0f14","body":"Hello from the Coral GraphQL API test.","clientMutationId":"m-create-comment-1"}}}'
实测响应(逐字):
{"data":{"createComment":{"edge":{"node":{"id":"308ca0a7-8f15-4cd3-8d10-664612bfb16d","body":"Hello from the Coral GraphQL API test.","status":"APPROVED","author":{"username":"root"}}}}}}
以管理员令牌发帖直接 status: APPROVED(管理员免审核)。回复用 createCommentReply;治理动作 approveComment/rejectComment/featureComment 等对应 3.2 清单。
5.3 计数与列表:story.commentCounts 与 comments 分页
实测坑三连(逐字报错):CommentCounts 没有 total/approved/system 字段;Story.comments 不接受 filter 参数;statuses 必须带子字段选择且枚举名大写:
{"errors":[{"message":"Cannot query field \"total\" on type \"CommentCounts\".","locations":[{"line":1,"column":94}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"Cannot query field \"approved\" on type \"CommentCounts\".","locations":[{"line":1,"column":100}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"Unknown argument \"filter\" on field \"comments\" of type \"Story\". Did you mean \"after\"?","locations":[{"line":1,"column":130}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
实测可用字段:CommentCounts: ['totalPublished', 'tags', 'statuses'];CommentStatusCounts 子字段为大写枚举 APPROVED / REJECTED / PREMOD。正确查询与响应(逐字):
curl -s -X POST http://127.0.0.1:8138/api/graphql \
-H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
-d '{"query":"{ story(id: \"14cb4c15-54bc-42fe-a7f7-e63bff2b0f14\") { url metadata { title } commentCounts { totalPublished statuses { APPROVED REJECTED PREMOD } } comments(first: 10) { nodes { id body status author { username } createdAt } } } }"}'
{"data":{"story":{"url":"http://127.0.0.1:8138/news/demo-article-1","metadata":{"title":"Demo Article One"},"commentCounts":{"totalPublished":1,"statuses":{"APPROVED":1,"REJECTED":0,"PREMOD":0}},"comments":{"nodes":[{"id":"308ca0a7-8f15-4cd3-8d10-664612bfb16d","body":"Hello from the Coral GraphQL API test.","status":"APPROVED","author":{"username":"root"},"createdAt":"2026-11-02T03:28:40.618Z"}]}}}}
5.2 写入的评论出现在 5.3 的列表里、计数 totalPublished: 1 与 statuses.APPROVED: 1 对账一致——写读闭环成立。分页约定见第六章。
5.4 站点域:sites / site / createSite
实测坑:Site 没有 slug,SitesConnection 没有 totalCount(逐字):
{"errors":[{"message":"Cannot query field \"slug\" on type \"Site\".","locations":[{"line":1,"column":61}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"Cannot query field \"totalCount\" on type \"SitesConnection\".","locations":[{"line":1,"column":83}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
introspection 实测 Site 可用字段(逐字):
["id", "name", "allowedOrigins", "canModerate", "topStories", "createdAt"]
成功查询(4.4 即此例)返回 demo-site 及其 allowedOrigins、createdAt。站点增改用 createSite / updateSite(allowedOrigins 即嵌入评论流的 Permitted Domains 白名单,改完即时生效)。
5.5 用户与 viewer
viewer 返回当前令牌持有者(4.4 同法):实测 {"viewer":{"id":"9d72f9a7-8e8e-4ce8-b0dc-7311c5df4ddf","username":"root","email":"root@example.com","role":"ADMIN"}}。用户治理域 mutations 齐全:banUser/removeUserBan、suspendUser/removeUserSuspension、warnUser/removeUserWarning、premodUser/removeUserPremod、updateUserRole、inviteUsers、GDPR 侧 requestCommentsDownload/requestUserCommentsDownload/deleteUserAccount。
5.6 Webhook 域:createWebhookEndpoint 与事件验签(端到端实测)
输入字段(introspection 实测,逐字):
[('clientMutationId', 'String'), ('url', 'String'), ('all', 'Boolean'), ('events', None)]
all 为必填(Boolean!),payload 字段名是 endpoint 而非 webhookEndpoint,signingSecret 是对象必须选子字段——三个坑的逐字报错:
{"errors":[{"message":"Field CreateWebhookEndpointInput.all of required type Boolean! was not provided.","locations":[{"line":1,"column":41}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"Cannot query field \"webhookEndpoint\" on type \"CreateWebhookEndpointPayload\". Did you mean \"endpoint\"?","locations":[{"line":1,"column":135}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
{"errors":[{"message":"Field \"signingSecret\" of type \"SigningSecret!\" must have a selection of subfields. Did you mean \"signingSecret { ... }\"?","locations":[{"line":1,"column":173}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
SigningSecret 子字段实测:['kid', 'secret', 'createdAt', 'lastUsedAt', 'rotatedAt', 'inactiveAt'];endpoint 可查字段:['id', 'enabled', 'url', 'signingSecret', 'deliveries', 'all', 'events']。
生产实例创建 http 端点被拒(5.10 错误码专题):production 强制 https,GraphQL 只回 INTERNAL_ERROR,服务日志 originalError 给出真因 invalid scheme provided in production: http:(逐字见 C1 篇 5.10)。因此端到端验证在 NODE_ENV=development 第二实例完成:
curl -s -X POST http://127.0.0.1:8140/api/graphql \
-H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' \
-d '{"query":"mutation($input: CreateWebhookEndpointInput!){ createWebhookEndpoint(input:$input){ endpoint { id url enabled signingSecret { kid secret } events } } }","variables":{"input":{"url":"http://10.89.64.1:4242/webhook","all":false,"events":["STORY_CREATED"],"clientMutationId":"m-wh-1"}}}'
实测响应(逐字,secret 节选):
{"data":{"createWebhookEndpoint":{"endpoint":{"id":"2f7a2367-fc3b-409b-b805-75391d7baa04","url":"http://10.89.64.1:4242/webhook","enabled":true,"signingSecret":{"kid":"3b1f05b8cb51ada42f80","secret":"whsec_525fe70247211c438260c03d7a387368826281815146c2ca1e390c4431430192d25235cb"},"events":["STORY_CREATED"]}}}}
接收端(app.listen(4242),按官方示例以原始 body 计算 HMAC-SHA256 比对 X-Coral-Signature 头)随后收到推送——先 createStory 触发 STORY_CREATED(响应逐字):
{"data":{"createStory":{"story":{"id":"37313272-1b2a-494b-8761-06579767332e","url":"http://127.0.0.1:8138/news/webhook-test-story"}}}}
接收端日志(逐字,一条完整事件):
{"path": "/webhook", "signature": "sha256=a710c0818158973e97681eea2180713d03c3b0bcc3b4506fc424b2d99b551248", "hmac_verified": true, "body": "{\n \"id\": \"8876d0c2-eafa-48d9-97bf-1d6b326511d2\",\n \"createdAt\": \"2026-11-02T03:41:52.347Z\",\n \"data\": {\n \"storyID\": \"37313272-1b2a-494b-8761-06579767332e\",\n \"storyURL\": \"http://127.0.0.1:8138/news/webhook-test-story\",\n \"siteID\": \"ceb9bf0f-01c1-42f8-b91c-a3f8da7876d0\"\n },\n \"type\": \"STORY_CREATED\",\n \"tenantID\": \"2d8dd6dc-bc2f-4973-bca4-c246f9051239\",\n \"tenantDomain\": \"127.0.0.1\"\n}"}
hmac_verified: true——签名校验通过,事件体 storyID 与刚才创建的故事一致,siteID/tenantID 与安装期记录一致。
事件全集(官方文档 + 本实测):STORY_CREATED、COMMENT_CREATED、COMMENT_REPLY_CREATED、COMMENT_ENTERED_MODERATION_QUEUE、COMMENT_LEFT_MODERATION_QUEUE(后两类评论事件仅发布态 NONE/APPROVED 触发 COMMENT_CREATED 类)。验签四步:按 , 拆 X-Coral-Signature → 取 sha256= 值 → 用端点 secret 对原始 body 做 HMAC-SHA256 hex → 常数时间比对(多签名并存是密钥轮换期新旧密钥并行,官方语义)。密钥轮换用 rotateWebhookEndpointSigningSecret。
5.7 运维端点速查
| 端点 | 方法 | 实测结果 |
|---|---|---|
/api/version | GET | 200,{"version":"9.11.8","revision":""} |
/api/health | GET | 200,OK |
/api/v1 | GET | 500(4.x 遗留端点,勿用) |
/metrics(8139) | GET | 无凭证 401;-u admin:q1w2e3r4 出 Prometheus 文本(coral_executed_graph_queries_total 等,详见 C1 篇 5.6) |
/graphiql | GET | ENABLE_GRAPHIQL=true 时可交互调试;false 时 404(实测对照见 C1 篇 5.5) |
六、分页 / 过滤 / 排序通用约定
Coral GraphQL 全量采用 Relay 连接模式,实测规律:
- 列表字段返回
nodes(或edges { node })+ 游标:5.3 实测形态为comments(first: 10) { nodes { id body status author { username } createdAt } };mutation payload 用edge.node(5.2 实测)。 - 分页参数只有
first/after(向后还有last/before):游标不透明,翻页把上一页末节点的 cursor 传给after。5.3 报错回显里服务端提示 "Did you mean "after"?" 即此语义。 - 没有通用
filter参数:给Story.comments传filter参数实测被拒(5.3 逐字报错:Unknown argument "filter" on field "comments" of type "Story". Did you mean "after"?)。过滤靠具名参数与专用查询:状态计数走commentCounts.statuses,队列走moderationQueues/flags查询,搜索走stories/comments查询自身的参数(如q、status,以 introspection 为准)。 - 排序:连接字段多带
sorting类枚举参数(如comments(sorting: {column: CREATED_AT, desc: true})),具体枚举值 introspection 可取;本文主线查询未显式排序,默认按创建时间。
七、嵌入侧配合(简述)
API 之外,读者侧评论流经 embed.js 挂进文章页(实测 GET /assets/js/embed.js -> 200,108227 字节)。Coral.createStreamEmbed({id, rootURL, storyID?, storyURL?, accessToken?}) 的 storyID/storyURL 与本文故事域同一套 ID/URL 体系:Lazy Story Creation 下嵌入即自动建档,关闭后就必须走 5.1 的 API 建档——两条路指向同一个 Story 对象。SSO 登录态用 accessToken(由 SIGNING_SECRET 体系签发 JWT,见 C1 篇 5.2)。
八、常见问题
8.1 USER_NOT_ENTITLED 与 RAW_QUERY_NOT_AUTHORIZED 分不清?
| 错误码 | 触发条件(实测) | 处置 |
|---|---|---|
USER_NOT_ENTITLED | 无令牌/非管理员令牌访问特权字段(如 sites),且 GraphiQL 放宽开着 | 换管理员 API 令牌(4.3) |
RAW_QUERY_NOT_AUTHORIZED | 生产默认(ENABLE_GRAPHIQL=false)下提交非白名单原始查询且无管理员令牌 | 带管理员令牌,或走持久化查询 |
INTERNAL_ERROR(HTTP 层 error 对象) | 令牌已被吊销(4.4 实测)、或 mutation 被服务端内部校验拒绝(如 webhook http 方案,5.6) | 重新签发令牌;翻服务日志找 originalError |
8.2 coral-cli login 卡死刷 Password
0.2.0 的 login 是交互式提示,无 TTY 环境会无限重打 Password:(实测)。用 CORAL_API_TOKEN=<会话令牌> 环境变量免交互(4.2),会话令牌经 POST /api/auth/local 获取(4.1)。
8.3 官方文档的 --json 参数不可用
coral-cli 0.2.0 全部子命令不支持 --json(实测 › Error: Unexpected argument: --json),官方 API 文档示例按 1.x 写的。脚本解析 Created Token <id>: <jwt> 文本行即可。
8.4 createStory 为什么必须自己传 id?
CreateStory.id 是必填 ID!(5.1 实测报错)。设计意图是让调用方用 CMS 主键当故事 ID,幂等且可回推;重复用同一 id 创建同一 URL 会命中既有故事(官方 Lazy Creation 语义)。
8.5 令牌会不会过期?
会话令牌带 exp(实测约 90 天);API 持久令牌(pat:true)官方文档明示当前永不过期,只能主动 token:revoke。泄露处置:吊销 + 观察 4.4 的 INTERNAL_ERROR 形态确认失效。
8.6 GraphQL 报 Internal Error 但不知道哪错了
Webhook 场景实锤(5.6):GraphQL 层把服务端内部校验错误统一包装成 INTERNAL_ERROR,真因只出现在服务日志的 originalError.message(如 invalid scheme provided in production: http:)。排障固定动作:复现请求 → 立刻 tail 服务日志按 level:50 过滤。
九、参考链接
- 官方 API 文档(GraphQL Overview):https://docs.coralproject.net/api/schema
- 官方 Webhooks 文档(签名与事件):https://docs.coralproject.net/webhooks
- coral-cli 仓库:https://github.com/coralproject/coral-cli
- 官方站点集成文档:https://docs.coralproject.net/getting-started/integrating-on-your-site/
- GitHub 仓库:https://github.com/coralproject/talk
- 站内姊妹篇:《Coral 介绍与选型指南》、《Coral Docker Compose 部署指南》、《Coral 9.11.8 配置参数详解》