ryot v10.5.6 API 使用详解

ryot 唯一程序化接口 GraphQL API 的 curl 实测详解:端点与认证、46 查询/70 变更字段规模、搜索/评论/进度/集合/用户五域逐条请求回显,含联合类型、异步作业与排障 FAQ。

ryotryotGraphQLAPI2026/11/03

ryot v10.5.6 API 使用详解

目标:讲清 ryot v10.5.6 唯一的程序化接口——GraphQL API 的端点、认证、核心查询/变更域与通用约定。本文全部请求与响应均为对 http://127.0.0.1:8141 运行实例的 curl 实测回显(含报错实录),覆盖搜索、评论、进度、集合、用户管理五个数据域。

一、环境信息

  • 操作系统:Ubuntu 24.04.5 LTS(x86_64)
  • ryot 版本:v10.5.6(coreDetails.version 实测)
  • API 端点:POST http://127.0.0.1:8141/backend/graphql
  • 演示账号:admin / q1w2e3r4(注册于本文实验环境)
  • 客户端工具:curl 8.5.0 + python3(JSON 格式化)
  • 服务部署方式:见 《ryot Docker Compose 部署指南》

二、前置条件

  1. 服务健康检查通过:
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:8141/health
# HTTP 200
  1. GraphQL 端点免认证可达(用于探测):
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -d '{"query":"query { __typename }"}'
# {"data":{"__typename":"QueryRoot"}}
  1. 已创建管理员账号(见第四章)。业务查询与变更均要求携带 Authorization: Bearer <apiKey>。

三、API 总览

维度结论(实测/实查)
协议GraphQL(单一 POST 端点),无 REST 管理 API
端点路径/backend/graphql(反向代理按 /backend* 前缀转发并剥前缀,误用 /graphql 返回前端 404 页,见第七章 Q1)
版本随服务版本 v10.5.6,经 coreDetails.version 查询
认证loginUser 换取不透明 apiKey,请求头 Authorization: Bearer <apiKey>;有效期由 USERS_TOKEN_VALID_FOR_DAYS 控制(默认 90 天)
Schema 规模QueryRoot 46 个查询字段、MutationRoot 70 个变更字段(__type introspection 实测计数)
错误风格业务错误走联合类型(如 `LoginResult = LoginError
限流自托管实例无内置限流约定,按部署侧(反向代理/防火墙)自行控制

核心枚举(introspection 实测全量值):

  • MediaLot(11):BOOK SHOW MOVIE ANIME MANGA MUSIC PODCAST AUDIO_BOOK VIDEO_GAME COMIC_BOOK VISUAL_NOVEL
  • MediaSource(19):IGDB TMDB TVDB VNDB CUSTOM METRON ITUNES ANILIST AUDIBLE SPOTIFY MUSIC_BRAINZ GIANT_BOMB HARDCOVER MYANIMELIST LISTENNOTES GOOGLE_BOOKS OPENLIBRARY MANGA_UPDATES YOUTUBE_MUSIC
  • EntityLot(10):GENRE PERSON REVIEW WORKOUT METADATA EXERCISE COLLECTION METADATA_GROUP WORKOUT_TEMPLATE USER_MEASUREMENT
  • SeenState(4):DROPPED ON_A_HOLD COMPLETED IN_PROGRESS

四、认证与首次调用

4.1 创建首个管理员(registerUser)

ryot 无内置默认账号,首个管理员用 registerUser 创建,adminAccessToken 必须与服务环境变量 SERVER_ADMIN_ACCESS_TOKEN 一致:

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -d '{"query":"mutation { registerUser(input: {data: {password: {username: \"admin\", password: \"q1w2e3r4\"}}, adminAccessToken: \"q1w2e3r4\"}) { __typename ... on StringIdObject { id } ... on RegisterError { error } } }"}'
# {"data":{"registerUser":{"__typename":"StringIdObject","id":"usr_0MfPIsb1mhEh"}}}

⚠️ 注意:registerUser 返回联合类型 RegisterResult,必须用内联片段(... on StringIdObject)选取字段,直接 { id } 会报 Unknown field "id" on type "RegisterResult"(实测)。

4.2 登录换取令牌(loginUser)

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -d '{"query":"mutation { loginUser(input: {password: {username: \"admin\", password: \"q1w2e3r4\"}}) { __typename ... on ApiKeyResponse { apiKey } ... on LoginError { error } } }"}'
# {"data":{"loginUser":{"__typename":"ApiKeyResponse","apiKey":"UFPP7KKBIKI4P4NXZXVFX2Q2MNGWXODSEQ7TIOIKLWD7RNB6TVPQ===="}}}

每次登录返回新的 apiKey(实测两次登录值不同),旧令牌在有效期内仍可用。后续示例统一用:

TOKEN=UFPP7KKBIKI4P4NXZXVFX2Q2MNGWXODSEQ7TIOIKLWD7RNB6TVPQ====

4.3 认证调用示例

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"query { coreDetails { version pageSize signupAllowed } }"}'
# {"data":{"coreDetails":{"version":"v10.5.6","pageSize":20,"signupAllowed":true}}}

五、API 域详解

5.1 元数据搜索域(metadataSearch / metadataDetails)

metadataSearch 按外部数据源搜索并返回 ryot 内部实体 ID 列表。入参 MetadataSearchInput{lot, source, search: SearchInput{query,take,page}};返回 CachedSearchIdResponse{cacheId, response: IdResults{items: [String!]!, details{totalItems, nextPage}}}。实测搜索 AniList 源的《葬送的芙莉莲》:

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"query { metadataSearch(input: {lot: ANIME, source: ANILIST, search: {query: \"Frieren\", take: 3}}) { cacheId response { items details { totalItems nextPage } } } }"}'

实测响应:

{"data":{"metadataSearch":{
  "cacheId":"8f89fc3d-b2ff-4773-b2a4-53e4c22da86e",
  "response":{
    "items":["met_6E_SB2LoFh5M","met_FW4DT0WOgaIf","met_vBMNiC1G0Sng","met_XgNcNA2u4zCu","met_Qx6652sTAUkf","met_wG-YztoFAK8i"],
    "details":{"totalItems":6,"nextPage":null}}}}}

💡 说明:take: 3 实返 6 条——上游源单页返回多于请求数时 ryot 原样透传,分页以 details.nextPage 为准。ANILIST 源免第三方密钥即可搜索(本实例未配置任何数据源密钥)。

metadataDetails(metadataId: String!) 拉取详情(返回 CachedGraphqlMetadataDetailsResponse,内层 GraphqlMetadataDetails 共 30 个字段):

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"query { metadataDetails(metadataId: \"met_6E_SB2LoFh5M\") { response { id title lot identifier source publishYear } } }"}'
# {"data":{"metadataDetails":{"response":{"id":"met_6E_SB2LoFh5M","title":"Sousou no Frieren","lot":"ANIME","identifier":"154587","source":"ANILIST","publishYear":2023}}}}

5.2 评论域(createOrUpdateReview / userMetadataDetails)

createOrUpdateReview(input: CreateOrUpdateReviewInput!):必填 entityId + entityLot(媒体实体用 METADATA),可选 rating(Decimal)、text、date、visibility,以及按媒体类型的进度字段(animeEpisodeNumber、showSeasonNumber/showEpisodeNumber、mangaChapterNumber 等)。返回 StringIdObject:

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"mutation { createOrUpdateReview(input: {entityId: \"met_6E_SB2LoFh5M\", entityLot: METADATA, rating: 9.2, text: \"Frieren - a gentle story about time and memory.\"}) { id } }"}'
# {"data":{"createOrUpdateReview":{"id":"rev_YYRZRCrqERPX"}}}

回查用 userMetadataDetails(metadataId)(返回 UserMetadataDetails,含 hasInteracted/reviews/history/averageRating/seenByUserCount/inProgress/collections 等 13 个字段):

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"query { userMetadataDetails(metadataId: \"met_6E_SB2LoFh5M\") { response { hasInteracted seenByUserCount averageRating reviews { id rating textOriginal postedOn } } } }"}'

实测响应:

{"data":{"userMetadataDetails":{"response":{
  "hasInteracted":true,"seenByUserCount":1,"averageRating":"9.2",
  "reviews":[{"id":"rev_YYRZRCrqERPX","rating":"9.2",
    "textOriginal":"Frieren - a gentle story about time and memory.",
    "postedOn":"2026-11-03T02:06:53.089932+00:00"}]}}}}

💡 说明:rating 等 Decimal 标量在 JSON 中序列化为字符串("9.2"),客户端解析需注意。

5.3 观看进度域(deployBulkMetadataProgressUpdate / updateSeenItem)

新建观看记录没有独立 create 变更,统一走批量进度变更 deployBulkMetadataProgressUpdate(input: [MetadataProgressUpdateInput!]!)。入参 {metadataId, change},change 为 Oneof 枚举:changeLatestState(SeenState) / changeLatestInProgress(Decimal) / createNewInProgress{startedOn + 进度字段} / createNewCompleted{withoutDates | startedOnDate | finishedOnDate | startedAndFinishedOnDate}(后三者携带 timestamp 与进度字段)。实测记一条「2026-11-03 完结 28 集」:

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"mutation { deployBulkMetadataProgressUpdate(input: [{metadataId: \"met_6E_SB2LoFh5M\", change: {createNewCompleted: {finishedOnDate: {timestamp: \"2026-11-03T12:00:00Z\", animeEpisodeNumber: 28}}}}]) }"}'
# {"data":{"deployBulkMetadataProgressUpdate":true}}

回查 history 确认落库:

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"query { userMetadataDetails(metadataId: \"met_6E_SB2LoFh5M\") { response { history { id state progress finishedOn } } } }"}'
# {"data":{"userMetadataDetails":{"response":{"history":[{"id":"see_EBlvS6dwO4Om","state":"COMPLETED","progress":"100","finishedOn":"2026-11-03T12:00:00+00:00"}]}}}}

updateSeenItem(input: UpdateSeenItemInput!) 仅更新既有记录(入参无实体字段,靠 seenId 定位),对不存在的 seenId 实测报错:

{"data":null,"errors":[{"message":"No seen found for this user and metadata","path":["updateSeenItem"]}]}

删除记录用 deleteSeenItem(seenId: String!)。

5.4 集合域(createOrUpdateCollection / deployAddEntitiesToCollectionJob / collectionContents)

创建集合(返回 StringIdObject,id 形如 col_*):

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"mutation { createOrUpdateCollection(input: {name: \"Anime Favorites\", description: \"Top anime tracked with ryot API\"}) { id } }"}'
# {"data":{"createOrUpdateCollection":{"id":"col_6AvxdIXxODeS"}}}

userCollectionsList 实测返回 8 个集合:自定义的 Anime Favorites 之外,新用户自动带有 7 个默认集合(Completed / In Progress / Monitoring / Reminders / Watchlist / Custom / Owned)。

向集合加实体是异步作业 deployAddEntitiesToCollectionJob(input: ChangeCollectionToEntitiesInput{creatorUserId, collectionName, entities: [EntityWithLotInput!]!})(注意按集合名而非 id 引用):

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"mutation { deployAddEntitiesToCollectionJob(input: {creatorUserId: \"usr_dXbDkWHbULeE\", collectionName: \"Anime Favorites\", entities: [{entityId: \"met_6E_SB2LoFh5M\", entityLot: METADATA}]}) }"}'
# {"data":{"deployAddEntitiesToCollectionJob":true}}

等待约 5 秒(高频调度周期)后用 collectionContents 验证:

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"query { collectionContents(input: {collectionId: \"col_6AvxdIXxODeS\"}) { response { totalItems results { items { entityId entityLot } } } } }"}'
# {"data":{"collectionContents":{"response":{"totalItems":1,"results":{"items":[{"entityId":"met_6E_SB2LoFh5M","entityLot":"METADATA"}]}}}}}

5.5 用户与系统域(usersList / generateAuthToken)

usersList 为管理员能力,返回 BasicUserDetails{id, name, lot, isDisabled}(name 即用户 ID 字符串):

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"query { usersList { id name lot } }"}'

实测响应(节选):

{"data":{"usersList":[
  {"id":"usr_dXbDkWHbULeE","name":"usr_dXbDkWHbULeE","lot":"NORMAL"},
  {"id":"usr_YIesAvHsO5E7","name":"usr_YIesAvHsO5E7","lot":"ADMIN"},
  {"id":"usr_R-xjh1FQ5Vwa","name":"usr_R-xjh1FQ5Vwa","lot":"NORMAL"},
  {"id":"usr_0MfPIsb1mhEh","name":"usr_0MfPIsb1mhEh","lot":"NORMAL"}]}}

generateAuthToken(无入参)为当前用户再签发一个令牌,实测:

curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"mutation { generateAuthToken }"}'
# {"data":{"generateAuthToken":"6A5JUWLFC4WH4UDH3MFDFGVEZTZAQDIFD7AYG2EUO7COPYXYESNA===="}}

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

  • 分页:列表类入参统一嵌 SearchInput{query, take, page};响应统一带 details{totalItems, nextPage},nextPage: null 表示无下一页(实测见 5.1)。
  • 过滤:userMetadataList(input: UserMetadataListInput{lot, filter, search, sort}),filter 为 MediaFilter{general, source, dateRange, collections},其中 MediaGeneralFilter 实测枚举:ALL RATED UNRATED DROPPED ON_A_HOLD UNFINISHED。实测按「已评分」过滤:
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"query { userMetadataList(input: {lot: ANIME, filter: {general: RATED}, search: {take: 5, page: 1}}) { response { items details { totalItems } } } }"}'
# {"data":{"userMetadataList":{"response":{"items":["met_6E_SB2LoFh5M"],"details":{"totalItems":1}}}}}
  • 排序:sort: MediaSortInput{by: MediaSortBy, order: GraphqlSortOrder};MediaSortBy 实测枚举:TITLE RANDOM USER_RATING RELEASE_DATE LAST_UPDATED LAST_CONSUMED TIMES_CONSUMED PROVIDER_RATING,GraphqlSortOrder 为 ASC/DESC。带排序复测同 5.1 查询:
curl -s http://127.0.0.1:8141/backend/graphql -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"query":"query { userMetadataList(input: {lot: ANIME, filter: {general: RATED}, search: {take: 5, page: 1}, sort: {by: USER_RATING, order: DESC}}) { response { items details { totalItems } } } }"}'
# {"data":{"userMetadataList":{"response":{"items":["met_6E_SB2LoFh5M"],"details":{"totalItems":1}}}}}
  • 缓存层:metadataSearch、metadataDetails、collectionContents 等返回体外层带 cacheId(UUID),ryot 用它做结果缓存失效(配套变更 expireCacheKey)。

七、常见问题(FAQ)

Q1:向 /graphql 发请求返回 404 HTML?

实测:

curl -s http://127.0.0.1:8141/graphql -H 'Content-Type: application/json' \
  -d '{"query":"query { __typename }"}'
# <!DOCTYPE html>...<h1>404 Not Found</h1>...(输出节选,实测首尾;react-router 页面,日志报 No route matches URL "/graphql")

该路径落到前端路由而非 API。正确端点为 /backend/graphql(见第三章)。

Q2:registerUser 直接选 id 报 Unknown field "id" on type "RegisterResult"?

返回类型是联合类型,必须用内联片段并带 __typename(见 4.1 的 ⚠️ 说明)。loginUser 同理(LoginResult)。

Q3:登录报 USERNAME_DOES_NOT_EXIST / CREDENTIALS_MISMATCH?

实测回显:

{"data":{"loginUser":{"__typename":"LoginError","error":"USERNAME_DOES_NOT_EXIST"}}}
{"data":{"loginUser":{"__typename":"LoginError","error":"CREDENTIALS_MISMATCH"}}}

前者表示从未注册(先走 4.1),后者为密码错误。注册重名报 IDENTIFIER_ALREADY_EXISTS(实测)。

Q4:带正确 adminAccessToken 注册的新用户为什么不是 ADMIN?

实测:系统已存在管理员后,再次携带正确 adminAccessToken 注册,新用户 lot 仍为 NORMAL(见 5.5 响应)。ADMIN 角色仅由「首管创建」获得,后续用户角色变更需走 updateUser(管理员)。

Q5:deployAddEntitiesToCollectionJob 返回 true 但 collectionContents 查不到?

该变更只是部署后台作业(高频调度默认 every 5 minutes),实测等待约 5 秒后作业即完成;若长期为空,检查是否关闭了后台任务(SERVER_DISABLE_BACKGROUND_JOBS=true 会使所有 deploy*Job 类变更失效)。

八、参考链接