Bläddra i källkod

feat(openapi): 生成器识别 v3 契约

v3 与 v2 的信封不同,生成器原本把 v2 的 {ok, message, data} 硬编码给了所有路由,
v3 端点的规格因此是错的。

- v3 路由单独分支:成功 {data} / {data + meta},错误引用 ProblemDetails
- 新增 @unauthenticated 与 @responseStatus 标签:前者去掉不该有的 401,
  后者声明额外状态码(如停机的 503)
- v2 的通用列表参数(search/order/dir/limit/offset)不再注入 v3——v3 用
  page/per_page,且参数一律要在文档注释里写明,不做兜底
- 数组参数的 Example 按 JSON 解析,否则与 schema 的 type 对不上(redocly 会警告)
- components 补 ProblemDetails / PaginationMeta,后者刻意不含 links/path

283 path / 760 operation,redocly lint 0 error。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
visuddhinanda 3 veckor sedan
förälder
incheckning
263bc2df3e

+ 71 - 6
openapi/public/assets/protocol/main.yaml

@@ -37,6 +37,18 @@ components:
         application/json:
           schema:
             $ref: '#/components/schemas/Envelope'
+    ProblemUnauthorized:
+      description: 未登录或无权限(v3)
+      content:
+        application/problem+json:
+          schema:
+            $ref: '#/components/schemas/ProblemDetails'
+    ProblemValidation:
+      description: 参数校验失败(v3)
+      content:
+        application/problem+json:
+          schema:
+            $ref: '#/components/schemas/ProblemDetails'
     ValidationError:
       description: 参数校验失败
       content:
@@ -47,6 +59,63 @@ components:
               message: { type: string }
               errors: { type: object, additionalProperties: { type: array, items: { type: string } } }
   schemas:
+    ProblemDetails:
+      type: object
+      description: |-
+        RFC 9457 Problem Details,v3 端点的错误响应体。
+        由 bootstrap/app.php 的异常处理器统一渲染,控制器不自己拼。
+        media type 是 application/problem+json。
+      properties:
+        type:
+          type: string
+          description: '问题类型标识,形如 urn:problem:not-found'
+          example: 'urn:problem:not-found'
+        title:
+          type: string
+          description: 稳定的英文摘要,同类问题恒定,供机器识别
+        status:
+          type: integer
+        detail:
+          type: string
+          description: '面向人的文案,已本地化;5xx 在生产环境会省略以免泄露内部信息'
+        instance:
+          type: string
+          description: '出问题的请求 URI'
+        errors:
+          type: object
+          description: '字段级校验错误,仅 422 时出现'
+          additionalProperties:
+            type: array
+            items:
+              type: string
+      required:
+        - type
+        - title
+        - status
+    PaginationMeta:
+      type: object
+      description: |-
+        v3 列表接口的分页信息,由 Laravel paginator 生成。
+        刻意不含 links / path:那些是 APP_URL 拼出的绝对地址,反代下会拼错,前端也用不到。
+        手工分页的接口会附加自己的字段(如 has_more / first_para / page_size)。
+      properties:
+        current_page:
+          type: integer
+        per_page:
+          type: integer
+        total:
+          type: integer
+        last_page:
+          type: integer
+        from:
+          type: integer
+          nullable: true
+        to:
+          type: integer
+          nullable: true
+      required:
+        - current_page
+        - total
     Envelope:
       type: object
       properties:
@@ -606,16 +675,14 @@ paths:
     $ref: ./resources/auto/v2-webhook.yaml
   '/v2/webhook/{webhook}':
     $ref: ./resources/auto/v2-webhook-webhook.yaml
+  /v3/heartbeat:
+    $ref: ./resources/auto/v3-heartbeat.yaml
   /v3/progress:
     $ref: ./resources/auto/v3-progress.yaml
-  '/v3/progress/{progress}':
-    $ref: ./resources/auto/v3-progress-progress.yaml
   /v3/search:
     $ref: ./resources/auto/v3-search.yaml
   /v3/search-suggest:
     $ref: ./resources/auto/v3-search-suggest.yaml
-  '/v3/search-suggest/{search_suggest}':
-    $ref: ./resources/auto/v3-search-suggest-search-suggest.yaml
   '/v3/search/{search}':
     $ref: ./resources/auto/v3-search-search.yaml
   /v3/tipitaka-read-chapter:
@@ -628,5 +695,3 @@ paths:
     $ref: ./resources/auto/v3-tipitaka-read-para-tipitaka-read-para.yaml
   /v3/upgrade:
     $ref: ./resources/auto/v3-upgrade.yaml
-  '/v3/upgrade/{upgrade}':
-    $ref: ./resources/auto/v3-upgrade-upgrade.yaml

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

@@ -213,7 +213,9 @@ delete:
       schema:
         type: string
       description: '要批量删除的词条 id 数组的 JSON 字符串'
-      example: '["123","456"]'
+      example:
+        - '123'
+        - '456'
   responses:
     200:
       description: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'

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

@@ -18,7 +18,9 @@ delete:
         type: string
       description: '要删除的词条 id 数组的 JSON 字符串'
       required: true
-      example: '["123","456"]'
+      example:
+        - '123'
+        - '456'
   responses:
     200:
       description: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'

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

@@ -0,0 +1,39 @@
+# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-heartbeat.yaml
+get:
+  summary: 心跳
+  tags:
+    - heartbeat
+  operationId: get_api_v3_heartbeat
+  description: |-
+    取代 `GET /v2/heartbeat`(`HeartbeatController@index`)。v2 那份在
+    dashboard-v4 下线前原样保留,不要改动。
+    **这不是健康检查。** 它不查数据库、不查 OpenSearch / RabbitMQ / Cache,
+    只回答一件事:进程还在服务吗、是不是被运维停机了。供前端与负载均衡做
+    存活探测(liveness)用,开销接近零,可以高频调用。
+    真正的健康检查是 `GET /v2/health-check`(会逐项探测外部依赖并返回 checks
+    明细),将来单独迁为 `/v3/health-check`,与本端点是两件事,不要混用。
+    仓库根目录存在 `.stop` 文件时返回 503(运维停机开关),此时响应体是
+    RFC 9457 Problem Details 而非正常结构。
+
+    实现:`HeartbeatV3Controller@show`
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: object
+                properties:
+                  status:
+                    type: string
+                  checked_at:
+                    type: string
+    503:
+      description: 服务已进入停机维护状态
+      content:
+        application/problem+json:
+          schema:
+            $ref: '../../main.yaml#/components/schemas/ProblemDetails'

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

@@ -1,125 +0,0 @@
-# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-progress-progress.yaml
-get:
-  summary: '获取单个 progress/{progress}'
-  tags:
-    - progress
-  operationId: get_api_v3_progress_progress_
-  description: 实现:`ProgressController@show`
-  parameters:
-    -
-      name: progress
-      in: path
-      required: true
-      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'
-put:
-  summary: '更新 progress/{progress}'
-  tags:
-    - progress
-  operationId: put_api_v3_progress_progress_
-  description: 实现:`ProgressController@update`
-  parameters:
-    -
-      name: progress
-      in: path
-      required: true
-      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'
-patch:
-  summary: '更新 progress/{progress}'
-  tags:
-    - progress
-  operationId: patch_api_v3_progress_progress_
-  description: 实现:`ProgressController@update`
-  parameters:
-    -
-      name: progress
-      in: path
-      required: true
-      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'
-delete:
-  summary: '删除 progress/{progress}'
-  tags:
-    - progress
-  operationId: delete_api_v3_progress_progress_
-  description: 实现:`ProgressController@destroy`
-  parameters:
-    -
-      name: progress
-      in: path
-      required: true
-      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'

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

@@ -1,10 +1,14 @@
 # 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-progress.yaml
 get:
-  summary: '列出 progress'
+  summary: 列出章节翻译进度
   tags:
     - progress
   operationId: get_api_v3_progress
-  description: 实现:`ProgressController@index`
+  description: |-
+    按 channel 查询各章节的翻译完成度。view 目前只支持 channel 一种口径,
+    传其它值返回 422。
+
+    实现:`ProgressController@index`
   parameters:
     -
       name: view
@@ -13,101 +17,101 @@ get:
         type: string
         enum:
           - channel
+      description: '查询口径,目前只有 channel'
+      required: true
     -
       name: channels
       in: query
       schema:
         type: string
         default: ''
+      description: 'channel uid 列表,**下划线分隔**(不是逗号)'
+      required: true
     -
       name: level
       in: query
       schema:
-        type: string
+        type: integer
+      description: '只返回该层级及以上的章节,需联查 pali_texts'
     -
       name: lang
       in: query
       schema:
         type: string
+      description: 按译文语言过滤
+      example: zh-Hans
     -
       name: book
       in: query
       schema:
-        type: string
+        type: integer
+      description: '按典籍 id 过滤'
     -
       name: order
       in: query
       schema:
         type: string
         default: completed_at
+      description: '排序字段(progress_chapters 的列)'
     -
       name: dir
       in: query
       schema:
         type: string
+        enum:
+          - asc
+          - desc
         default: desc
+      description: 排序方向
     -
-      name: offset
-      in: query
-      schema:
-        type: string
-    -
-      name: limit
+      name: per_page
       in: query
       schema:
-        type: string
+        type: integer
+        default: '10'
+      description: 每页数量
     -
-      name: search
+      name: page
       in: query
-      description: 搜索关键字
       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
-                properties:
-                  rows:
-                    type: array
-                    items:
-                      type: object
-                  count:
-                    type: integer
-    401:
-      $ref: '../../main.yaml#/components/responses/Unauthorized'
-    422:
-      $ref: '../../main.yaml#/components/responses/ValidationError'
-post:
-  summary: '新建 progress'
-  tags:
-    - progress
-  operationId: post_api_v3_progress
-  description: 实现:`ProgressController@store`
+        type: integer
+        default: '1'
+      description: 页码
   responses:
     200:
-      description: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'
+      description: 成功
       content:
         application/json:
           schema:
             type: object
             properties:
-              ok:
-                type: boolean
-              message:
-                type: string
               data:
-                type: object
-    401:
-      $ref: '../../main.yaml#/components/responses/Unauthorized'
+                type: array
+                items:
+                  type: object
+                  properties:
+                    book:
+                      type: integer
+                    para:
+                      type: integer
+                    lang:
+                      type: string
+                    progress:
+                      type: number
+                    channel_id:
+                      type: string
+                    title:
+                      type: string
+                      nullable: true
+                    last_chapter_completed_at:
+                      type: string
+                      nullable: true
+                    completed_at:
+                      type: string
+                      nullable: true
+                    updated_at:
+                      type: string
+              meta:
+                $ref: '../../main.yaml#/components/schemas/PaginationMeta'
     422:
-      $ref: '../../main.yaml#/components/responses/ValidationError'
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

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

@@ -1,10 +1,13 @@
 # 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-search-search.yaml
 get:
-  summary: '获取单个 search/{search}'
+  summary: '按 id 取单条检索结果'
   tags:
     - search
   operationId: get_api_v3_search_search_
-  description: 实现:`SearchPlusController@show`
+  description: |-
+    直接从 OpenSearch 按文档 id 取回,不走检索。取不到返回 404。
+
+    实现:`SearchPlusController@show`
   parameters:
     -
       name: search
@@ -12,114 +15,16 @@ get:
       required: true
       schema:
         type: string
+      description: 'OpenSearch 文档 id'
   responses:
     200:
-      description: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'
+      description: 成功
       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'
-put:
-  summary: 更新资源
-  tags:
-    - search
-  operationId: put_api_v3_search_search_
-  description: 实现:`SearchPlusController@update`
-  parameters:
-    -
-      name: search
-      in: path
-      required: true
-      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'
-patch:
-  summary: 更新资源
-  tags:
-    - search
-  operationId: patch_api_v3_search_search_
-  description: 实现:`SearchPlusController@update`
-  parameters:
-    -
-      name: search
-      in: path
-      required: true
-      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'
-delete:
-  summary: 删除资源
-  tags:
-    - search
-  operationId: delete_api_v3_search_search_
-  description: 实现:`SearchPlusController@destroy`
-  parameters:
-    -
-      name: search
-      in: path
-      required: true
-      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'
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

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

@@ -1,125 +0,0 @@
-# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-search-suggest-search-suggest.yaml
-get:
-  summary: '获取单个 search-suggest/{search_suggest}'
-  tags:
-    - search-suggest
-  operationId: get_api_v3_search_suggest_search_suggest_
-  description: 实现:`SearchSuggestController@show`
-  parameters:
-    -
-      name: search_suggest
-      in: path
-      required: true
-      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'
-put:
-  summary: '更新 search-suggest/{search_suggest}'
-  tags:
-    - search-suggest
-  operationId: put_api_v3_search_suggest_search_suggest_
-  description: 实现:`SearchSuggestController@update`
-  parameters:
-    -
-      name: search_suggest
-      in: path
-      required: true
-      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'
-patch:
-  summary: '更新 search-suggest/{search_suggest}'
-  tags:
-    - search-suggest
-  operationId: patch_api_v3_search_suggest_search_suggest_
-  description: 实现:`SearchSuggestController@update`
-  parameters:
-    -
-      name: search_suggest
-      in: path
-      required: true
-      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'
-delete:
-  summary: '删除 search-suggest/{search_suggest}'
-  tags:
-    - search-suggest
-  operationId: delete_api_v3_search_suggest_search_suggest_
-  description: 实现:`SearchSuggestController@destroy`
-  parameters:
-    -
-      name: search_suggest
-      in: path
-      required: true
-      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'

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

@@ -1,18 +1,12 @@
 # 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-search-suggest.yaml
 get:
-  summary: 自动建议接口
+  summary: 搜索建议(自动补全)
   tags:
     - search-suggest
   operationId: get_api_v3_search_suggest
   description: |-
-    基于 OpenSearch completion suggester,支持从不同字段获取建议。
-    - q (string): 输入的部分文本(必填)
-    - fields (string|array): 要查询的字段,可选值:
-    - 不传:查询所有字段 (title, content, page_refs)
-    - 单个字段:'title' | 'content' | 'page_refs'
-    - 多个字段:'title,content' 或 ['title', 'content']
-    - language (string): 语言过滤,可选(如:pali, zh, en)
-    - limit (int): 每个字段返回的建议数量,默认 10,最大 50
+    按前缀给出补全候选,供搜索框实时提示。每个字段各自返回一组建议。
+    q 为空时返回 400。
 
     实现:`SearchSuggestController@index`
   parameters:
@@ -22,94 +16,45 @@ get:
       schema:
         type: string
         default: ''
+      description: 已输入的查询文本
+      required: true
     -
       name: fields
       in: query
       schema:
         type: string
+        enum:
+          - title
+          - content
+          - page_refs
+      description: 在哪些字段上补全,逗号分隔;不传则全查
     -
       name: language
       in: query
       schema:
         type: string
+      description: 语言过滤
+      example: pali
     -
       name: limit
       in: query
       schema:
-        type: string
-    -
-      name: search
-      in: query
-      description: 搜索关键字
-      schema:
-        type: string
-    -
-      name: order
-      in: query
-      description: 排序字段
-      schema:
-        type: string
-    -
-      name: dir
-      in: query
-      description: 排序方向
-      schema:
-        type: string
-        enum:
-          - desc
-          - asc
-    -
-      name: offset
-      in: query
-      description: 从第几条记录开始提取
-      schema:
-        type: number
-  responses:
-    200:
-      description: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'
-      content:
-        application/json:
-          schema:
-            type: object
-            properties:
-              ok:
-                type: boolean
-              message:
-                type: string
-              data:
-                type: object
-                properties:
-                  rows:
-                    type: array
-                    items:
-                      type: object
-                  count:
-                    type: integer
-    401:
-      $ref: '../../main.yaml#/components/responses/Unauthorized'
-    422:
-      $ref: '../../main.yaml#/components/responses/ValidationError'
-post:
-  summary: '新建 search-suggest'
-  tags:
-    - search-suggest
-  operationId: post_api_v3_search_suggest
-  description: 实现:`SearchSuggestController@store`
+        type: integer
+        default: '10'
+      description: '每个字段返回的建议数,上限 50'
   responses:
     200:
-      description: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'
+      description: 成功
       content:
         application/json:
           schema:
             type: object
             properties:
-              ok:
-                type: boolean
-              message:
-                type: string
               data:
-                type: object
-    401:
-      $ref: '../../main.yaml#/components/responses/Unauthorized'
+                type: array
+                items:
+                  type: object
+              meta:
+                $ref: '../../main.yaml#/components/schemas/PaginationMeta'
     422:
-      $ref: '../../main.yaml#/components/responses/ValidationError'
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

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

@@ -1,25 +1,12 @@
 # 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-search.yaml
 get:
-  summary: '处理搜索请求,支持 fuzzy / exact / semantic / hybrid 四种模式。'
+  summary: 全文检索
   tags:
     - search
   operationId: get_api_v3_search
   description: |-
-    接收查询参数并调用 OpenSearchService 执行搜索。
-    支持 GET 和 POST 请求
-    - q (string): 搜索关键词
-    - resource_type (string): 资源类型 (article|term|dictionary|translation|origin_text|nissaya)
-    - granularity (string): 文档颗粒度 (book|chapter|sutta|section|paragraph|sentence)
-    - language (string): 语言,如 pali, zh-Hans, zh-Hant, en-US, my
-    - category (string): 文档分类 (pali|commentary|subcommentary)
-    - tags (array): 标签过滤
-    - page_refs (array): 页码标记 ["V3.81","M3.58"]
-    - related_id (array): 关联 ID,如 ["chapter_93-5","m.n. 38"]
-    - author (string): 作者或译者 (metadata.author)
-    - channel (string): 来源渠道 (metadata.channel)
-    - page (int): 页码,默认 1
-    - page_size (int): 每页数量,默认 20,最大 100
-    - search_mode (string): fuzzy|exact|semantic|hybrid,默认 fuzzy
+    支持 fuzzy / exact / semantic / hybrid 四种检索模式,底层走 OpenSearch。
+    同样的参数也可以用 POST /v3/search 提交(参数多、URL 放不下时用)。
 
     实现:`SearchPlusController@index`
   parameters:
@@ -29,151 +16,210 @@ get:
       schema:
         type: string
         default: ''
+      description: 搜索关键词
+      required: true
     -
       name: page
       in: query
       schema:
-        type: string
+        type: integer
+        default: '1'
+      description: 页码
     -
       name: page_size
       in: query
       schema:
-        type: string
+        type: integer
+        default: '20'
+      description: '每页数量,上限 100'
     -
       name: search_mode
       in: query
       schema:
         type: string
+        enum:
+          - fuzzy
+          - exact
+          - semantic
+          - hybrid
         default: fuzzy
+      description: 检索模式
     -
       name: resource_type
       in: query
       schema:
         type: string
+        enum:
+          - article
+          - term
+          - dictionary
+          - translation
+          - origin_text
+          - nissaya
+      description: 资源类型
     -
       name: resource_id
       in: query
       schema:
         type: string
+      description: '限定某个资源 id'
     -
       name: granularity
       in: query
       schema:
         type: string
+        enum:
+          - book
+          - chapter
+          - sutta
+          - section
+          - paragraph
+          - sentence
+      description: 文档颗粒度
     -
       name: language
       in: query
       schema:
         type: string
+      description: 语言
+      example: zh-Hans
     -
       name: category
       in: query
       schema:
         type: string
+        enum:
+          - pali
+          - commentary
+          - subcommentary
+      description: 文档分类,逗号分隔
     -
       name: tags
       in: query
       schema:
         type: string
+      description: 标签过滤,逗号分隔
     -
       name: page_refs
       in: query
       schema:
-        type: string
+        type: array
+      description: 页码标记
+      example:
+        - V3.81
+        - M3.58
     -
       name: related_id
       in: query
       schema:
-        type: string
+        type: array
+      description: '关联 id'
+      example:
+        - chapter_93-5
+        - 'm.n. 38'
     -
       name: author
       in: query
       schema:
         type: string
+      description: 按作者或译者过滤
     -
       name: channel
       in: query
       schema:
         type: string
-    -
-      name: search
-      in: query
-      description: 搜索关键字
-      schema:
-        type: string
-    -
-      name: order
-      in: query
-      description: 排序字段
-      schema:
-        type: string
-    -
-      name: dir
-      in: query
-      description: 排序方向
-      schema:
-        type: string
-        enum:
-          - desc
-          - asc
-    -
-      name: limit
-      in: query
-      description: 每页记录数
-      schema:
-        type: number
-    -
-      name: offset
-      in: query
-      description: 从第几条记录开始提取
-      schema:
-        type: number
+      description: '按来源 channel 过滤'
   responses:
     200:
-      description: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'
+      description: 成功
       content:
         application/json:
           schema:
             type: object
             properties:
-              ok:
-                type: boolean
-              message:
-                type: string
               data:
-                type: object
-                properties:
-                  rows:
-                    type: array
-                    items:
-                      type: object
-                  count:
-                    type: integer
-    401:
-      $ref: '../../main.yaml#/components/responses/Unauthorized'
+                type: array
+                items:
+                  type: object
+              meta:
+                $ref: '../../main.yaml#/components/schemas/PaginationMeta'
     422:
-      $ref: '../../main.yaml#/components/responses/ValidationError'
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'
 post:
-  summary: 'POST 方式调用搜索接口(与 index 方法功能相同)'
+  summary: 全文检索(POST)
   tags:
     - search
   operationId: post_api_v3_search
-  description: 实现:`SearchPlusController@store`
+  description: |-
+    与 `GET /v3/search` 完全等价,只是把参数放在请求体里——参数多、
+    尤其是 page_refs / related_id 这类数组时,URL 放不下。
+
+    实现:`SearchPlusController@store`
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            q:
+              type: string
+              description: 搜索关键词
+            search_mode:
+              type: string
+              description: 检索模式
+              enum:
+                - fuzzy
+                - exact
+                - semantic
+                - hybrid
+              default: fuzzy
+            resource_type:
+              type: string
+              description: 资源类型
+            granularity:
+              type: string
+              description: 文档颗粒度
+            language:
+              type: string
+              description: 语言
+            category:
+              type: string
+              description: 文档分类,逗号分隔
+            tags:
+              type: string
+              description: 标签,逗号分隔
+            page_refs:
+              type: array
+              description: 页码标记
+            related_id:
+              type: array
+              description: '关联 id'
+            author:
+              type: string
+              description: 作者或译者
+            channel:
+              type: string
+              description: '来源 channel'
+            page:
+              type: integer
+              description: 页码
+              default: '1'
+            page_size:
+              type: integer
+              description: '每页数量,上限 100'
+              default: '20'
+          required:
+            - q
   responses:
     200:
-      description: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'
+      description: 成功
       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'
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 49 - 10
openapi/public/assets/protocol/resources/auto/v3-tipitaka-read-chapter-tipitaka-read-chapter.yaml

@@ -1,10 +1,14 @@
 # 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-tipitaka-read-chapter-tipitaka-read-chapter.yaml
 get:
-  summary: '同 index,id 格式 {book}-{para}'
+  summary: '按 id 读取章节'
   tags:
     - tipitaka-read-chapter
   operationId: get_api_v3_tipitaka_read_chapter_tipitaka_read_chapter_
-  description: 实现:`TipitakaReadChapterController@show`
+  description: |-
+    与 index 等价,只是把 book 与 para 合成一个路径参数。格式不对或 channel
+    不是 uuid 返回 422。
+
+    实现:`TipitakaReadChapterController@show`
   parameters:
     -
       name: tipitaka_read_chapter
@@ -12,26 +16,61 @@ get:
       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: 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: string
+        default: 20000b
+      description: '每页大小,两种写法:`20000b` 按字节累加段落长度 (每页至少一段),`10p` 按段落数'
+    -
+      name: page
+      in: query
+      schema:
+        type: integer
+        default: '1'
+      description: '页码,从 1 开始。超出范围返回 422'
   responses:
     200:
-      description: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'
+      description: 成功
       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'
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 52 - 34
openapi/public/assets/protocol/resources/auto/v3-tipitaka-read-chapter.yaml

@@ -1,71 +1,89 @@
 # 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-tipitaka-read-chapter.yaml
 get:
-  summary: '阅读模式章节内容。输入章节起始 book/para,按 pagesize 分批返回段落。'
+  summary: 按章节读取译文
   tags:
     - tipitaka-read-chapter
   operationId: get_api_v3_tipitaka_read_chapter
   description: |-
-    pagesize 两种写法:
-    - `20000b` 按字节,累加 pali_texts.lenght 直到超过上限(每页至少一段)
-    - `10p`    按段落数
+    给定 book + 段落号,定位它所属的章节,返回该章节的段落内容并分页。
+    章节不存在或没有内容返回 404。
+    分页是手工做的:按字节时从头累加 `pali_texts.lenght` 逐页推进,所以 meta
+    里用 `page_size`(领域语法,非整数)而不是框架的 `per_page`,并附带
+    `first_para` / `last_para` / `has_more`。
 
     实现:`TipitakaReadChapterController@index`
   parameters:
     -
-      name: search
+      name: book
+      in: query
+      schema:
+        type: integer
+      description: '典籍 id'
+      required: true
+    -
+      name: para
+      in: query
+      schema:
+        type: integer
+      description: 章节内任一段落号,用于定位章节
+      required: true
+    -
+      name: channel
       in: query
-      description: 搜索关键字
       schema:
         type: string
+      description: '译文 channel 的 uuid'
+      required: true
     -
-      name: order
+      name: format
       in: query
-      description: 排序字段
       schema:
         type: string
+        enum:
+          - html
+          - markdown
+          - react
+          - text
+        default: html
+      description: 内容格式
     -
-      name: dir
+      name: view
       in: query
-      description: 排序方向
       schema:
         type: string
         enum:
-          - desc
-          - asc
+          - display
+          - sentences
+          - all
+        default: display
+      description: '输出口径:display 整段合并,sentences 逐句,all 两者都给'
     -
-      name: limit
+      name: pagesize
       in: query
-      description: 每页记录数
       schema:
-        type: number
+        type: string
+        default: 20000b
+      description: '每页大小,两种写法:`20000b` 按字节累加段落长度 (每页至少一段),`10p` 按段落数'
     -
-      name: offset
+      name: page
       in: query
-      description: 从第几条记录开始提取
       schema:
-        type: number
+        type: integer
+        default: '1'
+      description: '页码,从 1 开始。超出范围返回 422'
   responses:
     200:
-      description: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'
+      description: 成功
       content:
         application/json:
           schema:
             type: object
             properties:
-              ok:
-                type: boolean
-              message:
-                type: string
               data:
-                type: object
-                properties:
-                  rows:
-                    type: array
-                    items:
-                      type: object
-                  count:
-                    type: integer
-    401:
-      $ref: '../../main.yaml#/components/responses/Unauthorized'
+                type: array
+                items:
+                  type: object
+              meta:
+                $ref: '../../main.yaml#/components/schemas/PaginationMeta'
     422:
-      $ref: '../../main.yaml#/components/responses/ValidationError'
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 23 - 10
openapi/public/assets/protocol/resources/auto/v3-tipitaka-read-para-tipitaka-read-para.yaml

@@ -1,10 +1,14 @@
 # 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-tipitaka-read-para-tipitaka-read-para.yaml
 get:
-  summary: '单个段落内容。id 格式 {book}-{para}'
+  summary: 读取单个段落
   tags:
     - tipitaka-read-para
   operationId: get_api_v3_tipitaka_read_para_tipitaka_read_para_
-  description: 实现:`TipitakaReadParaController@show`
+  description: |-
+    id 是 `{book}-{para}` 复合形式,例如 `9001-1`。格式不对或 channel 不是
+    uuid 返回 422;段落无内容返回 404。
+
+    实现:`TipitakaReadParaController@show`
   parameters:
     -
       name: tipitaka_read_para
@@ -12,38 +16,47 @@ get:
       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: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'
+      description: 成功
       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'
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 48 - 35
openapi/public/assets/protocol/resources/auto/v3-tipitaka-read-para.yaml

@@ -1,66 +1,79 @@
 # 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-tipitaka-read-para.yaml
 get:
-  summary: '阅读模式段落内容列表。指定 book 段落区间和 channel'
+  summary: 按段落区间读取译文
   tags:
     - tipitaka-read-para
   operationId: get_api_v3_tipitaka_read_para
-  description: 实现:`TipitakaReadParaController@index`
+  description: |-
+    阅读模式下按 book + 段落区间取内容,空段落会被跳过。区间由 para 到 to,
+    不传 to 则只取一段;to 小于 para 返回 422。
+    这个接口不走 Eloquent 分页,meta 是手工给的:一次请求即一页。
+
+    实现:`TipitakaReadParaController@index`
   parameters:
     -
-      name: search
+      name: book
       in: query
-      description: 搜索关键字
       schema:
-        type: string
+        type: integer
+      description: '典籍 id'
+      required: true
     -
-      name: order
+      name: para
       in: query
-      description: 排序字段
       schema:
-        type: string
+        type: integer
+      description: 起始段落号
+      required: true
+    -
+      name: to
+      in: query
+      schema:
+        type: integer
+      description: '结束段落号(含)。不传则等于 para'
     -
-      name: dir
+      name: channel
       in: query
-      description: 排序方向
       schema:
         type: string
-        enum:
-          - desc
-          - asc
+      description: '译文 channel 的 uuid'
+      required: true
     -
-      name: limit
+      name: format
       in: query
-      description: 每页记录数
       schema:
-        type: number
+        type: string
+        enum:
+          - html
+          - markdown
+          - react
+          - text
+        default: html
+      description: 内容格式
     -
-      name: offset
+      name: view
       in: query
-      description: 从第几条记录开始提取
       schema:
-        type: number
+        type: string
+        enum:
+          - display
+          - sentences
+          - all
+        default: display
+      description: '输出口径:display 整段合并,sentences 逐句,all 两者都给'
   responses:
     200:
-      description: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'
+      description: 成功
       content:
         application/json:
           schema:
             type: object
             properties:
-              ok:
-                type: boolean
-              message:
-                type: string
               data:
-                type: object
-                properties:
-                  rows:
-                    type: array
-                    items:
-                      type: object
-                  count:
-                    type: integer
-    401:
-      $ref: '../../main.yaml#/components/responses/Unauthorized'
+                type: array
+                items:
+                  type: object
+              meta:
+                $ref: '../../main.yaml#/components/schemas/PaginationMeta'
     422:
-      $ref: '../../main.yaml#/components/responses/ValidationError'
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

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

@@ -1,125 +0,0 @@
-# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-upgrade-upgrade.yaml
-get:
-  summary: '获取单个 upgrade/{upgrade}'
-  tags:
-    - upgrade
-  operationId: get_api_v3_upgrade_upgrade_
-  description: 实现:`UpgradeController@show`
-  parameters:
-    -
-      name: upgrade
-      in: path
-      required: true
-      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'
-put:
-  summary: '更新 upgrade/{upgrade}'
-  tags:
-    - upgrade
-  operationId: put_api_v3_upgrade_upgrade_
-  description: 实现:`UpgradeController@update`
-  parameters:
-    -
-      name: upgrade
-      in: path
-      required: true
-      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'
-patch:
-  summary: '更新 upgrade/{upgrade}'
-  tags:
-    - upgrade
-  operationId: patch_api_v3_upgrade_upgrade_
-  description: 实现:`UpgradeController@update`
-  parameters:
-    -
-      name: upgrade
-      in: path
-      required: true
-      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'
-delete:
-  summary: '删除 upgrade/{upgrade}'
-  tags:
-    - upgrade
-  operationId: delete_api_v3_upgrade_upgrade_
-  description: 实现:`UpgradeController@destroy`
-  parameters:
-    -
-      name: upgrade
-      in: path
-      required: true
-      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'

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

@@ -1,90 +1,24 @@
 # 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-upgrade.yaml
 get:
-  summary: '列出 upgrade'
+  summary: 客户端升级检查
   tags:
     - upgrade
   operationId: get_api_v3_upgrade
-  description: 实现:`UpgradeController@index`
-  parameters:
-    -
-      name: search
-      in: query
-      description: 搜索关键字
-      schema:
-        type: string
-    -
-      name: order
-      in: query
-      description: 排序字段
-      schema:
-        type: string
-    -
-      name: dir
-      in: query
-      description: 排序方向
-      schema:
-        type: string
-        enum:
-          - desc
-          - asc
-    -
-      name: limit
-      in: query
-      description: 每页记录数
-      schema:
-        type: number
-    -
-      name: offset
-      in: query
-      description: 从第几条记录开始提取
-      schema:
-        type: number
+  description: |-
+    目前只回报服务可用,尚未接入版本比对逻辑。
+
+    实现:`UpgradeController@index`
   responses:
     200:
-      description: '成功。业务失败时 HTTP 仍可能为 200,以 ok 字段为准。'
+      description: 成功
       content:
         application/json:
           schema:
             type: object
             properties:
-              ok:
-                type: boolean
-              message:
-                type: string
               data:
-                type: object
-                properties:
-                  rows:
-                    type: array
-                    items:
-                      type: object
-                  count:
-                    type: integer
-    401:
-      $ref: '../../main.yaml#/components/responses/Unauthorized'
-    422:
-      $ref: '../../main.yaml#/components/responses/ValidationError'
-post:
-  summary: '新建 upgrade'
-  tags:
-    - upgrade
-  operationId: post_api_v3_upgrade
-  description: 实现:`UpgradeController@store`
-  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'
+                type: array
+                items:
+                  type: object
+              meta:
+                $ref: '../../main.yaml#/components/schemas/PaginationMeta'

+ 123 - 5
openapi/scripts/generate.php

@@ -52,7 +52,7 @@ function parseController(string $class, string $method): array
     if (isset($methodCache[$key])) {
         return $methodCache[$key];
     }
-    $empty = ['summary' => null, 'description' => null, 'params' => [], 'validate' => [], 'resource' => null, 'tags' => ['params' => [], 'deprecated' => false]];
+    $empty = ['summary' => null, 'description' => null, 'params' => [], 'validate' => [], 'resource' => null, 'tags' => ['params' => [], 'deprecated' => false, 'unauthenticated' => false, 'responses' => []]];
 
     $file = $api.'/'.str_replace(['App\\', '\\'], ['app/', '/'], $class).'.php';
     if (! is_file($file)) {
@@ -229,13 +229,24 @@ function parseController(string $class, string $method): array
  */
 function parseDocTags(array $tagLines): array
 {
-    $result = ['params' => [], 'deprecated' => false];
+    $result = ['params' => [], 'deprecated' => false, 'unauthenticated' => false, 'responses' => []];
     foreach ($tagLines as $line) {
         if (str_starts_with($line, '@deprecated')) {
             $result['deprecated'] = true;
 
             continue;
         }
+        if (str_starts_with($line, '@unauthenticated')) {
+            $result['unauthenticated'] = true;
+
+            continue;
+        }
+        // @responseStatus 503 服务停机维护中
+        if (preg_match('/^@responseStatus\s+(\d{3})\s*(.*)$/u', $line, $r)) {
+            $result['responses'][$r[1]] = trim($r[2]) ?: '';
+
+            continue;
+        }
         if (! preg_match('/^@(queryParam|bodyParam|urlParam)\s+(\S+)\s+(\S+)\s*(.*)$/u', $line, $m)) {
             continue;
         }
@@ -264,7 +275,16 @@ function parseDocTags(array $tagLines): array
             $rest = trim(str_replace($d[0], '', $rest));
         }
         if (preg_match('/\bExample:\s*([^\n]+)/u', $rest, $x)) {
-            $entry['example'] = trim($x[1]);
+            $example = trim($x[1]);
+            // 形如 ["a","b"] 或 {...} 的示例按 JSON 解析,否则数组参数的示例
+            // 会变成字符串,与 schema 的 type 对不上
+            if (preg_match('/^[\[{]/', $example)) {
+                $decoded = json_decode($example, true);
+                if (json_last_error() === JSON_ERROR_NONE) {
+                    $example = $decoded;
+                }
+            }
+            $entry['example'] = $example;
             $rest = trim(str_replace($x[0], '', $rest));
         }
         // trim() 按字节裁剪会把末尾的中文字符截断成非法 UTF-8,这里用 /u 正则
@@ -663,7 +683,9 @@ function buildOperation(array $route, string $httpMethod): array
         }
         $params[] = $param;
     }
-    if ($method === 'index' && $httpMethod === 'GET') {
+    // v2 列表的通用参数(search/order/dir/limit/offset)只属于 v2;
+    // v3 的分页是 page/per_page,且参数一律要在文档注释里写明,不做兜底注入
+    if ($method === 'index' && $httpMethod === 'GET' && ! isV3($route['uri'])) {
         $known = array_column($params, 'name');
         foreach ($listParams as $lp) {
             if (! in_array($lp['name'], $known, true)) {
@@ -737,11 +759,54 @@ function buildOperation(array $route, string $httpMethod): array
         }
     }
 
-    // 响应
+    // 响应。v2 与 v3 的信封不同:v2 是 {ok, message, data} 且业务失败也回 200,
+    // v3 用真实状态码、Laravel 默认的 {data} 包装、错误体是 RFC 9457 Problem Details
     $data = ['type' => 'object'];
     if ($props = resourceSchema($info['resource'])) {
         $data = ['type' => 'object', 'properties' => $props];
     }
+
+    if (isV3($route['uri'])) {
+        // v3 用 Laravel Resource 的原生形状:列表 {data: [...], meta: {...}},单个 {data: {...}}
+        $success = $method === 'index'
+            ? [
+                'type' => 'object',
+                'properties' => [
+                    'data' => ['type' => 'array', 'items' => $data],
+                    'meta' => ['$ref' => '../../main.yaml#/components/schemas/PaginationMeta'],
+                ],
+            ]
+            : ['type' => 'object', 'properties' => ['data' => $data]];
+
+        $responses = [
+            '200' => [
+                'description' => '成功',
+                'content' => ['application/json' => ['schema' => $success]],
+            ],
+        ];
+        // 无鉴权的端点用 @unauthenticated 标注,就不再挂 401
+        if (empty($info['tags']['unauthenticated'])) {
+            $responses['401'] = ['$ref' => '../../main.yaml#/components/responses/ProblemUnauthorized'];
+        }
+        // 有入参才可能 422
+        if (! empty($op['parameters']) || ! empty($op['requestBody'])) {
+            $responses['422'] = ['$ref' => '../../main.yaml#/components/responses/ProblemValidation'];
+        }
+        // @responseStatus 声明的额外状态码
+        foreach ($info['tags']['responses'] ?? [] as $code => $desc) {
+            $responses[(string) $code] = [
+                'description' => $desc !== '' ? $desc : 'Problem Details',
+                'content' => ['application/problem+json' => [
+                    'schema' => ['$ref' => '../../main.yaml#/components/schemas/ProblemDetails'],
+                ]],
+            ];
+        }
+        ksort($responses);
+        $op['responses'] = $responses;
+
+        return $op;
+    }
+
     if ($method === 'index') {
         $data = [
             'type' => 'object',
@@ -770,6 +835,12 @@ function buildOperation(array $route, string $httpMethod): array
     return $op;
 }
 
+/** v3 路由用新契约:真实状态码 + {data} 包装 + Problem Details */
+function isV3(string $uri): bool
+{
+    return str_starts_with($uri, 'api/v3/');
+}
+
 function defaultSummary(string $method, string $uri): string
 {
     $res = preg_replace('#^api/(v2|v3|ops)/#', '', $uri);
@@ -886,6 +957,18 @@ $main = [
                 'description' => '未登录或无权限',
                 'content' => ['application/json' => ['schema' => ['$ref' => '#/components/schemas/Envelope']]],
             ],
+            'ProblemUnauthorized' => [
+                'description' => '未登录或无权限(v3)',
+                'content' => ['application/problem+json' => [
+                    'schema' => ['$ref' => '#/components/schemas/ProblemDetails'],
+                ]],
+            ],
+            'ProblemValidation' => [
+                'description' => '参数校验失败(v3)',
+                'content' => ['application/problem+json' => [
+                    'schema' => ['$ref' => '#/components/schemas/ProblemDetails'],
+                ]],
+            ],
             'ValidationError' => [
                 'description' => '参数校验失败',
                 'content' => ['application/json' => ['schema' => [
@@ -898,6 +981,41 @@ $main = [
             ],
         ],
         'schemas' => [
+            'ProblemDetails' => [
+                'type' => 'object',
+                'description' => "RFC 9457 Problem Details,v3 端点的错误响应体。\n".
+                    "由 bootstrap/app.php 的异常处理器统一渲染,控制器不自己拼。\n".
+                    'media type 是 application/problem+json。',
+                'properties' => [
+                    'type' => ['type' => 'string', 'description' => '问题类型标识,形如 urn:problem:not-found', 'example' => 'urn:problem:not-found'],
+                    'title' => ['type' => 'string', 'description' => '稳定的英文摘要,同类问题恒定,供机器识别'],
+                    'status' => ['type' => 'integer'],
+                    'detail' => ['type' => 'string', 'description' => '面向人的文案,已本地化;5xx 在生产环境会省略以免泄露内部信息'],
+                    'instance' => ['type' => 'string', 'description' => '出问题的请求 URI'],
+                    'errors' => [
+                        'type' => 'object',
+                        'description' => '字段级校验错误,仅 422 时出现',
+                        'additionalProperties' => ['type' => 'array', 'items' => ['type' => 'string']],
+                    ],
+                ],
+                'required' => ['type', 'title', 'status'],
+            ],
+            'PaginationMeta' => [
+                'type' => 'object',
+                'description' => "v3 列表接口的分页信息,由 Laravel paginator 生成。\n".
+                    "刻意不含 links / path:那些是 APP_URL 拼出的绝对地址,反代下会拼错,前端也用不到。\n".
+                    '手工分页的接口会附加自己的字段(如 has_more / first_para / page_size)。',
+                'properties' => [
+                    'current_page' => ['type' => 'integer'],
+                    'per_page' => ['type' => 'integer'],
+                    'total' => ['type' => 'integer'],
+                    'last_page' => ['type' => 'integer'],
+                    'from' => ['type' => 'integer', 'nullable' => true],
+                    'to' => ['type' => 'integer', 'nullable' => true],
+                ],
+                // 手工分页的接口未必有 per_page(例如按字节切页的阅读接口用 page_size)
+                'required' => ['current_page', 'total'],
+            ],
             'Envelope' => [
                 'type' => 'object',
                 'properties' => [