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。

CoralCoral评论系统GraphQLNode.jsTypeScript2026/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 Domain http://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/versionGET200,{"version":"9.11.8","revision":""}
/api/healthGET200,OK
/api/v1GET500(4.x 遗留端点,勿用)
/metrics(8139)GET无凭证 401;-u admin:q1w2e3r4 出 Prometheus 文本(coral_executed_graph_queries_total 等,详见 C1 篇 5.6)
/graphiqlGETENABLE_GRAPHIQL=true 时可交互调试;false 时 404(实测对照见 C1 篇 5.5)

六、分页 / 过滤 / 排序通用约定

Coral GraphQL 全量采用 Relay 连接模式,实测规律:

  1. 列表字段返回 nodes(或 edges { node })+ 游标:5.3 实测形态为 comments(first: 10) { nodes { id body status author { username } createdAt } };mutation payload 用 edge.node(5.2 实测)。
  2. 分页参数只有 first/after(向后还有 last/before):游标不透明,翻页把上一页末节点的 cursor 传给 after。5.3 报错回显里服务端提示 "Did you mean "after"?" 即此语义。
  3. 没有通用 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 为准)。
  4. 排序:连接字段多带 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 过滤。

九、参考链接

Coral 9.11.8 API 使用详解 - Quzuo