فهرست منبع

chore(openapi): 重新生成规格与前端类型

生成产物,不要手改。本次变动来自三处:

1. 新增 / 重塑的 v3 端点:reactions 四条、tipitaka-reading 一条(取代原来的
   tipitaka-read-para / tipitaka-read-chapter 共四条)。
2. 生成器的四处修复:单动作控制器不再被漏掉、集合按 ::collection() 判定、
   短类名按 use 语句解析(子命名空间)、Example/Default 按声明类型转换。
   第四条影响面最广——所有在 docblock 里写了 Default: / Example: 的 v2 路径
   都重新生成了一遍。
3. 手写的 overrides:v2 的 auth-current / channel-channel / chapter /
   progress / progress-progress / sign-in,以及 v3-tipitaka-reading-channel
   的 data[] 逐字段类型。

redocly 0 error,5 warning(1 条 info-license、2 条 operation-4xx-response、
2 条 v2 userdict 的 example 类型,都是既有的)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
visuddhinanda 3 هفته پیش
والد
کامیت
9ae69a5625
43فایلهای تغییر یافته به همراه1314 افزوده شده و 676 حذف شده
  1. 483 250
      dashboard-v6/src/api/schema.d.ts
  2. 11 9
      openapi/public/assets/protocol/main.yaml
  3. 23 0
      openapi/public/assets/protocol/overrides/v2-auth-current.yaml
  4. 38 0
      openapi/public/assets/protocol/overrides/v2-channel-channel.yaml
  5. 31 0
      openapi/public/assets/protocol/overrides/v2-chapter.yaml
  6. 20 0
      openapi/public/assets/protocol/overrides/v2-progress-progress.yaml
  7. 77 0
      openapi/public/assets/protocol/overrides/v2-progress.yaml
  8. 11 0
      openapi/public/assets/protocol/overrides/v2-sign-in.yaml
  9. 19 0
      openapi/public/assets/protocol/overrides/v3-tipitaka-reading-channel.yaml
  10. 2 2
      openapi/public/assets/protocol/resources/auto/v2-article-article.yaml
  11. 2 2
      openapi/public/assets/protocol/resources/auto/v2-article.yaml
  12. 13 0
      openapi/public/assets/protocol/resources/auto/v2-auth-current.yaml
  13. 1 0
      openapi/public/assets/protocol/resources/auto/v2-book-title.yaml
  14. 22 0
      openapi/public/assets/protocol/resources/auto/v2-channel-channel.yaml
  15. 1 0
      openapi/public/assets/protocol/resources/auto/v2-chapter.yaml
  16. 2 2
      openapi/public/assets/protocol/resources/auto/v2-course-member.yaml
  17. 3 3
      openapi/public/assets/protocol/resources/auto/v2-discussion.yaml
  18. 0 40
      openapi/public/assets/protocol/resources/auto/v2-like.yaml
  19. 9 0
      openapi/public/assets/protocol/resources/auto/v2-progress-progress.yaml
  20. 1 1
      openapi/public/assets/protocol/resources/auto/v2-progress.yaml
  21. 2 2
      openapi/public/assets/protocol/resources/auto/v2-project.yaml
  22. 4 3
      openapi/public/assets/protocol/resources/auto/v2-recent-recent.yaml
  23. 21 5
      openapi/public/assets/protocol/resources/auto/v2-recent.yaml
  24. 2 1
      openapi/public/assets/protocol/resources/auto/v2-sign-in.yaml
  25. 2 2
      openapi/public/assets/protocol/resources/auto/v2-task.yaml
  26. 2 2
      openapi/public/assets/protocol/resources/auto/v2-terms.yaml
  27. 2 2
      openapi/public/assets/protocol/resources/auto/v2-user.yaml
  28. 2 2
      openapi/public/assets/protocol/resources/auto/v2-userdict.yaml
  29. 1 1
      openapi/public/assets/protocol/resources/auto/v3-heartbeat.yaml
  30. 48 0
      openapi/public/assets/protocol/resources/auto/v3-me-reactions-reaction.yaml
  31. 177 0
      openapi/public/assets/protocol/resources/auto/v3-me-reactions.yaml
  32. 3 3
      openapi/public/assets/protocol/resources/auto/v3-progress.yaml
  33. 45 0
      openapi/public/assets/protocol/resources/auto/v3-reactions-tally.yaml
  34. 82 0
      openapi/public/assets/protocol/resources/auto/v3-reactions.yaml
  35. 1 1
      openapi/public/assets/protocol/resources/auto/v3-search-search.yaml
  36. 2 2
      openapi/public/assets/protocol/resources/auto/v3-search-suggest.yaml
  37. 6 6
      openapi/public/assets/protocol/resources/auto/v3-search.yaml
  38. 0 85
      openapi/public/assets/protocol/resources/auto/v3-tipitaka-read-chapter-tipitaka-read-chapter.yaml
  39. 0 108
      openapi/public/assets/protocol/resources/auto/v3-tipitaka-read-chapter.yaml
  40. 0 62
      openapi/public/assets/protocol/resources/auto/v3-tipitaka-read-para-tipitaka-read-para.yaml
  41. 0 79
      openapi/public/assets/protocol/resources/auto/v3-tipitaka-read-para.yaml
  42. 142 0
      openapi/public/assets/protocol/resources/auto/v3-tipitaka-reading-channel.yaml
  43. 1 1
      openapi/public/assets/protocol/resources/auto/v3-upgrade.yaml

+ 483 - 250
dashboard-v6/src/api/schema.d.ts

@@ -3351,11 +3351,7 @@ export interface paths {
          * @description 实现:`LikeController@store`
          */
         post: operations["post_api_v2_like"];
-        /**
-         * delete like
-         * @description 实现:`LikeController@delete`
-         */
-        delete: operations["delete_api_v2_like"];
+        delete?: never;
         options?: never;
         head?: never;
         patch?: never;
@@ -4730,7 +4726,9 @@ export interface paths {
         };
         /**
          * 列出 recent
-         * @description 实现:`RecentController@index`
+         * @description 按用户拉取阅读记录,view 目前只有 user 一种口径。
+         *
+         *     实现:`RecentController@index`
          */
         get: operations["get_api_v2_recent"];
         put?: never;
@@ -7710,7 +7708,7 @@ export interface paths {
          *     仓库根目录存在 `.stop` 文件时返回 503(运维停机开关),此时响应体是
          *     RFC 9457 Problem Details 而非正常结构。
          *
-         *     实现:`HeartbeatV3Controller@show`
+         *     实现:`V3\HeartbeatController@show`
          */
         get: operations["get_api_v3_heartbeat"];
         put?: never;
@@ -7721,7 +7719,7 @@ export interface paths {
         patch?: never;
         trace?: never;
     };
-    "/v3/progress": {
+    "/v3/me/reactions": {
         parameters: {
             query?: never;
             header?: never;
@@ -7729,52 +7727,74 @@ export interface paths {
             cookie?: never;
         };
         /**
-         * 列出章节翻译进度
-         * @description 按 channel 查询各章节的翻译完成度。view 目前只支持 channel 一种口径,
-         *     传其它值返回 422。
-         *
-         *     实现:`ProgressController@index`
+         * 列出我自己的 reactions
+         * @description 实现:`V3\MeReactionController@index`
          */
-        get: operations["get_api_v3_progress"];
+        get: operations["get_api_v3_me_reactions"];
         put?: never;
-        post?: never;
+        /**
+         * 添加一条我自己的 reaction(幂等)
+         * @description 唯一键是 (type, target_id, user_id):重复提交不会产生第二条记录,返回同一条。
+         *     首次创建返回 201,命中已存在的记录返回 200。
+         *     201 不是手写的状态码:Laravel 的 ResourceResponse::calculateStatus() 看模型的
+         *     wasRecentlyCreated,新建时自动给 201。这里不要改成固定 200——幂等端点区分
+         *     「真的建了」与「早就有了」对调用方有用。
+         *
+         *     实现:`V3\MeReactionController@store`
+         */
+        post: operations["post_api_v3_me_reactions"];
         delete?: never;
         options?: never;
         head?: never;
         patch?: never;
         trace?: never;
     };
-    "/v3/search": {
+    "/v3/me/reactions/{reaction}": {
         parameters: {
             query?: never;
             header?: never;
             path?: never;
             cookie?: never;
         };
+        get?: never;
+        put?: never;
+        post?: never;
         /**
-         * 全文检索
-         * @description 支持 fuzzy / exact / semantic / hybrid 四种检索模式,底层走 OpenSearch。
-         *     同样的参数也可以用 POST /v3/search 提交(参数多、URL 放不下时用)。
+         * 删除我自己的 reaction
+         * @description 只能删自己那条(user_id 与当前用户不符返回 403)。
          *
-         *     实现:`SearchPlusController@index`
+         *     实现:`V3\MeReactionController@destroy`
          */
-        get: operations["get_api_v3_search"];
-        put?: never;
+        delete: operations["delete_api_v3_me_reactions_reaction_"];
+        options?: never;
+        head?: never;
+        patch?: never;
+        trace?: never;
+    };
+    "/v3/progress": {
+        parameters: {
+            query?: never;
+            header?: never;
+            path?: never;
+            cookie?: never;
+        };
         /**
-         * 全文检索(POST)
-         * @description 与 `GET /v3/search` 完全等价,只是把参数放在请求体里——参数多、
-         *     尤其是 page_refs / related_id 这类数组时,URL 放不下。
+         * 列出章节翻译进度
+         * @description 按 channel 查询各章节的翻译完成度。view 目前只支持 channel 一种口径,
+         *     传其它值返回 422。
          *
-         *     实现:`SearchPlusController@store`
+         *     实现:`V3\ProgressController@index`
          */
-        post: operations["post_api_v3_search"];
+        get: operations["get_api_v3_progress"];
+        put?: never;
+        post?: never;
         delete?: never;
         options?: never;
         head?: never;
         patch?: never;
         trace?: never;
     };
-    "/v3/search-suggest": {
+    "/v3/reactions": {
         parameters: {
             query?: never;
             header?: never;
@@ -7782,13 +7802,12 @@ export interface paths {
             cookie?: never;
         };
         /**
-         * 搜索建议(自动补全)
-         * @description 按前缀给出补全候选,供搜索框实时提示。每个字段各自返回一组建议。
-         *     q 为空时返回 400。
+         * 列出某个 target 下的 reactions
+         * @description 按 target 聚合的公共列表,不要求登录。分页由 Laravel paginator 提供。
          *
-         *     实现:`SearchSuggestController@index`
+         *     实现:`V3\ReactionController@index`
          */
-        get: operations["get_api_v3_search_suggest"];
+        get: operations["get_api_v3_reactions"];
         put?: never;
         post?: never;
         delete?: never;
@@ -7797,7 +7816,7 @@ export interface paths {
         patch?: never;
         trace?: never;
     };
-    "/v3/search/{search}": {
+    "/v3/reactions/tally": {
         parameters: {
             query?: never;
             header?: never;
@@ -7805,12 +7824,12 @@ export interface paths {
             cookie?: never;
         };
         /**
-         * 按 id 取单条检索结果
-         * @description 直接从 OpenSearch 按文档 id 取回,不走检索。取不到返回 404。
+         * 统计某个 target 下各 type 的计数
+         * @description 一次聚合查询出各 type 的 count;登录时再查一次当前用户的选择,补 selected / id。
          *
-         *     实现:`SearchPlusController@show`
+         *     实现:`V3\ReactionTallyController@index`
          */
-        get: operations["get_api_v3_search_search_"];
+        get: operations["get_api_v3_reactions_tally"];
         put?: never;
         post?: never;
         delete?: never;
@@ -7819,7 +7838,7 @@ export interface paths {
         patch?: never;
         trace?: never;
     };
-    "/v3/tipitaka-read-chapter": {
+    "/v3/search": {
         parameters: {
             query?: never;
             header?: never;
@@ -7827,35 +7846,29 @@ export interface paths {
             cookie?: never;
         };
         /**
-         * 按章节读取译文
-         * @description `para` 定位章节(它是章节的起始段落号,章节长度取那一行的 `chapter_len`),
-         *     `from` 是本次取数的起点游标,从它开始取 `pagesize` 大小的一块。章节不存在、
-         *     或该 channel 在这个章节里没有任何译文,返回 404。
-         *     取数的段落集合来自 sentences 表里该 channel 实际有译文的段落,不是章节的
-         *     全部段落——多数译文是残缺的,按 pali_texts 的整段区间切块会切出空块。
-         *     所以 `pagesize` 数的永远是有内容的段落。
-         *     游标式分块是为了前端的下载进度条与断点续传,meta 里给三个数:
-         *     `total_para` 是分母(该 channel 在本章节有多少段),`remaining_para` 是
-         *     `last_para` 之后本章节内还剩多少段,进度即
-         *     `(total_para - remaining_para) / total_para`;续传时把上次的
-         *     `last_para + 1` 当作下次的 `from` 传回来,`remaining_para` 为 0 即取完。
-         *     `first_para` / `last_para` 是本块实际覆盖的段落闭区间,取自切块结果而不是
-         *     `data`:渲染为空的段落会被剔出 `data` 但仍算在块内,所以 `data` 可能比这个
-         *     区间短,而续传必须按 `last_para` 推进,否则会卡在那一段上原地打转。
-         *     `first_para` 是游标顺延后的落点,可能大于请求的 `from`(即 `current_para`)。
+         * 全文检索
+         * @description 支持 fuzzy / exact / semantic / hybrid 四种检索模式,底层走 OpenSearch。
+         *     同样的参数也可以用 POST /v3/search 提交(参数多、URL 放不下时用)。
          *
-         *     实现:`TipitakaReadChapterController@index`
+         *     实现:`V3\SearchPlusController@index`
          */
-        get: operations["get_api_v3_tipitaka_read_chapter"];
+        get: operations["get_api_v3_search"];
         put?: never;
-        post?: never;
+        /**
+         * 全文检索(POST)
+         * @description 与 `GET /v3/search` 完全等价,只是把参数放在请求体里——参数多、
+         *     尤其是 page_refs / related_id 这类数组时,URL 放不下。
+         *
+         *     实现:`V3\SearchPlusController@store`
+         */
+        post: operations["post_api_v3_search"];
         delete?: never;
         options?: never;
         head?: never;
         patch?: never;
         trace?: never;
     };
-    "/v3/tipitaka-read-chapter/{tipitaka_read_chapter}": {
+    "/v3/search-suggest": {
         parameters: {
             query?: never;
             header?: never;
@@ -7863,13 +7876,13 @@ export interface paths {
             cookie?: never;
         };
         /**
-         * 按 id 读取章节
-         * @description 与 index 等价,只是把 book 与 para 合成一个路径参数;游标 `from` 仍走
-         *     查询参数。格式不对或 channel 不是 uuid 返回 422。
+         * 搜索建议(自动补全)
+         * @description 按前缀给出补全候选,供搜索框实时提示。每个字段各自返回一组建议。
+         *     q 为空时返回 400。
          *
-         *     实现:`TipitakaReadChapterController@show`
+         *     实现:`V3\SearchSuggestController@index`
          */
-        get: operations["get_api_v3_tipitaka_read_chapter_tipitaka_read_chapter_"];
+        get: operations["get_api_v3_search_suggest"];
         put?: never;
         post?: never;
         delete?: never;
@@ -7878,7 +7891,7 @@ export interface paths {
         patch?: never;
         trace?: never;
     };
-    "/v3/tipitaka-read-para": {
+    "/v3/search/{search}": {
         parameters: {
             query?: never;
             header?: never;
@@ -7886,14 +7899,12 @@ export interface paths {
             cookie?: never;
         };
         /**
-         * 按段落区间读取译文
-         * @description 阅读模式下按 book + 段落区间取内容,空段落会被跳过。区间由 para 到 to,
-         *     不传 to 则只取一段;to 小于 para 返回 422。
-         *     这个接口不走 Eloquent 分页,meta 是手工给的:一次请求即一页。
+         * 按 id 取单条检索结果
+         * @description 直接从 OpenSearch 按文档 id 取回,不走检索。取不到返回 404。
          *
-         *     实现:`TipitakaReadParaController@index`
+         *     实现:`V3\SearchPlusController@show`
          */
-        get: operations["get_api_v3_tipitaka_read_para"];
+        get: operations["get_api_v3_search_search_"];
         put?: never;
         post?: never;
         delete?: never;
@@ -7902,7 +7913,7 @@ export interface paths {
         patch?: never;
         trace?: never;
     };
-    "/v3/tipitaka-read-para/{tipitaka_read_para}": {
+    "/v3/tipitaka-reading/{channel}": {
         parameters: {
             query?: never;
             header?: never;
@@ -7910,13 +7921,19 @@ export interface paths {
             cookie?: never;
         };
         /**
-         * 读取单个段落
-         * @description id 是 `{book}-{para}` 复合形式,例如 `9001-1`。格式不对或 channel 不是
-         *     uuid 返回 422;段落无内容返回 404。
+         * 读取译文
+         * @description 取这个 channel 已经翻译的段落,按 (book, para) 升序,用游标分块。
+         *     不带任何查询串就是从头取整个 channel——下载场景要的就是这个;
+         *     阅读场景用 `book` + `chapter` 或 `book` + `para`/`to` 把范围收窄。
+         *     续传把 `meta.next_cursor` 原样传回 `after` 即可,它为 null 表示取完了。
+         *     **不要自己拼游标**:它的内部形式(`{book}-{para}`)是实现细节,会变。
+         *     `total` / `remaining` 只在带了 `book` 过滤时才给:不带过滤时结果集是整个
+         *     channel(实测最大的有 52 万段、跨 217 本书),每翻一页都去 count 一次不划算。
+         *     无过滤时用 `next_cursor` 是否为 null 判断结束,不要指望百分比。
          *
-         *     实现:`TipitakaReadParaController@show`
+         *     实现:`V3\TipitakaReadingController@__invoke`
          */
-        get: operations["get_api_v3_tipitaka_read_para_tipitaka_read_para_"];
+        get: operations["get_api_v3_tipitaka_reading_channel_"];
         put?: never;
         post?: never;
         delete?: never;
@@ -7936,7 +7953,7 @@ export interface paths {
          * 客户端升级检查
          * @description 目前只回报服务可用,尚未接入版本比对逻辑。
          *
-         *     实现:`UpgradeController@index`
+         *     实现:`V3\UpgradeController@index`
          */
         get: operations["get_api_v3_upgrade"];
         put?: never;
@@ -11274,7 +11291,13 @@ export interface operations {
                     "application/json": {
                         ok?: boolean;
                         message?: string;
-                        data?: Record<string, never>;
+                        data?: {
+                            id?: string;
+                            nickName?: string;
+                            realName?: string;
+                            avatar?: string;
+                            roles?: string[];
+                        };
                     };
                 };
             };
@@ -11684,7 +11707,17 @@ export interface operations {
                         ok?: boolean;
                         message?: string;
                         data?: {
-                            rows?: Record<string, never>[];
+                            rows?: {
+                                book?: number;
+                                paragraph?: number;
+                                sn?: number;
+                                title?: string;
+                                toc?: string | null;
+                                tags?: string[];
+                                related_name?: string | null;
+                                /** @description 仅在部分查询口径下返回。 */
+                                language?: string | null;
+                            }[];
                             count?: number;
                         };
                     };
@@ -12471,7 +12504,21 @@ export interface operations {
                     "application/json": {
                         ok?: boolean;
                         message?: string;
-                        data?: Record<string, never>;
+                        data?: {
+                            uid?: string;
+                            name?: string;
+                            summary?: string | null;
+                            lang?: string;
+                            type?: string;
+                            updated_at?: string;
+                            studio?: {
+                                id?: string;
+                                nickName?: string;
+                                realName?: string;
+                                studioName?: string;
+                                avatar?: string;
+                            };
+                        };
                     };
                 };
             };
@@ -12635,7 +12682,15 @@ export interface operations {
                         ok?: boolean;
                         message?: string;
                         data?: {
-                            rows?: Record<string, never>[];
+                            rows?: {
+                                book?: number;
+                                paragraph?: number;
+                                level?: number;
+                                text?: string | null;
+                                chapter_len?: number;
+                                chapter_strlen?: number;
+                                parent?: number;
+                            }[];
                             count?: number;
                         };
                     };
@@ -19113,36 +19168,6 @@ export interface operations {
             422: components["responses"]["ValidationError"];
         };
     };
-    delete_api_v2_like: {
-        parameters: {
-            query?: {
-                id?: string;
-                target_id?: string;
-                type?: string;
-            };
-            header?: never;
-            path?: never;
-            cookie?: never;
-        };
-        requestBody?: never;
-        responses: {
-            /** @description 成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。 */
-            200: {
-                headers: {
-                    [name: string]: unknown;
-                };
-                content: {
-                    "application/json": {
-                        ok?: boolean;
-                        message?: string;
-                        data?: Record<string, never>;
-                    };
-                };
-            };
-            401: components["responses"]["Unauthorized"];
-            422: components["responses"]["ValidationError"];
-        };
-    };
     get_api_v2_like_like_: {
         parameters: {
             query?: never;
@@ -22714,7 +22739,38 @@ export interface operations {
                         ok?: boolean;
                         message?: string;
                         data?: {
-                            rows?: Record<string, never>[];
+                            rows?: ({
+                                book?: number;
+                                para?: number;
+                                uid?: string;
+                                channel_id?: string;
+                                name?: string;
+                                type?: string;
+                                progress?: number;
+                                updated_at?: string;
+                                channel?: {
+                                    uid?: string;
+                                    name?: string;
+                                    type?: string;
+                                };
+                            } | {
+                                id?: string;
+                            } | {
+                                channel_id?: string;
+                                count?: number;
+                                studio?: {
+                                    id?: string;
+                                    nickName?: string;
+                                    realName?: string;
+                                    studioName?: string;
+                                    avatar?: string;
+                                };
+                                channel?: {
+                                    name?: string;
+                                    summary?: string | null;
+                                    lang?: string;
+                                };
+                            })[];
                             count?: number;
                         };
                     };
@@ -22948,7 +23004,12 @@ export interface operations {
                     "application/json": {
                         ok?: boolean;
                         message?: string;
-                        data?: Record<string, never>;
+                        data?: {
+                            book?: number;
+                            para?: number;
+                            channel_id?: string;
+                            title?: string;
+                        };
                     };
                 };
             };
@@ -23552,13 +23613,20 @@ export interface operations {
     };
     get_api_v2_recent: {
         parameters: {
-            query?: {
+            query: {
+                /** @description view=user 时的用户 uid,必须是 uuid */
                 id?: string;
+                /**
+                 * @description 按记录类型过滤
+                 * @example chapter
+                 */
                 type?: string;
                 order?: string;
                 dir?: string;
                 offset?: string;
                 limit?: string;
+                /** @description 查询口径 */
+                view: "user";
                 /** @description 搜索关键字 */
                 search?: string;
             };
@@ -23579,10 +23647,10 @@ export interface operations {
                         message?: string;
                         data?: {
                             rows?: {
-                                id?: number;
-                                type?: number;
-                                article_id?: number;
-                                param?: string;
+                                id?: string;
+                                type?: string;
+                                article_id?: string;
+                                param?: string | null;
                                 updated_at?: string;
                                 title?: string;
                             }[];
@@ -23624,10 +23692,10 @@ export interface operations {
                         ok?: boolean;
                         message?: string;
                         data?: {
-                            id?: number;
-                            type?: number;
-                            article_id?: number;
-                            param?: string;
+                            id?: string;
+                            type?: string;
+                            article_id?: string;
+                            param?: string | null;
                             updated_at?: string;
                             title?: string;
                         };
@@ -23659,10 +23727,10 @@ export interface operations {
                         ok?: boolean;
                         message?: string;
                         data?: {
-                            id?: number;
-                            type?: number;
-                            article_id?: number;
-                            param?: string;
+                            id?: string;
+                            type?: string;
+                            article_id?: string;
+                            param?: string | null;
                             updated_at?: string;
                             title?: string;
                         };
@@ -27458,7 +27526,8 @@ export interface operations {
                     "application/json": {
                         ok?: boolean;
                         message?: string;
-                        data?: Record<string, never>;
+                        /** @description 登录成功后返回的 JWT token */
+                        data?: string;
                     };
                 };
             };
@@ -33915,6 +33984,162 @@ export interface operations {
             };
         };
     };
+    get_api_v3_me_reactions: {
+        parameters: {
+            query?: {
+                /** @description 每页数量,最大 200 */
+                per_page?: number;
+                /** @description 操作类型 */
+                type?: "like" | "dislike" | "favorite" | "watch" | "bookmark" | "download";
+                /** @description target 的类型(task 任务、collection 文集、progress_chapter 书·章节、article 文章、terms 术语) */
+                target_type?: "task" | "collection" | "progress_chapter" | "article" | "terms";
+                /** @description 页码 */
+                page?: number;
+            };
+            header?: never;
+            path?: never;
+            cookie?: never;
+        };
+        requestBody?: never;
+        responses: {
+            /** @description 成功 */
+            200: {
+                headers: {
+                    [name: string]: unknown;
+                };
+                content: {
+                    "application/json": {
+                        data?: {
+                            id?: string;
+                            type?: string;
+                            target_id?: string;
+                            target_type?: string;
+                            context?: string | null;
+                            created_at?: string;
+                            updated_at?: string;
+                            user?: {
+                                id?: string;
+                                nickName?: string;
+                                userName?: string;
+                                realName?: string;
+                                sn?: number;
+                                avatar?: string;
+                                roles?: string[];
+                            } | null;
+                        }[];
+                        meta?: components["schemas"]["PaginationMeta"];
+                    };
+                };
+            };
+            401: components["responses"]["ProblemUnauthorized"];
+            422: components["responses"]["ProblemValidation"];
+        };
+    };
+    post_api_v3_me_reactions: {
+        parameters: {
+            query?: never;
+            header?: never;
+            path?: never;
+            cookie?: never;
+        };
+        requestBody: {
+            content: {
+                "application/json": {
+                    /**
+                     * @description 操作类型
+                     * @enum {string}
+                     */
+                    type: "like" | "dislike" | "favorite" | "watch" | "bookmark" | "download";
+                    /** @description target 的 uuid */
+                    target_id: string;
+                    /**
+                     * @description target 的类型(task 任务、collection 文集、progress_chapter 书·章节、article 文章、terms 术语)
+                     * @enum {string}
+                     */
+                    target_type: "task" | "collection" | "progress_chapter" | "article" | "terms";
+                    /** @description 附加上下文,最长 128 字符 */
+                    context?: string;
+                };
+            };
+        };
+        responses: {
+            /** @description 成功 */
+            200: {
+                headers: {
+                    [name: string]: unknown;
+                };
+                content: {
+                    "application/json": {
+                        data?: {
+                            type?: string;
+                            count?: number;
+                            selected?: boolean;
+                            id?: string | null;
+                        };
+                    };
+                };
+            };
+            /** @description 首次创建。响应体与 200 同形,区别只在这一条是本次新建的 */
+            201: {
+                headers: {
+                    [name: string]: unknown;
+                };
+                content: {
+                    "application/json": {
+                        data?: {
+                            type?: string;
+                            count?: number;
+                            selected?: boolean;
+                            id?: string | null;
+                        };
+                    };
+                };
+            };
+            401: components["responses"]["ProblemUnauthorized"];
+            422: components["responses"]["ProblemValidation"];
+        };
+    };
+    delete_api_v3_me_reactions_reaction_: {
+        parameters: {
+            query?: never;
+            header?: never;
+            path: {
+                /** @description reaction 的 uuid */
+                reaction: string;
+            };
+            cookie?: never;
+        };
+        requestBody?: never;
+        responses: {
+            /** @description 成功 */
+            200: {
+                headers: {
+                    [name: string]: unknown;
+                };
+                content: {
+                    "application/json": {
+                        data?: {
+                            type?: string;
+                            count?: number;
+                            selected?: boolean;
+                            id?: string | null;
+                        };
+                    };
+                };
+            };
+            401: components["responses"]["ProblemUnauthorized"];
+            /** @description 只能删除自己的 reaction */
+            403: {
+                headers: {
+                    [name: string]: unknown;
+                };
+                content: {
+                    "application/problem+json": components["schemas"]["ProblemDetails"];
+                };
+            };
+            422: components["responses"]["ProblemValidation"];
+        };
+    };
     get_api_v3_progress: {
         parameters: {
             query: {
@@ -33971,6 +34196,94 @@ export interface operations {
             422: components["responses"]["ProblemValidation"];
         };
     };
+    get_api_v3_reactions: {
+        parameters: {
+            query: {
+                /** @description 每页数量,最大 200 */
+                per_page?: number;
+                /**
+                 * @description target 的 uuid
+                 * @example 5b4511c1-c99f-47e7-9798-97b9e5229e18
+                 */
+                target_id: string;
+                /** @description 操作类型 */
+                type?: "like" | "dislike" | "favorite" | "watch" | "bookmark" | "download";
+                /** @description 页码 */
+                page?: number;
+            };
+            header?: never;
+            path?: never;
+            cookie?: never;
+        };
+        requestBody?: never;
+        responses: {
+            /** @description 成功 */
+            200: {
+                headers: {
+                    [name: string]: unknown;
+                };
+                content: {
+                    "application/json": {
+                        data?: {
+                            id?: string;
+                            type?: string;
+                            target_id?: string;
+                            target_type?: string;
+                            context?: string | null;
+                            created_at?: string;
+                            updated_at?: string;
+                            user?: {
+                                id?: string;
+                                nickName?: string;
+                                userName?: string;
+                                realName?: string;
+                                sn?: number;
+                                avatar?: string;
+                                roles?: string[];
+                            } | null;
+                        }[];
+                        meta?: components["schemas"]["PaginationMeta"];
+                    };
+                };
+            };
+            422: components["responses"]["ProblemValidation"];
+        };
+    };
+    get_api_v3_reactions_tally: {
+        parameters: {
+            query: {
+                /**
+                 * @description target 的 uuid
+                 * @example 5b4511c1-c99f-47e7-9798-97b9e5229e18
+                 */
+                target_id: string;
+            };
+            header?: never;
+            path?: never;
+            cookie?: never;
+        };
+        requestBody?: never;
+        responses: {
+            /** @description 成功 */
+            200: {
+                headers: {
+                    [name: string]: unknown;
+                };
+                content: {
+                    "application/json": {
+                        data?: {
+                            type?: string;
+                            count?: number;
+                            selected?: boolean;
+                            id?: string | null;
+                        }[];
+                        meta?: components["schemas"]["PaginationMeta"];
+                    };
+                };
+            };
+            422: components["responses"]["ProblemValidation"];
+        };
+    };
     get_api_v3_search: {
         parameters: {
             query: {
@@ -34165,107 +34478,39 @@ export interface operations {
             422: components["responses"]["ProblemValidation"];
         };
     };
-    get_api_v3_tipitaka_read_chapter: {
+    get_api_v3_tipitaka_reading_channel_: {
         parameters: {
-            query: {
-                /** @description 典籍 id */
-                book: number;
-                /** @description 章节的起始段落号,决定本次取数的段落区间 */
-                para: number;
-                /** @description 起点游标,从这一段开始取;不传则等于 para,即从章节 开头取起。可以是章节区间内的任意段落号,落在没有译文的段落上就 顺延到它之后的第一段。超出章节区间、或它之后已经没有译文, 返回 422 */
-                from?: number;
-                /** @description 译文 channel 的 uuid */
-                channel: string;
-                /** @description 内容格式 */
-                format?: "html" | "markdown" | "react" | "text";
-                /** @description 输出口径:display 整段合并,sentences 逐句,all 两者都给 */
-                view?: "display" | "sentences" | "all";
-                /** @description 每块大小,含义由 unit 决定。上限 unit=para 时 200、 unit=byte 时 5000,超出不报错按上限算,meta.page_size 回实际 生效的值 */
-                pagesize?: number;
+            query?: {
+                /**
+                 * @description 典籍 id。与 chapter / para / to 一起用时必填
+                 * @example 9002
+                 */
+                book?: number;
+                /** @description 章节起始段落号,服务端按 chapter_len 展开成段落区间。 与 para / to 互斥 */
+                chapter?: number;
+                /** @description 起始段落号 */
+                para?: number;
+                /** @description 结束段落号(含)。不传则等于 para,即只取一段 */
+                to?: number;
+                /**
+                 * @description 游标,取 meta.next_cursor 原样回传
+                 * @example 9002-15
+                 */
+                after?: string;
+                /** @description 每块大小,含义由 unit 决定。上限 unit=para 时 200、 unit=byte 时 5000,超出不报错按上限算,meta.page_size 回实际生效的值 */
+                page_size?: number;
                 /** @description 每块大小的单位:para 按段落数,byte 按段落长度累加。 两种单位下每块都至少一段、至多 200 段 */
                 unit?: "para" | "byte";
-            };
-            header?: never;
-            path?: never;
-            cookie?: never;
-        };
-        requestBody?: never;
-        responses: {
-            /** @description 成功 */
-            200: {
-                headers: {
-                    [name: string]: unknown;
-                };
-                content: {
-                    "application/json": {
-                        data?: Record<string, never>[];
-                        meta?: components["schemas"]["PaginationMeta"];
-                    };
-                };
-            };
-            422: components["responses"]["ProblemValidation"];
-        };
-    };
-    get_api_v3_tipitaka_read_chapter_tipitaka_read_chapter_: {
-        parameters: {
-            query: {
-                /** @description 译文 channel 的 uuid */
-                channel: string;
-                /** @description 起点游标,从这一段开始取;不传则从章节开头取起。 超出章节区间返回 422 */
-                from?: number;
                 /** @description 内容格式 */
                 format?: "html" | "markdown" | "react" | "text";
-                /** @description 输出口径:display 整段合并,sentences 逐句,all 两者都给 */
-                view?: "display" | "sentences" | "all";
-                /** @description 每块大小,含义由 unit 决定。上限 unit=para 时 200、 unit=byte 时 5000,超出不报错按上限算,meta.page_size 回实际 生效的值 */
-                pagesize?: number;
-                /** @description 每块大小的单位:para 按段落数,byte 按段落长度累加。 两种单位下每块都至少一段、至多 200 段 */
-                unit?: "para" | "byte";
+                /** @description 输出口径:display 整段合并,sentences 逐句,all 两者都给。 不叫 view 是因为 v2 的 `view=` 是必填互斥的业务路径开关,名字撞上会误导 */
+                include?: "display" | "sentences" | "all";
             };
             header?: never;
             path: {
-                /**
-                 * @description 章节 id,格式 {book}-{para}
-                 * @example 9002-1
-                 */
-                tipitaka_read_chapter: string;
-            };
-            cookie?: never;
-        };
-        requestBody?: never;
-        responses: {
-            /** @description 成功 */
-            200: {
-                headers: {
-                    [name: string]: unknown;
-                };
-                content: {
-                    "application/json": {
-                        data?: Record<string, never>;
-                    };
-                };
-            };
-            422: components["responses"]["ProblemValidation"];
-        };
-    };
-    get_api_v3_tipitaka_read_para: {
-        parameters: {
-            query: {
-                /** @description 典籍 id */
-                book: number;
-                /** @description 起始段落号 */
-                para: number;
-                /** @description 结束段落号(含)。不传则等于 para */
-                to?: number;
                 /** @description 译文 channel 的 uuid */
                 channel: string;
-                /** @description 内容格式 */
-                format?: "html" | "markdown" | "react" | "text";
-                /** @description 输出口径:display 整段合并,sentences 逐句,all 两者都给 */
-                view?: "display" | "sentences" | "all";
             };
-            header?: never;
-            path?: never;
             cookie?: never;
         };
         requestBody?: never;
@@ -34277,45 +34522,33 @@ export interface operations {
                 };
                 content: {
                     "application/json": {
-                        data?: Record<string, never>[];
-                        meta?: components["schemas"]["PaginationMeta"];
+                        data?: {
+                            para?: number;
+                            display?: string;
+                            book?: number;
+                        }[];
+                        meta?: {
+                            /** @description 每块大小(按 unit 计) */
+                            page_size?: number;
+                            /** @description 每块大小的单位:para 或 byte */
+                            page_size_unit?: string;
+                            /** @description 下一块的游标,原样传回 after;为 null 表示取完 */
+                            next_cursor?: string;
+                            /** @description 过滤范围内已翻译的段落总数。仅在带了 book 过滤时出现 */
+                            total?: number;
+                            /** @description 本块之后过滤范围内还剩多少段。仅在带了 book 过滤时出现 */
+                            remaining?: number;
+                        };
                     };
                 };
             };
-            422: components["responses"]["ProblemValidation"];
-        };
-    };
-    get_api_v3_tipitaka_read_para_tipitaka_read_para_: {
-        parameters: {
-            query: {
-                /** @description 译文 channel 的 uuid */
-                channel: string;
-                /** @description 内容格式 */
-                format?: "html" | "markdown" | "react" | "text";
-                /** @description 输出口径 */
-                view?: "display" | "sentences" | "all";
-            };
-            header?: never;
-            path: {
-                /**
-                 * @description 段落 id,格式 {book}-{para}
-                 * @example 9001-1
-                 */
-                tipitaka_read_para: string;
-            };
-            cookie?: never;
-        };
-        requestBody?: never;
-        responses: {
-            /** @description 成功 */
-            200: {
+            /** @description 指定的 chapter 在 pali_texts 里不存在 */
+            404: {
                 headers: {
                     [name: string]: unknown;
                 };
                 content: {
-                    "application/json": {
-                        data?: Record<string, never>;
-                    };
+                    "application/problem+json": components["schemas"]["ProblemDetails"];
                 };
             };
             422: components["responses"]["ProblemValidation"];

+ 11 - 9
openapi/public/assets/protocol/main.yaml

@@ -7,7 +7,7 @@ info:
 
     由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。
     响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
-  version: 2026.9.21
+  version: 2026.9.24
 servers:
   -
     url: /api
@@ -677,21 +677,23 @@ paths:
     $ref: ./resources/auto/v2-webhook-webhook.yaml
   /v3/heartbeat:
     $ref: ./resources/auto/v3-heartbeat.yaml
+  /v3/me/reactions:
+    $ref: ./resources/auto/v3-me-reactions.yaml
+  '/v3/me/reactions/{reaction}':
+    $ref: ./resources/auto/v3-me-reactions-reaction.yaml
   /v3/progress:
     $ref: ./resources/auto/v3-progress.yaml
+  /v3/reactions:
+    $ref: ./resources/auto/v3-reactions.yaml
+  /v3/reactions/tally:
+    $ref: ./resources/auto/v3-reactions-tally.yaml
   /v3/search:
     $ref: ./resources/auto/v3-search.yaml
   /v3/search-suggest:
     $ref: ./resources/auto/v3-search-suggest.yaml
   '/v3/search/{search}':
     $ref: ./resources/auto/v3-search-search.yaml
-  /v3/tipitaka-read-chapter:
-    $ref: ./resources/auto/v3-tipitaka-read-chapter.yaml
-  '/v3/tipitaka-read-chapter/{tipitaka_read_chapter}':
-    $ref: ./resources/auto/v3-tipitaka-read-chapter-tipitaka-read-chapter.yaml
-  /v3/tipitaka-read-para:
-    $ref: ./resources/auto/v3-tipitaka-read-para.yaml
-  '/v3/tipitaka-read-para/{tipitaka_read_para}':
-    $ref: ./resources/auto/v3-tipitaka-read-para-tipitaka-read-para.yaml
+  '/v3/tipitaka-reading/{channel}':
+    $ref: ./resources/auto/v3-tipitaka-reading-channel.yaml
   /v3/upgrade:
     $ref: ./resources/auto/v3-upgrade.yaml

+ 23 - 0
openapi/public/assets/protocol/overrides/v2-auth-current.yaml

@@ -0,0 +1,23 @@
+# 手写补充:GET /v2/auth/current 的 data 逐字段类型(对齐移动端 AuthUser)。
+get:
+  responses:
+    200:
+      content:
+        application/json:
+          schema:
+            properties:
+              data:
+                type: object
+                properties:
+                  id:
+                    type: string
+                  nickName:
+                    type: string
+                  realName:
+                    type: string
+                  avatar:
+                    type: string
+                  roles:
+                    type: array
+                    items:
+                      type: string

+ 38 - 0
openapi/public/assets/protocol/overrides/v2-channel-channel.yaml

@@ -0,0 +1,38 @@
+# 手写补充:GET /v2/channel/{channel} 的 data 逐字段类型(对齐移动端 ChannelInfo)。
+# 只补 get 的响应,put/patch/delete 保持自动生成结果。
+get:
+  responses:
+    200:
+      content:
+        application/json:
+          schema:
+            properties:
+              data:
+                type: object
+                properties:
+                  uid:
+                    type: string
+                  name:
+                    type: string
+                  summary:
+                    type: string
+                    nullable: true
+                  lang:
+                    type: string
+                  type:
+                    type: string
+                  updated_at:
+                    type: string
+                  studio:
+                    type: object
+                    properties:
+                      id:
+                        type: string
+                      nickName:
+                        type: string
+                      realName:
+                        type: string
+                      studioName:
+                        type: string
+                      avatar:
+                        type: string

+ 31 - 0
openapi/public/assets/protocol/overrides/v2-chapter.yaml

@@ -0,0 +1,31 @@
+# 手写补充:/v2/chapter?view=toc 的 rows 逐字段类型,与 dashboard-v6 手写的 IChapterToc 对齐
+# (src/api/pali-text.ts):text 可空,其余为 number。实际返回 PaliText 的
+# book/paragraph/level/text/chapter_len/chapter_strlen/parent。
+get:
+  responses:
+    200:
+      content:
+        application/json:
+          schema:
+            properties:
+              data:
+                properties:
+                  rows:
+                    items:
+                      type: object
+                      properties:
+                        book:
+                          type: integer
+                        paragraph:
+                          type: integer
+                        level:
+                          type: integer
+                        text:
+                          type: string
+                          nullable: true
+                        chapter_len:
+                          type: integer
+                        chapter_strlen:
+                          type: integer
+                        parent:
+                          type: integer

+ 20 - 0
openapi/public/assets/protocol/overrides/v2-progress-progress.yaml

@@ -0,0 +1,20 @@
+# 手写补充:GET /v2/progress/{progress} 的 data 逐字段类型(对齐移动端 ProgressChapterInfo)。
+# 只补 get 的响应,put/patch/delete 保持自动生成结果。
+get:
+  responses:
+    200:
+      content:
+        application/json:
+          schema:
+            properties:
+              data:
+                type: object
+                properties:
+                  book:
+                    type: integer
+                  para:
+                    type: integer
+                  channel_id:
+                    type: string
+                  title:
+                    type: string

+ 77 - 0
openapi/public/assets/protocol/overrides/v2-progress.yaml

@@ -0,0 +1,77 @@
+# 手写补充:/v2/progress 多 view 的 rows 按口径分 schema。
+# 只补 get 的响应 item(anyOf,各 view 形状有重叠字段,不能算互斥),post 与其它 view 保持自动生成结果。
+get:
+  responses:
+    200:
+      content:
+        application/json:
+          schema:
+            properties:
+              data:
+                properties:
+                  rows:
+                    items:
+                      anyOf:
+                        - type: object
+                          description: view=chapter_channels
+                          properties:
+                            book:
+                              type: integer
+                            para:
+                              type: integer
+                            uid:
+                              type: string
+                            channel_id:
+                              type: string
+                            name:
+                              type: string
+                            type:
+                              type: string
+                            progress:
+                              type: number
+                            updated_at:
+                              type: string
+                            channel:
+                              type: object
+                              properties:
+                                uid:
+                                  type: string
+                                name:
+                                  type: string
+                                type:
+                                  type: string
+                        - type: object
+                          description: view=ids
+                          properties:
+                            id:
+                              type: string
+                        - type: object
+                          description: view=channel
+                          properties:
+                            channel_id:
+                              type: string
+                            count:
+                              type: integer
+                            studio:
+                              type: object
+                              properties:
+                                id:
+                                  type: string
+                                nickName:
+                                  type: string
+                                realName:
+                                  type: string
+                                studioName:
+                                  type: string
+                                avatar:
+                                  type: string
+                            channel:
+                              type: object
+                              properties:
+                                name:
+                                  type: string
+                                summary:
+                                  type: string
+                                  nullable: true
+                                lang:
+                                  type: string

+ 11 - 0
openapi/public/assets/protocol/overrides/v2-sign-in.yaml

@@ -0,0 +1,11 @@
+# 手写补充:POST /v2/sign-in 的 data 是 token 字符串,不是对象。
+post:
+  responses:
+    200:
+      content:
+        application/json:
+          schema:
+            properties:
+              data:
+                type: string
+                description: 登录成功后返回的 JWT token

+ 19 - 0
openapi/public/assets/protocol/overrides/v3-tipitaka-reading-channel.yaml

@@ -0,0 +1,19 @@
+# 手写补充:/v3/tipitaka-reading/{channel} 的 data[] 逐字段类型。
+# meta 已由控制器 @meta 注解生成专用 schema,这里只补 data 的 item(para + display)。
+get:
+  responses:
+    200:
+      content:
+        application/json:
+          schema:
+            properties:
+              data:
+                items:
+                  type: object
+                  properties:
+                    book:
+                      type: integer
+                    para:
+                      type: integer
+                    display:
+                      type: string

+ 2 - 2
openapi/public/assets/protocol/resources/auto/v2-article-article.yaml

@@ -158,7 +158,7 @@ put:
             status:
               type: integer
               description: '公开状态,30 表示公开'
-              default: '10'
+              default: 10
           required:
             - title
             - content
@@ -291,7 +291,7 @@ patch:
             status:
               type: integer
               description: '公开状态,30 表示公开'
-              default: '10'
+              default: 10
           required:
             - title
             - content

+ 2 - 2
openapi/public/assets/protocol/resources/auto/v2-article.yaml

@@ -92,14 +92,14 @@ get:
       in: query
       schema:
         type: integer
-        default: '0'
+        default: 0
       description: 分页起始位置
     -
       name: limit
       in: query
       schema:
         type: integer
-        default: '1000'
+        default: 1000
       description: 每页条数
   responses:
     200:

+ 13 - 0
openapi/public/assets/protocol/resources/auto/v2-auth-current.yaml

@@ -19,6 +19,19 @@ get:
                 type: string
               data:
                 type: object
+                properties:
+                  id:
+                    type: string
+                  nickName:
+                    type: string
+                  realName:
+                    type: string
+                  avatar:
+                    type: string
+                  roles:
+                    type: array
+                    items:
+                      type: string
     401:
       $ref: '../../main.yaml#/components/responses/Unauthorized'
     422:

+ 1 - 0
openapi/public/assets/protocol/resources/auto/v2-book-title.yaml

@@ -58,6 +58,7 @@ get:
                     type: array
                     items:
                       type: object
+                      properties: { book: { type: integer }, paragraph: { type: integer }, sn: { type: integer }, title: { type: string }, toc: { type: string, nullable: true }, tags: { type: array, items: { type: string } }, related_name: { type: string, nullable: true }, language: { type: string, nullable: true, description: 仅在部分查询口径下返回。 } }
                   count:
                     type: integer
     401:

+ 22 - 0
openapi/public/assets/protocol/resources/auto/v2-channel-channel.yaml

@@ -30,6 +30,28 @@ get:
                 type: string
               data:
                 type: object
+                properties:
+                  uid:
+                    type: string
+                  name:
+                    type: string
+                  summary:
+                    type: string
+                    nullable: true
+                  lang:
+                    type: string
+                  type:
+                    type: string
+                  updated_at:
+                    type: string
+                  studio:
+                    type: object
+                    properties:
+                      id: { type: string }
+                      nickName: { type: string }
+                      realName: { type: string }
+                      studioName: { type: string }
+                      avatar: { type: string }
     401:
       $ref: '../../main.yaml#/components/responses/Unauthorized'
     422:

+ 1 - 0
openapi/public/assets/protocol/resources/auto/v2-chapter.yaml

@@ -70,6 +70,7 @@ get:
                     type: array
                     items:
                       type: object
+                      properties: { book: { type: integer }, paragraph: { type: integer }, level: { type: integer }, text: { type: string, nullable: true }, chapter_len: { type: integer }, chapter_strlen: { type: integer }, parent: { type: integer } }
                   count:
                     type: integer
     401:

+ 2 - 2
openapi/public/assets/protocol/resources/auto/v2-course-member.yaml

@@ -89,14 +89,14 @@ get:
       in: query
       schema:
         type: integer
-        default: '0'
+        default: 0
       description: 跳过的记录数
     -
       name: limit
       in: query
       schema:
         type: integer
-        default: '1000'
+        default: 1000
       description: 返回条数上限
   responses:
     200:

+ 3 - 3
openapi/public/assets/protocol/resources/auto/v2-discussion.yaml

@@ -104,14 +104,14 @@ get:
       in: query
       schema:
         type: integer
-        default: '0'
+        default: 0
       description: 跳过的记录数
     -
       name: limit
       in: query
       schema:
         type: integer
-        default: '100'
+        default: 100
       description: 返回条数上限
     -
       name: search
@@ -219,7 +219,7 @@ post:
             notification:
               type: boolean
               description: 是否推送消息队列通知
-              default: 'true'
+              default: true
           required:
             - res_id
             - res_type

+ 0 - 40
openapi/public/assets/protocol/resources/auto/v2-like.yaml

@@ -1,44 +1,4 @@
 # 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v2-like.yaml
-delete:
-  summary: 'delete like'
-  tags:
-    - like
-  operationId: delete_api_v2_like
-  description: 实现:`LikeController@delete`
-  parameters:
-    -
-      name: id
-      in: query
-      schema:
-        type: string
-    -
-      name: target_id
-      in: query
-      schema:
-        type: string
-    -
-      name: type
-      in: query
-      schema:
-        type: string
-  responses:
-    200:
-      description: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'
-      content:
-        application/json:
-          schema:
-            type: object
-            properties:
-              ok:
-                type: boolean
-              message:
-                type: string
-              data:
-                type: object
-    401:
-      $ref: '../../main.yaml#/components/responses/Unauthorized'
-    422:
-      $ref: '../../main.yaml#/components/responses/ValidationError'
 get:
   summary: '列出 like'
   tags:

+ 9 - 0
openapi/public/assets/protocol/resources/auto/v2-progress-progress.yaml

@@ -26,6 +26,15 @@ get:
                 type: string
               data:
                 type: object
+                properties:
+                  book:
+                    type: integer
+                  para:
+                    type: integer
+                  channel_id:
+                    type: string
+                  title:
+                    type: string
     401:
       $ref: '../../main.yaml#/components/responses/Unauthorized'
     422:

+ 1 - 1
openapi/public/assets/protocol/resources/auto/v2-progress.yaml

@@ -123,7 +123,7 @@ get:
                   rows:
                     type: array
                     items:
-                      type: object
+                      anyOf: [{ type: object, description: view=chapter_channels, properties: { book: { type: integer }, para: { type: integer }, uid: { type: string }, channel_id: { type: string }, name: { type: string }, type: { type: string }, progress: { type: number }, updated_at: { type: string }, channel: { type: object, properties: { uid: { type: string }, name: { type: string }, type: { type: string } } } } }, { type: object, description: view=ids, properties: { id: { type: string } } }, { type: object, description: view=channel, properties: { channel_id: { type: string }, count: { type: integer }, studio: { type: object, properties: { id: { type: string }, nickName: { type: string }, realName: { type: string }, studioName: { type: string }, avatar: { type: string } } }, channel: { type: object, properties: { name: { type: string }, summary: { type: string, nullable: true }, lang: { type: string } } } } }]
                   count:
                     type: integer
     401:

+ 2 - 2
openapi/public/assets/protocol/resources/auto/v2-project.yaml

@@ -79,14 +79,14 @@ get:
       in: query
       schema:
         type: integer
-        default: '0'
+        default: 0
       description: 分页偏移量
     -
       name: limit
       in: query
       schema:
         type: integer
-        default: '10000'
+        default: 10000
       description: 每页条数
     -
       name: search

+ 4 - 3
openapi/public/assets/protocol/resources/auto/v2-recent-recent.yaml

@@ -28,13 +28,14 @@ get:
                 type: object
                 properties:
                   id:
-                    type: integer
+                    type: string
                   type:
-                    type: integer
+                    type: string
                   article_id:
-                    type: integer
+                    type: string
                   param:
                     type: string
+                    nullable: true
                   updated_at:
                     type: string
                   title:

+ 21 - 5
openapi/public/assets/protocol/resources/auto/v2-recent.yaml

@@ -4,18 +4,24 @@ get:
   tags:
     - recent
   operationId: get_api_v2_recent
-  description: 实现:`RecentController@index`
+  description: |-
+    按用户拉取阅读记录,view 目前只有 user 一种口径。
+
+    实现:`RecentController@index`
   parameters:
     -
       name: id
       in: query
       schema:
         type: string
+      description: 'view=user 时的用户 uid,必须是 uuid'
     -
       name: type
       in: query
       schema:
         type: string
+      description: 按记录类型过滤
+      example: chapter
     -
       name: order
       in: query
@@ -38,6 +44,15 @@ get:
       in: query
       schema:
         type: string
+    -
+      name: view
+      in: query
+      schema:
+        type: string
+        enum:
+          - user
+      description: 查询口径
+      required: true
     -
       name: search
       in: query
@@ -63,7 +78,7 @@ get:
                     type: array
                     items:
                       type: object
-                      properties: { id: { type: integer }, type: { type: integer }, article_id: { type: integer }, param: { type: string }, updated_at: { type: string }, title: { type: string } }
+                      properties: { id: { type: string }, type: { type: string }, article_id: { type: string }, param: { type: string, nullable: true }, updated_at: { type: string }, title: { type: string } }
                   count:
                     type: integer
     401:
@@ -110,13 +125,14 @@ post:
                 type: object
                 properties:
                   id:
-                    type: integer
+                    type: string
                   type:
-                    type: integer
+                    type: string
                   article_id:
-                    type: integer
+                    type: string
                   param:
                     type: string
+                    nullable: true
                   updated_at:
                     type: string
                   title:

+ 2 - 1
openapi/public/assets/protocol/resources/auto/v2-sign-in.yaml

@@ -29,7 +29,8 @@ post:
               message:
                 type: string
               data:
-                type: object
+                type: string
+                description: '登录成功后返回的 JWT token'
     401:
       $ref: '../../main.yaml#/components/responses/Unauthorized'
     422:

+ 2 - 2
openapi/public/assets/protocol/resources/auto/v2-task.yaml

@@ -120,14 +120,14 @@ get:
       in: query
       schema:
         type: integer
-        default: '0'
+        default: 0
       description: 分页偏移量
     -
       name: limit
       in: query
       schema:
         type: integer
-        default: '1000'
+        default: 1000
       description: 每页条数
     -
       name: search

+ 2 - 2
openapi/public/assets/protocol/resources/auto/v2-terms.yaml

@@ -95,14 +95,14 @@ get:
       in: query
       schema:
         type: integer
-        default: '0'
+        default: 0
       description: 分页偏移量
     -
       name: limit
       in: query
       schema:
         type: integer
-        default: '1000'
+        default: 1000
       description: 每页条数
     -
       name: mode

+ 2 - 2
openapi/public/assets/protocol/resources/auto/v2-user.yaml

@@ -61,14 +61,14 @@ get:
       in: query
       schema:
         type: integer
-        default: '0'
+        default: 0
       description: 分页起始位置
     -
       name: limit
       in: query
       schema:
         type: integer
-        default: '20'
+        default: 20
       description: 每页条数
   responses:
     200:

+ 2 - 2
openapi/public/assets/protocol/resources/auto/v2-userdict.yaml

@@ -126,14 +126,14 @@ get:
       in: query
       schema:
         type: integer
-        default: '0'
+        default: 0
       description: 跳过的记录数
     -
       name: limit
       in: query
       schema:
         type: integer
-        default: '200'
+        default: 200
       description: 返回条数上限
   responses:
     200:

+ 1 - 1
openapi/public/assets/protocol/resources/auto/v3-heartbeat.yaml

@@ -15,7 +15,7 @@ get:
     仓库根目录存在 `.stop` 文件时返回 503(运维停机开关),此时响应体是
     RFC 9457 Problem Details 而非正常结构。
 
-    实现:`HeartbeatV3Controller@show`
+    实现:`V3\HeartbeatController@show`
   responses:
     200:
       description: 成功

+ 48 - 0
openapi/public/assets/protocol/resources/auto/v3-me-reactions-reaction.yaml

@@ -0,0 +1,48 @@
+# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-me-reactions-reaction.yaml
+delete:
+  summary: '删除我自己的 reaction'
+  tags:
+    - me
+  operationId: delete_api_v3_me_reactions_reaction_
+  description: |-
+    只能删自己那条(user_id 与当前用户不符返回 403)。
+
+    实现:`V3\MeReactionController@destroy`
+  parameters:
+    -
+      name: reaction
+      in: path
+      required: true
+      schema:
+        type: string
+      description: 'reaction 的 uuid'
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: object
+                properties:
+                  type:
+                    type: string
+                  count:
+                    type: integer
+                  selected:
+                    type: boolean
+                  id:
+                    type: string
+                    nullable: true
+    401:
+      $ref: '../../main.yaml#/components/responses/ProblemUnauthorized'
+    403:
+      description: '只能删除自己的 reaction'
+      content:
+        application/problem+json:
+          schema:
+            $ref: '../../main.yaml#/components/schemas/ProblemDetails'
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 177 - 0
openapi/public/assets/protocol/resources/auto/v3-me-reactions.yaml

@@ -0,0 +1,177 @@
+# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-me-reactions.yaml
+get:
+  summary: '列出我自己的 reactions'
+  tags:
+    - me
+  operationId: get_api_v3_me_reactions
+  description: 实现:`V3\MeReactionController@index`
+  parameters:
+    -
+      name: per_page
+      in: query
+      schema:
+        type: integer
+        default: 15
+      description: '每页数量,最大 200'
+    -
+      name: type
+      in: query
+      schema:
+        type: string
+        enum:
+          - like
+          - dislike
+          - favorite
+          - watch
+          - bookmark
+          - download
+      description: 操作类型
+    -
+      name: target_type
+      in: query
+      schema:
+        type: string
+        enum:
+          - task
+          - collection
+          - progress_chapter
+          - article
+          - terms
+      description: 'target 的类型(task 任务、collection 文集、progress_chapter 书·章节、article 文章、terms 术语)'
+    -
+      name: page
+      in: query
+      schema:
+        type: integer
+        default: 1
+      description: 页码
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    id:
+                      type: string
+                    type:
+                      type: string
+                    target_id:
+                      type: string
+                    target_type:
+                      type: string
+                    context:
+                      type: string
+                      nullable: true
+                    created_at:
+                      type: string
+                    updated_at:
+                      type: string
+                    user:
+                      type: object
+                      properties: { id: { type: string }, nickName: { type: string }, userName: { type: string }, realName: { type: string }, sn: { type: integer }, avatar: { type: string }, roles: { type: array, items: { type: string } } }
+                      nullable: true
+              meta:
+                $ref: '../../main.yaml#/components/schemas/PaginationMeta'
+    401:
+      $ref: '../../main.yaml#/components/responses/ProblemUnauthorized'
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'
+post:
+  summary: '添加一条我自己的 reaction(幂等)'
+  tags:
+    - me
+  operationId: post_api_v3_me_reactions
+  description: |-
+    唯一键是 (type, target_id, user_id):重复提交不会产生第二条记录,返回同一条。
+    首次创建返回 201,命中已存在的记录返回 200。
+    201 不是手写的状态码:Laravel 的 ResourceResponse::calculateStatus() 看模型的
+    wasRecentlyCreated,新建时自动给 201。这里不要改成固定 200——幂等端点区分
+    「真的建了」与「早就有了」对调用方有用。
+
+    实现:`V3\MeReactionController@store`
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            type:
+              type: string
+              description: 操作类型
+              enum:
+                - like
+                - dislike
+                - favorite
+                - watch
+                - bookmark
+                - download
+            target_id:
+              type: string
+              description: 'target 的 uuid'
+            target_type:
+              type: string
+              description: 'target 的类型(task 任务、collection 文集、progress_chapter 书·章节、article 文章、terms 术语)'
+              enum:
+                - task
+                - collection
+                - progress_chapter
+                - article
+                - terms
+            context:
+              type: string
+              description: '附加上下文,最长 128 字符'
+          required:
+            - type
+            - target_id
+            - target_type
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: object
+                properties:
+                  type:
+                    type: string
+                  count:
+                    type: integer
+                  selected:
+                    type: boolean
+                  id:
+                    type: string
+                    nullable: true
+    201:
+      description: '首次创建。响应体与 200 同形,区别只在这一条是本次新建的'
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: object
+                properties:
+                  type:
+                    type: string
+                  count:
+                    type: integer
+                  selected:
+                    type: boolean
+                  id:
+                    type: string
+                    nullable: true
+    401:
+      $ref: '../../main.yaml#/components/responses/ProblemUnauthorized'
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 3 - 3
openapi/public/assets/protocol/resources/auto/v3-progress.yaml

@@ -8,7 +8,7 @@ get:
     按 channel 查询各章节的翻译完成度。view 目前只支持 channel 一种口径,
     传其它值返回 422。
 
-    实现:`ProgressController@index`
+    实现:`V3\ProgressController@index`
   parameters:
     -
       name: view
@@ -68,14 +68,14 @@ get:
       in: query
       schema:
         type: integer
-        default: '10'
+        default: 10
       description: 每页数量
     -
       name: page
       in: query
       schema:
         type: integer
-        default: '1'
+        default: 1
       description: 页码
   responses:
     200:

+ 45 - 0
openapi/public/assets/protocol/resources/auto/v3-reactions-tally.yaml

@@ -0,0 +1,45 @@
+# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-reactions-tally.yaml
+get:
+  summary: '统计某个 target 下各 type 的计数'
+  tags:
+    - reactions
+  operationId: get_api_v3_reactions_tally
+  description: |-
+    一次聚合查询出各 type 的 count;登录时再查一次当前用户的选择,补 selected / id。
+
+    实现:`V3\ReactionTallyController@index`
+  parameters:
+    -
+      name: target_id
+      in: query
+      schema:
+        type: string
+      description: 'target 的 uuid'
+      required: true
+      example: 5b4511c1-c99f-47e7-9798-97b9e5229e18
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    type:
+                      type: string
+                    count:
+                      type: integer
+                    selected:
+                      type: boolean
+                    id:
+                      type: string
+                      nullable: true
+              meta:
+                $ref: '../../main.yaml#/components/schemas/PaginationMeta'
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 82 - 0
openapi/public/assets/protocol/resources/auto/v3-reactions.yaml

@@ -0,0 +1,82 @@
+# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-reactions.yaml
+get:
+  summary: '列出某个 target 下的 reactions'
+  tags:
+    - reactions
+  operationId: get_api_v3_reactions
+  description: |-
+    按 target 聚合的公共列表,不要求登录。分页由 Laravel paginator 提供。
+
+    实现:`V3\ReactionController@index`
+  parameters:
+    -
+      name: per_page
+      in: query
+      schema:
+        type: integer
+        default: 15
+      description: '每页数量,最大 200'
+    -
+      name: target_id
+      in: query
+      schema:
+        type: string
+      description: 'target 的 uuid'
+      required: true
+      example: 5b4511c1-c99f-47e7-9798-97b9e5229e18
+    -
+      name: type
+      in: query
+      schema:
+        type: string
+        enum:
+          - like
+          - dislike
+          - favorite
+          - watch
+          - bookmark
+          - download
+      description: 操作类型
+    -
+      name: page
+      in: query
+      schema:
+        type: integer
+        default: 1
+      description: 页码
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    id:
+                      type: string
+                    type:
+                      type: string
+                    target_id:
+                      type: string
+                    target_type:
+                      type: string
+                    context:
+                      type: string
+                      nullable: true
+                    created_at:
+                      type: string
+                    updated_at:
+                      type: string
+                    user:
+                      type: object
+                      properties: { id: { type: string }, nickName: { type: string }, userName: { type: string }, realName: { type: string }, sn: { type: integer }, avatar: { type: string }, roles: { type: array, items: { type: string } } }
+                      nullable: true
+              meta:
+                $ref: '../../main.yaml#/components/schemas/PaginationMeta'
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 1 - 1
openapi/public/assets/protocol/resources/auto/v3-search-search.yaml

@@ -7,7 +7,7 @@ get:
   description: |-
     直接从 OpenSearch 按文档 id 取回,不走检索。取不到返回 404。
 
-    实现:`SearchPlusController@show`
+    实现:`V3\SearchPlusController@show`
   parameters:
     -
       name: search

+ 2 - 2
openapi/public/assets/protocol/resources/auto/v3-search-suggest.yaml

@@ -8,7 +8,7 @@ get:
     按前缀给出补全候选,供搜索框实时提示。每个字段各自返回一组建议。
     q 为空时返回 400。
 
-    实现:`SearchSuggestController@index`
+    实现:`V3\SearchSuggestController@index`
   parameters:
     -
       name: q
@@ -40,7 +40,7 @@ get:
       in: query
       schema:
         type: integer
-        default: '10'
+        default: 10
       description: '每个字段返回的建议数,上限 50'
   responses:
     200:

+ 6 - 6
openapi/public/assets/protocol/resources/auto/v3-search.yaml

@@ -8,7 +8,7 @@ get:
     支持 fuzzy / exact / semantic / hybrid 四种检索模式,底层走 OpenSearch。
     同样的参数也可以用 POST /v3/search 提交(参数多、URL 放不下时用)。
 
-    实现:`SearchPlusController@index`
+    实现:`V3\SearchPlusController@index`
   parameters:
     -
       name: q
@@ -23,14 +23,14 @@ get:
       in: query
       schema:
         type: integer
-        default: '1'
+        default: 1
       description: 页码
     -
       name: page_size
       in: query
       schema:
         type: integer
-        default: '20'
+        default: 20
       description: '每页数量,上限 100'
     -
       name: search_mode
@@ -154,7 +154,7 @@ post:
     与 `GET /v3/search` 完全等价,只是把参数放在请求体里——参数多、
     尤其是 page_refs / related_id 这类数组时,URL 放不下。
 
-    实现:`SearchPlusController@store`
+    实现:`V3\SearchPlusController@store`
   requestBody:
     required: true
     content:
@@ -204,11 +204,11 @@ post:
             page:
               type: integer
               description: 页码
-              default: '1'
+              default: 1
             page_size:
               type: integer
               description: '每页数量,上限 100'
-              default: '20'
+              default: 20
           required:
             - q
   responses:

+ 0 - 85
openapi/public/assets/protocol/resources/auto/v3-tipitaka-read-chapter-tipitaka-read-chapter.yaml

@@ -1,85 +0,0 @@
-# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-tipitaka-read-chapter-tipitaka-read-chapter.yaml
-get:
-  summary: '按 id 读取章节'
-  tags:
-    - tipitaka-read-chapter
-  operationId: get_api_v3_tipitaka_read_chapter_tipitaka_read_chapter_
-  description: |-
-    与 index 等价,只是把 book 与 para 合成一个路径参数;游标 `from` 仍走
-    查询参数。格式不对或 channel 不是 uuid 返回 422。
-
-    实现:`TipitakaReadChapterController@show`
-  parameters:
-    -
-      name: tipitaka_read_chapter
-      in: path
-      required: true
-      schema:
-        type: string
-      description: '章节 id,格式 {book}-{para}'
-      example: 9002-1
-    -
-      name: channel
-      in: query
-      schema:
-        type: string
-      description: '译文 channel 的 uuid'
-      required: true
-    -
-      name: from
-      in: query
-      schema:
-        type: integer
-      description: '起点游标,从这一段开始取;不传则从章节开头取起。 超出章节区间返回 422'
-    -
-      name: format
-      in: query
-      schema:
-        type: string
-        enum:
-          - html
-          - markdown
-          - react
-          - text
-        default: html
-      description: 内容格式
-    -
-      name: view
-      in: query
-      schema:
-        type: string
-        enum:
-          - display
-          - sentences
-          - all
-        default: display
-      description: '输出口径:display 整段合并,sentences 逐句,all 两者都给'
-    -
-      name: pagesize
-      in: query
-      schema:
-        type: integer
-        default: '10'
-      description: '每块大小,含义由 unit 决定。上限 unit=para 时 200、 unit=byte 时 5000,超出不报错按上限算,meta.page_size 回实际 生效的值'
-    -
-      name: unit
-      in: query
-      schema:
-        type: string
-        enum:
-          - para
-          - byte
-        default: para
-      description: '每块大小的单位:para 按段落数,byte 按段落长度累加。 两种单位下每块都至少一段、至多 200 段'
-  responses:
-    200:
-      description: 成功
-      content:
-        application/json:
-          schema:
-            type: object
-            properties:
-              data:
-                type: object
-    422:
-      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 0 - 108
openapi/public/assets/protocol/resources/auto/v3-tipitaka-read-chapter.yaml

@@ -1,108 +0,0 @@
-# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-tipitaka-read-chapter.yaml
-get:
-  summary: 按章节读取译文
-  tags:
-    - tipitaka-read-chapter
-  operationId: get_api_v3_tipitaka_read_chapter
-  description: |-
-    `para` 定位章节(它是章节的起始段落号,章节长度取那一行的 `chapter_len`),
-    `from` 是本次取数的起点游标,从它开始取 `pagesize` 大小的一块。章节不存在、
-    或该 channel 在这个章节里没有任何译文,返回 404。
-    取数的段落集合来自 sentences 表里该 channel 实际有译文的段落,不是章节的
-    全部段落——多数译文是残缺的,按 pali_texts 的整段区间切块会切出空块。
-    所以 `pagesize` 数的永远是有内容的段落。
-    游标式分块是为了前端的下载进度条与断点续传,meta 里给三个数:
-    `total_para` 是分母(该 channel 在本章节有多少段),`remaining_para` 是
-    `last_para` 之后本章节内还剩多少段,进度即
-    `(total_para - remaining_para) / total_para`;续传时把上次的
-    `last_para + 1` 当作下次的 `from` 传回来,`remaining_para` 为 0 即取完。
-    `first_para` / `last_para` 是本块实际覆盖的段落闭区间,取自切块结果而不是
-    `data`:渲染为空的段落会被剔出 `data` 但仍算在块内,所以 `data` 可能比这个
-    区间短,而续传必须按 `last_para` 推进,否则会卡在那一段上原地打转。
-    `first_para` 是游标顺延后的落点,可能大于请求的 `from`(即 `current_para`)。
-
-    实现:`TipitakaReadChapterController@index`
-  parameters:
-    -
-      name: book
-      in: query
-      schema:
-        type: integer
-      description: '典籍 id'
-      required: true
-    -
-      name: para
-      in: query
-      schema:
-        type: integer
-      description: 章节的起始段落号,决定本次取数的段落区间
-      required: true
-    -
-      name: from
-      in: query
-      schema:
-        type: integer
-      description: '起点游标,从这一段开始取;不传则等于 para,即从章节 开头取起。可以是章节区间内的任意段落号,落在没有译文的段落上就 顺延到它之后的第一段。超出章节区间、或它之后已经没有译文, 返回 422'
-    -
-      name: channel
-      in: query
-      schema:
-        type: string
-      description: '译文 channel 的 uuid'
-      required: true
-    -
-      name: format
-      in: query
-      schema:
-        type: string
-        enum:
-          - html
-          - markdown
-          - react
-          - text
-        default: html
-      description: 内容格式
-    -
-      name: view
-      in: query
-      schema:
-        type: string
-        enum:
-          - display
-          - sentences
-          - all
-        default: display
-      description: '输出口径:display 整段合并,sentences 逐句,all 两者都给'
-    -
-      name: pagesize
-      in: query
-      schema:
-        type: integer
-        default: '10'
-      description: '每块大小,含义由 unit 决定。上限 unit=para 时 200、 unit=byte 时 5000,超出不报错按上限算,meta.page_size 回实际 生效的值'
-    -
-      name: unit
-      in: query
-      schema:
-        type: string
-        enum:
-          - para
-          - byte
-        default: para
-      description: '每块大小的单位:para 按段落数,byte 按段落长度累加。 两种单位下每块都至少一段、至多 200 段'
-  responses:
-    200:
-      description: 成功
-      content:
-        application/json:
-          schema:
-            type: object
-            properties:
-              data:
-                type: array
-                items:
-                  type: object
-              meta:
-                $ref: '../../main.yaml#/components/schemas/PaginationMeta'
-    422:
-      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 0 - 62
openapi/public/assets/protocol/resources/auto/v3-tipitaka-read-para-tipitaka-read-para.yaml

@@ -1,62 +0,0 @@
-# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-tipitaka-read-para-tipitaka-read-para.yaml
-get:
-  summary: 读取单个段落
-  tags:
-    - tipitaka-read-para
-  operationId: get_api_v3_tipitaka_read_para_tipitaka_read_para_
-  description: |-
-    id 是 `{book}-{para}` 复合形式,例如 `9001-1`。格式不对或 channel 不是
-    uuid 返回 422;段落无内容返回 404。
-
-    实现:`TipitakaReadParaController@show`
-  parameters:
-    -
-      name: tipitaka_read_para
-      in: path
-      required: true
-      schema:
-        type: string
-      description: '段落 id,格式 {book}-{para}'
-      example: 9001-1
-    -
-      name: channel
-      in: query
-      schema:
-        type: string
-      description: '译文 channel 的 uuid'
-      required: true
-    -
-      name: format
-      in: query
-      schema:
-        type: string
-        enum:
-          - html
-          - markdown
-          - react
-          - text
-        default: html
-      description: 内容格式
-    -
-      name: view
-      in: query
-      schema:
-        type: string
-        enum:
-          - display
-          - sentences
-          - all
-        default: display
-      description: 输出口径
-  responses:
-    200:
-      description: 成功
-      content:
-        application/json:
-          schema:
-            type: object
-            properties:
-              data:
-                type: object
-    422:
-      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 0 - 79
openapi/public/assets/protocol/resources/auto/v3-tipitaka-read-para.yaml

@@ -1,79 +0,0 @@
-# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-tipitaka-read-para.yaml
-get:
-  summary: 按段落区间读取译文
-  tags:
-    - tipitaka-read-para
-  operationId: get_api_v3_tipitaka_read_para
-  description: |-
-    阅读模式下按 book + 段落区间取内容,空段落会被跳过。区间由 para 到 to,
-    不传 to 则只取一段;to 小于 para 返回 422。
-    这个接口不走 Eloquent 分页,meta 是手工给的:一次请求即一页。
-
-    实现:`TipitakaReadParaController@index`
-  parameters:
-    -
-      name: book
-      in: query
-      schema:
-        type: integer
-      description: '典籍 id'
-      required: true
-    -
-      name: para
-      in: query
-      schema:
-        type: integer
-      description: 起始段落号
-      required: true
-    -
-      name: to
-      in: query
-      schema:
-        type: integer
-      description: '结束段落号(含)。不传则等于 para'
-    -
-      name: channel
-      in: query
-      schema:
-        type: string
-      description: '译文 channel 的 uuid'
-      required: true
-    -
-      name: format
-      in: query
-      schema:
-        type: string
-        enum:
-          - html
-          - markdown
-          - react
-          - text
-        default: html
-      description: 内容格式
-    -
-      name: view
-      in: query
-      schema:
-        type: string
-        enum:
-          - display
-          - sentences
-          - all
-        default: display
-      description: '输出口径:display 整段合并,sentences 逐句,all 两者都给'
-  responses:
-    200:
-      description: 成功
-      content:
-        application/json:
-          schema:
-            type: object
-            properties:
-              data:
-                type: array
-                items:
-                  type: object
-              meta:
-                $ref: '../../main.yaml#/components/schemas/PaginationMeta'
-    422:
-      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 142 - 0
openapi/public/assets/protocol/resources/auto/v3-tipitaka-reading-channel.yaml

@@ -0,0 +1,142 @@
+# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-tipitaka-reading-channel.yaml
+get:
+  summary: 读取译文
+  tags:
+    - tipitaka-reading
+  operationId: get_api_v3_tipitaka_reading_channel_
+  description: |-
+    取这个 channel 已经翻译的段落,按 (book, para) 升序,用游标分块。
+    不带任何查询串就是从头取整个 channel——下载场景要的就是这个;
+    阅读场景用 `book` + `chapter` 或 `book` + `para`/`to` 把范围收窄。
+    续传把 `meta.next_cursor` 原样传回 `after` 即可,它为 null 表示取完了。
+    **不要自己拼游标**:它的内部形式(`{book}-{para}`)是实现细节,会变。
+    `total` / `remaining` 只在带了 `book` 过滤时才给:不带过滤时结果集是整个
+    channel(实测最大的有 52 万段、跨 217 本书),每翻一页都去 count 一次不划算。
+    无过滤时用 `next_cursor` 是否为 null 判断结束,不要指望百分比。
+
+    实现:`V3\TipitakaReadingController@__invoke`
+  parameters:
+    -
+      name: channel
+      in: path
+      required: true
+      schema:
+        type: string
+      description: '译文 channel 的 uuid'
+    -
+      name: book
+      in: query
+      schema:
+        type: integer
+      description: '典籍 id。与 chapter / para / to 一起用时必填'
+      example: 9002
+    -
+      name: chapter
+      in: query
+      schema:
+        type: integer
+      description: '章节起始段落号,服务端按 chapter_len 展开成段落区间。 与 para / to 互斥'
+    -
+      name: para
+      in: query
+      schema:
+        type: integer
+      description: 起始段落号
+    -
+      name: to
+      in: query
+      schema:
+        type: integer
+      description: '结束段落号(含)。不传则等于 para,即只取一段'
+    -
+      name: after
+      in: query
+      schema:
+        type: string
+      description: '游标,取 meta.next_cursor 原样回传'
+      example: 9002-15
+    -
+      name: page_size
+      in: query
+      schema:
+        type: integer
+        default: 10
+      description: '每块大小,含义由 unit 决定。上限 unit=para 时 200、 unit=byte 时 5000,超出不报错按上限算,meta.page_size 回实际生效的值'
+    -
+      name: unit
+      in: query
+      schema:
+        type: string
+        enum:
+          - para
+          - byte
+        default: para
+      description: '每块大小的单位:para 按段落数,byte 按段落长度累加。 两种单位下每块都至少一段、至多 200 段'
+    -
+      name: format
+      in: query
+      schema:
+        type: string
+        enum:
+          - html
+          - markdown
+          - react
+          - text
+        default: html
+      description: 内容格式
+    -
+      name: include
+      in: query
+      schema:
+        type: string
+        enum:
+          - display
+          - sentences
+          - all
+        default: display
+      description: '输出口径:display 整段合并,sentences 逐句,all 两者都给。 不叫 view 是因为 v2 的 `view=` 是必填互斥的业务路径开关,名字撞上会误导'
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    para:
+                      type: integer
+                    display:
+                      type: string
+                    book:
+                      type: integer
+              meta:
+                type: object
+                properties:
+                  page_size:
+                    type: integer
+                    description: '每块大小(按 unit 计)'
+                  page_size_unit:
+                    type: string
+                    description: '每块大小的单位:para 或 byte'
+                  next_cursor:
+                    type: string
+                    description: '下一块的游标,原样传回 after;为 null 表示取完'
+                  total:
+                    type: integer
+                    description: '过滤范围内已翻译的段落总数。仅在带了 book 过滤时出现'
+                  remaining:
+                    type: integer
+                    description: '本块之后过滤范围内还剩多少段。仅在带了 book 过滤时出现'
+    404:
+      description: '指定的 chapter 在 pali_texts 里不存在'
+      content:
+        application/problem+json:
+          schema:
+            $ref: '../../main.yaml#/components/schemas/ProblemDetails'
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 1 - 1
openapi/public/assets/protocol/resources/auto/v3-upgrade.yaml

@@ -7,7 +7,7 @@ get:
   description: |-
     目前只回报服务可用,尚未接入版本比对逻辑。
 
-    实现:`UpgradeController@index`
+    实现:`V3\UpgradeController@index`
   responses:
     200:
       description: 成功