Procházet zdrojové kódy

Merge pull request #2486 from visuddhinanda/development

Development
visuddhinanda před 1 týdnem
rodič
revize
28664a09a1
100 změnil soubory, kde provedl 15415 přidání a 1122 odebrání
  1. 37 3
      .claude/skills/v3-resource/SKILL.md
  2. 31 1
      CLAUDE.md
  3. 3 0
      api-test/.gitignore
  4. 47 28
      api-test/README.md
  5. 8 9
      api-test/cases/03_reactions.php
  6. 224 0
      api-test/cases/07_discussions.php
  7. 82 0
      api-test/cases/08_agent_grants.php
  8. 60 3
      api-test/client/.openapi-generator/FILES
  9. 532 0
      api-test/client/lib/Api/AgentGrantsApi.php
  10. 3210 0
      api-test/client/lib/Api/DiscussionsApi.php
  11. 5 1
      api-test/client/lib/Api/EmailCertificationsApi.php
  12. 5 1
      api-test/client/lib/Api/HeartbeatApi.php
  13. 9 1
      api-test/client/lib/Api/InvitesApi.php
  14. 1 925
      api-test/client/lib/Api/MeApi.php
  15. 13 1
      api-test/client/lib/Api/PasswordResetsApi.php
  16. 5 1
      api-test/client/lib/Api/ProgressApi.php
  17. 993 60
      api-test/client/lib/Api/ReactionsApi.php
  18. 13 1
      api-test/client/lib/Api/SearchApi.php
  19. 5 1
      api-test/client/lib/Api/SearchSuggestApi.php
  20. 5 1
      api-test/client/lib/Api/SessionsApi.php
  21. 5 1
      api-test/client/lib/Api/TipitakaReadingApi.php
  22. 13 1
      api-test/client/lib/Api/TipitakaRelatedParagraphsApi.php
  23. 5 1
      api-test/client/lib/Api/UpgradeApi.php
  24. 5 1
      api-test/client/lib/Api/UsersApi.php
  25. 1 1
      api-test/client/lib/ApiException.php
  26. 2 2
      api-test/client/lib/Configuration.php
  27. 1 1
      api-test/client/lib/FormDataProcessor.php
  28. 1 1
      api-test/client/lib/HeaderSelector.php
  29. 443 0
      api-test/client/lib/Model/GetApiV3Discussions200Response.php
  30. 1316 0
      api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInner.php
  31. 511 0
      api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInnerAbilities.php
  32. 5 5
      api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInnerEditor.php
  33. 409 0
      api-test/client/lib/Model/GetApiV3DiscussionsSummary200Response.php
  34. 443 0
      api-test/client/lib/Model/GetApiV3DiscussionsSummary200ResponseData.php
  35. 443 0
      api-test/client/lib/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCounts.php
  36. 1 1
      api-test/client/lib/Model/GetApiV3Heartbeat200Response.php
  37. 1 1
      api-test/client/lib/Model/GetApiV3Heartbeat200ResponseData.php
  38. 1 1
      api-test/client/lib/Model/GetApiV3Me200Response.php
  39. 1 1
      api-test/client/lib/Model/GetApiV3Me200ResponseData.php
  40. 1 1
      api-test/client/lib/Model/GetApiV3MeReactions200Response.php
  41. 4 4
      api-test/client/lib/Model/GetApiV3MeReactions200ResponseDataInner.php
  42. 1 1
      api-test/client/lib/Model/GetApiV3PasswordResetsToken200Response.php
  43. 1 1
      api-test/client/lib/Model/GetApiV3PasswordResetsToken200ResponseData.php
  44. 1 1
      api-test/client/lib/Model/GetApiV3Progress200Response.php
  45. 1 1
      api-test/client/lib/Model/GetApiV3Progress200ResponseDataInner.php
  46. 1 1
      api-test/client/lib/Model/GetApiV3ReactionsTally200Response.php
  47. 1 1
      api-test/client/lib/Model/GetApiV3ReactionsTally200ResponseDataInner.php
  48. 1 1
      api-test/client/lib/Model/GetApiV3Search200Response.php
  49. 1 1
      api-test/client/lib/Model/GetApiV3SearchSearch200Response.php
  50. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaReadingChannel200Response.php
  51. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaReadingChannel200ResponseDataInner.php
  52. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaReadingChannel200ResponseMeta.php
  53. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200Response.php
  54. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200ResponseDataInner.php
  55. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200ResponseDataInnerPathInner.php
  56. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200ResponseMeta.php
  57. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregate200Response.php
  58. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregate200ResponseDataInner.php
  59. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200Response.php
  60. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200ResponseDataInner.php
  61. 1 1
      api-test/client/lib/Model/ModelInterface.php
  62. 1 1
      api-test/client/lib/Model/PaginationMeta.php
  63. 831 0
      api-test/client/lib/Model/PatchApiV3DiscussionsDiscussionRequest.php
  64. 1 1
      api-test/client/lib/Model/PatchApiV3PasswordResetsTokenRequest.php
  65. 409 0
      api-test/client/lib/Model/PostApiV3AgentGrants200Response.php
  66. 613 0
      api-test/client/lib/Model/PostApiV3AgentGrants200ResponseData.php
  67. 552 0
      api-test/client/lib/Model/PostApiV3AgentGrantsRequest.php
  68. 409 0
      api-test/client/lib/Model/PostApiV3Discussions200Response.php
  69. 443 0
      api-test/client/lib/Model/PostApiV3DiscussionsBatchCount200Response.php
  70. 511 0
      api-test/client/lib/Model/PostApiV3DiscussionsBatchCount200ResponseDataInner.php
  71. 412 0
      api-test/client/lib/Model/PostApiV3DiscussionsBatchCountRequest.php
  72. 483 0
      api-test/client/lib/Model/PostApiV3DiscussionsBatchCountRequestTargetsInner.php
  73. 446 0
      api-test/client/lib/Model/PostApiV3DiscussionsDiscussionRepliesRequest.php
  74. 945 0
      api-test/client/lib/Model/PostApiV3DiscussionsRequest.php
  75. 1 1
      api-test/client/lib/Model/PostApiV3EmailCertificationsRequest.php
  76. 1 1
      api-test/client/lib/Model/PostApiV3Invites200Response.php
  77. 1 1
      api-test/client/lib/Model/PostApiV3Invites200ResponseData.php
  78. 1 1
      api-test/client/lib/Model/PostApiV3InvitesRequest.php
  79. 1 1
      api-test/client/lib/Model/PostApiV3MeReactions200Response.php
  80. 1 1
      api-test/client/lib/Model/PostApiV3MeReactionsRequest.php
  81. 1 1
      api-test/client/lib/Model/PostApiV3PasswordResetsRequest.php
  82. 1 1
      api-test/client/lib/Model/PostApiV3SearchRequest.php
  83. 1 1
      api-test/client/lib/Model/PostApiV3Sessions200Response.php
  84. 1 1
      api-test/client/lib/Model/PostApiV3Sessions200ResponseData.php
  85. 1 1
      api-test/client/lib/Model/PostApiV3SessionsRequest.php
  86. 1 1
      api-test/client/lib/Model/PostApiV3Users200Response.php
  87. 1 1
      api-test/client/lib/Model/PostApiV3Users200ResponseData.php
  88. 1 1
      api-test/client/lib/Model/PostApiV3UsersRequest.php
  89. 1 1
      api-test/client/lib/Model/ProblemDetails.php
  90. 1 1
      api-test/client/lib/ObjectSerializer.php
  91. 31 0
      api-test/config.local.example.php
  92. 16 6
      api-test/config.php
  93. 11 1
      api-test/fixtures.php
  94. 5 2
      api-test/run.php
  95. 3 15
      api-test/tools/filter-v3.php
  96. 4 0
      api-v13/app/Console/Commands/ExportDiscussion.php
  97. 48 0
      api-v13/app/Http/Controllers/V3/AgentGrantController.php
  98. 46 0
      api-v13/app/Http/Controllers/V3/DiscussionBatchCountController.php
  99. 183 0
      api-v13/app/Http/Controllers/V3/DiscussionController.php
  100. 76 0
      api-v13/app/Http/Controllers/V3/DiscussionReplyController.php

+ 37 - 3
.claude/skills/v3-resource/SKILL.md

@@ -139,7 +139,7 @@ v3 端点可以自由改:v4 那两处 v3 引用(`ChatInput.tsx:127`、`agent
 ## 一次只迁一个资源
 
 不要大爆炸,不要一次迁多个资源。v2 与 v3 并存,逐个资源走完整条链,
-每次的 diff 要小到人能 review 完。**没走完全部 7 步就不算迁完。**
+每次的 diff 要小到人能 review 完。**没走完全部 9 步就不算迁完。**
 
 ## v3 硬规范
 
@@ -840,7 +840,7 @@ Route::prefix('v3')->as('v3.')->group(function () {
 index 用 `spatie/laravel-query-builder` 把 scope 暴露成 `filter[]`,
 scope 和 filter 的组装写在 Service 里(硬规范第 5 条),控制器只负责调它。
 
-## 迁移七步
+## 迁移九步
 
 ### 0. 先盘点,列给用户确认
 
@@ -853,6 +853,10 @@ cd api-v13 && php artisan tinker --execute 'print_r(\Illuminate\Support\Facades\
 产出三份清单:这个资源在 v2 有哪些端点(含 view 值)、前端哪些文件在调、
 表有哪些列及其**真实类型**。列给用户确认后再动手。
 
+
+**盘点时必问一条:这个模块要不要支持 agent 协作**(AI 模型代用户写入,见根目录
+CLAUDE.md「agent 协作」)。要的话,权限规则不能只认「人类 + 对象管理者」,否则模型
+token 写不进来;回答写进盘点清单再动手。
 ### 1. FormRequest 定入参(有入参才需要)
 
 **有入参就必须用 FormRequest,禁止 inline `validate()`;零入参的端点不要建
@@ -1090,6 +1094,36 @@ get:
 
 同时在 v3 控制器的文档注释里写明它取代了哪个 v2 端点,让两边都能追溯。
 
+### 8. 更新 api-test(只测 v3)
+
+`api-test/` 是对真实服务器跑的 `/api/v3` 契约冒烟,客户端由 openapi-generator 生成。
+**每迁一个资源,都要给它在 `api-test/cases/` 加用例**,不测 v2:
+
+```bash
+bash api-test/tools/regen.sh                                   # 重生客户端(规格变了就要跑)
+php api-test/run.php --username=test161 --password=12345 --filter=<资源>
+```
+
+- 一个资源一个 `cases/NN_<资源>.php`,调用走生成的 `<Tag>Api` 类(tag 规则见生成器 `tagOf()`)
+- 至少覆盖:公开读 200、缺必填 422、无 token 写 401、带 token 的「建 → 改 → 删」完整一轮
+- 写操作**会在所有服务器上真实执行**,只用自建的临时数据并测完即删
+- 与 Pest 的分工:Pest 覆盖权限矩阵与边界;api-test 验证**部署出来的服务器**与规格一致
+
+### 9. 更新 dashboard-test(只测 v6)
+
+`dashboard-test/` 是 dashboard-v6 的 Playwright 端到端测试。**前端切过去的页面都要有 spec**,
+不测 v4:
+
+```bash
+cd dashboard-test && npx playwright test <资源>
+```
+
+- 一个路由 / 功能一个 `src/tests/<资源>.spec.ts`,`collect.ts` 采集 console / HTTP 错误
+- 覆盖用户真实操作路径:打开、新建、编辑、删除,并断言**页面上**看到的结果
+- 建的数据命名 `e2e-tmp-<timestamp>`,测完清理;`E2E_DESTRUCTIVE=0` 可跳过
+- 迁移让某个页面「能链接到但没有路由」的(如复制链接),**在 v6 补上那个页面**再测
+- 正式跑一次就在 `dashboard-test/testlog/` 写测试日志(gitignore,只留本机)
+
 ## 验收:CI 三道闸
 
 ```bash
@@ -1117,5 +1151,5 @@ cd dashboard-v6 && npm run build                                  # 前端类型
 ## 收尾报告
 
 用一句话说清:迁了哪个资源、v2 的哪几个端点/view 映射成了 v3 的哪几个端点、
-补了几条契约测试、三道闸的结果。**改了名的话,提醒用户往 CLAUDE.md 的
+补了几条 Pest 测试、api-test 与 dashboard-test 各加了什么、三道闸的结果。**改了名的话,提醒用户往 CLAUDE.md 的
 「v2 → v3 对照表」加一行。**

+ 31 - 1
CLAUDE.md

@@ -273,6 +273,27 @@ v3 前端代码不要 import v2 的东西:v3 用 `src/api/error.ts` 的 `ApiEr
 v2 用 `src/request.ts` 的 `HttpError`。等 v2 调用点全部消失,`request.ts`
 整个删掉,不牵连 v3。
 
+**agent 协作(2026-10-15 定):每迁一个资源,先问用户这个模块要不要支持 agent 写入。**
+用户会让 AI(Claude 等,经 wikipali-plugins)代为写入,迁 v3 时不能把这条路堵死
+(例:discussions 的批注要允许账号下的 ai model 创建、允许他人回复)。现有两种写入方式:
+
+1. **模型 token**:`ai_models` 一行就是一个身份,用它签出的 token 以「普通用户」身份写,
+   `editor_uid` = 模型 uid。它自己没有任何 channel / 文章权限。
+2. **access token**:人类用**自己的登录 token** 调 `POST /v2/access-token` 为某个 channel
+   (可限 book)签一张 7 天的 JWT;写入时模型用**模型 token** 登录、把 access token 放在请求体里,
+   过 `ChecksChannelEditPower` 的第三条分支(sentences、术语用这种;wikipali-plugins `cmd_write.py`)。
+   JWT 里不记签发人、也不绑定模型,谁拿到都能用。
+
+两种都只记模型、不记指使它的人。
+
+**v3 的做法(2026-10-15 定,discussions 已落地)**:仿 git 的 Co-Authored-By。人类用自己的
+登录 token 调 `POST /v3/agent-grants`,为自己名下的模型签一张 channel(可限 book)授权;
+模型用模型 token 登录、写入时带请求头 `X-Agent-Grant`。记录上 editor = 授权人、
+`co_editor_uid` = 模型,权限按授权人判,只能写授权覆盖的 channel / book。模型不带授权写 v3 一律 403。
+授权人、模型、范围全由服务端写进 token(独立签名密钥,v2 access token 冒充不了),撤销模型
+token 时授权一并失效。实现在 `App\Services\V3\AgentGrantService`,新模块照用,不要另发明。
+存量数据(editor_uid 直接是模型 uid)不回填;v2 access token 的漏洞暂不修(用户决定)。
+
 **v3 响应信封(已定死)**:就是 Laravel Resource 的原生形状——单个 `{data: {...}}`,
 列表 `{data: [...], meta: {...}}`,失败 `{type, title, status, detail, instance, errors?}`
 + `application/problem+json`。**判断成败只看 HTTP 状态码。**
@@ -329,6 +350,8 @@ Resource / FormRequest / 测试同理;URL 只换版本前缀
 | `related-paragraph` / `RelatedParagraphController` | `tipitaka-related-paragraphs` (+`/aggregate`) / `V3\TipitakaRelatedParagraphController` + `V3\TipitakaRelatedParagraphAggregateController` | 字段改名 `book_title_pali`→`title`、`cs6_para`→`cs_para`;响应 `{ok,…}`→原生 Resource;消除逐行 N+1(批量 whereIn);新增 `book_name`/`cs_para` 过滤器与独立 `/aggregate` 聚合端点(`group_by=book_id`→book_name 列表 / `group_by=book_name`→cs_para 列表,分页) |
 | `auth/forgot-password` + `auth/reset-password` / `ForgotPasswordController` + `ResetPasswordController` | `password-resets` (+`/{token}`) / `V3\PasswordResetController` | 三条并成一个资源:POST 发信、GET 看账号、PATCH 设密码(204)。库里只存 token 的 sha256、60 分钟过期、邮箱未注册也 204、不收 `dashboard` 参数(链接用 `mint.server.dashboard_v6_base_path` 拼到 v6 的 `/anonymous/reset-password/{token}`)、限流 |
 | `email-certification` + `invite/{id}`(读)+ `sign-up` / `EmailCertificationController` + `InviteController@show` + `SignUpController@store` | `email-certifications` + `invites` (+`/{invite}`) + `users` / `V3\EmailCertificationController` + `V3\InviteController` + `V3\UserController` | 注册。验证码改在服务端比对:v2 的 `GET email-certification/{id}`(把码交给前端)在 v3 没有对应端点,换成 `POST invites {email, code}` 签发 invite;自助与邀请注册都凭 invite 走 `POST users`。验证码 6 位、30 分钟、试错 5 次作废;验证通过前不写 invites 表。studio 发邀请(`POST /v2/invite`)不在此列,将来迁成 `studios/{studio}/invites` |
+| `discussion`(列表、单条、新建、修改、删除)/ `DiscussionController` | `discussions` (+`/summary`、`/{d}/replies`) / `V3\DiscussionController` + `V3\DiscussionSummaryController` + `V3\DiscussionReplyController` | 管评论线程(type `discussion` / `qa` / `help` / `issue`)与批注(`note` 边注、`commentary` 义注 / 复注对应——巴利注释书,不是普通评论)。批注只挂句子、只有该句管理者能建(AI 凭授权代写按授权人判)、带锚点 `pos_*`/`quote_*`、可回复、无开关、新建不发通知;列表与 summary 不带 `type` 时只算评论线程。写入支持 agent 代写(`X-Agent-Grant`),新增 `co_editor`(人亲自改内容时清空)。字段改名 `res_type`/`res_id`→`target_type`/`target_id`、`parent`→`parent_id`、`children_count`→`reply_count`;`view=question/res_id/question-by-topic`→列表,`view=answer`+带 parent 的 POST→`replies`,响应里的 `active`/`close`/`can_create`→`summary`;每条带 `abilities`;只有 PATCH;软删除(2026-10-15 起 v2 也是),有回复的话题留作墓碑。`view=topic-by-user`/`all`、课程参数、`discussion-anchor`、`sent-discussion-tree` 未迁;`discussion-count` 拆开:讨论角标迁成 `POST discussions/batch-count`(只给计数,入参改为对象 uid,套 v3 可见性与类型口径),标签部分仍走 v2、待迁标签时处理;课程模式的「学生的讨论」待课程迁移后补(TODO 在 `DiscussionService::batchCount`) |
+| `access-token`(agent 用途)/ `AccessTokenController@store` | `agent-grants` / `V3\AgentGrantController` | 人类为自己名下的 AI 模型签 channel 写入授权。载荷由服务端决定(v2 原样签客户端 payload)、绑定模型(act)与签发人(grantor)、带模型 token_version、独立密钥;写入时放 `X-Agent-Grant` 头而不是请求体 |
 | `sign-in` + `auth/current` / `AuthController@signIn` + `@getUserInfoByToken` | `sessions` + `me` / `V3\SessionController` + `V3\MeController` | 登录一次返回 `{token, user}`,不再二次查询;`login` 字段收用户名或邮箱(邮箱不区分大小写),失败统一 422 `errors.login`;按账号 + IP 限流。`me` 的字段沿用 v2 的驼峰用户摘要(`UserService::profile()`),`roles` 恒为数组,另加 `email`,不再回显 token。token 与 v2 完全相同、两边互认 |
 
 **注意**:`tipitaka-reading` **不是** v2 `paragraph-content` / `chapter-content` 的替代品。
@@ -339,7 +362,14 @@ v3 这条只服务单 channel 的只读渲染。前端 `dashboard-v6/src/api/pal
 补偿手段要写进代码:v3 控制器的类注释写明「取代 `GET /v2/xxx`」,模型注释写明底层表名
 (如 `Reaction` 写明仍是 `likes` 表),这样从任一端都能找到另一端。
 
-**做迁移时使用 `v3-resource` skill**,里面有完整的七步链路与模板。
+**做迁移时使用 `v3-resource` skill**,里面有完整的九步链路与模板。
+
+**每迁一个资源,两套外部测试都要跟着更新**(skill 第 8、9 步),没更新就不算迁完:
+
+1. **`api-test/`(只测 v3)**:`bash api-test/tools/regen.sh` 重生客户端,在 `cases/` 加该资源的
+   用例(公开读、422、无 token 401、带 token 的建改删一轮),`php api-test/run.php` 跑绿。
+2. **`dashboard-test/`(只测 v6)**:给切过去的页面加 `src/tests/<资源>.spec.ts`,
+   `npx playwright test <资源>` 跑绿。迁移牵出的「有链接没页面」在 v6 补上页面一起测。
 一次只迁一个资源。试点顺序:先 A 类 3 条(零线上风险,用来打磨样板与 CI 三道闸),
 再从 `channel` 开始啃 B 类。
 

+ 3 - 0
api-test/.gitignore

@@ -3,3 +3,6 @@
 /client/composer.lock
 /spec/openapi.json
 /spec/openapi-v3.json
+
+# 本机配置(账号密码),模板见 config.local.example.php
+/config.local.php

+ 47 - 28
api-test/README.md

@@ -9,12 +9,12 @@
 
 ### 0. 环境要求
 
-| 依赖 | 用途 | 版本 |
-| --- | --- | --- |
-| PHP | 跑测试脚本 | 8.1+(需 `ext-curl` / `ext-json` / `ext-mbstring`) |
-| Composer | 装 Guzzle 依赖 | 2.x |
-| Java | 仅「重新生成客户端」时用(openapi-generator 是 jar) | 17+ |
-| Node / npx | 仅「重新生成客户端」时用(redocly bundle) | 18+ |
+| 依赖       | 用途                                                 | 版本                                                |
+| ---------- | ---------------------------------------------------- | --------------------------------------------------- |
+| PHP        | 跑测试脚本                                           | 8.1+(需 `ext-curl` / `ext-json` / `ext-mbstring`) |
+| Composer   | 装 Guzzle 依赖                                       | 2.x                                                 |
+| Java       | 仅「重新生成客户端」时用(openapi-generator 是 jar) | 17+                                                 |
+| Node / npx | 仅「重新生成客户端」时用(redocly bundle)           | 18+                                                 |
 
 > 只跑测试、不重新生成客户端的话,Java / Node 不是必须的。
 
@@ -28,10 +28,21 @@ composer install --no-dev
 cd ../..
 ```
 
-### 2. 运行测试
+### 2. 生成本机配置(只需一次)
 
 ```bash
 cd api-test
+cp config.local.example.php config.local.php   # 填上测试账号的密码(已 gitignore)
+```
+
+`config.local.php` 放账号、默认服务器、换环境时要覆盖的种子数据(`fixtures`)。
+优先级:命令行参数 > 环境变量 `API_TEST_*` > `config.local.php` > `config.php`。
+
+### 3. 运行测试
+
+```bash
+cd api-test
+php run.php                                                  # 用 config.local.php 的账号,跑全量
 php run.php --username=<username> --password=<password>            # 调 /v3/sessions 换 token,跑全量
 php run.php --username=<username> --password=<password>  --server=staging
 php run.php --token=<bearer>                                # 或直接给 token
@@ -57,14 +68,14 @@ php run.php --username=alice --password=secret                 # 调 /v3/session
 
 ### 可用 `--server` 一览
 
-| alias | URL | 说明 |
-| --- | --- | --- |
-| `local` | `http://127.0.0.1:8000/api` | 本地开发(默认) |
-| `staging` | `https://staging.wikipali.org/api` | 测试 |
-| `next` | `https://next.wikipali.org/api` | 生产预览 |
-| `prod` | `https://www.wikipali.org/api` | 生产 |
-| `next-cn` | `https://next.wikipali.cc/api` | 生产预览(中国) |
-| `prod-cn` | `https://www.wikipali.cc/api` | 生产(中国) |
+| alias     | URL                                | 说明             |
+| --------- | ---------------------------------- | ---------------- |
+| `local`   | `http://127.0.0.1:8000/api`        | 本地开发(默认) |
+| `staging` | `https://staging.wikipali.org/api` | 测试             |
+| `next`    | `https://next.wikipali.org/api`    | 生产预览         |
+| `prod`    | `https://www.wikipali.org/api`     | 生产             |
+| `next-cn` | `https://next.wikipali.cc/api`     | 生产预览(中国) |
+| `prod-cn` | `https://www.wikipali.cc/api`      | 生产(中国)     |
 
 > 也可以 `--server=<完整 base URL>` 直连任意服务器(含 /api 前缀)。
 
@@ -97,6 +108,7 @@ php run.php --username=alice --password=secret                 # 调 /v3/session
 api-test/
 ├── run.php               # 入口 + 运行器
 ├── config.php            # 服务器 alias 与 token
+├── config.local.example.php  # 本机配置模板 → 复制成 config.local.php(gitignore)
 ├── fixtures.php          # 种子数据(channel/target_id/book 等,按环境可能要改)
 ├── src/bootstrap.php     # 断言/注册 helper(test/request/assert_*)
 ├── cases/*.php           # 用例,按领域分文件
@@ -141,12 +153,12 @@ bash api-test/tools/regen.sh
 
 `regen.sh` 内部四步,都对应 spec 的某一部分:
 
-| 步骤 | 做什么 | 产物 |
-| --- | --- | --- |
-| bundle | redocly 把 `main.yaml` 及所有 `$ref` 合并成单文件 | `spec/openapi.json` |
-| filter | `filter-v3.php` 切出 `/v3/*` + 修数组 items + 修 3 个漂移端点 schema | `spec/openapi-v3.json` |
-| generate | openapi-generator 生成 PHP(Guzzle) 客户端 | `client/`(会先 `rm -rf` 重来) |
-| install | composer 装 Guzzle | `client/vendor/` |
+| 步骤     | 做什么                                                               | 产物                            |
+| -------- | -------------------------------------------------------------------- | ------------------------------- |
+| bundle   | redocly 把 `main.yaml` 及所有 `$ref` 合并成单文件                    | `spec/openapi.json`             |
+| filter   | `filter-v3.php` 切出 `/v3/*` + 修数组 items + 修 3 个漂移端点 schema | `spec/openapi-v3.json`          |
+| generate | openapi-generator 生成 PHP(Guzzle) 客户端                            | `client/`(会先 `rm -rf` 重来) |
+| install  | composer 装 Guzzle                                                   | `client/vendor/`                |
 
 > ⚠️ `client/` 是纯生成物,**不要手改**——`regen.sh` 会整个删掉重生成,手改会丢。
 
@@ -162,8 +174,8 @@ bash api-test/tools/regen.sh
 ### 4. 跑全量验证
 
 ```bash
-php run.php --username=test161 --password=12345          # 默认 local
-php run.php --username=test161 --password=12345 --server=staging
+php run.php --username=<username> --password=<password>
+php run.php --username=<username> --password=<password> --server=staging
 ```
 
 - 如果新 spec 又冒出「spec 与实现不一致」导致客户端反序列化报错(如之前的
@@ -173,11 +185,11 @@ php run.php --username=test161 --password=12345 --server=staging
 
 ## 已知的 spec ↔ 实现漂移(记录在案,不是测试脚本的 bug)
 
-| 端点 | spec 说 | 实际 | 处理 |
-| --- | --- | --- | --- |
-| `GET/POST /v3/search` | 200 `{data:数组, meta}` + 非法 `search_mode` 422 | 200 `{success, data:{took,hits}}`;`search_mode` 不校验 | `filter-v3.php` 把 200 schema 修为 `{success,data:对象}`;用例只断言状态码 + `data` 键 |
-| `GET /v3/search-suggest` | 200 `{data:数组, meta}`;空 q 422 | 200 `{success, data:{query,suggestions}}`;空 q 400 `{success:false,error}` | 同上修 schema;空 q 用例按实际 400 断言 |
-| `GET /v3/upgrade` | 200 `{data:数组, meta}` | 200 `{data:{status:"ok"}}` | 修 schema 为 `{data:对象}` |
+| 端点                     | spec 说                                          | 实际                                                                        | 处理                                                                                   |
+| ------------------------ | ------------------------------------------------ | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
+| `GET/POST /v3/search`    | 200 `{data:数组, meta}` + 非法 `search_mode` 422 | 200 `{success, data:{took,hits}}`;`search_mode` 不校验                     | `filter-v3.php` 把 200 schema 修为 `{success,data:对象}`;用例只断言状态码 + `data` 键 |
+| `GET /v3/search-suggest` | 200 `{data:数组, meta}`;空 q 422                | 200 `{success, data:{query,suggestions}}`;空 q 400 `{success:false,error}` | 同上修 schema;空 q 用例按实际 400 断言                                                |
+| `GET /v3/upgrade`        | 200 `{data:数组, meta}`                          | 200 `{data:{status:"ok"}}`                                                  | 修 schema 为 `{data:对象}`                                                             |
 
 根因是这三个控制器还没迁到 v3 的 Resource 信封(`{data}` 原生形状、错误体 RFC 9457
 Problem Details、成败只看状态码)。正确的长期修法在 api-v13 侧,这里只让测试客户端能用、
@@ -196,6 +208,13 @@ Problem Details、成败只看状态码)。正确的长期修法在 api-v13 
 - **种子数据按环境不同**:`fixtures.php` 里的 channel/target_id/doc_id 取自本地开发库,
   换环境后可能需要更新。跨环境跑时,数据不存在通常只是返回空集(仍 200),不会假红,
   但 `search/{id}` 的 200 用例依赖真实 doc id。
+- **discussions 的写用例(`cases/07_discussions.php`)挂在 `discussion_sentence` 上**:测试账号自己
+  channel 里、自己编辑的句子,带 `notify=false` 不发通知。v3 的删除是**软删**,所以每跑一遍会在
+  `discussions` 表里留下两行带 `deleted_at` 的记录(对外已不可见)。换环境时用
+  `config.local.php` 的 `fixtures.discussion_sentence` 指定该环境里测试账号自己的句子。
+- **鉴权由 spec 决定**:生成器按路由中间件给每个 v3 operation 写 `security`——挂 `auth.v3` 的必须带
+  token,其余是可选(带了按登录用户处理)。所以同一路径「读公开、写要登录」也能分清,
+  `filter-v3.php` 不再猜。
 - **写操作用垃圾数据 + 测完即删**:`/v3/me/reactions` 的增删查用例用固定垃圾
   `target_id`(uuid 格式),DELETE 会清理;若中途失败留下残留,下次跑时幂等逻辑会
   兜住(POST 返回 200 命中已有),最后由测试账号定期清理。

+ 8 - 9
api-test/cases/03_reactions.php

@@ -1,6 +1,5 @@
 <?php
 
-use \WikipaliApi\Api\MeApi;
 use \WikipaliApi\Api\ReactionsApi;
 use \WikipaliApi\Model\PostApiV3MeReactionsRequest;
 
@@ -37,7 +36,7 @@ test('GET /v3/reactions/tally(缺 target_id)→ 422', function () {
 /* ---- /v3/me/reactions:无 token → 401(每个动作各一条)---- */
 
 test('GET /v3/me/reactions(无 token)→ 401', function () {
-    $res = request(fn () => api(MeApi::class)->getApiV3MeReactionsWithHttpInfo());
+    $res = request(fn () => api(ReactionsApi::class)->getApiV3MeReactionsWithHttpInfo());
     assert_status(401, $res, 'me/reactions 无 token');
     assert_problem($res, 'me/reactions 无 token');
 });
@@ -47,13 +46,13 @@ test('POST /v3/me/reactions(无 token)→ 401', function () {
     $body->setType('watch');
     $body->setTargetId('11111111-1111-4111-8111-111111111111');
     $body->setTargetType('collection');
-    $res = request(fn () => api(MeApi::class)->postApiV3MeReactionsWithHttpInfo($body));
+    $res = request(fn () => api(ReactionsApi::class)->postApiV3MeReactionsWithHttpInfo($body));
     assert_status(401, $res, 'me/reactions POST 无 token');
     assert_problem($res, 'me/reactions POST 无 token');
 });
 
 test('GET /v3/me/reactions(非法 token)→ 401', function () {
-    $res = request(fn () => api(MeApi::class, 'garbage-token')->getApiV3MeReactionsWithHttpInfo());
+    $res = request(fn () => api(ReactionsApi::class, 'garbage-token')->getApiV3MeReactionsWithHttpInfo());
     assert_status(401, $res, 'me/reactions 非法 token');
     assert_problem($res, 'me/reactions 非法 token');
 });
@@ -71,7 +70,7 @@ test('me/reactions 增删查:POST 建 → GET 查到 → 再 POST 幂等 → 
     $body->setTargetType('collection');
 
     // 增:POST
-    $create = request(fn () => api(MeApi::class, $token)->postApiV3MeReactionsWithHttpInfo($body));
+    $create = request(fn () => api(ReactionsApi::class, $token)->postApiV3MeReactionsWithHttpInfo($body));
     if (! in_array($create['status'], [200, 201], true)) {
         fail('POST 创建:期望 201(新建)或 200(已存在),实际 ' . $create['status']);
     }
@@ -81,7 +80,7 @@ test('me/reactions 增删查:POST 建 → GET 查到 → 再 POST 幂等 → 
     }
 
     // 查:GET 按 type 过滤,能查到刚建的(用 target_id 定位,因为列表没有 target_id 过滤参数)
-    $list = request(fn () => api(MeApi::class, $token)->getApiV3MeReactionsWithHttpInfo(200, 'watch'));
+    $list = request(fn () => api(ReactionsApi::class, $token)->getApiV3MeReactionsWithHttpInfo(200, 'watch'));
     assert_status(200, $list, 'GET 列表');
     $found = false;
     foreach (($list['body']['data'] ?? []) as $row) {
@@ -95,7 +94,7 @@ test('me/reactions 增删查:POST 建 → GET 查到 → 再 POST 幂等 → 
     }
 
     // 幂等:再 POST 同一条 → 200,id 不变
-    $again = request(fn () => api(MeApi::class, $token)->postApiV3MeReactionsWithHttpInfo($body));
+    $again = request(fn () => api(ReactionsApi::class, $token)->postApiV3MeReactionsWithHttpInfo($body));
     assert_status(200, $again, '再 POST 幂等');
     assert_eq($id, $again['body']['data']['id'] ?? null, '幂等:两次 id 一致');
 
@@ -105,11 +104,11 @@ test('me/reactions 增删查:POST 建 → GET 查到 → 再 POST 幂等 → 
     assert_problem($noTokenDel, '无 token DELETE');
 
     // 删:DELETE → 204
-    $del = request(fn () => api(MeApi::class, $token)->deleteApiV3MeReactionsReactionWithHttpInfo($id));
+    $del = request(fn () => api(ReactionsApi::class, $token)->deleteApiV3MeReactionsReactionWithHttpInfo($id));
     assert_status(204, $del, 'DELETE');
 
     // 再删:已删除 → 404
-    $delAgain = request(fn () => api(MeApi::class, $token)->deleteApiV3MeReactionsReactionWithHttpInfo($id));
+    $delAgain = request(fn () => api(ReactionsApi::class, $token)->deleteApiV3MeReactionsReactionWithHttpInfo($id));
     assert_status(404, $delAgain, '再 DELETE');
     assert_problem($delAgain, '再 DELETE');
 });

+ 224 - 0
api-test/cases/07_discussions.php

@@ -0,0 +1,224 @@
+<?php
+
+use \WikipaliApi\Api\DiscussionsApi;
+use \WikipaliApi\Model\PatchApiV3DiscussionsDiscussionRequest;
+use \WikipaliApi\Model\PostApiV3DiscussionsBatchCountRequest;
+use \WikipaliApi\Model\PostApiV3DiscussionsBatchCountRequestTargetsInner;
+use \WikipaliApi\Model\PostApiV3DiscussionsDiscussionRepliesRequest;
+use \WikipaliApi\Model\PostApiV3DiscussionsRequest;
+
+/*
+ * 讨论(评论线程):话题列表 / 统计 / 单条 / 回复 / 新建 / PATCH / 软删除。
+ *
+ * ⚠️ 写用例在所有服务器上真实执行。挂在 fixtures 的 discussion_sentence 上(测试账号
+ * 自己的句子),带 notify=false 不发通知,最后删掉。删除是软删:行还在表里、标了
+ * deleted_at,对外已不可见(再 GET 404)。
+ * 权限矩阵(basic 角色、匿名可见性、谁能关闭)由 api-v13 的 DiscussionV3Test 覆盖。
+ */
+
+function discussion_target(): string
+{
+    return fixtures()['discussion_sentence'];
+}
+
+/* ---- 只读 ---- */
+
+test('GET /v3/discussions?target_type=sentence&target_id=… → 200', function () {
+    $res = request(fn () => api(DiscussionsApi::class)->getApiV3DiscussionsWithHttpInfo('sentence', discussion_target()));
+    assert_status(200, $res, 'discussions');
+    assert_has_key('data', $res['body'], 'discussions');
+    assert_has_key('meta', $res['body'], 'discussions');
+});
+
+test('GET /v3/discussions(缺 target_id)→ 422', function () {
+    $res = raw('GET', '/v3/discussions', ['target_type' => 'sentence']);
+    assert_status(422, $res, 'discussions 缺 target_id');
+    assert_field_error('target_id', $res, 'discussions 缺 target_id');
+});
+
+test('GET /v3/discussions(非法 target_type)→ 422', function () {
+    $res = raw('GET', '/v3/discussions', ['target_type' => 'channel', 'target_id' => discussion_target()]);
+    assert_status(422, $res, 'discussions 非法 target_type');
+    assert_field_error('target_type', $res, 'discussions 非法 target_type');
+});
+
+test('GET /v3/discussions/summary → 200,含 status_counts 与 creatable_types', function () {
+    $res = request(fn () => api(DiscussionsApi::class)->getApiV3DiscussionsSummaryWithHttpInfo('sentence', discussion_target()));
+    assert_status(200, $res, 'summary');
+    $data = $res['body']['data'] ?? null;
+    assert_has_key('status_counts', $data, 'summary.data');
+    assert_has_key('creatable_types', $data, 'summary.data');
+    // 匿名:不能发起任何话题
+    assert_eq([], $data['creatable_types'], '匿名 creatable_types');
+});
+
+test('GET /v3/discussions/{不存在} → 404', function () {
+    $res = raw('GET', '/v3/discussions/00000000-0000-4000-8000-000000000000');
+    assert_status(404, $res, 'discussions 不存在');
+    assert_problem($res, 'discussions 不存在');
+});
+
+test('PUT /v3/discussions/{id} → 405(只有 PATCH)', function () {
+    $res = raw('PUT', '/v3/discussions/00000000-0000-4000-8000-000000000000', null, ['title' => 'x'], required_token());
+    assert_status(405, $res, 'PUT');
+});
+
+test('POST /v3/discussions/batch-count → 200,按请求顺序、没有讨论的给 0', function () {
+    $sentence = new PostApiV3DiscussionsBatchCountRequestTargetsInner();
+    $sentence->setType('sentence');
+    $sentence->setId(discussion_target());
+    $wbw = new PostApiV3DiscussionsBatchCountRequestTargetsInner();
+    $wbw->setType('wbw');
+    $wbw->setId('00000000-0000-4000-8000-000000000000');
+    $body = new PostApiV3DiscussionsBatchCountRequest();
+    $body->setTargets([$sentence, $wbw]);
+
+    $res = request(fn () => api(DiscussionsApi::class)->postApiV3DiscussionsBatchCountWithHttpInfo($body));
+    assert_status(200, $res, 'batch-count');
+    assert_eq(discussion_target(), $res['body']['data'][0]['target_id'] ?? null, 'batch-count 顺序');
+    assert_eq(0, $res['body']['data'][1]['total'] ?? null, 'batch-count 没有讨论给 0');
+    assert_eq(0, $res['body']['data'][0]['mine'] ?? null, '匿名 mine 恒为 0');
+});
+
+test('POST /v3/discussions/batch-count(空 / 非法类型)→ 422', function () {
+    assert_status(422, raw('POST', '/v3/discussions/batch-count', null, ['targets' => []]), 'batch-count 空');
+    $bad = raw('POST', '/v3/discussions/batch-count', null, ['targets' => [['type' => 'article', 'id' => discussion_target()]]]);
+    assert_field_error('targets.0.type', $bad, 'batch-count 非法类型');
+});
+
+/* ---- 无 token → 401 ---- */
+
+test('POST /v3/discussions(无 token)→ 401', function () {
+    $body = new PostApiV3DiscussionsRequest();
+    $body->setTargetType('sentence');
+    $body->setTargetId(discussion_target());
+    $body->setType('discussion');
+    $body->setTitle('api-test 无 token');
+    $res = request(fn () => api(DiscussionsApi::class)->postApiV3DiscussionsWithHttpInfo($body));
+    assert_status(401, $res, 'POST 无 token');
+    assert_problem($res, 'POST 无 token');
+});
+
+/* ---- 带 token:一整轮 ---- */
+
+test('discussions 一轮:建话题 → 回复 → 关闭后回复 409 → 重开 → 改类型 → 无 token 删 401 → 删回复 → 删话题(软删)→ 再查 404', function () {
+    $token = required_token();
+    $client = api(DiscussionsApi::class, $token);
+    $stamp = 'api-test-' . date('Ymd-His');
+
+    // 建话题:201,带 abilities,作者能改能删
+    $body = new PostApiV3DiscussionsRequest();
+    $body->setTargetType('sentence');
+    $body->setTargetId(discussion_target());
+    $body->setType('issue');
+    $body->setSeverity('warning');
+    $body->setTitle($stamp);
+    $body->setContent('由 api-test 建立,测完即删');
+    $body->setNotify(false);
+    $create = request(fn () => $client->postApiV3DiscussionsWithHttpInfo($body));
+    assert_status(201, $create, '建话题');
+    $topic = $create['body']['data'] ?? [];
+    $id = $topic['id'] ?? null;
+    if (! $id) {
+        fail('建话题:应返回 id');
+    }
+
+    try {
+        assert_eq('warning', $topic['severity'] ?? null, '建话题 severity');
+        assert_eq(true, $topic['abilities']['update'] ?? null, '作者 abilities.update');
+        assert_eq(true, $topic['abilities']['delete'] ?? null, '作者 abilities.delete');
+
+        // severity 与类型不匹配 → 422
+        $bad = raw('POST', '/v3/discussions', null, [
+            'target_type' => 'sentence', 'target_id' => discussion_target(),
+            'type' => 'discussion', 'severity' => 'error', 'title' => $stamp, 'notify' => false,
+        ], $token);
+        assert_status(422, $bad, 'discussion 带 severity');
+        assert_field_error('severity', $bad, 'discussion 带 severity');
+
+        // 角标:数到这个话题,而且是我发起的
+        $badge = raw('POST', '/v3/discussions/batch-count', null, ['targets' => [['type' => 'sentence', 'id' => discussion_target()]]], $token);
+        if (($badge['body']['data'][0]['mine'] ?? 0) < 1) {
+            fail('batch-count:mine 应至少为 1,body=' . json_encode($badge['body']));
+        }
+
+        // 列表里查得到
+        $list = request(fn () => $client->getApiV3DiscussionsWithHttpInfo('sentence', discussion_target(), 50, 'issue'));
+        assert_status(200, $list, '列表');
+        $ids = array_column($list['body']['data'] ?? [], 'id');
+        if (! in_array($id, $ids, true)) {
+            fail('列表:查不到刚建的话题');
+        }
+
+        // 回复:201,类型跟着话题
+        $reply = new PostApiV3DiscussionsDiscussionRepliesRequest();
+        $reply->setContent('api-test 回复');
+        $reply->setNotify(false);
+        $r = request(fn () => $client->postApiV3DiscussionsDiscussionRepliesWithHttpInfo($id, $reply));
+        assert_status(201, $r, '回复');
+        $replyId = $r['body']['data']['id'] ?? null;
+        assert_eq($id, $r['body']['data']['parent_id'] ?? null, '回复 parent_id');
+        assert_eq('issue', $r['body']['data']['type'] ?? null, '回复类型跟话题');
+
+        // 回复上改话题专属字段(标题、状态、类型)→ 422
+        $topicOnly = raw('PATCH', "/v3/discussions/{$replyId}", null, ['title' => 'x'], $token);
+        assert_field_error('title', $topicOnly, 'PATCH 回复的 title');
+
+        // 不能回复一条回复 → 422
+        $nested = raw('POST', "/v3/discussions/{$replyId}/replies", null, ['content' => 'x', 'notify' => false], $token);
+        assert_status(422, $nested, '回复回复');
+
+        $replies = request(fn () => $client->getApiV3DiscussionsDiscussionRepliesWithHttpInfo($id));
+        assert_status(200, $replies, '回复列表');
+        assert_eq([$replyId], array_column($replies['body']['data'] ?? [], 'id'), '回复列表内容');
+
+        // 关闭 → 回复 409 → 重新打开
+        $close = new PatchApiV3DiscussionsDiscussionRequest();
+        $close->setStatus('close');
+        $closed = request(fn () => $client->patchApiV3DiscussionsDiscussionWithHttpInfo($id, $close));
+        assert_status(200, $closed, '关闭');
+        assert_eq(false, $closed['body']['data']['abilities']['reply'] ?? null, '关闭后 abilities.reply');
+        $conflict = raw('POST', "/v3/discussions/{$id}/replies", null, ['content' => 'x', 'notify' => false], $token);
+        assert_status(409, $conflict, '关闭后回复');
+        assert_problem($conflict, '关闭后回复');
+
+        $open = new PatchApiV3DiscussionsDiscussionRequest();
+        $open->setStatus('active');
+        assert_status(200, request(fn () => $client->patchApiV3DiscussionsDiscussionWithHttpInfo($id, $open)), '重新打开');
+
+        // 改类型:issue → discussion,severity 被清掉,回复跟着改
+        $convert = new PatchApiV3DiscussionsDiscussionRequest();
+        $convert->setType('discussion');
+        $converted = request(fn () => $client->patchApiV3DiscussionsDiscussionWithHttpInfo($id, $convert));
+        assert_status(200, $converted, '改类型');
+        assert_eq(null, $converted['body']['data']['severity'] ?? null, '改类型后 severity');
+        $reread = request(fn () => $client->getApiV3DiscussionsDiscussionWithHttpInfo($replyId));
+        assert_eq('discussion', $reread['body']['data']['type'] ?? null, '回复类型跟着改');
+
+        // 无 token 删:路由绑定先于鉴权,所以要拿真实存在的 id 才测得到 401
+        $noToken = raw('DELETE', "/v3/discussions/{$replyId}");
+        assert_status(401, $noToken, '无 token DELETE');
+        assert_problem($noToken, '无 token DELETE');
+
+        // 删回复:200 + 带 deleted_at 的资源;话题回复数归零
+        $delReply = request(fn () => $client->deleteApiV3DiscussionsDiscussionWithHttpInfo($replyId));
+        assert_status(200, $delReply, '删回复');
+        if (empty($delReply['body']['data']['deleted_at'] ?? null)) {
+            fail('删回复:响应应带 deleted_at');
+        }
+        $after = request(fn () => $client->getApiV3DiscussionsDiscussionWithHttpInfo($id));
+        assert_eq(0, $after['body']['data']['reply_count'] ?? null, '删回复后 reply_count');
+    } finally {
+        // 无论中途哪步失败都清干净:先删剩下的回复,否则话题会留成墓碑
+        $left = request(fn () => $client->getApiV3DiscussionsDiscussionRepliesWithHttpInfo($id));
+        foreach (array_column($left['body']['data'] ?? [], 'id') as $rid) {
+            request(fn () => $client->deleteApiV3DiscussionsDiscussionWithHttpInfo($rid));
+        }
+        $del = request(fn () => $client->deleteApiV3DiscussionsDiscussionWithHttpInfo($id));
+    }
+    assert_status(200, $del, '删话题');
+
+    // 没有回复的话题删掉后不留墓碑 → 404
+    $gone = raw('GET', "/v3/discussions/{$id}", null, null, $token);
+    assert_status(404, $gone, '删后再查');
+});

+ 82 - 0
api-test/cases/08_agent_grants.php

@@ -0,0 +1,82 @@
+<?php
+
+use \WikipaliApi\Api\AgentGrantsApi;
+use \WikipaliApi\Model\PostApiV3AgentGrantsRequest;
+
+/*
+ * agent 协作:人类签发写入授权(POST /v3/agent-grants),模型凭模型 token + X-Agent-Grant 代写。
+ *
+ * 完整代写链路需要一个归测试账号所有的 AI 模型:在 config.local.php 的
+ * fixtures.agent_model 填它的 uid,没填则 SKIP。模型 token 目前只有 v2 端点能取
+ * (GET /v2/ai-model-token/{uid}),这里只把它当准备步骤用。
+ * 权限矩阵(别的 channel / book、撤销、伪造、v2 token 冒充)由 api-v13 的 AgentGrantV3Test 覆盖。
+ */
+
+function agent_grant_body(string $model, string $channel): PostApiV3AgentGrantsRequest
+{
+    $body = new PostApiV3AgentGrantsRequest();
+    $body->setAiModel($model);
+    $body->setTargetType('channel');
+    $body->setTargetId($channel);
+
+    return $body;
+}
+
+test('POST /v3/agent-grants(无 token)→ 401', function () {
+    $res = request(fn () => api(AgentGrantsApi::class)->postApiV3AgentGrantsWithHttpInfo(
+        agent_grant_body('00000000-0000-4000-8000-000000000000', fixtures()['channel'])
+    ));
+    assert_status(401, $res, 'agent-grants 无 token');
+    assert_problem($res, 'agent-grants 无 token');
+});
+
+test('POST /v3/agent-grants(模型不存在)→ 422 errors.ai_model', function () {
+    $res = request(fn () => api(AgentGrantsApi::class, required_token())->postApiV3AgentGrantsWithHttpInfo(
+        agent_grant_body('00000000-0000-4000-8000-000000000000', fixtures()['channel'])
+    ));
+    assert_field_error('ai_model', $res, 'agent-grants 模型不存在');
+});
+
+test('POST /v3/agent-grants(target_type 不是 channel)→ 422', function () {
+    $res = raw('POST', '/v3/agent-grants', null, [
+        'ai_model' => '00000000-0000-4000-8000-000000000000', 'target_type' => 'article', 'target_id' => fixtures()['channel'],
+    ], required_token());
+    assert_field_error('target_type', $res, 'agent-grants target_type');
+});
+
+test('agent 代写一轮:签授权 → 模型带授权建批注(editor=人、co_editor=模型)→ 不带授权 403 → 人删掉', function () {
+    $model = fixtures()['agent_model'] ?? null;
+    if (! $model) {
+        skip('config.local.php 未配置 fixtures.agent_model(归测试账号所有的 AI 模型 uid)');
+    }
+    $token = required_token();
+    $sentence = fixtures()['discussion_sentence'];
+    $channel = fixtures()['discussion_channel'];
+
+    $grant = request(fn () => api(AgentGrantsApi::class, $token)->postApiV3AgentGrantsWithHttpInfo(agent_grant_body($model, $channel)));
+    assert_status(201, $grant, '签授权');
+    $grantToken = $grant['body']['data']['token'] ?? null;
+
+    // 准备:取模型 token(只有 v2 端点)
+    $modelToken = raw('GET', "/v2/ai-model-token/{$model}", null, null, $token)['body']['data']['token'] ?? null;
+    if (! $modelToken) {
+        fail('取不到模型 token:fixtures.agent_model 是否归测试账号所有?');
+    }
+
+    $body = ['target_type' => 'sentence', 'target_id' => $sentence, 'type' => 'note', 'content' => 'api-test 代写的批注'];
+    $denied = raw('POST', '/v3/discussions', null, $body, $modelToken);
+    assert_status(403, $denied, '模型不带授权');
+
+    $created = raw('POST', '/v3/discussions', null, $body, $modelToken, ['X-Agent-Grant' => $grantToken]);
+    assert_status(201, $created, '模型带授权');
+    $id = $created['body']['data']['id'] ?? null;
+    try {
+        assert_eq($model, $created['body']['data']['co_editor']['id'] ?? null, 'co_editor 是模型');
+        if (($created['body']['data']['editor']['id'] ?? null) === $model) {
+            fail('editor 应是授权人,不是模型');
+        }
+    } finally {
+        $del = raw('DELETE', "/v3/discussions/{$id}", null, null, $token);
+    }
+    assert_status(200, $del, '人删掉模型写的');
+});

+ 60 - 3
api-test/client/.openapi-generator/FILES

@@ -4,6 +4,8 @@
 .travis.yml
 README.md
 composer.json
+docs/Api/AgentGrantsApi.md
+docs/Api/DiscussionsApi.md
 docs/Api/EmailCertificationsApi.md
 docs/Api/HeartbeatApi.md
 docs/Api/InvitesApi.md
@@ -18,13 +20,19 @@ docs/Api/TipitakaReadingApi.md
 docs/Api/TipitakaRelatedParagraphsApi.md
 docs/Api/UpgradeApi.md
 docs/Api/UsersApi.md
+docs/Model/GetApiV3Discussions200Response.md
+docs/Model/GetApiV3Discussions200ResponseDataInner.md
+docs/Model/GetApiV3Discussions200ResponseDataInnerAbilities.md
+docs/Model/GetApiV3Discussions200ResponseDataInnerEditor.md
+docs/Model/GetApiV3DiscussionsSummary200Response.md
+docs/Model/GetApiV3DiscussionsSummary200ResponseData.md
+docs/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCounts.md
 docs/Model/GetApiV3Heartbeat200Response.md
 docs/Model/GetApiV3Heartbeat200ResponseData.md
 docs/Model/GetApiV3Me200Response.md
 docs/Model/GetApiV3Me200ResponseData.md
 docs/Model/GetApiV3MeReactions200Response.md
 docs/Model/GetApiV3MeReactions200ResponseDataInner.md
-docs/Model/GetApiV3MeReactions200ResponseDataInnerUser.md
 docs/Model/GetApiV3PasswordResetsToken200Response.md
 docs/Model/GetApiV3PasswordResetsToken200ResponseData.md
 docs/Model/GetApiV3Progress200Response.md
@@ -45,7 +53,18 @@ docs/Model/GetApiV3TipitakaRelatedParagraphsAggregate200ResponseDataInner.md
 docs/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200Response.md
 docs/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200ResponseDataInner.md
 docs/Model/PaginationMeta.md
+docs/Model/PatchApiV3DiscussionsDiscussionRequest.md
 docs/Model/PatchApiV3PasswordResetsTokenRequest.md
+docs/Model/PostApiV3AgentGrants200Response.md
+docs/Model/PostApiV3AgentGrants200ResponseData.md
+docs/Model/PostApiV3AgentGrantsRequest.md
+docs/Model/PostApiV3Discussions200Response.md
+docs/Model/PostApiV3DiscussionsBatchCount200Response.md
+docs/Model/PostApiV3DiscussionsBatchCount200ResponseDataInner.md
+docs/Model/PostApiV3DiscussionsBatchCountRequest.md
+docs/Model/PostApiV3DiscussionsBatchCountRequestTargetsInner.md
+docs/Model/PostApiV3DiscussionsDiscussionRepliesRequest.md
+docs/Model/PostApiV3DiscussionsRequest.md
 docs/Model/PostApiV3EmailCertificationsRequest.md
 docs/Model/PostApiV3Invites200Response.md
 docs/Model/PostApiV3Invites200ResponseData.md
@@ -62,6 +81,8 @@ docs/Model/PostApiV3Users200ResponseData.md
 docs/Model/PostApiV3UsersRequest.md
 docs/Model/ProblemDetails.md
 git_push.sh
+lib/Api/AgentGrantsApi.php
+lib/Api/DiscussionsApi.php
 lib/Api/EmailCertificationsApi.php
 lib/Api/HeartbeatApi.php
 lib/Api/InvitesApi.php
@@ -80,13 +101,19 @@ lib/ApiException.php
 lib/Configuration.php
 lib/FormDataProcessor.php
 lib/HeaderSelector.php
+lib/Model/GetApiV3Discussions200Response.php
+lib/Model/GetApiV3Discussions200ResponseDataInner.php
+lib/Model/GetApiV3Discussions200ResponseDataInnerAbilities.php
+lib/Model/GetApiV3Discussions200ResponseDataInnerEditor.php
+lib/Model/GetApiV3DiscussionsSummary200Response.php
+lib/Model/GetApiV3DiscussionsSummary200ResponseData.php
+lib/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCounts.php
 lib/Model/GetApiV3Heartbeat200Response.php
 lib/Model/GetApiV3Heartbeat200ResponseData.php
 lib/Model/GetApiV3Me200Response.php
 lib/Model/GetApiV3Me200ResponseData.php
 lib/Model/GetApiV3MeReactions200Response.php
 lib/Model/GetApiV3MeReactions200ResponseDataInner.php
-lib/Model/GetApiV3MeReactions200ResponseDataInnerUser.php
 lib/Model/GetApiV3PasswordResetsToken200Response.php
 lib/Model/GetApiV3PasswordResetsToken200ResponseData.php
 lib/Model/GetApiV3Progress200Response.php
@@ -108,7 +135,18 @@ lib/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200Response.php
 lib/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200ResponseDataInner.php
 lib/Model/ModelInterface.php
 lib/Model/PaginationMeta.php
+lib/Model/PatchApiV3DiscussionsDiscussionRequest.php
 lib/Model/PatchApiV3PasswordResetsTokenRequest.php
+lib/Model/PostApiV3AgentGrants200Response.php
+lib/Model/PostApiV3AgentGrants200ResponseData.php
+lib/Model/PostApiV3AgentGrantsRequest.php
+lib/Model/PostApiV3Discussions200Response.php
+lib/Model/PostApiV3DiscussionsBatchCount200Response.php
+lib/Model/PostApiV3DiscussionsBatchCount200ResponseDataInner.php
+lib/Model/PostApiV3DiscussionsBatchCountRequest.php
+lib/Model/PostApiV3DiscussionsBatchCountRequestTargetsInner.php
+lib/Model/PostApiV3DiscussionsDiscussionRepliesRequest.php
+lib/Model/PostApiV3DiscussionsRequest.php
 lib/Model/PostApiV3EmailCertificationsRequest.php
 lib/Model/PostApiV3Invites200Response.php
 lib/Model/PostApiV3Invites200ResponseData.php
@@ -126,6 +164,8 @@ lib/Model/PostApiV3UsersRequest.php
 lib/Model/ProblemDetails.php
 lib/ObjectSerializer.php
 phpunit.xml.dist
+test/Api/AgentGrantsApiTest.php
+test/Api/DiscussionsApiTest.php
 test/Api/EmailCertificationsApiTest.php
 test/Api/HeartbeatApiTest.php
 test/Api/InvitesApiTest.php
@@ -140,12 +180,18 @@ test/Api/TipitakaReadingApiTest.php
 test/Api/TipitakaRelatedParagraphsApiTest.php
 test/Api/UpgradeApiTest.php
 test/Api/UsersApiTest.php
+test/Model/GetApiV3Discussions200ResponseDataInnerAbilitiesTest.php
+test/Model/GetApiV3Discussions200ResponseDataInnerEditorTest.php
+test/Model/GetApiV3Discussions200ResponseDataInnerTest.php
+test/Model/GetApiV3Discussions200ResponseTest.php
+test/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCountsTest.php
+test/Model/GetApiV3DiscussionsSummary200ResponseDataTest.php
+test/Model/GetApiV3DiscussionsSummary200ResponseTest.php
 test/Model/GetApiV3Heartbeat200ResponseDataTest.php
 test/Model/GetApiV3Heartbeat200ResponseTest.php
 test/Model/GetApiV3Me200ResponseDataTest.php
 test/Model/GetApiV3Me200ResponseTest.php
 test/Model/GetApiV3MeReactions200ResponseDataInnerTest.php
-test/Model/GetApiV3MeReactions200ResponseDataInnerUserTest.php
 test/Model/GetApiV3MeReactions200ResponseTest.php
 test/Model/GetApiV3PasswordResetsToken200ResponseDataTest.php
 test/Model/GetApiV3PasswordResetsToken200ResponseTest.php
@@ -167,7 +213,18 @@ test/Model/GetApiV3TipitakaRelatedParagraphsAggregate200ResponseTest.php
 test/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200ResponseDataInnerTest.php
 test/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200ResponseTest.php
 test/Model/PaginationMetaTest.php
+test/Model/PatchApiV3DiscussionsDiscussionRequestTest.php
 test/Model/PatchApiV3PasswordResetsTokenRequestTest.php
+test/Model/PostApiV3AgentGrants200ResponseDataTest.php
+test/Model/PostApiV3AgentGrants200ResponseTest.php
+test/Model/PostApiV3AgentGrantsRequestTest.php
+test/Model/PostApiV3Discussions200ResponseTest.php
+test/Model/PostApiV3DiscussionsBatchCount200ResponseDataInnerTest.php
+test/Model/PostApiV3DiscussionsBatchCount200ResponseTest.php
+test/Model/PostApiV3DiscussionsBatchCountRequestTargetsInnerTest.php
+test/Model/PostApiV3DiscussionsBatchCountRequestTest.php
+test/Model/PostApiV3DiscussionsDiscussionRepliesRequestTest.php
+test/Model/PostApiV3DiscussionsRequestTest.php
 test/Model/PostApiV3EmailCertificationsRequestTest.php
 test/Model/PostApiV3Invites200ResponseDataTest.php
 test/Model/PostApiV3Invites200ResponseTest.php

+ 532 - 0
api-test/client/lib/Api/AgentGrantsApi.php

@@ -0,0 +1,532 @@
+<?php
+/**
+ * AgentGrantsApi
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Api;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\ClientInterface;
+use GuzzleHttp\Exception\ConnectException;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Psr7\MultipartStream;
+use GuzzleHttp\Psr7\Request;
+use GuzzleHttp\RequestOptions;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use WikipaliApi\ApiException;
+use WikipaliApi\Configuration;
+use WikipaliApi\FormDataProcessor;
+use WikipaliApi\HeaderSelector;
+use WikipaliApi\ObjectSerializer;
+
+/**
+ * AgentGrantsApi Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+class AgentGrantsApi
+{
+    /**
+     * @var ClientInterface
+     */
+    protected $client;
+
+    /**
+     * @var Configuration
+     */
+    protected $config;
+
+    /**
+     * @var HeaderSelector
+     */
+    protected $headerSelector;
+
+    /**
+     * @var int Host index
+     */
+    protected $hostIndex;
+
+    /** @var string[] $contentTypes **/
+    public const contentTypes = [
+        'postApiV3AgentGrants' => [
+            'application/json',
+        ],
+    ];
+
+    /**
+     * @param ClientInterface $client
+     * @param Configuration   $config
+     * @param HeaderSelector  $selector
+     * @param int             $hostIndex (Optional) host index to select the list of hosts if defined in the OpenAPI spec
+     */
+    public function __construct(
+        ?ClientInterface $client = null,
+        ?Configuration $config = null,
+        ?HeaderSelector $selector = null,
+        int $hostIndex = 0
+    ) {
+        $this->client = $client ?: new Client();
+        $this->config = $config ?: Configuration::getDefaultConfiguration();
+        $this->headerSelector = $selector ?: new HeaderSelector();
+        $this->hostIndex = $hostIndex;
+    }
+
+    /**
+     * Set the host index
+     *
+     * @param int $hostIndex Host index (required)
+     */
+    public function setHostIndex($hostIndex): void
+    {
+        $this->hostIndex = $hostIndex;
+    }
+
+    /**
+     * Get the host index
+     *
+     * @return int Host index
+     */
+    public function getHostIndex()
+    {
+        return $this->hostIndex;
+    }
+
+    /**
+     * @return Configuration
+     */
+    public function getConfig()
+    {
+        return $this->config;
+    }
+
+    /**
+     * Operation postApiV3AgentGrants
+     *
+     * 签发 agent 写入授权
+     *
+     * @param  \WikipaliApi\Model\PostApiV3AgentGrantsRequest $post_api_v3_agent_grants_request post_api_v3_agent_grants_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3AgentGrants'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return \WikipaliApi\Model\PostApiV3AgentGrants200Response|\WikipaliApi\Model\PostApiV3AgentGrants200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails
+     */
+    public function postApiV3AgentGrants($post_api_v3_agent_grants_request, string $contentType = self::contentTypes['postApiV3AgentGrants'][0])
+    {
+        list($response) = $this->postApiV3AgentGrantsWithHttpInfo($post_api_v3_agent_grants_request, $contentType);
+        return $response;
+    }
+
+    /**
+     * Operation postApiV3AgentGrantsWithHttpInfo
+     *
+     * 签发 agent 写入授权
+     *
+     * @param  \WikipaliApi\Model\PostApiV3AgentGrantsRequest $post_api_v3_agent_grants_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3AgentGrants'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return array of \WikipaliApi\Model\PostApiV3AgentGrants200Response|\WikipaliApi\Model\PostApiV3AgentGrants200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function postApiV3AgentGrantsWithHttpInfo($post_api_v3_agent_grants_request, string $contentType = self::contentTypes['postApiV3AgentGrants'][0])
+    {
+        $request = $this->postApiV3AgentGrantsRequest($post_api_v3_agent_grants_request, $contentType);
+
+        try {
+            $options = $this->createHttpClientOption();
+            try {
+                $response = $this->client->send($request, $options);
+            } catch (RequestException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
+                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
+                );
+            } catch (ConnectException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    null,
+                    null
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+
+            switch($statusCode) {
+                case 200:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\PostApiV3AgentGrants200Response',
+                        $request,
+                        $response,
+                    );
+                case 201:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\PostApiV3AgentGrants200Response',
+                        $request,
+                        $response,
+                    );
+                case 401:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 403:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 422:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+            }
+
+            
+
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
+
+            return $this->handleResponseWithDataType(
+                '\WikipaliApi\Model\PostApiV3AgentGrants200Response',
+                $request,
+                $response,
+            );
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\PostApiV3AgentGrants200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 201:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\PostApiV3AgentGrants200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 401:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 403:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 422:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+            }
+        
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation postApiV3AgentGrantsAsync
+     *
+     * 签发 agent 写入授权
+     *
+     * @param  \WikipaliApi\Model\PostApiV3AgentGrantsRequest $post_api_v3_agent_grants_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3AgentGrants'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function postApiV3AgentGrantsAsync($post_api_v3_agent_grants_request, string $contentType = self::contentTypes['postApiV3AgentGrants'][0])
+    {
+        return $this->postApiV3AgentGrantsAsyncWithHttpInfo($post_api_v3_agent_grants_request, $contentType)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation postApiV3AgentGrantsAsyncWithHttpInfo
+     *
+     * 签发 agent 写入授权
+     *
+     * @param  \WikipaliApi\Model\PostApiV3AgentGrantsRequest $post_api_v3_agent_grants_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3AgentGrants'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function postApiV3AgentGrantsAsyncWithHttpInfo($post_api_v3_agent_grants_request, string $contentType = self::contentTypes['postApiV3AgentGrants'][0])
+    {
+        $returnType = '\WikipaliApi\Model\PostApiV3AgentGrants200Response';
+        $request = $this->postApiV3AgentGrantsRequest($post_api_v3_agent_grants_request, $contentType);
+
+        return $this->client
+            ->sendAsync($request, $this->createHttpClientOption())
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ($returnType !== 'string') {
+                            $content = json_decode($content);
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function ($exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $statusCode,
+                        $response->getHeaders(),
+                        (string) $response->getBody()
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'postApiV3AgentGrants'
+     *
+     * @param  \WikipaliApi\Model\PostApiV3AgentGrantsRequest $post_api_v3_agent_grants_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3AgentGrants'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    public function postApiV3AgentGrantsRequest($post_api_v3_agent_grants_request, string $contentType = self::contentTypes['postApiV3AgentGrants'][0])
+    {
+
+        // verify the required parameter 'post_api_v3_agent_grants_request' is set
+        if ($post_api_v3_agent_grants_request === null || (is_array($post_api_v3_agent_grants_request) && count($post_api_v3_agent_grants_request) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $post_api_v3_agent_grants_request when calling postApiV3AgentGrants'
+            );
+        }
+
+
+        $resourcePath = '/v3/agent-grants';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = '';
+        $multipart = false;
+
+
+
+
+
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json', 'application/problem+json', ],
+            $contentType,
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (isset($post_api_v3_agent_grants_request)) {
+            if (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the body
+                try {
+                    $httpBody = json_encode(ObjectSerializer::sanitizeForSerialization($post_api_v3_agent_grants_request), JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                $httpBody = $post_api_v3_agent_grants_request;
+            }
+        } elseif (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the form parameters
+                try {
+                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+        $query = ObjectSerializer::buildQuery($queryParams);
+        return new Request(
+            'POST',
+            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+            $headers,
+            $httpBody
+        );
+    }
+
+    /**
+     * Create http client option
+     *
+     * @throws \RuntimeException on file opening failure
+     * @return array of http client options
+     */
+    protected function createHttpClientOption()
+    {
+        $options = [];
+        if ($this->config->getDebug()) {
+            $options[RequestOptions::DEBUG] = fopen($this->config->getDebugFile(), 'a');
+            if (!$options[RequestOptions::DEBUG]) {
+                throw new \RuntimeException('Failed to open the debug file: ' . $this->config->getDebugFile());
+            }
+        }
+
+        if ($this->config->getCertFile()) {
+            $options[RequestOptions::CERT] = $this->config->getCertFile();
+        }
+
+        if ($this->config->getKeyFile()) {
+            $options[RequestOptions::SSL_KEY] = $this->config->getKeyFile();
+        }
+
+        return $options;
+    }
+
+    private function handleResponseWithDataType(
+        string $dataType,
+        RequestInterface $request,
+        ResponseInterface $response
+    ): array {
+        if ($dataType === '\SplFileObject') {
+            $content = $response->getBody(); //stream goes to serializer
+        } else {
+            $content = (string) $response->getBody();
+            if ($dataType !== 'string') {
+                try {
+                    $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $exception) {
+                    throw new ApiException(
+                        sprintf(
+                            'Error JSON decoding server response (%s)',
+                            $request->getUri()
+                        ),
+                        $response->getStatusCode(),
+                        $response->getHeaders(),
+                        $content
+                    );
+                }
+            }
+        }
+
+        return [
+            ObjectSerializer::deserialize($content, $dataType, []),
+            $response->getStatusCode(),
+            $response->getHeaders()
+        ];
+    }
+
+    private function responseWithinRangeCode(
+        string $rangeCode,
+        int $statusCode
+    ): bool {
+        $left = (int) ($rangeCode[0].'00');
+        $right = (int) ($rangeCode[0].'99');
+
+        return $statusCode >= $left && $statusCode <= $right;
+    }
+}

+ 3210 - 0
api-test/client/lib/Api/DiscussionsApi.php

@@ -0,0 +1,3210 @@
+<?php
+/**
+ * DiscussionsApi
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Api;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\ClientInterface;
+use GuzzleHttp\Exception\ConnectException;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Psr7\MultipartStream;
+use GuzzleHttp\Psr7\Request;
+use GuzzleHttp\RequestOptions;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use WikipaliApi\ApiException;
+use WikipaliApi\Configuration;
+use WikipaliApi\FormDataProcessor;
+use WikipaliApi\HeaderSelector;
+use WikipaliApi\ObjectSerializer;
+
+/**
+ * DiscussionsApi Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+class DiscussionsApi
+{
+    /**
+     * @var ClientInterface
+     */
+    protected $client;
+
+    /**
+     * @var Configuration
+     */
+    protected $config;
+
+    /**
+     * @var HeaderSelector
+     */
+    protected $headerSelector;
+
+    /**
+     * @var int Host index
+     */
+    protected $hostIndex;
+
+    /** @var string[] $contentTypes **/
+    public const contentTypes = [
+        'deleteApiV3DiscussionsDiscussion' => [
+            'application/json',
+        ],
+        'getApiV3Discussions' => [
+            'application/json',
+        ],
+        'getApiV3DiscussionsDiscussion' => [
+            'application/json',
+        ],
+        'getApiV3DiscussionsDiscussionReplies' => [
+            'application/json',
+        ],
+        'getApiV3DiscussionsSummary' => [
+            'application/json',
+        ],
+        'patchApiV3DiscussionsDiscussion' => [
+            'application/json',
+        ],
+        'postApiV3Discussions' => [
+            'application/json',
+        ],
+        'postApiV3DiscussionsBatchCount' => [
+            'application/json',
+        ],
+        'postApiV3DiscussionsDiscussionReplies' => [
+            'application/json',
+        ],
+    ];
+
+    /**
+     * @param ClientInterface $client
+     * @param Configuration   $config
+     * @param HeaderSelector  $selector
+     * @param int             $hostIndex (Optional) host index to select the list of hosts if defined in the OpenAPI spec
+     */
+    public function __construct(
+        ?ClientInterface $client = null,
+        ?Configuration $config = null,
+        ?HeaderSelector $selector = null,
+        int $hostIndex = 0
+    ) {
+        $this->client = $client ?: new Client();
+        $this->config = $config ?: Configuration::getDefaultConfiguration();
+        $this->headerSelector = $selector ?: new HeaderSelector();
+        $this->hostIndex = $hostIndex;
+    }
+
+    /**
+     * Set the host index
+     *
+     * @param int $hostIndex Host index (required)
+     */
+    public function setHostIndex($hostIndex): void
+    {
+        $this->hostIndex = $hostIndex;
+    }
+
+    /**
+     * Get the host index
+     *
+     * @return int Host index
+     */
+    public function getHostIndex()
+    {
+        return $this->hostIndex;
+    }
+
+    /**
+     * @return Configuration
+     */
+    public function getConfig()
+    {
+        return $this->config;
+    }
+
+    /**
+     * Operation deleteApiV3DiscussionsDiscussion
+     *
+     * 删除话题或回复(软删除)
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['deleteApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return \WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails
+     */
+    public function deleteApiV3DiscussionsDiscussion($discussion, string $contentType = self::contentTypes['deleteApiV3DiscussionsDiscussion'][0])
+    {
+        list($response) = $this->deleteApiV3DiscussionsDiscussionWithHttpInfo($discussion, $contentType);
+        return $response;
+    }
+
+    /**
+     * Operation deleteApiV3DiscussionsDiscussionWithHttpInfo
+     *
+     * 删除话题或回复(软删除)
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['deleteApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return array of \WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function deleteApiV3DiscussionsDiscussionWithHttpInfo($discussion, string $contentType = self::contentTypes['deleteApiV3DiscussionsDiscussion'][0])
+    {
+        $request = $this->deleteApiV3DiscussionsDiscussionRequest($discussion, $contentType);
+
+        try {
+            $options = $this->createHttpClientOption();
+            try {
+                $response = $this->client->send($request, $options);
+            } catch (RequestException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
+                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
+                );
+            } catch (ConnectException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    null,
+                    null
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+
+            switch($statusCode) {
+                case 200:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $request,
+                        $response,
+                    );
+                case 401:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 403:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 422:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+            }
+
+            
+
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
+
+            return $this->handleResponseWithDataType(
+                '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                $request,
+                $response,
+            );
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 401:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 403:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 422:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+            }
+        
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation deleteApiV3DiscussionsDiscussionAsync
+     *
+     * 删除话题或回复(软删除)
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['deleteApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function deleteApiV3DiscussionsDiscussionAsync($discussion, string $contentType = self::contentTypes['deleteApiV3DiscussionsDiscussion'][0])
+    {
+        return $this->deleteApiV3DiscussionsDiscussionAsyncWithHttpInfo($discussion, $contentType)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation deleteApiV3DiscussionsDiscussionAsyncWithHttpInfo
+     *
+     * 删除话题或回复(软删除)
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['deleteApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function deleteApiV3DiscussionsDiscussionAsyncWithHttpInfo($discussion, string $contentType = self::contentTypes['deleteApiV3DiscussionsDiscussion'][0])
+    {
+        $returnType = '\WikipaliApi\Model\PostApiV3Discussions200Response';
+        $request = $this->deleteApiV3DiscussionsDiscussionRequest($discussion, $contentType);
+
+        return $this->client
+            ->sendAsync($request, $this->createHttpClientOption())
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ($returnType !== 'string') {
+                            $content = json_decode($content);
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function ($exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $statusCode,
+                        $response->getHeaders(),
+                        (string) $response->getBody()
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'deleteApiV3DiscussionsDiscussion'
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['deleteApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    public function deleteApiV3DiscussionsDiscussionRequest($discussion, string $contentType = self::contentTypes['deleteApiV3DiscussionsDiscussion'][0])
+    {
+
+        // verify the required parameter 'discussion' is set
+        if ($discussion === null || (is_array($discussion) && count($discussion) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $discussion when calling deleteApiV3DiscussionsDiscussion'
+            );
+        }
+
+
+        $resourcePath = '/v3/discussions/{discussion}';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = '';
+        $multipart = false;
+
+
+
+        // path params
+        if ($discussion !== null) {
+            $resourcePath = str_replace(
+                '{discussion}',
+                ObjectSerializer::toPathValue($discussion),
+                $resourcePath
+            );
+        }
+
+
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json', 'application/problem+json', ],
+            $contentType,
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the form parameters
+                try {
+                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+        $query = ObjectSerializer::buildQuery($queryParams);
+        return new Request(
+            'DELETE',
+            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+            $headers,
+            $httpBody
+        );
+    }
+
+    /**
+     * Operation getApiV3Discussions
+     *
+     * 列出某个对象上的话题
+     *
+     * @param  string $target_type 挂靠对象的类型 (required)
+     * @param  string $target_id 挂靠对象的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
+     * @param  string|null $type 类型,不传则只列评论线程 (optional)
+     * @param  string|null $status 话题状态,不传则两种都要 (optional)
+     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3Discussions'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return \WikipaliApi\Model\GetApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails
+     */
+    public function getApiV3Discussions($target_type, $target_id, $per_page = 15, $type = null, $status = null, $severity = null, $sort = '-created_at', $page = 1, string $contentType = self::contentTypes['getApiV3Discussions'][0])
+    {
+        list($response) = $this->getApiV3DiscussionsWithHttpInfo($target_type, $target_id, $per_page, $type, $status, $severity, $sort, $page, $contentType);
+        return $response;
+    }
+
+    /**
+     * Operation getApiV3DiscussionsWithHttpInfo
+     *
+     * 列出某个对象上的话题
+     *
+     * @param  string $target_type 挂靠对象的类型 (required)
+     * @param  string $target_id 挂靠对象的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
+     * @param  string|null $type 类型,不传则只列评论线程 (optional)
+     * @param  string|null $status 话题状态,不传则两种都要 (optional)
+     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3Discussions'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return array of \WikipaliApi\Model\GetApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function getApiV3DiscussionsWithHttpInfo($target_type, $target_id, $per_page = 15, $type = null, $status = null, $severity = null, $sort = '-created_at', $page = 1, string $contentType = self::contentTypes['getApiV3Discussions'][0])
+    {
+        $request = $this->getApiV3DiscussionsRequest($target_type, $target_id, $per_page, $type, $status, $severity, $sort, $page, $contentType);
+
+        try {
+            $options = $this->createHttpClientOption();
+            try {
+                $response = $this->client->send($request, $options);
+            } catch (RequestException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
+                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
+                );
+            } catch (ConnectException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    null,
+                    null
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+
+            switch($statusCode) {
+                case 200:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\GetApiV3Discussions200Response',
+                        $request,
+                        $response,
+                    );
+                case 422:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+            }
+
+            
+
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
+
+            return $this->handleResponseWithDataType(
+                '\WikipaliApi\Model\GetApiV3Discussions200Response',
+                $request,
+                $response,
+            );
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\GetApiV3Discussions200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 422:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+            }
+        
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation getApiV3DiscussionsAsync
+     *
+     * 列出某个对象上的话题
+     *
+     * @param  string $target_type 挂靠对象的类型 (required)
+     * @param  string $target_id 挂靠对象的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
+     * @param  string|null $type 类型,不传则只列评论线程 (optional)
+     * @param  string|null $status 话题状态,不传则两种都要 (optional)
+     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3Discussions'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function getApiV3DiscussionsAsync($target_type, $target_id, $per_page = 15, $type = null, $status = null, $severity = null, $sort = '-created_at', $page = 1, string $contentType = self::contentTypes['getApiV3Discussions'][0])
+    {
+        return $this->getApiV3DiscussionsAsyncWithHttpInfo($target_type, $target_id, $per_page, $type, $status, $severity, $sort, $page, $contentType)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation getApiV3DiscussionsAsyncWithHttpInfo
+     *
+     * 列出某个对象上的话题
+     *
+     * @param  string $target_type 挂靠对象的类型 (required)
+     * @param  string $target_id 挂靠对象的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
+     * @param  string|null $type 类型,不传则只列评论线程 (optional)
+     * @param  string|null $status 话题状态,不传则两种都要 (optional)
+     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3Discussions'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function getApiV3DiscussionsAsyncWithHttpInfo($target_type, $target_id, $per_page = 15, $type = null, $status = null, $severity = null, $sort = '-created_at', $page = 1, string $contentType = self::contentTypes['getApiV3Discussions'][0])
+    {
+        $returnType = '\WikipaliApi\Model\GetApiV3Discussions200Response';
+        $request = $this->getApiV3DiscussionsRequest($target_type, $target_id, $per_page, $type, $status, $severity, $sort, $page, $contentType);
+
+        return $this->client
+            ->sendAsync($request, $this->createHttpClientOption())
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ($returnType !== 'string') {
+                            $content = json_decode($content);
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function ($exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $statusCode,
+                        $response->getHeaders(),
+                        (string) $response->getBody()
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'getApiV3Discussions'
+     *
+     * @param  string $target_type 挂靠对象的类型 (required)
+     * @param  string $target_id 挂靠对象的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
+     * @param  string|null $type 类型,不传则只列评论线程 (optional)
+     * @param  string|null $status 话题状态,不传则两种都要 (optional)
+     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  string|null $sort 排序,前缀 - 表示倒序 (optional, default to '-created_at')
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3Discussions'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    public function getApiV3DiscussionsRequest($target_type, $target_id, $per_page = 15, $type = null, $status = null, $severity = null, $sort = '-created_at', $page = 1, string $contentType = self::contentTypes['getApiV3Discussions'][0])
+    {
+
+        // verify the required parameter 'target_type' is set
+        if ($target_type === null || (is_array($target_type) && count($target_type) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $target_type when calling getApiV3Discussions'
+            );
+        }
+
+        // verify the required parameter 'target_id' is set
+        if ($target_id === null || (is_array($target_id) && count($target_id) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $target_id when calling getApiV3Discussions'
+            );
+        }
+
+
+
+
+
+
+
+
+        $resourcePath = '/v3/discussions';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = '';
+        $multipart = false;
+
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $per_page,
+            'per_page', // param base name
+            'integer', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $target_type,
+            'target_type', // param base name
+            'string', // openApiType
+            'form', // style
+            true, // explode
+            true // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $target_id,
+            'target_id', // param base name
+            'string', // openApiType
+            'form', // style
+            true, // explode
+            true // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $type,
+            'type', // param base name
+            'string', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $status,
+            'status', // param base name
+            'string', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $severity,
+            'severity', // param base name
+            'string', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $sort,
+            'sort', // param base name
+            'string', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $page,
+            'page', // param base name
+            'integer', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+
+
+
+
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json', 'application/problem+json', ],
+            $contentType,
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the form parameters
+                try {
+                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+        $query = ObjectSerializer::buildQuery($queryParams);
+        return new Request(
+            'GET',
+            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+            $headers,
+            $httpBody
+        );
+    }
+
+    /**
+     * Operation getApiV3DiscussionsDiscussion
+     *
+     * 获取单条话题或回复
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return \WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails
+     */
+    public function getApiV3DiscussionsDiscussion($discussion, string $contentType = self::contentTypes['getApiV3DiscussionsDiscussion'][0])
+    {
+        list($response) = $this->getApiV3DiscussionsDiscussionWithHttpInfo($discussion, $contentType);
+        return $response;
+    }
+
+    /**
+     * Operation getApiV3DiscussionsDiscussionWithHttpInfo
+     *
+     * 获取单条话题或回复
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return array of \WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function getApiV3DiscussionsDiscussionWithHttpInfo($discussion, string $contentType = self::contentTypes['getApiV3DiscussionsDiscussion'][0])
+    {
+        $request = $this->getApiV3DiscussionsDiscussionRequest($discussion, $contentType);
+
+        try {
+            $options = $this->createHttpClientOption();
+            try {
+                $response = $this->client->send($request, $options);
+            } catch (RequestException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
+                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
+                );
+            } catch (ConnectException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    null,
+                    null
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+
+            switch($statusCode) {
+                case 200:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $request,
+                        $response,
+                    );
+                case 422:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+            }
+
+            
+
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
+
+            return $this->handleResponseWithDataType(
+                '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                $request,
+                $response,
+            );
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 422:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+            }
+        
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation getApiV3DiscussionsDiscussionAsync
+     *
+     * 获取单条话题或回复
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function getApiV3DiscussionsDiscussionAsync($discussion, string $contentType = self::contentTypes['getApiV3DiscussionsDiscussion'][0])
+    {
+        return $this->getApiV3DiscussionsDiscussionAsyncWithHttpInfo($discussion, $contentType)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation getApiV3DiscussionsDiscussionAsyncWithHttpInfo
+     *
+     * 获取单条话题或回复
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function getApiV3DiscussionsDiscussionAsyncWithHttpInfo($discussion, string $contentType = self::contentTypes['getApiV3DiscussionsDiscussion'][0])
+    {
+        $returnType = '\WikipaliApi\Model\PostApiV3Discussions200Response';
+        $request = $this->getApiV3DiscussionsDiscussionRequest($discussion, $contentType);
+
+        return $this->client
+            ->sendAsync($request, $this->createHttpClientOption())
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ($returnType !== 'string') {
+                            $content = json_decode($content);
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function ($exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $statusCode,
+                        $response->getHeaders(),
+                        (string) $response->getBody()
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'getApiV3DiscussionsDiscussion'
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    public function getApiV3DiscussionsDiscussionRequest($discussion, string $contentType = self::contentTypes['getApiV3DiscussionsDiscussion'][0])
+    {
+
+        // verify the required parameter 'discussion' is set
+        if ($discussion === null || (is_array($discussion) && count($discussion) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $discussion when calling getApiV3DiscussionsDiscussion'
+            );
+        }
+
+
+        $resourcePath = '/v3/discussions/{discussion}';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = '';
+        $multipart = false;
+
+
+
+        // path params
+        if ($discussion !== null) {
+            $resourcePath = str_replace(
+                '{discussion}',
+                ObjectSerializer::toPathValue($discussion),
+                $resourcePath
+            );
+        }
+
+
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json', 'application/problem+json', ],
+            $contentType,
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the form parameters
+                try {
+                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+        $query = ObjectSerializer::buildQuery($queryParams);
+        return new Request(
+            'GET',
+            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+            $headers,
+            $httpBody
+        );
+    }
+
+    /**
+     * Operation getApiV3DiscussionsDiscussionReplies
+     *
+     * 列出话题的回复
+     *
+     * @param  string $discussion 话题的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 50)
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsDiscussionReplies'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return \WikipaliApi\Model\GetApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails
+     */
+    public function getApiV3DiscussionsDiscussionReplies($discussion, $per_page = 50, $page = 1, string $contentType = self::contentTypes['getApiV3DiscussionsDiscussionReplies'][0])
+    {
+        list($response) = $this->getApiV3DiscussionsDiscussionRepliesWithHttpInfo($discussion, $per_page, $page, $contentType);
+        return $response;
+    }
+
+    /**
+     * Operation getApiV3DiscussionsDiscussionRepliesWithHttpInfo
+     *
+     * 列出话题的回复
+     *
+     * @param  string $discussion 话题的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 50)
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsDiscussionReplies'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return array of \WikipaliApi\Model\GetApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function getApiV3DiscussionsDiscussionRepliesWithHttpInfo($discussion, $per_page = 50, $page = 1, string $contentType = self::contentTypes['getApiV3DiscussionsDiscussionReplies'][0])
+    {
+        $request = $this->getApiV3DiscussionsDiscussionRepliesRequest($discussion, $per_page, $page, $contentType);
+
+        try {
+            $options = $this->createHttpClientOption();
+            try {
+                $response = $this->client->send($request, $options);
+            } catch (RequestException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
+                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
+                );
+            } catch (ConnectException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    null,
+                    null
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+
+            switch($statusCode) {
+                case 200:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\GetApiV3Discussions200Response',
+                        $request,
+                        $response,
+                    );
+                case 422:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+            }
+
+            
+
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
+
+            return $this->handleResponseWithDataType(
+                '\WikipaliApi\Model\GetApiV3Discussions200Response',
+                $request,
+                $response,
+            );
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\GetApiV3Discussions200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 422:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+            }
+        
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation getApiV3DiscussionsDiscussionRepliesAsync
+     *
+     * 列出话题的回复
+     *
+     * @param  string $discussion 话题的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 50)
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsDiscussionReplies'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function getApiV3DiscussionsDiscussionRepliesAsync($discussion, $per_page = 50, $page = 1, string $contentType = self::contentTypes['getApiV3DiscussionsDiscussionReplies'][0])
+    {
+        return $this->getApiV3DiscussionsDiscussionRepliesAsyncWithHttpInfo($discussion, $per_page, $page, $contentType)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation getApiV3DiscussionsDiscussionRepliesAsyncWithHttpInfo
+     *
+     * 列出话题的回复
+     *
+     * @param  string $discussion 话题的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 50)
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsDiscussionReplies'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function getApiV3DiscussionsDiscussionRepliesAsyncWithHttpInfo($discussion, $per_page = 50, $page = 1, string $contentType = self::contentTypes['getApiV3DiscussionsDiscussionReplies'][0])
+    {
+        $returnType = '\WikipaliApi\Model\GetApiV3Discussions200Response';
+        $request = $this->getApiV3DiscussionsDiscussionRepliesRequest($discussion, $per_page, $page, $contentType);
+
+        return $this->client
+            ->sendAsync($request, $this->createHttpClientOption())
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ($returnType !== 'string') {
+                            $content = json_decode($content);
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function ($exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $statusCode,
+                        $response->getHeaders(),
+                        (string) $response->getBody()
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'getApiV3DiscussionsDiscussionReplies'
+     *
+     * @param  string $discussion 话题的 uuid (required)
+     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 50)
+     * @param  int|null $page 页码 (optional, default to 1)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsDiscussionReplies'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    public function getApiV3DiscussionsDiscussionRepliesRequest($discussion, $per_page = 50, $page = 1, string $contentType = self::contentTypes['getApiV3DiscussionsDiscussionReplies'][0])
+    {
+
+        // verify the required parameter 'discussion' is set
+        if ($discussion === null || (is_array($discussion) && count($discussion) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $discussion when calling getApiV3DiscussionsDiscussionReplies'
+            );
+        }
+
+
+
+
+        $resourcePath = '/v3/discussions/{discussion}/replies';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = '';
+        $multipart = false;
+
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $per_page,
+            'per_page', // param base name
+            'integer', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $page,
+            'page', // param base name
+            'integer', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+
+
+        // path params
+        if ($discussion !== null) {
+            $resourcePath = str_replace(
+                '{discussion}',
+                ObjectSerializer::toPathValue($discussion),
+                $resourcePath
+            );
+        }
+
+
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json', 'application/problem+json', ],
+            $contentType,
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the form parameters
+                try {
+                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+        $query = ObjectSerializer::buildQuery($queryParams);
+        return new Request(
+            'GET',
+            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+            $headers,
+            $httpBody
+        );
+    }
+
+    /**
+     * Operation getApiV3DiscussionsSummary
+     *
+     * 话题概况
+     *
+     * @param  string $target_type 挂靠对象的类型 (required)
+     * @param  string $target_id 挂靠对象的 uuid (required)
+     * @param  string|null $type 只数这种类型,不传则只数评论线程 (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return \WikipaliApi\Model\GetApiV3DiscussionsSummary200Response|\WikipaliApi\Model\ProblemDetails
+     */
+    public function getApiV3DiscussionsSummary($target_type, $target_id, $type = null, string $contentType = self::contentTypes['getApiV3DiscussionsSummary'][0])
+    {
+        list($response) = $this->getApiV3DiscussionsSummaryWithHttpInfo($target_type, $target_id, $type, $contentType);
+        return $response;
+    }
+
+    /**
+     * Operation getApiV3DiscussionsSummaryWithHttpInfo
+     *
+     * 话题概况
+     *
+     * @param  string $target_type 挂靠对象的类型 (required)
+     * @param  string $target_id 挂靠对象的 uuid (required)
+     * @param  string|null $type 只数这种类型,不传则只数评论线程 (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return array of \WikipaliApi\Model\GetApiV3DiscussionsSummary200Response|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function getApiV3DiscussionsSummaryWithHttpInfo($target_type, $target_id, $type = null, string $contentType = self::contentTypes['getApiV3DiscussionsSummary'][0])
+    {
+        $request = $this->getApiV3DiscussionsSummaryRequest($target_type, $target_id, $type, $contentType);
+
+        try {
+            $options = $this->createHttpClientOption();
+            try {
+                $response = $this->client->send($request, $options);
+            } catch (RequestException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
+                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
+                );
+            } catch (ConnectException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    null,
+                    null
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+
+            switch($statusCode) {
+                case 200:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\GetApiV3DiscussionsSummary200Response',
+                        $request,
+                        $response,
+                    );
+                case 422:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+            }
+
+            
+
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
+
+            return $this->handleResponseWithDataType(
+                '\WikipaliApi\Model\GetApiV3DiscussionsSummary200Response',
+                $request,
+                $response,
+            );
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\GetApiV3DiscussionsSummary200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 422:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+            }
+        
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation getApiV3DiscussionsSummaryAsync
+     *
+     * 话题概况
+     *
+     * @param  string $target_type 挂靠对象的类型 (required)
+     * @param  string $target_id 挂靠对象的 uuid (required)
+     * @param  string|null $type 只数这种类型,不传则只数评论线程 (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function getApiV3DiscussionsSummaryAsync($target_type, $target_id, $type = null, string $contentType = self::contentTypes['getApiV3DiscussionsSummary'][0])
+    {
+        return $this->getApiV3DiscussionsSummaryAsyncWithHttpInfo($target_type, $target_id, $type, $contentType)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation getApiV3DiscussionsSummaryAsyncWithHttpInfo
+     *
+     * 话题概况
+     *
+     * @param  string $target_type 挂靠对象的类型 (required)
+     * @param  string $target_id 挂靠对象的 uuid (required)
+     * @param  string|null $type 只数这种类型,不传则只数评论线程 (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function getApiV3DiscussionsSummaryAsyncWithHttpInfo($target_type, $target_id, $type = null, string $contentType = self::contentTypes['getApiV3DiscussionsSummary'][0])
+    {
+        $returnType = '\WikipaliApi\Model\GetApiV3DiscussionsSummary200Response';
+        $request = $this->getApiV3DiscussionsSummaryRequest($target_type, $target_id, $type, $contentType);
+
+        return $this->client
+            ->sendAsync($request, $this->createHttpClientOption())
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ($returnType !== 'string') {
+                            $content = json_decode($content);
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function ($exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $statusCode,
+                        $response->getHeaders(),
+                        (string) $response->getBody()
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'getApiV3DiscussionsSummary'
+     *
+     * @param  string $target_type 挂靠对象的类型 (required)
+     * @param  string $target_id 挂靠对象的 uuid (required)
+     * @param  string|null $type 只数这种类型,不传则只数评论线程 (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3DiscussionsSummary'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    public function getApiV3DiscussionsSummaryRequest($target_type, $target_id, $type = null, string $contentType = self::contentTypes['getApiV3DiscussionsSummary'][0])
+    {
+
+        // verify the required parameter 'target_type' is set
+        if ($target_type === null || (is_array($target_type) && count($target_type) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $target_type when calling getApiV3DiscussionsSummary'
+            );
+        }
+
+        // verify the required parameter 'target_id' is set
+        if ($target_id === null || (is_array($target_id) && count($target_id) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $target_id when calling getApiV3DiscussionsSummary'
+            );
+        }
+
+
+
+        $resourcePath = '/v3/discussions/summary';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = '';
+        $multipart = false;
+
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $target_type,
+            'target_type', // param base name
+            'string', // openApiType
+            'form', // style
+            true, // explode
+            true // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $target_id,
+            'target_id', // param base name
+            'string', // openApiType
+            'form', // style
+            true, // explode
+            true // required
+        ) ?? []);
+        // query params
+        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
+            $type,
+            'type', // param base name
+            'string', // openApiType
+            'form', // style
+            true, // explode
+            false // required
+        ) ?? []);
+
+
+
+
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json', 'application/problem+json', ],
+            $contentType,
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the form parameters
+                try {
+                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+        $query = ObjectSerializer::buildQuery($queryParams);
+        return new Request(
+            'GET',
+            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+            $headers,
+            $httpBody
+        );
+    }
+
+    /**
+     * Operation patchApiV3DiscussionsDiscussion
+     *
+     * 修改话题或回复(局部更新)
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  \WikipaliApi\Model\PatchApiV3DiscussionsDiscussionRequest|null $patch_api_v3_discussions_discussion_request patch_api_v3_discussions_discussion_request (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['patchApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return \WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails
+     */
+    public function patchApiV3DiscussionsDiscussion($discussion, $patch_api_v3_discussions_discussion_request = null, string $contentType = self::contentTypes['patchApiV3DiscussionsDiscussion'][0])
+    {
+        list($response) = $this->patchApiV3DiscussionsDiscussionWithHttpInfo($discussion, $patch_api_v3_discussions_discussion_request, $contentType);
+        return $response;
+    }
+
+    /**
+     * Operation patchApiV3DiscussionsDiscussionWithHttpInfo
+     *
+     * 修改话题或回复(局部更新)
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  \WikipaliApi\Model\PatchApiV3DiscussionsDiscussionRequest|null $patch_api_v3_discussions_discussion_request (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['patchApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return array of \WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function patchApiV3DiscussionsDiscussionWithHttpInfo($discussion, $patch_api_v3_discussions_discussion_request = null, string $contentType = self::contentTypes['patchApiV3DiscussionsDiscussion'][0])
+    {
+        $request = $this->patchApiV3DiscussionsDiscussionRequest($discussion, $patch_api_v3_discussions_discussion_request, $contentType);
+
+        try {
+            $options = $this->createHttpClientOption();
+            try {
+                $response = $this->client->send($request, $options);
+            } catch (RequestException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
+                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
+                );
+            } catch (ConnectException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    null,
+                    null
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+
+            switch($statusCode) {
+                case 200:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $request,
+                        $response,
+                    );
+                case 401:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 403:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 422:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+            }
+
+            
+
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
+
+            return $this->handleResponseWithDataType(
+                '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                $request,
+                $response,
+            );
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 401:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 403:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 422:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+            }
+        
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation patchApiV3DiscussionsDiscussionAsync
+     *
+     * 修改话题或回复(局部更新)
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  \WikipaliApi\Model\PatchApiV3DiscussionsDiscussionRequest|null $patch_api_v3_discussions_discussion_request (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['patchApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function patchApiV3DiscussionsDiscussionAsync($discussion, $patch_api_v3_discussions_discussion_request = null, string $contentType = self::contentTypes['patchApiV3DiscussionsDiscussion'][0])
+    {
+        return $this->patchApiV3DiscussionsDiscussionAsyncWithHttpInfo($discussion, $patch_api_v3_discussions_discussion_request, $contentType)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation patchApiV3DiscussionsDiscussionAsyncWithHttpInfo
+     *
+     * 修改话题或回复(局部更新)
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  \WikipaliApi\Model\PatchApiV3DiscussionsDiscussionRequest|null $patch_api_v3_discussions_discussion_request (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['patchApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function patchApiV3DiscussionsDiscussionAsyncWithHttpInfo($discussion, $patch_api_v3_discussions_discussion_request = null, string $contentType = self::contentTypes['patchApiV3DiscussionsDiscussion'][0])
+    {
+        $returnType = '\WikipaliApi\Model\PostApiV3Discussions200Response';
+        $request = $this->patchApiV3DiscussionsDiscussionRequest($discussion, $patch_api_v3_discussions_discussion_request, $contentType);
+
+        return $this->client
+            ->sendAsync($request, $this->createHttpClientOption())
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ($returnType !== 'string') {
+                            $content = json_decode($content);
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function ($exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $statusCode,
+                        $response->getHeaders(),
+                        (string) $response->getBody()
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'patchApiV3DiscussionsDiscussion'
+     *
+     * @param  string $discussion 话题或回复的 uuid (required)
+     * @param  \WikipaliApi\Model\PatchApiV3DiscussionsDiscussionRequest|null $patch_api_v3_discussions_discussion_request (optional)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['patchApiV3DiscussionsDiscussion'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    public function patchApiV3DiscussionsDiscussionRequest($discussion, $patch_api_v3_discussions_discussion_request = null, string $contentType = self::contentTypes['patchApiV3DiscussionsDiscussion'][0])
+    {
+
+        // verify the required parameter 'discussion' is set
+        if ($discussion === null || (is_array($discussion) && count($discussion) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $discussion when calling patchApiV3DiscussionsDiscussion'
+            );
+        }
+
+
+
+        $resourcePath = '/v3/discussions/{discussion}';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = '';
+        $multipart = false;
+
+
+
+        // path params
+        if ($discussion !== null) {
+            $resourcePath = str_replace(
+                '{discussion}',
+                ObjectSerializer::toPathValue($discussion),
+                $resourcePath
+            );
+        }
+
+
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json', 'application/problem+json', ],
+            $contentType,
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (isset($patch_api_v3_discussions_discussion_request)) {
+            if (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the body
+                try {
+                    $httpBody = json_encode(ObjectSerializer::sanitizeForSerialization($patch_api_v3_discussions_discussion_request), JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                $httpBody = $patch_api_v3_discussions_discussion_request;
+            }
+        } elseif (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the form parameters
+                try {
+                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+        $query = ObjectSerializer::buildQuery($queryParams);
+        return new Request(
+            'PATCH',
+            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+            $headers,
+            $httpBody
+        );
+    }
+
+    /**
+     * Operation postApiV3Discussions
+     *
+     * 发起话题或新建批注
+     *
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsRequest $post_api_v3_discussions_request post_api_v3_discussions_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3Discussions'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return \WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails
+     */
+    public function postApiV3Discussions($post_api_v3_discussions_request, string $contentType = self::contentTypes['postApiV3Discussions'][0])
+    {
+        list($response) = $this->postApiV3DiscussionsWithHttpInfo($post_api_v3_discussions_request, $contentType);
+        return $response;
+    }
+
+    /**
+     * Operation postApiV3DiscussionsWithHttpInfo
+     *
+     * 发起话题或新建批注
+     *
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsRequest $post_api_v3_discussions_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3Discussions'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return array of \WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function postApiV3DiscussionsWithHttpInfo($post_api_v3_discussions_request, string $contentType = self::contentTypes['postApiV3Discussions'][0])
+    {
+        $request = $this->postApiV3DiscussionsRequest($post_api_v3_discussions_request, $contentType);
+
+        try {
+            $options = $this->createHttpClientOption();
+            try {
+                $response = $this->client->send($request, $options);
+            } catch (RequestException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
+                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
+                );
+            } catch (ConnectException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    null,
+                    null
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+
+            switch($statusCode) {
+                case 200:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $request,
+                        $response,
+                    );
+                case 201:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $request,
+                        $response,
+                    );
+                case 401:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 403:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 422:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+            }
+
+            
+
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
+
+            return $this->handleResponseWithDataType(
+                '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                $request,
+                $response,
+            );
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 201:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 401:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 403:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 422:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+            }
+        
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation postApiV3DiscussionsAsync
+     *
+     * 发起话题或新建批注
+     *
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsRequest $post_api_v3_discussions_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3Discussions'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function postApiV3DiscussionsAsync($post_api_v3_discussions_request, string $contentType = self::contentTypes['postApiV3Discussions'][0])
+    {
+        return $this->postApiV3DiscussionsAsyncWithHttpInfo($post_api_v3_discussions_request, $contentType)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation postApiV3DiscussionsAsyncWithHttpInfo
+     *
+     * 发起话题或新建批注
+     *
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsRequest $post_api_v3_discussions_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3Discussions'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function postApiV3DiscussionsAsyncWithHttpInfo($post_api_v3_discussions_request, string $contentType = self::contentTypes['postApiV3Discussions'][0])
+    {
+        $returnType = '\WikipaliApi\Model\PostApiV3Discussions200Response';
+        $request = $this->postApiV3DiscussionsRequest($post_api_v3_discussions_request, $contentType);
+
+        return $this->client
+            ->sendAsync($request, $this->createHttpClientOption())
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ($returnType !== 'string') {
+                            $content = json_decode($content);
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function ($exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $statusCode,
+                        $response->getHeaders(),
+                        (string) $response->getBody()
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'postApiV3Discussions'
+     *
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsRequest $post_api_v3_discussions_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3Discussions'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    public function postApiV3DiscussionsRequest($post_api_v3_discussions_request, string $contentType = self::contentTypes['postApiV3Discussions'][0])
+    {
+
+        // verify the required parameter 'post_api_v3_discussions_request' is set
+        if ($post_api_v3_discussions_request === null || (is_array($post_api_v3_discussions_request) && count($post_api_v3_discussions_request) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $post_api_v3_discussions_request when calling postApiV3Discussions'
+            );
+        }
+
+
+        $resourcePath = '/v3/discussions';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = '';
+        $multipart = false;
+
+
+
+
+
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json', 'application/problem+json', ],
+            $contentType,
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (isset($post_api_v3_discussions_request)) {
+            if (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the body
+                try {
+                    $httpBody = json_encode(ObjectSerializer::sanitizeForSerialization($post_api_v3_discussions_request), JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                $httpBody = $post_api_v3_discussions_request;
+            }
+        } elseif (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the form parameters
+                try {
+                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+        $query = ObjectSerializer::buildQuery($queryParams);
+        return new Request(
+            'POST',
+            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+            $headers,
+            $httpBody
+        );
+    }
+
+    /**
+     * Operation postApiV3DiscussionsBatchCount
+     *
+     * 批量取讨论角标
+     *
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsBatchCountRequest $post_api_v3_discussions_batch_count_request post_api_v3_discussions_batch_count_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3DiscussionsBatchCount'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return \WikipaliApi\Model\PostApiV3DiscussionsBatchCount200Response|\WikipaliApi\Model\ProblemDetails
+     */
+    public function postApiV3DiscussionsBatchCount($post_api_v3_discussions_batch_count_request, string $contentType = self::contentTypes['postApiV3DiscussionsBatchCount'][0])
+    {
+        list($response) = $this->postApiV3DiscussionsBatchCountWithHttpInfo($post_api_v3_discussions_batch_count_request, $contentType);
+        return $response;
+    }
+
+    /**
+     * Operation postApiV3DiscussionsBatchCountWithHttpInfo
+     *
+     * 批量取讨论角标
+     *
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsBatchCountRequest $post_api_v3_discussions_batch_count_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3DiscussionsBatchCount'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return array of \WikipaliApi\Model\PostApiV3DiscussionsBatchCount200Response|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function postApiV3DiscussionsBatchCountWithHttpInfo($post_api_v3_discussions_batch_count_request, string $contentType = self::contentTypes['postApiV3DiscussionsBatchCount'][0])
+    {
+        $request = $this->postApiV3DiscussionsBatchCountRequest($post_api_v3_discussions_batch_count_request, $contentType);
+
+        try {
+            $options = $this->createHttpClientOption();
+            try {
+                $response = $this->client->send($request, $options);
+            } catch (RequestException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
+                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
+                );
+            } catch (ConnectException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    null,
+                    null
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+
+            switch($statusCode) {
+                case 200:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\PostApiV3DiscussionsBatchCount200Response',
+                        $request,
+                        $response,
+                    );
+                case 422:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+            }
+
+            
+
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
+
+            return $this->handleResponseWithDataType(
+                '\WikipaliApi\Model\PostApiV3DiscussionsBatchCount200Response',
+                $request,
+                $response,
+            );
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\PostApiV3DiscussionsBatchCount200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 422:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+            }
+        
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation postApiV3DiscussionsBatchCountAsync
+     *
+     * 批量取讨论角标
+     *
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsBatchCountRequest $post_api_v3_discussions_batch_count_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3DiscussionsBatchCount'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function postApiV3DiscussionsBatchCountAsync($post_api_v3_discussions_batch_count_request, string $contentType = self::contentTypes['postApiV3DiscussionsBatchCount'][0])
+    {
+        return $this->postApiV3DiscussionsBatchCountAsyncWithHttpInfo($post_api_v3_discussions_batch_count_request, $contentType)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation postApiV3DiscussionsBatchCountAsyncWithHttpInfo
+     *
+     * 批量取讨论角标
+     *
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsBatchCountRequest $post_api_v3_discussions_batch_count_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3DiscussionsBatchCount'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function postApiV3DiscussionsBatchCountAsyncWithHttpInfo($post_api_v3_discussions_batch_count_request, string $contentType = self::contentTypes['postApiV3DiscussionsBatchCount'][0])
+    {
+        $returnType = '\WikipaliApi\Model\PostApiV3DiscussionsBatchCount200Response';
+        $request = $this->postApiV3DiscussionsBatchCountRequest($post_api_v3_discussions_batch_count_request, $contentType);
+
+        return $this->client
+            ->sendAsync($request, $this->createHttpClientOption())
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ($returnType !== 'string') {
+                            $content = json_decode($content);
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function ($exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $statusCode,
+                        $response->getHeaders(),
+                        (string) $response->getBody()
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'postApiV3DiscussionsBatchCount'
+     *
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsBatchCountRequest $post_api_v3_discussions_batch_count_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3DiscussionsBatchCount'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    public function postApiV3DiscussionsBatchCountRequest($post_api_v3_discussions_batch_count_request, string $contentType = self::contentTypes['postApiV3DiscussionsBatchCount'][0])
+    {
+
+        // verify the required parameter 'post_api_v3_discussions_batch_count_request' is set
+        if ($post_api_v3_discussions_batch_count_request === null || (is_array($post_api_v3_discussions_batch_count_request) && count($post_api_v3_discussions_batch_count_request) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $post_api_v3_discussions_batch_count_request when calling postApiV3DiscussionsBatchCount'
+            );
+        }
+
+
+        $resourcePath = '/v3/discussions/batch-count';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = '';
+        $multipart = false;
+
+
+
+
+
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json', 'application/problem+json', ],
+            $contentType,
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (isset($post_api_v3_discussions_batch_count_request)) {
+            if (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the body
+                try {
+                    $httpBody = json_encode(ObjectSerializer::sanitizeForSerialization($post_api_v3_discussions_batch_count_request), JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                $httpBody = $post_api_v3_discussions_batch_count_request;
+            }
+        } elseif (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the form parameters
+                try {
+                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+        $query = ObjectSerializer::buildQuery($queryParams);
+        return new Request(
+            'POST',
+            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+            $headers,
+            $httpBody
+        );
+    }
+
+    /**
+     * Operation postApiV3DiscussionsDiscussionReplies
+     *
+     * 回复话题
+     *
+     * @param  string $discussion 话题的 uuid (required)
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsDiscussionRepliesRequest $post_api_v3_discussions_discussion_replies_request post_api_v3_discussions_discussion_replies_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3DiscussionsDiscussionReplies'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return \WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails
+     */
+    public function postApiV3DiscussionsDiscussionReplies($discussion, $post_api_v3_discussions_discussion_replies_request, string $contentType = self::contentTypes['postApiV3DiscussionsDiscussionReplies'][0])
+    {
+        list($response) = $this->postApiV3DiscussionsDiscussionRepliesWithHttpInfo($discussion, $post_api_v3_discussions_discussion_replies_request, $contentType);
+        return $response;
+    }
+
+    /**
+     * Operation postApiV3DiscussionsDiscussionRepliesWithHttpInfo
+     *
+     * 回复话题
+     *
+     * @param  string $discussion 话题的 uuid (required)
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsDiscussionRepliesRequest $post_api_v3_discussions_discussion_replies_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3DiscussionsDiscussionReplies'] to see the possible values for this operation
+     *
+     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
+     * @throws \InvalidArgumentException
+     * @return array of \WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\PostApiV3Discussions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
+     */
+    public function postApiV3DiscussionsDiscussionRepliesWithHttpInfo($discussion, $post_api_v3_discussions_discussion_replies_request, string $contentType = self::contentTypes['postApiV3DiscussionsDiscussionReplies'][0])
+    {
+        $request = $this->postApiV3DiscussionsDiscussionRepliesRequest($discussion, $post_api_v3_discussions_discussion_replies_request, $contentType);
+
+        try {
+            $options = $this->createHttpClientOption();
+            try {
+                $response = $this->client->send($request, $options);
+            } catch (RequestException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
+                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
+                );
+            } catch (ConnectException $e) {
+                throw new ApiException(
+                    "[{$e->getCode()}] {$e->getMessage()}",
+                    (int) $e->getCode(),
+                    null,
+                    null
+                );
+            }
+
+            $statusCode = $response->getStatusCode();
+
+
+            switch($statusCode) {
+                case 200:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $request,
+                        $response,
+                    );
+                case 201:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $request,
+                        $response,
+                    );
+                case 401:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 403:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 409:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+                case 422:
+                    return $this->handleResponseWithDataType(
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $request,
+                        $response,
+                    );
+            }
+
+            
+
+            if ($statusCode < 200 || $statusCode > 299) {
+                throw new ApiException(
+                    sprintf(
+                        '[%d] Error connecting to the API (%s)',
+                        $statusCode,
+                        (string) $request->getUri()
+                    ),
+                    $statusCode,
+                    $response->getHeaders(),
+                    (string) $response->getBody()
+                );
+            }
+
+            return $this->handleResponseWithDataType(
+                '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                $request,
+                $response,
+            );
+        } catch (ApiException $e) {
+            switch ($e->getCode()) {
+                case 200:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 201:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\PostApiV3Discussions200Response',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 401:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 403:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 409:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+                case 422:
+                    $data = ObjectSerializer::deserialize(
+                        $e->getResponseBody(),
+                        '\WikipaliApi\Model\ProblemDetails',
+                        $e->getResponseHeaders()
+                    );
+                    $e->setResponseObject($data);
+                    throw $e;
+            }
+        
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Operation postApiV3DiscussionsDiscussionRepliesAsync
+     *
+     * 回复话题
+     *
+     * @param  string $discussion 话题的 uuid (required)
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsDiscussionRepliesRequest $post_api_v3_discussions_discussion_replies_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3DiscussionsDiscussionReplies'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function postApiV3DiscussionsDiscussionRepliesAsync($discussion, $post_api_v3_discussions_discussion_replies_request, string $contentType = self::contentTypes['postApiV3DiscussionsDiscussionReplies'][0])
+    {
+        return $this->postApiV3DiscussionsDiscussionRepliesAsyncWithHttpInfo($discussion, $post_api_v3_discussions_discussion_replies_request, $contentType)
+            ->then(
+                function ($response) {
+                    return $response[0];
+                }
+            );
+    }
+
+    /**
+     * Operation postApiV3DiscussionsDiscussionRepliesAsyncWithHttpInfo
+     *
+     * 回复话题
+     *
+     * @param  string $discussion 话题的 uuid (required)
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsDiscussionRepliesRequest $post_api_v3_discussions_discussion_replies_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3DiscussionsDiscussionReplies'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Promise\PromiseInterface
+     */
+    public function postApiV3DiscussionsDiscussionRepliesAsyncWithHttpInfo($discussion, $post_api_v3_discussions_discussion_replies_request, string $contentType = self::contentTypes['postApiV3DiscussionsDiscussionReplies'][0])
+    {
+        $returnType = '\WikipaliApi\Model\PostApiV3Discussions200Response';
+        $request = $this->postApiV3DiscussionsDiscussionRepliesRequest($discussion, $post_api_v3_discussions_discussion_replies_request, $contentType);
+
+        return $this->client
+            ->sendAsync($request, $this->createHttpClientOption())
+            ->then(
+                function ($response) use ($returnType) {
+                    if ($returnType === '\SplFileObject') {
+                        $content = $response->getBody(); //stream goes to serializer
+                    } else {
+                        $content = (string) $response->getBody();
+                        if ($returnType !== 'string') {
+                            $content = json_decode($content);
+                        }
+                    }
+
+                    return [
+                        ObjectSerializer::deserialize($content, $returnType, []),
+                        $response->getStatusCode(),
+                        $response->getHeaders()
+                    ];
+                },
+                function ($exception) {
+                    $response = $exception->getResponse();
+                    $statusCode = $response->getStatusCode();
+                    throw new ApiException(
+                        sprintf(
+                            '[%d] Error connecting to the API (%s)',
+                            $statusCode,
+                            $exception->getRequest()->getUri()
+                        ),
+                        $statusCode,
+                        $response->getHeaders(),
+                        (string) $response->getBody()
+                    );
+                }
+            );
+    }
+
+    /**
+     * Create request for operation 'postApiV3DiscussionsDiscussionReplies'
+     *
+     * @param  string $discussion 话题的 uuid (required)
+     * @param  \WikipaliApi\Model\PostApiV3DiscussionsDiscussionRepliesRequest $post_api_v3_discussions_discussion_replies_request (required)
+     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3DiscussionsDiscussionReplies'] to see the possible values for this operation
+     *
+     * @throws \InvalidArgumentException
+     * @return \GuzzleHttp\Psr7\Request
+     */
+    public function postApiV3DiscussionsDiscussionRepliesRequest($discussion, $post_api_v3_discussions_discussion_replies_request, string $contentType = self::contentTypes['postApiV3DiscussionsDiscussionReplies'][0])
+    {
+
+        // verify the required parameter 'discussion' is set
+        if ($discussion === null || (is_array($discussion) && count($discussion) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $discussion when calling postApiV3DiscussionsDiscussionReplies'
+            );
+        }
+
+        // verify the required parameter 'post_api_v3_discussions_discussion_replies_request' is set
+        if ($post_api_v3_discussions_discussion_replies_request === null || (is_array($post_api_v3_discussions_discussion_replies_request) && count($post_api_v3_discussions_discussion_replies_request) === 0)) {
+            throw new \InvalidArgumentException(
+                'Missing the required parameter $post_api_v3_discussions_discussion_replies_request when calling postApiV3DiscussionsDiscussionReplies'
+            );
+        }
+
+
+        $resourcePath = '/v3/discussions/{discussion}/replies';
+        $formParams = [];
+        $queryParams = [];
+        $headerParams = [];
+        $httpBody = '';
+        $multipart = false;
+
+
+
+        // path params
+        if ($discussion !== null) {
+            $resourcePath = str_replace(
+                '{discussion}',
+                ObjectSerializer::toPathValue($discussion),
+                $resourcePath
+            );
+        }
+
+
+        $headers = $this->headerSelector->selectHeaders(
+            ['application/json', 'application/problem+json', ],
+            $contentType,
+            $multipart
+        );
+
+        // for model (json/xml)
+        if (isset($post_api_v3_discussions_discussion_replies_request)) {
+            if (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the body
+                try {
+                    $httpBody = json_encode(ObjectSerializer::sanitizeForSerialization($post_api_v3_discussions_discussion_replies_request), JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                $httpBody = $post_api_v3_discussions_discussion_replies_request;
+            }
+        } elseif (count($formParams) > 0) {
+            if ($multipart) {
+                $multipartContents = [];
+                foreach ($formParams as $formParamName => $formParamValue) {
+                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
+                    foreach ($formParamValueItems as $formParamValueItem) {
+                        $multipartContents[] = [
+                            'name' => $formParamName,
+                            'contents' => $formParamValueItem
+                        ];
+                    }
+                }
+                // for HTTP post (form)
+                $httpBody = new MultipartStream($multipartContents);
+
+            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
+                # if Content-Type contains "application/json", json_encode the form parameters
+                try {
+                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $e) {
+                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
+                }
+            } else {
+                // for HTTP post (form)
+                $httpBody = ObjectSerializer::buildQuery($formParams);
+            }
+        }
+
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
+
+        $defaultHeaders = [];
+        if ($this->config->getUserAgent()) {
+            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
+        }
+
+        $headers = array_merge(
+            $defaultHeaders,
+            $headerParams,
+            $headers
+        );
+
+        $operationHost = $this->config->getHost();
+        $query = ObjectSerializer::buildQuery($queryParams);
+        return new Request(
+            'POST',
+            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+            $headers,
+            $httpBody
+        );
+    }
+
+    /**
+     * Create http client option
+     *
+     * @throws \RuntimeException on file opening failure
+     * @return array of http client options
+     */
+    protected function createHttpClientOption()
+    {
+        $options = [];
+        if ($this->config->getDebug()) {
+            $options[RequestOptions::DEBUG] = fopen($this->config->getDebugFile(), 'a');
+            if (!$options[RequestOptions::DEBUG]) {
+                throw new \RuntimeException('Failed to open the debug file: ' . $this->config->getDebugFile());
+            }
+        }
+
+        if ($this->config->getCertFile()) {
+            $options[RequestOptions::CERT] = $this->config->getCertFile();
+        }
+
+        if ($this->config->getKeyFile()) {
+            $options[RequestOptions::SSL_KEY] = $this->config->getKeyFile();
+        }
+
+        return $options;
+    }
+
+    private function handleResponseWithDataType(
+        string $dataType,
+        RequestInterface $request,
+        ResponseInterface $response
+    ): array {
+        if ($dataType === '\SplFileObject') {
+            $content = $response->getBody(); //stream goes to serializer
+        } else {
+            $content = (string) $response->getBody();
+            if ($dataType !== 'string') {
+                try {
+                    $content = json_decode($content, false, 512, JSON_THROW_ON_ERROR);
+                } catch (\JsonException $exception) {
+                    throw new ApiException(
+                        sprintf(
+                            'Error JSON decoding server response (%s)',
+                            $request->getUri()
+                        ),
+                        $response->getStatusCode(),
+                        $response->getHeaders(),
+                        $content
+                    );
+                }
+            }
+        }
+
+        return [
+            ObjectSerializer::deserialize($content, $dataType, []),
+            $response->getStatusCode(),
+            $response->getHeaders()
+        ];
+    }
+
+    private function responseWithinRangeCode(
+        string $rangeCode,
+        int $statusCode
+    ): bool {
+        $left = (int) ($rangeCode[0].'00');
+        $right = (int) ($rangeCode[0].'99');
+
+        return $statusCode >= $left && $statusCode <= $right;
+    }
+}

+ 5 - 1
api-test/client/lib/Api/EmailCertificationsApi.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -352,6 +352,10 @@ class EmailCertificationsApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {

+ 5 - 1
api-test/client/lib/Api/HeartbeatApi.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -369,6 +369,10 @@ class HeartbeatApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {

+ 9 - 1
api-test/client/lib/Api/InvitesApi.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -406,6 +406,10 @@ class InvitesApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {
@@ -723,6 +727,10 @@ class InvitesApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {

+ 1 - 925
api-test/client/lib/Api/MeApi.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -74,18 +74,9 @@ class MeApi
 
     /** @var string[] $contentTypes **/
     public const contentTypes = [
-        'deleteApiV3MeReactionsReaction' => [
-            'application/json',
-        ],
         'getApiV3Me' => [
             'application/json',
         ],
-        'getApiV3MeReactions' => [
-            'application/json',
-        ],
-        'postApiV3MeReactions' => [
-            'application/json',
-        ],
     ];
 
     /**
@@ -134,256 +125,6 @@ class MeApi
         return $this->config;
     }
 
-    /**
-     * Operation deleteApiV3MeReactionsReaction
-     *
-     * 删除我自己的 reaction
-     *
-     * @param  string $reaction reaction 的 uuid (required)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['deleteApiV3MeReactionsReaction'] to see the possible values for this operation
-     *
-     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
-     * @throws \InvalidArgumentException
-     * @return void
-     */
-    public function deleteApiV3MeReactionsReaction($reaction, string $contentType = self::contentTypes['deleteApiV3MeReactionsReaction'][0])
-    {
-        $this->deleteApiV3MeReactionsReactionWithHttpInfo($reaction, $contentType);
-    }
-
-    /**
-     * Operation deleteApiV3MeReactionsReactionWithHttpInfo
-     *
-     * 删除我自己的 reaction
-     *
-     * @param  string $reaction reaction 的 uuid (required)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['deleteApiV3MeReactionsReaction'] to see the possible values for this operation
-     *
-     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
-     * @throws \InvalidArgumentException
-     * @return array of null, HTTP status code, HTTP response headers (array of strings)
-     */
-    public function deleteApiV3MeReactionsReactionWithHttpInfo($reaction, string $contentType = self::contentTypes['deleteApiV3MeReactionsReaction'][0])
-    {
-        $request = $this->deleteApiV3MeReactionsReactionRequest($reaction, $contentType);
-
-        try {
-            $options = $this->createHttpClientOption();
-            try {
-                $response = $this->client->send($request, $options);
-            } catch (RequestException $e) {
-                throw new ApiException(
-                    "[{$e->getCode()}] {$e->getMessage()}",
-                    (int) $e->getCode(),
-                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
-                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
-                );
-            } catch (ConnectException $e) {
-                throw new ApiException(
-                    "[{$e->getCode()}] {$e->getMessage()}",
-                    (int) $e->getCode(),
-                    null,
-                    null
-                );
-            }
-
-            $statusCode = $response->getStatusCode();
-
-
-            return [null, $statusCode, $response->getHeaders()];
-        } catch (ApiException $e) {
-            switch ($e->getCode()) {
-                case 401:
-                    $data = ObjectSerializer::deserialize(
-                        $e->getResponseBody(),
-                        '\WikipaliApi\Model\ProblemDetails',
-                        $e->getResponseHeaders()
-                    );
-                    $e->setResponseObject($data);
-                    throw $e;
-                case 403:
-                    $data = ObjectSerializer::deserialize(
-                        $e->getResponseBody(),
-                        '\WikipaliApi\Model\ProblemDetails',
-                        $e->getResponseHeaders()
-                    );
-                    $e->setResponseObject($data);
-                    throw $e;
-                case 422:
-                    $data = ObjectSerializer::deserialize(
-                        $e->getResponseBody(),
-                        '\WikipaliApi\Model\ProblemDetails',
-                        $e->getResponseHeaders()
-                    );
-                    $e->setResponseObject($data);
-                    throw $e;
-            }
-        
-
-            throw $e;
-        }
-    }
-
-    /**
-     * Operation deleteApiV3MeReactionsReactionAsync
-     *
-     * 删除我自己的 reaction
-     *
-     * @param  string $reaction reaction 的 uuid (required)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['deleteApiV3MeReactionsReaction'] to see the possible values for this operation
-     *
-     * @throws \InvalidArgumentException
-     * @return \GuzzleHttp\Promise\PromiseInterface
-     */
-    public function deleteApiV3MeReactionsReactionAsync($reaction, string $contentType = self::contentTypes['deleteApiV3MeReactionsReaction'][0])
-    {
-        return $this->deleteApiV3MeReactionsReactionAsyncWithHttpInfo($reaction, $contentType)
-            ->then(
-                function ($response) {
-                    return $response[0];
-                }
-            );
-    }
-
-    /**
-     * Operation deleteApiV3MeReactionsReactionAsyncWithHttpInfo
-     *
-     * 删除我自己的 reaction
-     *
-     * @param  string $reaction reaction 的 uuid (required)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['deleteApiV3MeReactionsReaction'] to see the possible values for this operation
-     *
-     * @throws \InvalidArgumentException
-     * @return \GuzzleHttp\Promise\PromiseInterface
-     */
-    public function deleteApiV3MeReactionsReactionAsyncWithHttpInfo($reaction, string $contentType = self::contentTypes['deleteApiV3MeReactionsReaction'][0])
-    {
-        $returnType = '';
-        $request = $this->deleteApiV3MeReactionsReactionRequest($reaction, $contentType);
-
-        return $this->client
-            ->sendAsync($request, $this->createHttpClientOption())
-            ->then(
-                function ($response) use ($returnType) {
-                    return [null, $response->getStatusCode(), $response->getHeaders()];
-                },
-                function ($exception) {
-                    $response = $exception->getResponse();
-                    $statusCode = $response->getStatusCode();
-                    throw new ApiException(
-                        sprintf(
-                            '[%d] Error connecting to the API (%s)',
-                            $statusCode,
-                            $exception->getRequest()->getUri()
-                        ),
-                        $statusCode,
-                        $response->getHeaders(),
-                        (string) $response->getBody()
-                    );
-                }
-            );
-    }
-
-    /**
-     * Create request for operation 'deleteApiV3MeReactionsReaction'
-     *
-     * @param  string $reaction reaction 的 uuid (required)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['deleteApiV3MeReactionsReaction'] to see the possible values for this operation
-     *
-     * @throws \InvalidArgumentException
-     * @return \GuzzleHttp\Psr7\Request
-     */
-    public function deleteApiV3MeReactionsReactionRequest($reaction, string $contentType = self::contentTypes['deleteApiV3MeReactionsReaction'][0])
-    {
-
-        // verify the required parameter 'reaction' is set
-        if ($reaction === null || (is_array($reaction) && count($reaction) === 0)) {
-            throw new \InvalidArgumentException(
-                'Missing the required parameter $reaction when calling deleteApiV3MeReactionsReaction'
-            );
-        }
-
-
-        $resourcePath = '/v3/me/reactions/{reaction}';
-        $formParams = [];
-        $queryParams = [];
-        $headerParams = [];
-        $httpBody = '';
-        $multipart = false;
-
-
-
-        // path params
-        if ($reaction !== null) {
-            $resourcePath = str_replace(
-                '{reaction}',
-                ObjectSerializer::toPathValue($reaction),
-                $resourcePath
-            );
-        }
-
-
-        $headers = $this->headerSelector->selectHeaders(
-            ['application/problem+json', ],
-            $contentType,
-            $multipart
-        );
-
-        // for model (json/xml)
-        if (count($formParams) > 0) {
-            if ($multipart) {
-                $multipartContents = [];
-                foreach ($formParams as $formParamName => $formParamValue) {
-                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
-                    foreach ($formParamValueItems as $formParamValueItem) {
-                        $multipartContents[] = [
-                            'name' => $formParamName,
-                            'contents' => $formParamValueItem
-                        ];
-                    }
-                }
-                // for HTTP post (form)
-                $httpBody = new MultipartStream($multipartContents);
-
-            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
-                # if Content-Type contains "application/json", json_encode the form parameters
-                try {
-                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
-                } catch (\JsonException $e) {
-                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
-                }
-            } else {
-                // for HTTP post (form)
-                $httpBody = ObjectSerializer::buildQuery($formParams);
-            }
-        }
-
-        // this endpoint requires Bearer authentication (access token)
-        if (!empty($this->config->getAccessToken())) {
-            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
-        }
-
-        $defaultHeaders = [];
-        if ($this->config->getUserAgent()) {
-            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
-        }
-
-        $headers = array_merge(
-            $defaultHeaders,
-            $headerParams,
-            $headers
-        );
-
-        $operationHost = $this->config->getHost();
-        $query = ObjectSerializer::buildQuery($queryParams);
-        return new Request(
-            'DELETE',
-            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
-            $headers,
-            $httpBody
-        );
-    }
-
     /**
      * Operation getApiV3Me
      *
@@ -654,671 +395,6 @@ class MeApi
         );
     }
 
-    /**
-     * Operation getApiV3MeReactions
-     *
-     * 列出我自己的 reactions
-     *
-     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
-     * @param  string|null $type 操作类型 (optional)
-     * @param  string|null $target_type target 的类型(task 任务、collection 文集、progress_chapter 书·章节、article 文章、terms 术语) (optional)
-     * @param  int|null $page 页码 (optional, default to 1)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3MeReactions'] to see the possible values for this operation
-     *
-     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
-     * @throws \InvalidArgumentException
-     * @return \WikipaliApi\Model\GetApiV3MeReactions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails
-     */
-    public function getApiV3MeReactions($per_page = 15, $type = null, $target_type = null, $page = 1, string $contentType = self::contentTypes['getApiV3MeReactions'][0])
-    {
-        list($response) = $this->getApiV3MeReactionsWithHttpInfo($per_page, $type, $target_type, $page, $contentType);
-        return $response;
-    }
-
-    /**
-     * Operation getApiV3MeReactionsWithHttpInfo
-     *
-     * 列出我自己的 reactions
-     *
-     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
-     * @param  string|null $type 操作类型 (optional)
-     * @param  string|null $target_type target 的类型(task 任务、collection 文集、progress_chapter 书·章节、article 文章、terms 术语) (optional)
-     * @param  int|null $page 页码 (optional, default to 1)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3MeReactions'] to see the possible values for this operation
-     *
-     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
-     * @throws \InvalidArgumentException
-     * @return array of \WikipaliApi\Model\GetApiV3MeReactions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
-     */
-    public function getApiV3MeReactionsWithHttpInfo($per_page = 15, $type = null, $target_type = null, $page = 1, string $contentType = self::contentTypes['getApiV3MeReactions'][0])
-    {
-        $request = $this->getApiV3MeReactionsRequest($per_page, $type, $target_type, $page, $contentType);
-
-        try {
-            $options = $this->createHttpClientOption();
-            try {
-                $response = $this->client->send($request, $options);
-            } catch (RequestException $e) {
-                throw new ApiException(
-                    "[{$e->getCode()}] {$e->getMessage()}",
-                    (int) $e->getCode(),
-                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
-                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
-                );
-            } catch (ConnectException $e) {
-                throw new ApiException(
-                    "[{$e->getCode()}] {$e->getMessage()}",
-                    (int) $e->getCode(),
-                    null,
-                    null
-                );
-            }
-
-            $statusCode = $response->getStatusCode();
-
-
-            switch($statusCode) {
-                case 200:
-                    return $this->handleResponseWithDataType(
-                        '\WikipaliApi\Model\GetApiV3MeReactions200Response',
-                        $request,
-                        $response,
-                    );
-                case 401:
-                    return $this->handleResponseWithDataType(
-                        '\WikipaliApi\Model\ProblemDetails',
-                        $request,
-                        $response,
-                    );
-                case 422:
-                    return $this->handleResponseWithDataType(
-                        '\WikipaliApi\Model\ProblemDetails',
-                        $request,
-                        $response,
-                    );
-            }
-
-            
-
-            if ($statusCode < 200 || $statusCode > 299) {
-                throw new ApiException(
-                    sprintf(
-                        '[%d] Error connecting to the API (%s)',
-                        $statusCode,
-                        (string) $request->getUri()
-                    ),
-                    $statusCode,
-                    $response->getHeaders(),
-                    (string) $response->getBody()
-                );
-            }
-
-            return $this->handleResponseWithDataType(
-                '\WikipaliApi\Model\GetApiV3MeReactions200Response',
-                $request,
-                $response,
-            );
-        } catch (ApiException $e) {
-            switch ($e->getCode()) {
-                case 200:
-                    $data = ObjectSerializer::deserialize(
-                        $e->getResponseBody(),
-                        '\WikipaliApi\Model\GetApiV3MeReactions200Response',
-                        $e->getResponseHeaders()
-                    );
-                    $e->setResponseObject($data);
-                    throw $e;
-                case 401:
-                    $data = ObjectSerializer::deserialize(
-                        $e->getResponseBody(),
-                        '\WikipaliApi\Model\ProblemDetails',
-                        $e->getResponseHeaders()
-                    );
-                    $e->setResponseObject($data);
-                    throw $e;
-                case 422:
-                    $data = ObjectSerializer::deserialize(
-                        $e->getResponseBody(),
-                        '\WikipaliApi\Model\ProblemDetails',
-                        $e->getResponseHeaders()
-                    );
-                    $e->setResponseObject($data);
-                    throw $e;
-            }
-        
-
-            throw $e;
-        }
-    }
-
-    /**
-     * Operation getApiV3MeReactionsAsync
-     *
-     * 列出我自己的 reactions
-     *
-     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
-     * @param  string|null $type 操作类型 (optional)
-     * @param  string|null $target_type target 的类型(task 任务、collection 文集、progress_chapter 书·章节、article 文章、terms 术语) (optional)
-     * @param  int|null $page 页码 (optional, default to 1)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3MeReactions'] to see the possible values for this operation
-     *
-     * @throws \InvalidArgumentException
-     * @return \GuzzleHttp\Promise\PromiseInterface
-     */
-    public function getApiV3MeReactionsAsync($per_page = 15, $type = null, $target_type = null, $page = 1, string $contentType = self::contentTypes['getApiV3MeReactions'][0])
-    {
-        return $this->getApiV3MeReactionsAsyncWithHttpInfo($per_page, $type, $target_type, $page, $contentType)
-            ->then(
-                function ($response) {
-                    return $response[0];
-                }
-            );
-    }
-
-    /**
-     * Operation getApiV3MeReactionsAsyncWithHttpInfo
-     *
-     * 列出我自己的 reactions
-     *
-     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
-     * @param  string|null $type 操作类型 (optional)
-     * @param  string|null $target_type target 的类型(task 任务、collection 文集、progress_chapter 书·章节、article 文章、terms 术语) (optional)
-     * @param  int|null $page 页码 (optional, default to 1)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3MeReactions'] to see the possible values for this operation
-     *
-     * @throws \InvalidArgumentException
-     * @return \GuzzleHttp\Promise\PromiseInterface
-     */
-    public function getApiV3MeReactionsAsyncWithHttpInfo($per_page = 15, $type = null, $target_type = null, $page = 1, string $contentType = self::contentTypes['getApiV3MeReactions'][0])
-    {
-        $returnType = '\WikipaliApi\Model\GetApiV3MeReactions200Response';
-        $request = $this->getApiV3MeReactionsRequest($per_page, $type, $target_type, $page, $contentType);
-
-        return $this->client
-            ->sendAsync($request, $this->createHttpClientOption())
-            ->then(
-                function ($response) use ($returnType) {
-                    if ($returnType === '\SplFileObject') {
-                        $content = $response->getBody(); //stream goes to serializer
-                    } else {
-                        $content = (string) $response->getBody();
-                        if ($returnType !== 'string') {
-                            $content = json_decode($content);
-                        }
-                    }
-
-                    return [
-                        ObjectSerializer::deserialize($content, $returnType, []),
-                        $response->getStatusCode(),
-                        $response->getHeaders()
-                    ];
-                },
-                function ($exception) {
-                    $response = $exception->getResponse();
-                    $statusCode = $response->getStatusCode();
-                    throw new ApiException(
-                        sprintf(
-                            '[%d] Error connecting to the API (%s)',
-                            $statusCode,
-                            $exception->getRequest()->getUri()
-                        ),
-                        $statusCode,
-                        $response->getHeaders(),
-                        (string) $response->getBody()
-                    );
-                }
-            );
-    }
-
-    /**
-     * Create request for operation 'getApiV3MeReactions'
-     *
-     * @param  int|null $per_page 每页数量,最大 200 (optional, default to 15)
-     * @param  string|null $type 操作类型 (optional)
-     * @param  string|null $target_type target 的类型(task 任务、collection 文集、progress_chapter 书·章节、article 文章、terms 术语) (optional)
-     * @param  int|null $page 页码 (optional, default to 1)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3MeReactions'] to see the possible values for this operation
-     *
-     * @throws \InvalidArgumentException
-     * @return \GuzzleHttp\Psr7\Request
-     */
-    public function getApiV3MeReactionsRequest($per_page = 15, $type = null, $target_type = null, $page = 1, string $contentType = self::contentTypes['getApiV3MeReactions'][0])
-    {
-
-
-
-
-
-
-        $resourcePath = '/v3/me/reactions';
-        $formParams = [];
-        $queryParams = [];
-        $headerParams = [];
-        $httpBody = '';
-        $multipart = false;
-
-        // query params
-        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
-            $per_page,
-            'per_page', // param base name
-            'integer', // openApiType
-            'form', // style
-            true, // explode
-            false // required
-        ) ?? []);
-        // query params
-        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
-            $type,
-            'type', // param base name
-            'string', // openApiType
-            'form', // style
-            true, // explode
-            false // required
-        ) ?? []);
-        // query params
-        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
-            $target_type,
-            'target_type', // param base name
-            'string', // openApiType
-            'form', // style
-            true, // explode
-            false // required
-        ) ?? []);
-        // query params
-        $queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
-            $page,
-            'page', // param base name
-            'integer', // openApiType
-            'form', // style
-            true, // explode
-            false // required
-        ) ?? []);
-
-
-
-
-        $headers = $this->headerSelector->selectHeaders(
-            ['application/json', 'application/problem+json', ],
-            $contentType,
-            $multipart
-        );
-
-        // for model (json/xml)
-        if (count($formParams) > 0) {
-            if ($multipart) {
-                $multipartContents = [];
-                foreach ($formParams as $formParamName => $formParamValue) {
-                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
-                    foreach ($formParamValueItems as $formParamValueItem) {
-                        $multipartContents[] = [
-                            'name' => $formParamName,
-                            'contents' => $formParamValueItem
-                        ];
-                    }
-                }
-                // for HTTP post (form)
-                $httpBody = new MultipartStream($multipartContents);
-
-            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
-                # if Content-Type contains "application/json", json_encode the form parameters
-                try {
-                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
-                } catch (\JsonException $e) {
-                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
-                }
-            } else {
-                // for HTTP post (form)
-                $httpBody = ObjectSerializer::buildQuery($formParams);
-            }
-        }
-
-        // this endpoint requires Bearer authentication (access token)
-        if (!empty($this->config->getAccessToken())) {
-            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
-        }
-
-        $defaultHeaders = [];
-        if ($this->config->getUserAgent()) {
-            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
-        }
-
-        $headers = array_merge(
-            $defaultHeaders,
-            $headerParams,
-            $headers
-        );
-
-        $operationHost = $this->config->getHost();
-        $query = ObjectSerializer::buildQuery($queryParams);
-        return new Request(
-            'GET',
-            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
-            $headers,
-            $httpBody
-        );
-    }
-
-    /**
-     * Operation postApiV3MeReactions
-     *
-     * 添加一条我自己的 reaction(幂等)
-     *
-     * @param  \WikipaliApi\Model\PostApiV3MeReactionsRequest $post_api_v3_me_reactions_request post_api_v3_me_reactions_request (required)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3MeReactions'] to see the possible values for this operation
-     *
-     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
-     * @throws \InvalidArgumentException
-     * @return \WikipaliApi\Model\PostApiV3MeReactions200Response|\WikipaliApi\Model\PostApiV3MeReactions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails
-     */
-    public function postApiV3MeReactions($post_api_v3_me_reactions_request, string $contentType = self::contentTypes['postApiV3MeReactions'][0])
-    {
-        list($response) = $this->postApiV3MeReactionsWithHttpInfo($post_api_v3_me_reactions_request, $contentType);
-        return $response;
-    }
-
-    /**
-     * Operation postApiV3MeReactionsWithHttpInfo
-     *
-     * 添加一条我自己的 reaction(幂等)
-     *
-     * @param  \WikipaliApi\Model\PostApiV3MeReactionsRequest $post_api_v3_me_reactions_request (required)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3MeReactions'] to see the possible values for this operation
-     *
-     * @throws \WikipaliApi\ApiException on non-2xx response or if the response body is not in the expected format
-     * @throws \InvalidArgumentException
-     * @return array of \WikipaliApi\Model\PostApiV3MeReactions200Response|\WikipaliApi\Model\PostApiV3MeReactions200Response|\WikipaliApi\Model\ProblemDetails|\WikipaliApi\Model\ProblemDetails, HTTP status code, HTTP response headers (array of strings)
-     */
-    public function postApiV3MeReactionsWithHttpInfo($post_api_v3_me_reactions_request, string $contentType = self::contentTypes['postApiV3MeReactions'][0])
-    {
-        $request = $this->postApiV3MeReactionsRequest($post_api_v3_me_reactions_request, $contentType);
-
-        try {
-            $options = $this->createHttpClientOption();
-            try {
-                $response = $this->client->send($request, $options);
-            } catch (RequestException $e) {
-                throw new ApiException(
-                    "[{$e->getCode()}] {$e->getMessage()}",
-                    (int) $e->getCode(),
-                    $e->getResponse() ? $e->getResponse()->getHeaders() : null,
-                    $e->getResponse() ? (string) $e->getResponse()->getBody() : null
-                );
-            } catch (ConnectException $e) {
-                throw new ApiException(
-                    "[{$e->getCode()}] {$e->getMessage()}",
-                    (int) $e->getCode(),
-                    null,
-                    null
-                );
-            }
-
-            $statusCode = $response->getStatusCode();
-
-
-            switch($statusCode) {
-                case 200:
-                    return $this->handleResponseWithDataType(
-                        '\WikipaliApi\Model\PostApiV3MeReactions200Response',
-                        $request,
-                        $response,
-                    );
-                case 201:
-                    return $this->handleResponseWithDataType(
-                        '\WikipaliApi\Model\PostApiV3MeReactions200Response',
-                        $request,
-                        $response,
-                    );
-                case 401:
-                    return $this->handleResponseWithDataType(
-                        '\WikipaliApi\Model\ProblemDetails',
-                        $request,
-                        $response,
-                    );
-                case 422:
-                    return $this->handleResponseWithDataType(
-                        '\WikipaliApi\Model\ProblemDetails',
-                        $request,
-                        $response,
-                    );
-            }
-
-            
-
-            if ($statusCode < 200 || $statusCode > 299) {
-                throw new ApiException(
-                    sprintf(
-                        '[%d] Error connecting to the API (%s)',
-                        $statusCode,
-                        (string) $request->getUri()
-                    ),
-                    $statusCode,
-                    $response->getHeaders(),
-                    (string) $response->getBody()
-                );
-            }
-
-            return $this->handleResponseWithDataType(
-                '\WikipaliApi\Model\PostApiV3MeReactions200Response',
-                $request,
-                $response,
-            );
-        } catch (ApiException $e) {
-            switch ($e->getCode()) {
-                case 200:
-                    $data = ObjectSerializer::deserialize(
-                        $e->getResponseBody(),
-                        '\WikipaliApi\Model\PostApiV3MeReactions200Response',
-                        $e->getResponseHeaders()
-                    );
-                    $e->setResponseObject($data);
-                    throw $e;
-                case 201:
-                    $data = ObjectSerializer::deserialize(
-                        $e->getResponseBody(),
-                        '\WikipaliApi\Model\PostApiV3MeReactions200Response',
-                        $e->getResponseHeaders()
-                    );
-                    $e->setResponseObject($data);
-                    throw $e;
-                case 401:
-                    $data = ObjectSerializer::deserialize(
-                        $e->getResponseBody(),
-                        '\WikipaliApi\Model\ProblemDetails',
-                        $e->getResponseHeaders()
-                    );
-                    $e->setResponseObject($data);
-                    throw $e;
-                case 422:
-                    $data = ObjectSerializer::deserialize(
-                        $e->getResponseBody(),
-                        '\WikipaliApi\Model\ProblemDetails',
-                        $e->getResponseHeaders()
-                    );
-                    $e->setResponseObject($data);
-                    throw $e;
-            }
-        
-
-            throw $e;
-        }
-    }
-
-    /**
-     * Operation postApiV3MeReactionsAsync
-     *
-     * 添加一条我自己的 reaction(幂等)
-     *
-     * @param  \WikipaliApi\Model\PostApiV3MeReactionsRequest $post_api_v3_me_reactions_request (required)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3MeReactions'] to see the possible values for this operation
-     *
-     * @throws \InvalidArgumentException
-     * @return \GuzzleHttp\Promise\PromiseInterface
-     */
-    public function postApiV3MeReactionsAsync($post_api_v3_me_reactions_request, string $contentType = self::contentTypes['postApiV3MeReactions'][0])
-    {
-        return $this->postApiV3MeReactionsAsyncWithHttpInfo($post_api_v3_me_reactions_request, $contentType)
-            ->then(
-                function ($response) {
-                    return $response[0];
-                }
-            );
-    }
-
-    /**
-     * Operation postApiV3MeReactionsAsyncWithHttpInfo
-     *
-     * 添加一条我自己的 reaction(幂等)
-     *
-     * @param  \WikipaliApi\Model\PostApiV3MeReactionsRequest $post_api_v3_me_reactions_request (required)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3MeReactions'] to see the possible values for this operation
-     *
-     * @throws \InvalidArgumentException
-     * @return \GuzzleHttp\Promise\PromiseInterface
-     */
-    public function postApiV3MeReactionsAsyncWithHttpInfo($post_api_v3_me_reactions_request, string $contentType = self::contentTypes['postApiV3MeReactions'][0])
-    {
-        $returnType = '\WikipaliApi\Model\PostApiV3MeReactions200Response';
-        $request = $this->postApiV3MeReactionsRequest($post_api_v3_me_reactions_request, $contentType);
-
-        return $this->client
-            ->sendAsync($request, $this->createHttpClientOption())
-            ->then(
-                function ($response) use ($returnType) {
-                    if ($returnType === '\SplFileObject') {
-                        $content = $response->getBody(); //stream goes to serializer
-                    } else {
-                        $content = (string) $response->getBody();
-                        if ($returnType !== 'string') {
-                            $content = json_decode($content);
-                        }
-                    }
-
-                    return [
-                        ObjectSerializer::deserialize($content, $returnType, []),
-                        $response->getStatusCode(),
-                        $response->getHeaders()
-                    ];
-                },
-                function ($exception) {
-                    $response = $exception->getResponse();
-                    $statusCode = $response->getStatusCode();
-                    throw new ApiException(
-                        sprintf(
-                            '[%d] Error connecting to the API (%s)',
-                            $statusCode,
-                            $exception->getRequest()->getUri()
-                        ),
-                        $statusCode,
-                        $response->getHeaders(),
-                        (string) $response->getBody()
-                    );
-                }
-            );
-    }
-
-    /**
-     * Create request for operation 'postApiV3MeReactions'
-     *
-     * @param  \WikipaliApi\Model\PostApiV3MeReactionsRequest $post_api_v3_me_reactions_request (required)
-     * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['postApiV3MeReactions'] to see the possible values for this operation
-     *
-     * @throws \InvalidArgumentException
-     * @return \GuzzleHttp\Psr7\Request
-     */
-    public function postApiV3MeReactionsRequest($post_api_v3_me_reactions_request, string $contentType = self::contentTypes['postApiV3MeReactions'][0])
-    {
-
-        // verify the required parameter 'post_api_v3_me_reactions_request' is set
-        if ($post_api_v3_me_reactions_request === null || (is_array($post_api_v3_me_reactions_request) && count($post_api_v3_me_reactions_request) === 0)) {
-            throw new \InvalidArgumentException(
-                'Missing the required parameter $post_api_v3_me_reactions_request when calling postApiV3MeReactions'
-            );
-        }
-
-
-        $resourcePath = '/v3/me/reactions';
-        $formParams = [];
-        $queryParams = [];
-        $headerParams = [];
-        $httpBody = '';
-        $multipart = false;
-
-
-
-
-
-        $headers = $this->headerSelector->selectHeaders(
-            ['application/json', 'application/problem+json', ],
-            $contentType,
-            $multipart
-        );
-
-        // for model (json/xml)
-        if (isset($post_api_v3_me_reactions_request)) {
-            if (stripos($headers['Content-Type'], 'application/json') !== false) {
-                # if Content-Type contains "application/json", json_encode the body
-                try {
-                    $httpBody = json_encode(ObjectSerializer::sanitizeForSerialization($post_api_v3_me_reactions_request), JSON_THROW_ON_ERROR);
-                } catch (\JsonException $e) {
-                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
-                }
-            } else {
-                $httpBody = $post_api_v3_me_reactions_request;
-            }
-        } elseif (count($formParams) > 0) {
-            if ($multipart) {
-                $multipartContents = [];
-                foreach ($formParams as $formParamName => $formParamValue) {
-                    $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
-                    foreach ($formParamValueItems as $formParamValueItem) {
-                        $multipartContents[] = [
-                            'name' => $formParamName,
-                            'contents' => $formParamValueItem
-                        ];
-                    }
-                }
-                // for HTTP post (form)
-                $httpBody = new MultipartStream($multipartContents);
-
-            } elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
-                # if Content-Type contains "application/json", json_encode the form parameters
-                try {
-                    $httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
-                } catch (\JsonException $e) {
-                    throw new \InvalidArgumentException('json_encode error: ' . $e->getMessage(), 0, $e);
-                }
-            } else {
-                // for HTTP post (form)
-                $httpBody = ObjectSerializer::buildQuery($formParams);
-            }
-        }
-
-        // this endpoint requires Bearer authentication (access token)
-        if (!empty($this->config->getAccessToken())) {
-            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
-        }
-
-        $defaultHeaders = [];
-        if ($this->config->getUserAgent()) {
-            $defaultHeaders['User-Agent'] = $this->config->getUserAgent();
-        }
-
-        $headers = array_merge(
-            $defaultHeaders,
-            $headerParams,
-            $headers
-        );
-
-        $operationHost = $this->config->getHost();
-        $query = ObjectSerializer::buildQuery($queryParams);
-        return new Request(
-            'POST',
-            $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
-            $headers,
-            $httpBody
-        );
-    }
-
     /**
      * Create http client option
      *

+ 13 - 1
api-test/client/lib/Api/PasswordResetsApi.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -409,6 +409,10 @@ class PasswordResetsApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {
@@ -670,6 +674,10 @@ class PasswordResetsApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {
@@ -919,6 +927,10 @@ class PasswordResetsApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {

+ 5 - 1
api-test/client/lib/Api/ProgressApi.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -495,6 +495,10 @@ class ProgressApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 993 - 60
api-test/client/lib/Api/ReactionsApi.php


+ 13 - 1
api-test/client/lib/Api/SearchApi.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -591,6 +591,10 @@ class SearchApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {
@@ -877,6 +881,10 @@ class SearchApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {
@@ -1166,6 +1174,10 @@ class SearchApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {

+ 5 - 1
api-test/client/lib/Api/SearchSuggestApi.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -435,6 +435,10 @@ class SearchSuggestApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {

+ 5 - 1
api-test/client/lib/Api/SessionsApi.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -420,6 +420,10 @@ class SessionsApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {

+ 5 - 1
api-test/client/lib/Api/TipitakaReadingApi.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -538,6 +538,10 @@ class TipitakaReadingApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {

+ 13 - 1
api-test/client/lib/Api/TipitakaRelatedParagraphsApi.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -450,6 +450,10 @@ class TipitakaRelatedParagraphsApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {
@@ -776,6 +780,10 @@ class TipitakaRelatedParagraphsApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {
@@ -1122,6 +1130,10 @@ class TipitakaRelatedParagraphsApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {

+ 5 - 1
api-test/client/lib/Api/UpgradeApi.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -355,6 +355,10 @@ class UpgradeApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {

+ 5 - 1
api-test/client/lib/Api/UsersApi.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -420,6 +420,10 @@ class UsersApi
             }
         }
 
+        // this endpoint requires Bearer authentication (access token)
+        if (!empty($this->config->getAccessToken())) {
+            $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
+        }
 
         $defaultHeaders = [];
         if ($this->config->getUserAgent()) {

+ 1 - 1
api-test/client/lib/ApiException.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 2 - 2
api-test/client/lib/Configuration.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -489,7 +489,7 @@ class Configuration
         $report  = 'PHP SDK (WikipaliApi) Debug Report:' . PHP_EOL;
         $report .= '    OS: ' . php_uname() . PHP_EOL;
         $report .= '    PHP Version: ' . PHP_VERSION . PHP_EOL;
-        $report .= '    The version of the OpenAPI document: 2026.10.8' . PHP_EOL;
+        $report .= '    The version of the OpenAPI document: 2026.10.15' . PHP_EOL;
         $report .= '    SDK Package Version: 1.0.0' . PHP_EOL;
         $report .= '    Temp Folder Path: ' . self::getDefaultConfiguration()->getTempFolderPath() . PHP_EOL;
 

+ 1 - 1
api-test/client/lib/FormDataProcessor.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/HeaderSelector.php

@@ -14,7 +14,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 443 - 0
api-test/client/lib/Model/GetApiV3Discussions200Response.php

@@ -0,0 +1,443 @@
+<?php
+/**
+ * GetApiV3Discussions200Response
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * GetApiV3Discussions200Response Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class GetApiV3Discussions200Response implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'get_api_v3_discussions_200_response';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'data' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInner[]',
+        'meta' => '\WikipaliApi\Model\PaginationMeta'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'data' => null,
+        'meta' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'data' => false,
+        'meta' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'data' => 'data',
+        'meta' => 'meta'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'data' => 'setData',
+        'meta' => 'setMeta'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'data' => 'getData',
+        'meta' => 'getMeta'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('data', $data ?? [], null);
+        $this->setIfExists('meta', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets data
+     *
+     * @return \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInner[]|null
+     */
+    public function getData()
+    {
+        return $this->container['data'];
+    }
+
+    /**
+     * Sets data
+     *
+     * @param \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInner[]|null $data data
+     *
+     * @return self
+     */
+    public function setData($data)
+    {
+        if (is_null($data)) {
+            throw new \InvalidArgumentException('non-nullable data cannot be null');
+        }
+        $this->container['data'] = $data;
+
+        return $this;
+    }
+
+    /**
+     * Gets meta
+     *
+     * @return \WikipaliApi\Model\PaginationMeta|null
+     */
+    public function getMeta()
+    {
+        return $this->container['meta'];
+    }
+
+    /**
+     * Sets meta
+     *
+     * @param \WikipaliApi\Model\PaginationMeta|null $meta meta
+     *
+     * @return self
+     */
+    public function setMeta($meta)
+    {
+        if (is_null($meta)) {
+            throw new \InvalidArgumentException('non-nullable meta cannot be null');
+        }
+        $this->container['meta'] = $meta;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 1316 - 0
api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInner.php

@@ -0,0 +1,1316 @@
+<?php
+/**
+ * GetApiV3Discussions200ResponseDataInner
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * GetApiV3Discussions200ResponseDataInner Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'get_api_v3_discussions_200_response_data_inner';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'id' => 'string',
+        'target_type' => 'string',
+        'target_id' => 'string',
+        'parent_id' => 'string',
+        'type' => 'string',
+        'severity' => 'string',
+        'status' => 'string',
+        'title' => 'string',
+        'content' => 'string',
+        'content_type' => 'string',
+        'html' => 'string',
+        'summary' => 'string',
+        'tpl_id' => 'string',
+        'pos_start' => 'int',
+        'pos_end' => 'int',
+        'quote_exact' => 'string',
+        'quote_prefix' => 'string',
+        'quote_suffix' => 'string',
+        'reply_count' => 'int',
+        'editor' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor',
+        'co_editor' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor',
+        'abilities' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerAbilities',
+        'created_at' => 'string',
+        'updated_at' => 'string',
+        'deleted_at' => 'string'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'id' => null,
+        'target_type' => null,
+        'target_id' => null,
+        'parent_id' => null,
+        'type' => null,
+        'severity' => null,
+        'status' => null,
+        'title' => null,
+        'content' => null,
+        'content_type' => null,
+        'html' => null,
+        'summary' => null,
+        'tpl_id' => null,
+        'pos_start' => null,
+        'pos_end' => null,
+        'quote_exact' => null,
+        'quote_prefix' => null,
+        'quote_suffix' => null,
+        'reply_count' => null,
+        'editor' => null,
+        'co_editor' => null,
+        'abilities' => null,
+        'created_at' => null,
+        'updated_at' => null,
+        'deleted_at' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'id' => false,
+        'target_type' => false,
+        'target_id' => false,
+        'parent_id' => true,
+        'type' => false,
+        'severity' => true,
+        'status' => false,
+        'title' => true,
+        'content' => true,
+        'content_type' => false,
+        'html' => false,
+        'summary' => false,
+        'tpl_id' => true,
+        'pos_start' => true,
+        'pos_end' => true,
+        'quote_exact' => true,
+        'quote_prefix' => true,
+        'quote_suffix' => true,
+        'reply_count' => false,
+        'editor' => true,
+        'co_editor' => true,
+        'abilities' => false,
+        'created_at' => false,
+        'updated_at' => false,
+        'deleted_at' => true
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'id' => 'id',
+        'target_type' => 'target_type',
+        'target_id' => 'target_id',
+        'parent_id' => 'parent_id',
+        'type' => 'type',
+        'severity' => 'severity',
+        'status' => 'status',
+        'title' => 'title',
+        'content' => 'content',
+        'content_type' => 'content_type',
+        'html' => 'html',
+        'summary' => 'summary',
+        'tpl_id' => 'tpl_id',
+        'pos_start' => 'pos_start',
+        'pos_end' => 'pos_end',
+        'quote_exact' => 'quote_exact',
+        'quote_prefix' => 'quote_prefix',
+        'quote_suffix' => 'quote_suffix',
+        'reply_count' => 'reply_count',
+        'editor' => 'editor',
+        'co_editor' => 'co_editor',
+        'abilities' => 'abilities',
+        'created_at' => 'created_at',
+        'updated_at' => 'updated_at',
+        'deleted_at' => 'deleted_at'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'id' => 'setId',
+        'target_type' => 'setTargetType',
+        'target_id' => 'setTargetId',
+        'parent_id' => 'setParentId',
+        'type' => 'setType',
+        'severity' => 'setSeverity',
+        'status' => 'setStatus',
+        'title' => 'setTitle',
+        'content' => 'setContent',
+        'content_type' => 'setContentType',
+        'html' => 'setHtml',
+        'summary' => 'setSummary',
+        'tpl_id' => 'setTplId',
+        'pos_start' => 'setPosStart',
+        'pos_end' => 'setPosEnd',
+        'quote_exact' => 'setQuoteExact',
+        'quote_prefix' => 'setQuotePrefix',
+        'quote_suffix' => 'setQuoteSuffix',
+        'reply_count' => 'setReplyCount',
+        'editor' => 'setEditor',
+        'co_editor' => 'setCoEditor',
+        'abilities' => 'setAbilities',
+        'created_at' => 'setCreatedAt',
+        'updated_at' => 'setUpdatedAt',
+        'deleted_at' => 'setDeletedAt'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'id' => 'getId',
+        'target_type' => 'getTargetType',
+        'target_id' => 'getTargetId',
+        'parent_id' => 'getParentId',
+        'type' => 'getType',
+        'severity' => 'getSeverity',
+        'status' => 'getStatus',
+        'title' => 'getTitle',
+        'content' => 'getContent',
+        'content_type' => 'getContentType',
+        'html' => 'getHtml',
+        'summary' => 'getSummary',
+        'tpl_id' => 'getTplId',
+        'pos_start' => 'getPosStart',
+        'pos_end' => 'getPosEnd',
+        'quote_exact' => 'getQuoteExact',
+        'quote_prefix' => 'getQuotePrefix',
+        'quote_suffix' => 'getQuoteSuffix',
+        'reply_count' => 'getReplyCount',
+        'editor' => 'getEditor',
+        'co_editor' => 'getCoEditor',
+        'abilities' => 'getAbilities',
+        'created_at' => 'getCreatedAt',
+        'updated_at' => 'getUpdatedAt',
+        'deleted_at' => 'getDeletedAt'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('id', $data ?? [], null);
+        $this->setIfExists('target_type', $data ?? [], null);
+        $this->setIfExists('target_id', $data ?? [], null);
+        $this->setIfExists('parent_id', $data ?? [], null);
+        $this->setIfExists('type', $data ?? [], null);
+        $this->setIfExists('severity', $data ?? [], null);
+        $this->setIfExists('status', $data ?? [], null);
+        $this->setIfExists('title', $data ?? [], null);
+        $this->setIfExists('content', $data ?? [], null);
+        $this->setIfExists('content_type', $data ?? [], null);
+        $this->setIfExists('html', $data ?? [], null);
+        $this->setIfExists('summary', $data ?? [], null);
+        $this->setIfExists('tpl_id', $data ?? [], null);
+        $this->setIfExists('pos_start', $data ?? [], null);
+        $this->setIfExists('pos_end', $data ?? [], null);
+        $this->setIfExists('quote_exact', $data ?? [], null);
+        $this->setIfExists('quote_prefix', $data ?? [], null);
+        $this->setIfExists('quote_suffix', $data ?? [], null);
+        $this->setIfExists('reply_count', $data ?? [], null);
+        $this->setIfExists('editor', $data ?? [], null);
+        $this->setIfExists('co_editor', $data ?? [], null);
+        $this->setIfExists('abilities', $data ?? [], null);
+        $this->setIfExists('created_at', $data ?? [], null);
+        $this->setIfExists('updated_at', $data ?? [], null);
+        $this->setIfExists('deleted_at', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets id
+     *
+     * @return string|null
+     */
+    public function getId()
+    {
+        return $this->container['id'];
+    }
+
+    /**
+     * Sets id
+     *
+     * @param string|null $id id
+     *
+     * @return self
+     */
+    public function setId($id)
+    {
+        if (is_null($id)) {
+            throw new \InvalidArgumentException('non-nullable id cannot be null');
+        }
+        $this->container['id'] = $id;
+
+        return $this;
+    }
+
+    /**
+     * Gets target_type
+     *
+     * @return string|null
+     */
+    public function getTargetType()
+    {
+        return $this->container['target_type'];
+    }
+
+    /**
+     * Sets target_type
+     *
+     * @param string|null $target_type target_type
+     *
+     * @return self
+     */
+    public function setTargetType($target_type)
+    {
+        if (is_null($target_type)) {
+            throw new \InvalidArgumentException('non-nullable target_type cannot be null');
+        }
+        $this->container['target_type'] = $target_type;
+
+        return $this;
+    }
+
+    /**
+     * Gets target_id
+     *
+     * @return string|null
+     */
+    public function getTargetId()
+    {
+        return $this->container['target_id'];
+    }
+
+    /**
+     * Sets target_id
+     *
+     * @param string|null $target_id target_id
+     *
+     * @return self
+     */
+    public function setTargetId($target_id)
+    {
+        if (is_null($target_id)) {
+            throw new \InvalidArgumentException('non-nullable target_id cannot be null');
+        }
+        $this->container['target_id'] = $target_id;
+
+        return $this;
+    }
+
+    /**
+     * Gets parent_id
+     *
+     * @return string|null
+     */
+    public function getParentId()
+    {
+        return $this->container['parent_id'];
+    }
+
+    /**
+     * Sets parent_id
+     *
+     * @param string|null $parent_id parent_id
+     *
+     * @return self
+     */
+    public function setParentId($parent_id)
+    {
+        if (is_null($parent_id)) {
+            array_push($this->openAPINullablesSetToNull, 'parent_id');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('parent_id', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['parent_id'] = $parent_id;
+
+        return $this;
+    }
+
+    /**
+     * Gets type
+     *
+     * @return string|null
+     */
+    public function getType()
+    {
+        return $this->container['type'];
+    }
+
+    /**
+     * Sets type
+     *
+     * @param string|null $type type
+     *
+     * @return self
+     */
+    public function setType($type)
+    {
+        if (is_null($type)) {
+            throw new \InvalidArgumentException('non-nullable type cannot be null');
+        }
+        $this->container['type'] = $type;
+
+        return $this;
+    }
+
+    /**
+     * Gets severity
+     *
+     * @return string|null
+     */
+    public function getSeverity()
+    {
+        return $this->container['severity'];
+    }
+
+    /**
+     * Sets severity
+     *
+     * @param string|null $severity severity
+     *
+     * @return self
+     */
+    public function setSeverity($severity)
+    {
+        if (is_null($severity)) {
+            array_push($this->openAPINullablesSetToNull, 'severity');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('severity', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['severity'] = $severity;
+
+        return $this;
+    }
+
+    /**
+     * Gets status
+     *
+     * @return string|null
+     */
+    public function getStatus()
+    {
+        return $this->container['status'];
+    }
+
+    /**
+     * Sets status
+     *
+     * @param string|null $status status
+     *
+     * @return self
+     */
+    public function setStatus($status)
+    {
+        if (is_null($status)) {
+            throw new \InvalidArgumentException('non-nullable status cannot be null');
+        }
+        $this->container['status'] = $status;
+
+        return $this;
+    }
+
+    /**
+     * Gets title
+     *
+     * @return string|null
+     */
+    public function getTitle()
+    {
+        return $this->container['title'];
+    }
+
+    /**
+     * Sets title
+     *
+     * @param string|null $title title
+     *
+     * @return self
+     */
+    public function setTitle($title)
+    {
+        if (is_null($title)) {
+            array_push($this->openAPINullablesSetToNull, 'title');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('title', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['title'] = $title;
+
+        return $this;
+    }
+
+    /**
+     * Gets content
+     *
+     * @return string|null
+     */
+    public function getContent()
+    {
+        return $this->container['content'];
+    }
+
+    /**
+     * Sets content
+     *
+     * @param string|null $content content
+     *
+     * @return self
+     */
+    public function setContent($content)
+    {
+        if (is_null($content)) {
+            array_push($this->openAPINullablesSetToNull, 'content');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('content', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['content'] = $content;
+
+        return $this;
+    }
+
+    /**
+     * Gets content_type
+     *
+     * @return string|null
+     */
+    public function getContentType()
+    {
+        return $this->container['content_type'];
+    }
+
+    /**
+     * Sets content_type
+     *
+     * @param string|null $content_type content_type
+     *
+     * @return self
+     */
+    public function setContentType($content_type)
+    {
+        if (is_null($content_type)) {
+            throw new \InvalidArgumentException('non-nullable content_type cannot be null');
+        }
+        $this->container['content_type'] = $content_type;
+
+        return $this;
+    }
+
+    /**
+     * Gets html
+     *
+     * @return string|null
+     */
+    public function getHtml()
+    {
+        return $this->container['html'];
+    }
+
+    /**
+     * Sets html
+     *
+     * @param string|null $html 仅在部分查询口径下返回。
+     *
+     * @return self
+     */
+    public function setHtml($html)
+    {
+        if (is_null($html)) {
+            throw new \InvalidArgumentException('non-nullable html cannot be null');
+        }
+        $this->container['html'] = $html;
+
+        return $this;
+    }
+
+    /**
+     * Gets summary
+     *
+     * @return string|null
+     */
+    public function getSummary()
+    {
+        return $this->container['summary'];
+    }
+
+    /**
+     * Sets summary
+     *
+     * @param string|null $summary 仅在部分查询口径下返回。
+     *
+     * @return self
+     */
+    public function setSummary($summary)
+    {
+        if (is_null($summary)) {
+            throw new \InvalidArgumentException('non-nullable summary cannot be null');
+        }
+        $this->container['summary'] = $summary;
+
+        return $this;
+    }
+
+    /**
+     * Gets tpl_id
+     *
+     * @return string|null
+     */
+    public function getTplId()
+    {
+        return $this->container['tpl_id'];
+    }
+
+    /**
+     * Sets tpl_id
+     *
+     * @param string|null $tpl_id tpl_id
+     *
+     * @return self
+     */
+    public function setTplId($tpl_id)
+    {
+        if (is_null($tpl_id)) {
+            array_push($this->openAPINullablesSetToNull, 'tpl_id');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('tpl_id', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['tpl_id'] = $tpl_id;
+
+        return $this;
+    }
+
+    /**
+     * Gets pos_start
+     *
+     * @return int|null
+     */
+    public function getPosStart()
+    {
+        return $this->container['pos_start'];
+    }
+
+    /**
+     * Sets pos_start
+     *
+     * @param int|null $pos_start pos_start
+     *
+     * @return self
+     */
+    public function setPosStart($pos_start)
+    {
+        if (is_null($pos_start)) {
+            array_push($this->openAPINullablesSetToNull, 'pos_start');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('pos_start', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['pos_start'] = $pos_start;
+
+        return $this;
+    }
+
+    /**
+     * Gets pos_end
+     *
+     * @return int|null
+     */
+    public function getPosEnd()
+    {
+        return $this->container['pos_end'];
+    }
+
+    /**
+     * Sets pos_end
+     *
+     * @param int|null $pos_end pos_end
+     *
+     * @return self
+     */
+    public function setPosEnd($pos_end)
+    {
+        if (is_null($pos_end)) {
+            array_push($this->openAPINullablesSetToNull, 'pos_end');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('pos_end', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['pos_end'] = $pos_end;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_exact
+     *
+     * @return string|null
+     */
+    public function getQuoteExact()
+    {
+        return $this->container['quote_exact'];
+    }
+
+    /**
+     * Sets quote_exact
+     *
+     * @param string|null $quote_exact quote_exact
+     *
+     * @return self
+     */
+    public function setQuoteExact($quote_exact)
+    {
+        if (is_null($quote_exact)) {
+            array_push($this->openAPINullablesSetToNull, 'quote_exact');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('quote_exact', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['quote_exact'] = $quote_exact;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_prefix
+     *
+     * @return string|null
+     */
+    public function getQuotePrefix()
+    {
+        return $this->container['quote_prefix'];
+    }
+
+    /**
+     * Sets quote_prefix
+     *
+     * @param string|null $quote_prefix quote_prefix
+     *
+     * @return self
+     */
+    public function setQuotePrefix($quote_prefix)
+    {
+        if (is_null($quote_prefix)) {
+            array_push($this->openAPINullablesSetToNull, 'quote_prefix');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('quote_prefix', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['quote_prefix'] = $quote_prefix;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_suffix
+     *
+     * @return string|null
+     */
+    public function getQuoteSuffix()
+    {
+        return $this->container['quote_suffix'];
+    }
+
+    /**
+     * Sets quote_suffix
+     *
+     * @param string|null $quote_suffix quote_suffix
+     *
+     * @return self
+     */
+    public function setQuoteSuffix($quote_suffix)
+    {
+        if (is_null($quote_suffix)) {
+            array_push($this->openAPINullablesSetToNull, 'quote_suffix');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('quote_suffix', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['quote_suffix'] = $quote_suffix;
+
+        return $this;
+    }
+
+    /**
+     * Gets reply_count
+     *
+     * @return int|null
+     */
+    public function getReplyCount()
+    {
+        return $this->container['reply_count'];
+    }
+
+    /**
+     * Sets reply_count
+     *
+     * @param int|null $reply_count 仅在部分查询口径下返回。
+     *
+     * @return self
+     */
+    public function setReplyCount($reply_count)
+    {
+        if (is_null($reply_count)) {
+            throw new \InvalidArgumentException('non-nullable reply_count cannot be null');
+        }
+        $this->container['reply_count'] = $reply_count;
+
+        return $this;
+    }
+
+    /**
+     * Gets editor
+     *
+     * @return \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor|null
+     */
+    public function getEditor()
+    {
+        return $this->container['editor'];
+    }
+
+    /**
+     * Sets editor
+     *
+     * @param \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor|null $editor editor
+     *
+     * @return self
+     */
+    public function setEditor($editor)
+    {
+        if (is_null($editor)) {
+            array_push($this->openAPINullablesSetToNull, 'editor');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('editor', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['editor'] = $editor;
+
+        return $this;
+    }
+
+    /**
+     * Gets co_editor
+     *
+     * @return \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor|null
+     */
+    public function getCoEditor()
+    {
+        return $this->container['co_editor'];
+    }
+
+    /**
+     * Sets co_editor
+     *
+     * @param \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor|null $co_editor co_editor
+     *
+     * @return self
+     */
+    public function setCoEditor($co_editor)
+    {
+        if (is_null($co_editor)) {
+            array_push($this->openAPINullablesSetToNull, 'co_editor');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('co_editor', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['co_editor'] = $co_editor;
+
+        return $this;
+    }
+
+    /**
+     * Gets abilities
+     *
+     * @return \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerAbilities|null
+     */
+    public function getAbilities()
+    {
+        return $this->container['abilities'];
+    }
+
+    /**
+     * Sets abilities
+     *
+     * @param \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerAbilities|null $abilities abilities
+     *
+     * @return self
+     */
+    public function setAbilities($abilities)
+    {
+        if (is_null($abilities)) {
+            throw new \InvalidArgumentException('non-nullable abilities cannot be null');
+        }
+        $this->container['abilities'] = $abilities;
+
+        return $this;
+    }
+
+    /**
+     * Gets created_at
+     *
+     * @return string|null
+     */
+    public function getCreatedAt()
+    {
+        return $this->container['created_at'];
+    }
+
+    /**
+     * Sets created_at
+     *
+     * @param string|null $created_at created_at
+     *
+     * @return self
+     */
+    public function setCreatedAt($created_at)
+    {
+        if (is_null($created_at)) {
+            throw new \InvalidArgumentException('non-nullable created_at cannot be null');
+        }
+        $this->container['created_at'] = $created_at;
+
+        return $this;
+    }
+
+    /**
+     * Gets updated_at
+     *
+     * @return string|null
+     */
+    public function getUpdatedAt()
+    {
+        return $this->container['updated_at'];
+    }
+
+    /**
+     * Sets updated_at
+     *
+     * @param string|null $updated_at updated_at
+     *
+     * @return self
+     */
+    public function setUpdatedAt($updated_at)
+    {
+        if (is_null($updated_at)) {
+            throw new \InvalidArgumentException('non-nullable updated_at cannot be null');
+        }
+        $this->container['updated_at'] = $updated_at;
+
+        return $this;
+    }
+
+    /**
+     * Gets deleted_at
+     *
+     * @return string|null
+     */
+    public function getDeletedAt()
+    {
+        return $this->container['deleted_at'];
+    }
+
+    /**
+     * Sets deleted_at
+     *
+     * @param string|null $deleted_at deleted_at
+     *
+     * @return self
+     */
+    public function setDeletedAt($deleted_at)
+    {
+        if (is_null($deleted_at)) {
+            array_push($this->openAPINullablesSetToNull, 'deleted_at');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('deleted_at', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['deleted_at'] = $deleted_at;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 511 - 0
api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInnerAbilities.php

@@ -0,0 +1,511 @@
+<?php
+/**
+ * GetApiV3Discussions200ResponseDataInnerAbilities
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * GetApiV3Discussions200ResponseDataInnerAbilities Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class GetApiV3Discussions200ResponseDataInnerAbilities implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'get_api_v3_discussions_200_response_data_inner_abilities';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'update' => 'bool',
+        'delete' => 'bool',
+        'close' => 'bool',
+        'reply' => 'bool'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'update' => null,
+        'delete' => null,
+        'close' => null,
+        'reply' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'update' => false,
+        'delete' => false,
+        'close' => false,
+        'reply' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'update' => 'update',
+        'delete' => 'delete',
+        'close' => 'close',
+        'reply' => 'reply'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'update' => 'setUpdate',
+        'delete' => 'setDelete',
+        'close' => 'setClose',
+        'reply' => 'setReply'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'update' => 'getUpdate',
+        'delete' => 'getDelete',
+        'close' => 'getClose',
+        'reply' => 'getReply'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('update', $data ?? [], null);
+        $this->setIfExists('delete', $data ?? [], null);
+        $this->setIfExists('close', $data ?? [], null);
+        $this->setIfExists('reply', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets update
+     *
+     * @return bool|null
+     */
+    public function getUpdate()
+    {
+        return $this->container['update'];
+    }
+
+    /**
+     * Sets update
+     *
+     * @param bool|null $update update
+     *
+     * @return self
+     */
+    public function setUpdate($update)
+    {
+        if (is_null($update)) {
+            throw new \InvalidArgumentException('non-nullable update cannot be null');
+        }
+        $this->container['update'] = $update;
+
+        return $this;
+    }
+
+    /**
+     * Gets delete
+     *
+     * @return bool|null
+     */
+    public function getDelete()
+    {
+        return $this->container['delete'];
+    }
+
+    /**
+     * Sets delete
+     *
+     * @param bool|null $delete delete
+     *
+     * @return self
+     */
+    public function setDelete($delete)
+    {
+        if (is_null($delete)) {
+            throw new \InvalidArgumentException('non-nullable delete cannot be null');
+        }
+        $this->container['delete'] = $delete;
+
+        return $this;
+    }
+
+    /**
+     * Gets close
+     *
+     * @return bool|null
+     */
+    public function getClose()
+    {
+        return $this->container['close'];
+    }
+
+    /**
+     * Sets close
+     *
+     * @param bool|null $close close
+     *
+     * @return self
+     */
+    public function setClose($close)
+    {
+        if (is_null($close)) {
+            throw new \InvalidArgumentException('non-nullable close cannot be null');
+        }
+        $this->container['close'] = $close;
+
+        return $this;
+    }
+
+    /**
+     * Gets reply
+     *
+     * @return bool|null
+     */
+    public function getReply()
+    {
+        return $this->container['reply'];
+    }
+
+    /**
+     * Sets reply
+     *
+     * @param bool|null $reply reply
+     *
+     * @return self
+     */
+    public function setReply($reply)
+    {
+        if (is_null($reply)) {
+            throw new \InvalidArgumentException('non-nullable reply cannot be null');
+        }
+        $this->container['reply'] = $reply;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 5 - 5
api-test/client/lib/Model/GetApiV3MeReactions200ResponseDataInnerUser.php → api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInnerEditor.php

@@ -1,6 +1,6 @@
 <?php
 /**
- * GetApiV3MeReactions200ResponseDataInnerUser
+ * GetApiV3Discussions200ResponseDataInnerEditor
  *
  * PHP version 8.1
  *
@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -32,7 +32,7 @@ use \ArrayAccess;
 use \WikipaliApi\ObjectSerializer;
 
 /**
- * GetApiV3MeReactions200ResponseDataInnerUser Class Doc Comment
+ * GetApiV3Discussions200ResponseDataInnerEditor Class Doc Comment
  *
  * @category Class
  * @package  WikipaliApi
@@ -40,7 +40,7 @@ use \WikipaliApi\ObjectSerializer;
  * @link     https://openapi-generator.tech
  * @implements \ArrayAccess<string, mixed>
  */
-class GetApiV3MeReactions200ResponseDataInnerUser implements ModelInterface, ArrayAccess, \JsonSerializable
+class GetApiV3Discussions200ResponseDataInnerEditor implements ModelInterface, ArrayAccess, \JsonSerializable
 {
     public const DISCRIMINATOR = null;
 
@@ -49,7 +49,7 @@ class GetApiV3MeReactions200ResponseDataInnerUser implements ModelInterface, Arr
      *
      * @var string
      */
-    protected static $openAPIModelName = 'get_api_v3_me_reactions_200_response_data_inner_user';
+    protected static $openAPIModelName = 'get_api_v3_discussions_200_response_data_inner_editor';
 
     /**
      * Array of property to type mappings. Used for (de)serialization

+ 409 - 0
api-test/client/lib/Model/GetApiV3DiscussionsSummary200Response.php

@@ -0,0 +1,409 @@
+<?php
+/**
+ * GetApiV3DiscussionsSummary200Response
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * GetApiV3DiscussionsSummary200Response Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class GetApiV3DiscussionsSummary200Response implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'get_api_v3_discussions_summary_200_response';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'data' => '\WikipaliApi\Model\GetApiV3DiscussionsSummary200ResponseData'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'data' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'data' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'data' => 'data'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'data' => 'setData'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'data' => 'getData'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('data', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets data
+     *
+     * @return \WikipaliApi\Model\GetApiV3DiscussionsSummary200ResponseData|null
+     */
+    public function getData()
+    {
+        return $this->container['data'];
+    }
+
+    /**
+     * Sets data
+     *
+     * @param \WikipaliApi\Model\GetApiV3DiscussionsSummary200ResponseData|null $data data
+     *
+     * @return self
+     */
+    public function setData($data)
+    {
+        if (is_null($data)) {
+            throw new \InvalidArgumentException('non-nullable data cannot be null');
+        }
+        $this->container['data'] = $data;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 443 - 0
api-test/client/lib/Model/GetApiV3DiscussionsSummary200ResponseData.php

@@ -0,0 +1,443 @@
+<?php
+/**
+ * GetApiV3DiscussionsSummary200ResponseData
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * GetApiV3DiscussionsSummary200ResponseData Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class GetApiV3DiscussionsSummary200ResponseData implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'get_api_v3_discussions_summary_200_response_data';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'status_counts' => '\WikipaliApi\Model\GetApiV3DiscussionsSummary200ResponseDataStatusCounts',
+        'creatable_types' => 'string[]'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'status_counts' => null,
+        'creatable_types' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'status_counts' => false,
+        'creatable_types' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'status_counts' => 'status_counts',
+        'creatable_types' => 'creatable_types'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'status_counts' => 'setStatusCounts',
+        'creatable_types' => 'setCreatableTypes'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'status_counts' => 'getStatusCounts',
+        'creatable_types' => 'getCreatableTypes'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('status_counts', $data ?? [], null);
+        $this->setIfExists('creatable_types', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets status_counts
+     *
+     * @return \WikipaliApi\Model\GetApiV3DiscussionsSummary200ResponseDataStatusCounts|null
+     */
+    public function getStatusCounts()
+    {
+        return $this->container['status_counts'];
+    }
+
+    /**
+     * Sets status_counts
+     *
+     * @param \WikipaliApi\Model\GetApiV3DiscussionsSummary200ResponseDataStatusCounts|null $status_counts status_counts
+     *
+     * @return self
+     */
+    public function setStatusCounts($status_counts)
+    {
+        if (is_null($status_counts)) {
+            throw new \InvalidArgumentException('non-nullable status_counts cannot be null');
+        }
+        $this->container['status_counts'] = $status_counts;
+
+        return $this;
+    }
+
+    /**
+     * Gets creatable_types
+     *
+     * @return string[]|null
+     */
+    public function getCreatableTypes()
+    {
+        return $this->container['creatable_types'];
+    }
+
+    /**
+     * Sets creatable_types
+     *
+     * @param string[]|null $creatable_types creatable_types
+     *
+     * @return self
+     */
+    public function setCreatableTypes($creatable_types)
+    {
+        if (is_null($creatable_types)) {
+            throw new \InvalidArgumentException('non-nullable creatable_types cannot be null');
+        }
+        $this->container['creatable_types'] = $creatable_types;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 443 - 0
api-test/client/lib/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCounts.php

@@ -0,0 +1,443 @@
+<?php
+/**
+ * GetApiV3DiscussionsSummary200ResponseDataStatusCounts
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * GetApiV3DiscussionsSummary200ResponseDataStatusCounts Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class GetApiV3DiscussionsSummary200ResponseDataStatusCounts implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'get_api_v3_discussions_summary_200_response_data_status_counts';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'active' => 'int',
+        'close' => 'int'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'active' => null,
+        'close' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'active' => false,
+        'close' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'active' => 'active',
+        'close' => 'close'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'active' => 'setActive',
+        'close' => 'setClose'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'active' => 'getActive',
+        'close' => 'getClose'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('active', $data ?? [], null);
+        $this->setIfExists('close', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets active
+     *
+     * @return int|null
+     */
+    public function getActive()
+    {
+        return $this->container['active'];
+    }
+
+    /**
+     * Sets active
+     *
+     * @param int|null $active active
+     *
+     * @return self
+     */
+    public function setActive($active)
+    {
+        if (is_null($active)) {
+            throw new \InvalidArgumentException('non-nullable active cannot be null');
+        }
+        $this->container['active'] = $active;
+
+        return $this;
+    }
+
+    /**
+     * Gets close
+     *
+     * @return int|null
+     */
+    public function getClose()
+    {
+        return $this->container['close'];
+    }
+
+    /**
+     * Sets close
+     *
+     * @param int|null $close close
+     *
+     * @return self
+     */
+    public function setClose($close)
+    {
+        if (is_null($close)) {
+            throw new \InvalidArgumentException('non-nullable close cannot be null');
+        }
+        $this->container['close'] = $close;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 1 - 1
api-test/client/lib/Model/GetApiV3Heartbeat200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3Heartbeat200ResponseData.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3Me200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3Me200ResponseData.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3MeReactions200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 4 - 4
api-test/client/lib/Model/GetApiV3MeReactions200ResponseDataInner.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -64,7 +64,7 @@ class GetApiV3MeReactions200ResponseDataInner implements ModelInterface, ArrayAc
         'context' => 'string',
         'created_at' => 'string',
         'updated_at' => 'string',
-        'user' => '\WikipaliApi\Model\GetApiV3MeReactions200ResponseDataInnerUser'
+        'user' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor'
     ];
 
     /**
@@ -537,7 +537,7 @@ class GetApiV3MeReactions200ResponseDataInner implements ModelInterface, ArrayAc
     /**
      * Gets user
      *
-     * @return \WikipaliApi\Model\GetApiV3MeReactions200ResponseDataInnerUser|null
+     * @return \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor|null
      */
     public function getUser()
     {
@@ -547,7 +547,7 @@ class GetApiV3MeReactions200ResponseDataInner implements ModelInterface, ArrayAc
     /**
      * Sets user
      *
-     * @param \WikipaliApi\Model\GetApiV3MeReactions200ResponseDataInnerUser|null $user user
+     * @param \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInnerEditor|null $user user
      *
      * @return self
      */

+ 1 - 1
api-test/client/lib/Model/GetApiV3PasswordResetsToken200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3PasswordResetsToken200ResponseData.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3Progress200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3Progress200ResponseDataInner.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3ReactionsTally200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3ReactionsTally200ResponseDataInner.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3Search200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3SearchSearch200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3TipitakaReadingChannel200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3TipitakaReadingChannel200ResponseDataInner.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3TipitakaReadingChannel200ResponseMeta.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200ResponseDataInner.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200ResponseDataInnerPathInner.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200ResponseMeta.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregate200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregate200ResponseDataInner.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200ResponseDataInner.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/ModelInterface.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PaginationMeta.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 831 - 0
api-test/client/lib/Model/PatchApiV3DiscussionsDiscussionRequest.php

@@ -0,0 +1,831 @@
+<?php
+/**
+ * PatchApiV3DiscussionsDiscussionRequest
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PatchApiV3DiscussionsDiscussionRequest Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'patch_api_v3_discussions_discussion__request';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'title' => 'string',
+        'content' => 'string',
+        'status' => 'string',
+        'type' => 'string',
+        'severity' => 'string',
+        'pos_start' => 'int',
+        'pos_end' => 'int',
+        'quote_exact' => 'string',
+        'quote_prefix' => 'string',
+        'quote_suffix' => 'string'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'title' => null,
+        'content' => null,
+        'status' => null,
+        'type' => null,
+        'severity' => null,
+        'pos_start' => null,
+        'pos_end' => null,
+        'quote_exact' => null,
+        'quote_prefix' => null,
+        'quote_suffix' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'title' => false,
+        'content' => false,
+        'status' => false,
+        'type' => false,
+        'severity' => false,
+        'pos_start' => false,
+        'pos_end' => false,
+        'quote_exact' => false,
+        'quote_prefix' => false,
+        'quote_suffix' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'title' => 'title',
+        'content' => 'content',
+        'status' => 'status',
+        'type' => 'type',
+        'severity' => 'severity',
+        'pos_start' => 'pos_start',
+        'pos_end' => 'pos_end',
+        'quote_exact' => 'quote_exact',
+        'quote_prefix' => 'quote_prefix',
+        'quote_suffix' => 'quote_suffix'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'title' => 'setTitle',
+        'content' => 'setContent',
+        'status' => 'setStatus',
+        'type' => 'setType',
+        'severity' => 'setSeverity',
+        'pos_start' => 'setPosStart',
+        'pos_end' => 'setPosEnd',
+        'quote_exact' => 'setQuoteExact',
+        'quote_prefix' => 'setQuotePrefix',
+        'quote_suffix' => 'setQuoteSuffix'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'title' => 'getTitle',
+        'content' => 'getContent',
+        'status' => 'getStatus',
+        'type' => 'getType',
+        'severity' => 'getSeverity',
+        'pos_start' => 'getPosStart',
+        'pos_end' => 'getPosEnd',
+        'quote_exact' => 'getQuoteExact',
+        'quote_prefix' => 'getQuotePrefix',
+        'quote_suffix' => 'getQuoteSuffix'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+    public const STATUS_ACTIVE = 'active';
+    public const STATUS_CLOSE = 'close';
+    public const TYPE_DISCUSSION = 'discussion';
+    public const TYPE_QA = 'qa';
+    public const TYPE_HELP = 'help';
+    public const TYPE_ISSUE = 'issue';
+    public const TYPE_NOTE = 'note';
+    public const TYPE_COMMENTARY = 'commentary';
+    public const SEVERITY_ERROR = 'error';
+    public const SEVERITY_WARNING = 'warning';
+    public const SEVERITY_SUGGESTION = 'suggestion';
+    public const SEVERITY_URGENT = 'urgent';
+    public const SEVERITY_NORMAL = 'normal';
+
+    /**
+     * Gets allowable values of the enum
+     *
+     * @return string[]
+     */
+    public function getStatusAllowableValues()
+    {
+        return [
+            self::STATUS_ACTIVE,
+            self::STATUS_CLOSE,
+        ];
+    }
+
+    /**
+     * Gets allowable values of the enum
+     *
+     * @return string[]
+     */
+    public function getTypeAllowableValues()
+    {
+        return [
+            self::TYPE_DISCUSSION,
+            self::TYPE_QA,
+            self::TYPE_HELP,
+            self::TYPE_ISSUE,
+            self::TYPE_NOTE,
+            self::TYPE_COMMENTARY,
+        ];
+    }
+
+    /**
+     * Gets allowable values of the enum
+     *
+     * @return string[]
+     */
+    public function getSeverityAllowableValues()
+    {
+        return [
+            self::SEVERITY_ERROR,
+            self::SEVERITY_WARNING,
+            self::SEVERITY_SUGGESTION,
+            self::SEVERITY_URGENT,
+            self::SEVERITY_NORMAL,
+        ];
+    }
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('title', $data ?? [], null);
+        $this->setIfExists('content', $data ?? [], null);
+        $this->setIfExists('status', $data ?? [], null);
+        $this->setIfExists('type', $data ?? [], null);
+        $this->setIfExists('severity', $data ?? [], null);
+        $this->setIfExists('pos_start', $data ?? [], null);
+        $this->setIfExists('pos_end', $data ?? [], null);
+        $this->setIfExists('quote_exact', $data ?? [], null);
+        $this->setIfExists('quote_prefix', $data ?? [], null);
+        $this->setIfExists('quote_suffix', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        $allowedValues = $this->getStatusAllowableValues();
+        if (!is_null($this->container['status']) && !in_array($this->container['status'], $allowedValues, true)) {
+            $invalidProperties[] = sprintf(
+                "invalid value '%s' for 'status', must be one of '%s'",
+                $this->container['status'],
+                implode("', '", $allowedValues)
+            );
+        }
+
+        $allowedValues = $this->getTypeAllowableValues();
+        if (!is_null($this->container['type']) && !in_array($this->container['type'], $allowedValues, true)) {
+            $invalidProperties[] = sprintf(
+                "invalid value '%s' for 'type', must be one of '%s'",
+                $this->container['type'],
+                implode("', '", $allowedValues)
+            );
+        }
+
+        $allowedValues = $this->getSeverityAllowableValues();
+        if (!is_null($this->container['severity']) && !in_array($this->container['severity'], $allowedValues, true)) {
+            $invalidProperties[] = sprintf(
+                "invalid value '%s' for 'severity', must be one of '%s'",
+                $this->container['severity'],
+                implode("', '", $allowedValues)
+            );
+        }
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets title
+     *
+     * @return string|null
+     */
+    public function getTitle()
+    {
+        return $this->container['title'];
+    }
+
+    /**
+     * Sets title
+     *
+     * @param string|null $title 标题(只限话题)
+     *
+     * @return self
+     */
+    public function setTitle($title)
+    {
+        if (is_null($title)) {
+            throw new \InvalidArgumentException('non-nullable title cannot be null');
+        }
+        $this->container['title'] = $title;
+
+        return $this;
+    }
+
+    /**
+     * Gets content
+     *
+     * @return string|null
+     */
+    public function getContent()
+    {
+        return $this->container['content'];
+    }
+
+    /**
+     * Sets content
+     *
+     * @param string|null $content 正文(markdown)
+     *
+     * @return self
+     */
+    public function setContent($content)
+    {
+        if (is_null($content)) {
+            throw new \InvalidArgumentException('non-nullable content cannot be null');
+        }
+        $this->container['content'] = $content;
+
+        return $this;
+    }
+
+    /**
+     * Gets status
+     *
+     * @return string|null
+     */
+    public function getStatus()
+    {
+        return $this->container['status'];
+    }
+
+    /**
+     * Sets status
+     *
+     * @param string|null $status 状态(只限话题)
+     *
+     * @return self
+     */
+    public function setStatus($status)
+    {
+        if (is_null($status)) {
+            throw new \InvalidArgumentException('non-nullable status cannot be null');
+        }
+        $allowedValues = $this->getStatusAllowableValues();
+        if (!in_array($status, $allowedValues, true)) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    "Invalid value '%s' for 'status', must be one of '%s'",
+                    $status,
+                    implode("', '", $allowedValues)
+                )
+            );
+        }
+        $this->container['status'] = $status;
+
+        return $this;
+    }
+
+    /**
+     * Gets type
+     *
+     * @return string|null
+     */
+    public function getType()
+    {
+        return $this->container['type'];
+    }
+
+    /**
+     * Sets type
+     *
+     * @param string|null $type 类型(只限话题;评论线程与批注不能互转)
+     *
+     * @return self
+     */
+    public function setType($type)
+    {
+        if (is_null($type)) {
+            throw new \InvalidArgumentException('non-nullable type cannot be null');
+        }
+        $allowedValues = $this->getTypeAllowableValues();
+        if (!in_array($type, $allowedValues, true)) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    "Invalid value '%s' for 'type', must be one of '%s'",
+                    $type,
+                    implode("', '", $allowedValues)
+                )
+            );
+        }
+        $this->container['type'] = $type;
+
+        return $this;
+    }
+
+    /**
+     * Gets severity
+     *
+     * @return string|null
+     */
+    public function getSeverity()
+    {
+        return $this->container['severity'];
+    }
+
+    /**
+     * Sets severity
+     *
+     * @param string|null $severity 严重程度(只限话题),null 表示清空
+     *
+     * @return self
+     */
+    public function setSeverity($severity)
+    {
+        if (is_null($severity)) {
+            throw new \InvalidArgumentException('non-nullable severity cannot be null');
+        }
+        $allowedValues = $this->getSeverityAllowableValues();
+        if (!in_array($severity, $allowedValues, true)) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    "Invalid value '%s' for 'severity', must be one of '%s'",
+                    $severity,
+                    implode("', '", $allowedValues)
+                )
+            );
+        }
+        $this->container['severity'] = $severity;
+
+        return $this;
+    }
+
+    /**
+     * Gets pos_start
+     *
+     * @return int|null
+     */
+    public function getPosStart()
+    {
+        return $this->container['pos_start'];
+    }
+
+    /**
+     * Sets pos_start
+     *
+     * @param int|null $pos_start 锚点起始偏移(只限批注)
+     *
+     * @return self
+     */
+    public function setPosStart($pos_start)
+    {
+        if (is_null($pos_start)) {
+            throw new \InvalidArgumentException('non-nullable pos_start cannot be null');
+        }
+        $this->container['pos_start'] = $pos_start;
+
+        return $this;
+    }
+
+    /**
+     * Gets pos_end
+     *
+     * @return int|null
+     */
+    public function getPosEnd()
+    {
+        return $this->container['pos_end'];
+    }
+
+    /**
+     * Sets pos_end
+     *
+     * @param int|null $pos_end 锚点结束偏移(只限批注)
+     *
+     * @return self
+     */
+    public function setPosEnd($pos_end)
+    {
+        if (is_null($pos_end)) {
+            throw new \InvalidArgumentException('non-nullable pos_end cannot be null');
+        }
+        $this->container['pos_end'] = $pos_end;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_exact
+     *
+     * @return string|null
+     */
+    public function getQuoteExact()
+    {
+        return $this->container['quote_exact'];
+    }
+
+    /**
+     * Sets quote_exact
+     *
+     * @param string|null $quote_exact 锚定的原文片段(只限批注)
+     *
+     * @return self
+     */
+    public function setQuoteExact($quote_exact)
+    {
+        if (is_null($quote_exact)) {
+            throw new \InvalidArgumentException('non-nullable quote_exact cannot be null');
+        }
+        $this->container['quote_exact'] = $quote_exact;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_prefix
+     *
+     * @return string|null
+     */
+    public function getQuotePrefix()
+    {
+        return $this->container['quote_prefix'];
+    }
+
+    /**
+     * Sets quote_prefix
+     *
+     * @param string|null $quote_prefix 锚定片段前的上下文(只限批注)
+     *
+     * @return self
+     */
+    public function setQuotePrefix($quote_prefix)
+    {
+        if (is_null($quote_prefix)) {
+            throw new \InvalidArgumentException('non-nullable quote_prefix cannot be null');
+        }
+        $this->container['quote_prefix'] = $quote_prefix;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_suffix
+     *
+     * @return string|null
+     */
+    public function getQuoteSuffix()
+    {
+        return $this->container['quote_suffix'];
+    }
+
+    /**
+     * Sets quote_suffix
+     *
+     * @param string|null $quote_suffix 锚定片段后的上下文(只限批注)
+     *
+     * @return self
+     */
+    public function setQuoteSuffix($quote_suffix)
+    {
+        if (is_null($quote_suffix)) {
+            throw new \InvalidArgumentException('non-nullable quote_suffix cannot be null');
+        }
+        $this->container['quote_suffix'] = $quote_suffix;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 1 - 1
api-test/client/lib/Model/PatchApiV3PasswordResetsTokenRequest.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 409 - 0
api-test/client/lib/Model/PostApiV3AgentGrants200Response.php

@@ -0,0 +1,409 @@
+<?php
+/**
+ * PostApiV3AgentGrants200Response
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PostApiV3AgentGrants200Response Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PostApiV3AgentGrants200Response implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'post_api_v3_agent_grants_200_response';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'data' => '\WikipaliApi\Model\PostApiV3AgentGrants200ResponseData'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'data' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'data' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'data' => 'data'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'data' => 'setData'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'data' => 'getData'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('data', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets data
+     *
+     * @return \WikipaliApi\Model\PostApiV3AgentGrants200ResponseData|null
+     */
+    public function getData()
+    {
+        return $this->container['data'];
+    }
+
+    /**
+     * Sets data
+     *
+     * @param \WikipaliApi\Model\PostApiV3AgentGrants200ResponseData|null $data data
+     *
+     * @return self
+     */
+    public function setData($data)
+    {
+        if (is_null($data)) {
+            throw new \InvalidArgumentException('non-nullable data cannot be null');
+        }
+        $this->container['data'] = $data;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 613 - 0
api-test/client/lib/Model/PostApiV3AgentGrants200ResponseData.php

@@ -0,0 +1,613 @@
+<?php
+/**
+ * PostApiV3AgentGrants200ResponseData
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PostApiV3AgentGrants200ResponseData Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PostApiV3AgentGrants200ResponseData implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'post_api_v3_agent_grants_200_response_data';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'token' => 'string',
+        'grantor' => 'string',
+        'ai_model' => 'string',
+        'target_type' => 'string',
+        'target_id' => 'string',
+        'book' => 'int',
+        'expires_at' => 'string'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'token' => null,
+        'grantor' => null,
+        'ai_model' => null,
+        'target_type' => null,
+        'target_id' => null,
+        'book' => null,
+        'expires_at' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'token' => false,
+        'grantor' => false,
+        'ai_model' => false,
+        'target_type' => false,
+        'target_id' => false,
+        'book' => false,
+        'expires_at' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'token' => 'token',
+        'grantor' => 'grantor',
+        'ai_model' => 'ai_model',
+        'target_type' => 'target_type',
+        'target_id' => 'target_id',
+        'book' => 'book',
+        'expires_at' => 'expires_at'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'token' => 'setToken',
+        'grantor' => 'setGrantor',
+        'ai_model' => 'setAiModel',
+        'target_type' => 'setTargetType',
+        'target_id' => 'setTargetId',
+        'book' => 'setBook',
+        'expires_at' => 'setExpiresAt'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'token' => 'getToken',
+        'grantor' => 'getGrantor',
+        'ai_model' => 'getAiModel',
+        'target_type' => 'getTargetType',
+        'target_id' => 'getTargetId',
+        'book' => 'getBook',
+        'expires_at' => 'getExpiresAt'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('token', $data ?? [], null);
+        $this->setIfExists('grantor', $data ?? [], null);
+        $this->setIfExists('ai_model', $data ?? [], null);
+        $this->setIfExists('target_type', $data ?? [], null);
+        $this->setIfExists('target_id', $data ?? [], null);
+        $this->setIfExists('book', $data ?? [], null);
+        $this->setIfExists('expires_at', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets token
+     *
+     * @return string|null
+     */
+    public function getToken()
+    {
+        return $this->container['token'];
+    }
+
+    /**
+     * Sets token
+     *
+     * @param string|null $token token
+     *
+     * @return self
+     */
+    public function setToken($token)
+    {
+        if (is_null($token)) {
+            throw new \InvalidArgumentException('non-nullable token cannot be null');
+        }
+        $this->container['token'] = $token;
+
+        return $this;
+    }
+
+    /**
+     * Gets grantor
+     *
+     * @return string|null
+     */
+    public function getGrantor()
+    {
+        return $this->container['grantor'];
+    }
+
+    /**
+     * Sets grantor
+     *
+     * @param string|null $grantor grantor
+     *
+     * @return self
+     */
+    public function setGrantor($grantor)
+    {
+        if (is_null($grantor)) {
+            throw new \InvalidArgumentException('non-nullable grantor cannot be null');
+        }
+        $this->container['grantor'] = $grantor;
+
+        return $this;
+    }
+
+    /**
+     * Gets ai_model
+     *
+     * @return string|null
+     */
+    public function getAiModel()
+    {
+        return $this->container['ai_model'];
+    }
+
+    /**
+     * Sets ai_model
+     *
+     * @param string|null $ai_model ai_model
+     *
+     * @return self
+     */
+    public function setAiModel($ai_model)
+    {
+        if (is_null($ai_model)) {
+            throw new \InvalidArgumentException('non-nullable ai_model cannot be null');
+        }
+        $this->container['ai_model'] = $ai_model;
+
+        return $this;
+    }
+
+    /**
+     * Gets target_type
+     *
+     * @return string|null
+     */
+    public function getTargetType()
+    {
+        return $this->container['target_type'];
+    }
+
+    /**
+     * Sets target_type
+     *
+     * @param string|null $target_type target_type
+     *
+     * @return self
+     */
+    public function setTargetType($target_type)
+    {
+        if (is_null($target_type)) {
+            throw new \InvalidArgumentException('non-nullable target_type cannot be null');
+        }
+        $this->container['target_type'] = $target_type;
+
+        return $this;
+    }
+
+    /**
+     * Gets target_id
+     *
+     * @return string|null
+     */
+    public function getTargetId()
+    {
+        return $this->container['target_id'];
+    }
+
+    /**
+     * Sets target_id
+     *
+     * @param string|null $target_id target_id
+     *
+     * @return self
+     */
+    public function setTargetId($target_id)
+    {
+        if (is_null($target_id)) {
+            throw new \InvalidArgumentException('non-nullable target_id cannot be null');
+        }
+        $this->container['target_id'] = $target_id;
+
+        return $this;
+    }
+
+    /**
+     * Gets book
+     *
+     * @return int|null
+     */
+    public function getBook()
+    {
+        return $this->container['book'];
+    }
+
+    /**
+     * Sets book
+     *
+     * @param int|null $book book
+     *
+     * @return self
+     */
+    public function setBook($book)
+    {
+        if (is_null($book)) {
+            throw new \InvalidArgumentException('non-nullable book cannot be null');
+        }
+        $this->container['book'] = $book;
+
+        return $this;
+    }
+
+    /**
+     * Gets expires_at
+     *
+     * @return string|null
+     */
+    public function getExpiresAt()
+    {
+        return $this->container['expires_at'];
+    }
+
+    /**
+     * Sets expires_at
+     *
+     * @param string|null $expires_at expires_at
+     *
+     * @return self
+     */
+    public function setExpiresAt($expires_at)
+    {
+        if (is_null($expires_at)) {
+            throw new \InvalidArgumentException('non-nullable expires_at cannot be null');
+        }
+        $this->container['expires_at'] = $expires_at;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 552 - 0
api-test/client/lib/Model/PostApiV3AgentGrantsRequest.php

@@ -0,0 +1,552 @@
+<?php
+/**
+ * PostApiV3AgentGrantsRequest
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PostApiV3AgentGrantsRequest Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PostApiV3AgentGrantsRequest implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'post_api_v3_agent_grants_request';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'ai_model' => 'string',
+        'target_type' => 'string',
+        'target_id' => 'string',
+        'book' => 'int'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'ai_model' => null,
+        'target_type' => null,
+        'target_id' => null,
+        'book' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'ai_model' => false,
+        'target_type' => false,
+        'target_id' => false,
+        'book' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'ai_model' => 'ai_model',
+        'target_type' => 'target_type',
+        'target_id' => 'target_id',
+        'book' => 'book'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'ai_model' => 'setAiModel',
+        'target_type' => 'setTargetType',
+        'target_id' => 'setTargetId',
+        'book' => 'setBook'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'ai_model' => 'getAiModel',
+        'target_type' => 'getTargetType',
+        'target_id' => 'getTargetId',
+        'book' => 'getBook'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+    public const TARGET_TYPE_CHANNEL = 'channel';
+
+    /**
+     * Gets allowable values of the enum
+     *
+     * @return string[]
+     */
+    public function getTargetTypeAllowableValues()
+    {
+        return [
+            self::TARGET_TYPE_CHANNEL,
+        ];
+    }
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('ai_model', $data ?? [], null);
+        $this->setIfExists('target_type', $data ?? [], null);
+        $this->setIfExists('target_id', $data ?? [], null);
+        $this->setIfExists('book', $data ?? [], 0);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        if ($this->container['ai_model'] === null) {
+            $invalidProperties[] = "'ai_model' can't be null";
+        }
+        if ($this->container['target_type'] === null) {
+            $invalidProperties[] = "'target_type' can't be null";
+        }
+        $allowedValues = $this->getTargetTypeAllowableValues();
+        if (!is_null($this->container['target_type']) && !in_array($this->container['target_type'], $allowedValues, true)) {
+            $invalidProperties[] = sprintf(
+                "invalid value '%s' for 'target_type', must be one of '%s'",
+                $this->container['target_type'],
+                implode("', '", $allowedValues)
+            );
+        }
+
+        if ($this->container['target_id'] === null) {
+            $invalidProperties[] = "'target_id' can't be null";
+        }
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets ai_model
+     *
+     * @return string
+     */
+    public function getAiModel()
+    {
+        return $this->container['ai_model'];
+    }
+
+    /**
+     * Sets ai_model
+     *
+     * @param string $ai_model 被授权的 AI 模型 uid
+     *
+     * @return self
+     */
+    public function setAiModel($ai_model)
+    {
+        if (is_null($ai_model)) {
+            throw new \InvalidArgumentException('non-nullable ai_model cannot be null');
+        }
+        $this->container['ai_model'] = $ai_model;
+
+        return $this;
+    }
+
+    /**
+     * Gets target_type
+     *
+     * @return string
+     */
+    public function getTargetType()
+    {
+        return $this->container['target_type'];
+    }
+
+    /**
+     * Sets target_type
+     *
+     * @param string $target_type 授权范围的类型
+     *
+     * @return self
+     */
+    public function setTargetType($target_type)
+    {
+        if (is_null($target_type)) {
+            throw new \InvalidArgumentException('non-nullable target_type cannot be null');
+        }
+        $allowedValues = $this->getTargetTypeAllowableValues();
+        if (!in_array($target_type, $allowedValues, true)) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    "Invalid value '%s' for 'target_type', must be one of '%s'",
+                    $target_type,
+                    implode("', '", $allowedValues)
+                )
+            );
+        }
+        $this->container['target_type'] = $target_type;
+
+        return $this;
+    }
+
+    /**
+     * Gets target_id
+     *
+     * @return string
+     */
+    public function getTargetId()
+    {
+        return $this->container['target_id'];
+    }
+
+    /**
+     * Sets target_id
+     *
+     * @param string $target_id channel 的 uid
+     *
+     * @return self
+     */
+    public function setTargetId($target_id)
+    {
+        if (is_null($target_id)) {
+            throw new \InvalidArgumentException('non-nullable target_id cannot be null');
+        }
+        $this->container['target_id'] = $target_id;
+
+        return $this;
+    }
+
+    /**
+     * Gets book
+     *
+     * @return int|null
+     */
+    public function getBook()
+    {
+        return $this->container['book'];
+    }
+
+    /**
+     * Sets book
+     *
+     * @param int|null $book 限定 book,0 或不传表示不限
+     *
+     * @return self
+     */
+    public function setBook($book)
+    {
+        if (is_null($book)) {
+            throw new \InvalidArgumentException('non-nullable book cannot be null');
+        }
+        $this->container['book'] = $book;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 409 - 0
api-test/client/lib/Model/PostApiV3Discussions200Response.php

@@ -0,0 +1,409 @@
+<?php
+/**
+ * PostApiV3Discussions200Response
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PostApiV3Discussions200Response Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PostApiV3Discussions200Response implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'post_api_v3_discussions_200_response';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'data' => '\WikipaliApi\Model\GetApiV3Discussions200ResponseDataInner'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'data' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'data' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'data' => 'data'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'data' => 'setData'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'data' => 'getData'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('data', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets data
+     *
+     * @return \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInner|null
+     */
+    public function getData()
+    {
+        return $this->container['data'];
+    }
+
+    /**
+     * Sets data
+     *
+     * @param \WikipaliApi\Model\GetApiV3Discussions200ResponseDataInner|null $data data
+     *
+     * @return self
+     */
+    public function setData($data)
+    {
+        if (is_null($data)) {
+            throw new \InvalidArgumentException('non-nullable data cannot be null');
+        }
+        $this->container['data'] = $data;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 443 - 0
api-test/client/lib/Model/PostApiV3DiscussionsBatchCount200Response.php

@@ -0,0 +1,443 @@
+<?php
+/**
+ * PostApiV3DiscussionsBatchCount200Response
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PostApiV3DiscussionsBatchCount200Response Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PostApiV3DiscussionsBatchCount200Response implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'post_api_v3_discussions_batch_count_200_response';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'data' => '\WikipaliApi\Model\PostApiV3DiscussionsBatchCount200ResponseDataInner[]',
+        'meta' => '\WikipaliApi\Model\PaginationMeta'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'data' => null,
+        'meta' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'data' => false,
+        'meta' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'data' => 'data',
+        'meta' => 'meta'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'data' => 'setData',
+        'meta' => 'setMeta'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'data' => 'getData',
+        'meta' => 'getMeta'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('data', $data ?? [], null);
+        $this->setIfExists('meta', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets data
+     *
+     * @return \WikipaliApi\Model\PostApiV3DiscussionsBatchCount200ResponseDataInner[]|null
+     */
+    public function getData()
+    {
+        return $this->container['data'];
+    }
+
+    /**
+     * Sets data
+     *
+     * @param \WikipaliApi\Model\PostApiV3DiscussionsBatchCount200ResponseDataInner[]|null $data data
+     *
+     * @return self
+     */
+    public function setData($data)
+    {
+        if (is_null($data)) {
+            throw new \InvalidArgumentException('non-nullable data cannot be null');
+        }
+        $this->container['data'] = $data;
+
+        return $this;
+    }
+
+    /**
+     * Gets meta
+     *
+     * @return \WikipaliApi\Model\PaginationMeta|null
+     */
+    public function getMeta()
+    {
+        return $this->container['meta'];
+    }
+
+    /**
+     * Sets meta
+     *
+     * @param \WikipaliApi\Model\PaginationMeta|null $meta meta
+     *
+     * @return self
+     */
+    public function setMeta($meta)
+    {
+        if (is_null($meta)) {
+            throw new \InvalidArgumentException('non-nullable meta cannot be null');
+        }
+        $this->container['meta'] = $meta;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 511 - 0
api-test/client/lib/Model/PostApiV3DiscussionsBatchCount200ResponseDataInner.php

@@ -0,0 +1,511 @@
+<?php
+/**
+ * PostApiV3DiscussionsBatchCount200ResponseDataInner
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PostApiV3DiscussionsBatchCount200ResponseDataInner Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PostApiV3DiscussionsBatchCount200ResponseDataInner implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'post_api_v3_discussions_batch_count_200_response_data_inner';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'target_type' => 'string',
+        'target_id' => 'string',
+        'total' => 'int',
+        'mine' => 'int'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'target_type' => null,
+        'target_id' => null,
+        'total' => null,
+        'mine' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'target_type' => false,
+        'target_id' => false,
+        'total' => false,
+        'mine' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'target_type' => 'target_type',
+        'target_id' => 'target_id',
+        'total' => 'total',
+        'mine' => 'mine'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'target_type' => 'setTargetType',
+        'target_id' => 'setTargetId',
+        'total' => 'setTotal',
+        'mine' => 'setMine'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'target_type' => 'getTargetType',
+        'target_id' => 'getTargetId',
+        'total' => 'getTotal',
+        'mine' => 'getMine'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('target_type', $data ?? [], null);
+        $this->setIfExists('target_id', $data ?? [], null);
+        $this->setIfExists('total', $data ?? [], null);
+        $this->setIfExists('mine', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets target_type
+     *
+     * @return string|null
+     */
+    public function getTargetType()
+    {
+        return $this->container['target_type'];
+    }
+
+    /**
+     * Sets target_type
+     *
+     * @param string|null $target_type target_type
+     *
+     * @return self
+     */
+    public function setTargetType($target_type)
+    {
+        if (is_null($target_type)) {
+            throw new \InvalidArgumentException('non-nullable target_type cannot be null');
+        }
+        $this->container['target_type'] = $target_type;
+
+        return $this;
+    }
+
+    /**
+     * Gets target_id
+     *
+     * @return string|null
+     */
+    public function getTargetId()
+    {
+        return $this->container['target_id'];
+    }
+
+    /**
+     * Sets target_id
+     *
+     * @param string|null $target_id target_id
+     *
+     * @return self
+     */
+    public function setTargetId($target_id)
+    {
+        if (is_null($target_id)) {
+            throw new \InvalidArgumentException('non-nullable target_id cannot be null');
+        }
+        $this->container['target_id'] = $target_id;
+
+        return $this;
+    }
+
+    /**
+     * Gets total
+     *
+     * @return int|null
+     */
+    public function getTotal()
+    {
+        return $this->container['total'];
+    }
+
+    /**
+     * Sets total
+     *
+     * @param int|null $total total
+     *
+     * @return self
+     */
+    public function setTotal($total)
+    {
+        if (is_null($total)) {
+            throw new \InvalidArgumentException('non-nullable total cannot be null');
+        }
+        $this->container['total'] = $total;
+
+        return $this;
+    }
+
+    /**
+     * Gets mine
+     *
+     * @return int|null
+     */
+    public function getMine()
+    {
+        return $this->container['mine'];
+    }
+
+    /**
+     * Sets mine
+     *
+     * @param int|null $mine mine
+     *
+     * @return self
+     */
+    public function setMine($mine)
+    {
+        if (is_null($mine)) {
+            throw new \InvalidArgumentException('non-nullable mine cannot be null');
+        }
+        $this->container['mine'] = $mine;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 412 - 0
api-test/client/lib/Model/PostApiV3DiscussionsBatchCountRequest.php

@@ -0,0 +1,412 @@
+<?php
+/**
+ * PostApiV3DiscussionsBatchCountRequest
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PostApiV3DiscussionsBatchCountRequest Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PostApiV3DiscussionsBatchCountRequest implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'post_api_v3_discussions_batch_count_request';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'targets' => '\WikipaliApi\Model\PostApiV3DiscussionsBatchCountRequestTargetsInner[]'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'targets' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'targets' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'targets' => 'targets'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'targets' => 'setTargets'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'targets' => 'getTargets'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('targets', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        if ($this->container['targets'] === null) {
+            $invalidProperties[] = "'targets' can't be null";
+        }
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets targets
+     *
+     * @return \WikipaliApi\Model\PostApiV3DiscussionsBatchCountRequestTargetsInner[]
+     */
+    public function getTargets()
+    {
+        return $this->container['targets'];
+    }
+
+    /**
+     * Sets targets
+     *
+     * @param \WikipaliApi\Model\PostApiV3DiscussionsBatchCountRequestTargetsInner[] $targets 讨论对象列表,每项 {type, id},最多 500 个
+     *
+     * @return self
+     */
+    public function setTargets($targets)
+    {
+        if (is_null($targets)) {
+            throw new \InvalidArgumentException('non-nullable targets cannot be null');
+        }
+        $this->container['targets'] = $targets;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 483 - 0
api-test/client/lib/Model/PostApiV3DiscussionsBatchCountRequestTargetsInner.php

@@ -0,0 +1,483 @@
+<?php
+/**
+ * PostApiV3DiscussionsBatchCountRequestTargetsInner
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PostApiV3DiscussionsBatchCountRequestTargetsInner Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PostApiV3DiscussionsBatchCountRequestTargetsInner implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'post_api_v3_discussions_batch_count_request_targets_inner';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'type' => 'string',
+        'id' => 'string'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'type' => null,
+        'id' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'type' => false,
+        'id' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'type' => 'type',
+        'id' => 'id'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'type' => 'setType',
+        'id' => 'setId'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'type' => 'getType',
+        'id' => 'getId'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+    public const TYPE_SENTENCE = 'sentence';
+    public const TYPE_WBW = 'wbw';
+
+    /**
+     * Gets allowable values of the enum
+     *
+     * @return string[]
+     */
+    public function getTypeAllowableValues()
+    {
+        return [
+            self::TYPE_SENTENCE,
+            self::TYPE_WBW,
+        ];
+    }
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('type', $data ?? [], null);
+        $this->setIfExists('id', $data ?? [], null);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        if ($this->container['type'] === null) {
+            $invalidProperties[] = "'type' can't be null";
+        }
+        $allowedValues = $this->getTypeAllowableValues();
+        if (!is_null($this->container['type']) && !in_array($this->container['type'], $allowedValues, true)) {
+            $invalidProperties[] = sprintf(
+                "invalid value '%s' for 'type', must be one of '%s'",
+                $this->container['type'],
+                implode("', '", $allowedValues)
+            );
+        }
+
+        if ($this->container['id'] === null) {
+            $invalidProperties[] = "'id' can't be null";
+        }
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets type
+     *
+     * @return string
+     */
+    public function getType()
+    {
+        return $this->container['type'];
+    }
+
+    /**
+     * Sets type
+     *
+     * @param string $type 对象类型
+     *
+     * @return self
+     */
+    public function setType($type)
+    {
+        if (is_null($type)) {
+            throw new \InvalidArgumentException('non-nullable type cannot be null');
+        }
+        $allowedValues = $this->getTypeAllowableValues();
+        if (!in_array($type, $allowedValues, true)) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    "Invalid value '%s' for 'type', must be one of '%s'",
+                    $type,
+                    implode("', '", $allowedValues)
+                )
+            );
+        }
+        $this->container['type'] = $type;
+
+        return $this;
+    }
+
+    /**
+     * Gets id
+     *
+     * @return string
+     */
+    public function getId()
+    {
+        return $this->container['id'];
+    }
+
+    /**
+     * Sets id
+     *
+     * @param string $id 对象 uid
+     *
+     * @return self
+     */
+    public function setId($id)
+    {
+        if (is_null($id)) {
+            throw new \InvalidArgumentException('non-nullable id cannot be null');
+        }
+        $this->container['id'] = $id;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 446 - 0
api-test/client/lib/Model/PostApiV3DiscussionsDiscussionRepliesRequest.php

@@ -0,0 +1,446 @@
+<?php
+/**
+ * PostApiV3DiscussionsDiscussionRepliesRequest
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PostApiV3DiscussionsDiscussionRepliesRequest Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PostApiV3DiscussionsDiscussionRepliesRequest implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'post_api_v3_discussions_discussion_replies_request';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'content' => 'string',
+        'notify' => 'bool'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'content' => null,
+        'notify' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'content' => false,
+        'notify' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'content' => 'content',
+        'notify' => 'notify'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'content' => 'setContent',
+        'notify' => 'setNotify'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'content' => 'getContent',
+        'notify' => 'getNotify'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('content', $data ?? [], null);
+        $this->setIfExists('notify', $data ?? [], true);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        if ($this->container['content'] === null) {
+            $invalidProperties[] = "'content' can't be null";
+        }
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets content
+     *
+     * @return string
+     */
+    public function getContent()
+    {
+        return $this->container['content'];
+    }
+
+    /**
+     * Sets content
+     *
+     * @param string $content 正文(markdown)
+     *
+     * @return self
+     */
+    public function setContent($content)
+    {
+        if (is_null($content)) {
+            throw new \InvalidArgumentException('non-nullable content cannot be null');
+        }
+        $this->container['content'] = $content;
+
+        return $this;
+    }
+
+    /**
+     * Gets notify
+     *
+     * @return bool|null
+     */
+    public function getNotify()
+    {
+        return $this->container['notify'];
+    }
+
+    /**
+     * Sets notify
+     *
+     * @param bool|null $notify 是否发通知
+     *
+     * @return self
+     */
+    public function setNotify($notify)
+    {
+        if (is_null($notify)) {
+            throw new \InvalidArgumentException('non-nullable notify cannot be null');
+        }
+        $this->container['notify'] = $notify;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 945 - 0
api-test/client/lib/Model/PostApiV3DiscussionsRequest.php

@@ -0,0 +1,945 @@
+<?php
+/**
+ * PostApiV3DiscussionsRequest
+ *
+ * PHP version 8.1
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ */
+
+/**
+ * MINT API
+ *
+ * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
+ *
+ * The version of the OpenAPI document: 2026.10.15
+ * Generated by: https://openapi-generator.tech
+ * Generator version: 7.25.0
+ */
+
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+namespace WikipaliApi\Model;
+
+use \ArrayAccess;
+use \WikipaliApi\ObjectSerializer;
+
+/**
+ * PostApiV3DiscussionsRequest Class Doc Comment
+ *
+ * @category Class
+ * @package  WikipaliApi
+ * @author   OpenAPI Generator team
+ * @link     https://openapi-generator.tech
+ * @implements \ArrayAccess<string, mixed>
+ */
+class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonSerializable
+{
+    public const DISCRIMINATOR = null;
+
+    /**
+     * The original name of the model.
+     *
+     * @var string
+     */
+    protected static $openAPIModelName = 'post_api_v3_discussions_request';
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @var string[]
+     */
+    protected static $openAPITypes = [
+        'target_type' => 'string',
+        'target_id' => 'string',
+        'type' => 'string',
+        'title' => 'string',
+        'content' => 'string',
+        'severity' => 'string',
+        'pos_start' => 'int',
+        'pos_end' => 'int',
+        'quote_exact' => 'string',
+        'quote_prefix' => 'string',
+        'quote_suffix' => 'string',
+        'tpl_id' => 'string',
+        'notify' => 'bool'
+    ];
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @var string[]
+     * @phpstan-var array<string, string|null>
+     * @psalm-var array<string, string|null>
+     */
+    protected static $openAPIFormats = [
+        'target_type' => null,
+        'target_id' => null,
+        'type' => null,
+        'title' => null,
+        'content' => null,
+        'severity' => null,
+        'pos_start' => null,
+        'pos_end' => null,
+        'quote_exact' => null,
+        'quote_prefix' => null,
+        'quote_suffix' => null,
+        'tpl_id' => null,
+        'notify' => null
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'target_type' => false,
+        'target_id' => false,
+        'type' => false,
+        'title' => false,
+        'content' => false,
+        'severity' => false,
+        'pos_start' => false,
+        'pos_end' => false,
+        'quote_exact' => false,
+        'quote_prefix' => false,
+        'quote_suffix' => false,
+        'tpl_id' => false,
+        'notify' => false
+    ];
+
+    /**
+     * If a nullable field gets set to null, insert it here
+     *
+     * @var boolean[]
+     */
+    protected array $openAPINullablesSetToNull = [];
+
+    /**
+     * Array of property to type mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPITypes()
+    {
+        return self::$openAPITypes;
+    }
+
+    /**
+     * Array of property to format mappings. Used for (de)serialization
+     *
+     * @return array
+     */
+    public static function openAPIFormats()
+    {
+        return self::$openAPIFormats;
+    }
+
+    /**
+     * Array of nullable properties
+     *
+     * @return array
+     */
+    protected static function openAPINullables(): array
+    {
+        return self::$openAPINullables;
+    }
+
+    /**
+     * Array of nullable field names deliberately set to null
+     *
+     * @return boolean[]
+     */
+    private function getOpenAPINullablesSetToNull(): array
+    {
+        return $this->openAPINullablesSetToNull;
+    }
+
+    /**
+     * Setter - Array of nullable field names deliberately set to null
+     *
+     * @param boolean[] $openAPINullablesSetToNull
+     */
+    private function setOpenAPINullablesSetToNull(array $openAPINullablesSetToNull): void
+    {
+        $this->openAPINullablesSetToNull = $openAPINullablesSetToNull;
+    }
+
+    /**
+     * Checks if a property is nullable
+     *
+     * @param string $property
+     * @return bool
+     */
+    public static function isNullable(string $property): bool
+    {
+        return self::openAPINullables()[$property] ?? false;
+    }
+
+    /**
+     * Checks if a nullable property is set to null.
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function isNullableSetToNull(string $property): bool
+    {
+        return in_array($property, $this->getOpenAPINullablesSetToNull(), true);
+    }
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @var string[]
+     */
+    protected static $attributeMap = [
+        'target_type' => 'target_type',
+        'target_id' => 'target_id',
+        'type' => 'type',
+        'title' => 'title',
+        'content' => 'content',
+        'severity' => 'severity',
+        'pos_start' => 'pos_start',
+        'pos_end' => 'pos_end',
+        'quote_exact' => 'quote_exact',
+        'quote_prefix' => 'quote_prefix',
+        'quote_suffix' => 'quote_suffix',
+        'tpl_id' => 'tpl_id',
+        'notify' => 'notify'
+    ];
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @var string[]
+     */
+    protected static $setters = [
+        'target_type' => 'setTargetType',
+        'target_id' => 'setTargetId',
+        'type' => 'setType',
+        'title' => 'setTitle',
+        'content' => 'setContent',
+        'severity' => 'setSeverity',
+        'pos_start' => 'setPosStart',
+        'pos_end' => 'setPosEnd',
+        'quote_exact' => 'setQuoteExact',
+        'quote_prefix' => 'setQuotePrefix',
+        'quote_suffix' => 'setQuoteSuffix',
+        'tpl_id' => 'setTplId',
+        'notify' => 'setNotify'
+    ];
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @var string[]
+     */
+    protected static $getters = [
+        'target_type' => 'getTargetType',
+        'target_id' => 'getTargetId',
+        'type' => 'getType',
+        'title' => 'getTitle',
+        'content' => 'getContent',
+        'severity' => 'getSeverity',
+        'pos_start' => 'getPosStart',
+        'pos_end' => 'getPosEnd',
+        'quote_exact' => 'getQuoteExact',
+        'quote_prefix' => 'getQuotePrefix',
+        'quote_suffix' => 'getQuoteSuffix',
+        'tpl_id' => 'getTplId',
+        'notify' => 'getNotify'
+    ];
+
+    /**
+     * Array of attributes where the key is the local name,
+     * and the value is the original name
+     *
+     * @return array
+     */
+    public static function attributeMap()
+    {
+        return self::$attributeMap;
+    }
+
+    /**
+     * Array of attributes to setter functions (for deserialization of responses)
+     *
+     * @return array
+     */
+    public static function setters()
+    {
+        return self::$setters;
+    }
+
+    /**
+     * Array of attributes to getter functions (for serialization of requests)
+     *
+     * @return array
+     */
+    public static function getters()
+    {
+        return self::$getters;
+    }
+
+    /**
+     * The original name of the model.
+     *
+     * @return string
+     */
+    public function getModelName()
+    {
+        return self::$openAPIModelName;
+    }
+
+    public const TARGET_TYPE_SENTENCE = 'sentence';
+    public const TARGET_TYPE_WBW = 'wbw';
+    public const TARGET_TYPE_ARTICLE = 'article';
+    public const TARGET_TYPE_TERM = 'term';
+    public const TARGET_TYPE_TASK = 'task';
+    public const TYPE_DISCUSSION = 'discussion';
+    public const TYPE_QA = 'qa';
+    public const TYPE_HELP = 'help';
+    public const TYPE_ISSUE = 'issue';
+    public const TYPE_NOTE = 'note';
+    public const TYPE_COMMENTARY = 'commentary';
+    public const SEVERITY_ERROR = 'error';
+    public const SEVERITY_WARNING = 'warning';
+    public const SEVERITY_SUGGESTION = 'suggestion';
+    public const SEVERITY_URGENT = 'urgent';
+    public const SEVERITY_NORMAL = 'normal';
+
+    /**
+     * Gets allowable values of the enum
+     *
+     * @return string[]
+     */
+    public function getTargetTypeAllowableValues()
+    {
+        return [
+            self::TARGET_TYPE_SENTENCE,
+            self::TARGET_TYPE_WBW,
+            self::TARGET_TYPE_ARTICLE,
+            self::TARGET_TYPE_TERM,
+            self::TARGET_TYPE_TASK,
+        ];
+    }
+
+    /**
+     * Gets allowable values of the enum
+     *
+     * @return string[]
+     */
+    public function getTypeAllowableValues()
+    {
+        return [
+            self::TYPE_DISCUSSION,
+            self::TYPE_QA,
+            self::TYPE_HELP,
+            self::TYPE_ISSUE,
+            self::TYPE_NOTE,
+            self::TYPE_COMMENTARY,
+        ];
+    }
+
+    /**
+     * Gets allowable values of the enum
+     *
+     * @return string[]
+     */
+    public function getSeverityAllowableValues()
+    {
+        return [
+            self::SEVERITY_ERROR,
+            self::SEVERITY_WARNING,
+            self::SEVERITY_SUGGESTION,
+            self::SEVERITY_URGENT,
+            self::SEVERITY_NORMAL,
+        ];
+    }
+
+    /**
+     * Associative array for storing property values
+     *
+     * @var mixed[]
+     */
+    protected $container = [];
+
+    /**
+     * Constructor
+     *
+     * @param mixed[]|null $data Associated array of property values
+     *                      initializing the model
+     */
+    public function __construct(?array $data = null)
+    {
+        $this->setIfExists('target_type', $data ?? [], null);
+        $this->setIfExists('target_id', $data ?? [], null);
+        $this->setIfExists('type', $data ?? [], 'discussion');
+        $this->setIfExists('title', $data ?? [], null);
+        $this->setIfExists('content', $data ?? [], null);
+        $this->setIfExists('severity', $data ?? [], null);
+        $this->setIfExists('pos_start', $data ?? [], null);
+        $this->setIfExists('pos_end', $data ?? [], null);
+        $this->setIfExists('quote_exact', $data ?? [], null);
+        $this->setIfExists('quote_prefix', $data ?? [], null);
+        $this->setIfExists('quote_suffix', $data ?? [], null);
+        $this->setIfExists('tpl_id', $data ?? [], null);
+        $this->setIfExists('notify', $data ?? [], true);
+    }
+
+    /**
+     * Sets $this->container[$variableName] to the given data or to the given default Value; if $variableName
+     * is nullable and its value is set to null in the $fields array, then mark it as "set to null" in the
+     * $this->openAPINullablesSetToNull array
+     *
+     * @param string $variableName
+     * @param array  $fields
+     * @param mixed  $defaultValue
+     */
+    private function setIfExists(string $variableName, array $fields, $defaultValue): void
+    {
+        if (self::isNullable($variableName) && array_key_exists($variableName, $fields) && is_null($fields[$variableName])) {
+            $this->openAPINullablesSetToNull[] = $variableName;
+        }
+
+        $this->container[$variableName] = $fields[$variableName] ?? $defaultValue;
+    }
+
+    /**
+     * Show all the invalid properties with reasons.
+     *
+     * @return array invalid properties with reasons
+     */
+    public function listInvalidProperties()
+    {
+        $invalidProperties = [];
+
+        if ($this->container['target_type'] === null) {
+            $invalidProperties[] = "'target_type' can't be null";
+        }
+        $allowedValues = $this->getTargetTypeAllowableValues();
+        if (!is_null($this->container['target_type']) && !in_array($this->container['target_type'], $allowedValues, true)) {
+            $invalidProperties[] = sprintf(
+                "invalid value '%s' for 'target_type', must be one of '%s'",
+                $this->container['target_type'],
+                implode("', '", $allowedValues)
+            );
+        }
+
+        if ($this->container['target_id'] === null) {
+            $invalidProperties[] = "'target_id' can't be null";
+        }
+        $allowedValues = $this->getTypeAllowableValues();
+        if (!is_null($this->container['type']) && !in_array($this->container['type'], $allowedValues, true)) {
+            $invalidProperties[] = sprintf(
+                "invalid value '%s' for 'type', must be one of '%s'",
+                $this->container['type'],
+                implode("', '", $allowedValues)
+            );
+        }
+
+        $allowedValues = $this->getSeverityAllowableValues();
+        if (!is_null($this->container['severity']) && !in_array($this->container['severity'], $allowedValues, true)) {
+            $invalidProperties[] = sprintf(
+                "invalid value '%s' for 'severity', must be one of '%s'",
+                $this->container['severity'],
+                implode("', '", $allowedValues)
+            );
+        }
+
+        return $invalidProperties;
+    }
+
+    /**
+     * Validate all the properties in the model
+     * return true if all passed
+     *
+     * @return bool True if all properties are valid
+     */
+    public function valid()
+    {
+        return count($this->listInvalidProperties()) === 0;
+    }
+
+
+    /**
+     * Gets target_type
+     *
+     * @return string
+     */
+    public function getTargetType()
+    {
+        return $this->container['target_type'];
+    }
+
+    /**
+     * Sets target_type
+     *
+     * @param string $target_type 挂靠对象的类型
+     *
+     * @return self
+     */
+    public function setTargetType($target_type)
+    {
+        if (is_null($target_type)) {
+            throw new \InvalidArgumentException('non-nullable target_type cannot be null');
+        }
+        $allowedValues = $this->getTargetTypeAllowableValues();
+        if (!in_array($target_type, $allowedValues, true)) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    "Invalid value '%s' for 'target_type', must be one of '%s'",
+                    $target_type,
+                    implode("', '", $allowedValues)
+                )
+            );
+        }
+        $this->container['target_type'] = $target_type;
+
+        return $this;
+    }
+
+    /**
+     * Gets target_id
+     *
+     * @return string
+     */
+    public function getTargetId()
+    {
+        return $this->container['target_id'];
+    }
+
+    /**
+     * Sets target_id
+     *
+     * @param string $target_id 挂靠对象的 uuid
+     *
+     * @return self
+     */
+    public function setTargetId($target_id)
+    {
+        if (is_null($target_id)) {
+            throw new \InvalidArgumentException('non-nullable target_id cannot be null');
+        }
+        $this->container['target_id'] = $target_id;
+
+        return $this;
+    }
+
+    /**
+     * Gets type
+     *
+     * @return string|null
+     */
+    public function getType()
+    {
+        return $this->container['type'];
+    }
+
+    /**
+     * Sets type
+     *
+     * @param string|null $type 类型
+     *
+     * @return self
+     */
+    public function setType($type)
+    {
+        if (is_null($type)) {
+            throw new \InvalidArgumentException('non-nullable type cannot be null');
+        }
+        $allowedValues = $this->getTypeAllowableValues();
+        if (!in_array($type, $allowedValues, true)) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    "Invalid value '%s' for 'type', must be one of '%s'",
+                    $type,
+                    implode("', '", $allowedValues)
+                )
+            );
+        }
+        $this->container['type'] = $type;
+
+        return $this;
+    }
+
+    /**
+     * Gets title
+     *
+     * @return string|null
+     */
+    public function getTitle()
+    {
+        return $this->container['title'];
+    }
+
+    /**
+     * Sets title
+     *
+     * @param string|null $title 标题,最长 256 字符;评论线程必填,批注可省
+     *
+     * @return self
+     */
+    public function setTitle($title)
+    {
+        if (is_null($title)) {
+            throw new \InvalidArgumentException('non-nullable title cannot be null');
+        }
+        $this->container['title'] = $title;
+
+        return $this;
+    }
+
+    /**
+     * Gets content
+     *
+     * @return string|null
+     */
+    public function getContent()
+    {
+        return $this->container['content'];
+    }
+
+    /**
+     * Sets content
+     *
+     * @param string|null $content 正文(markdown);批注必填
+     *
+     * @return self
+     */
+    public function setContent($content)
+    {
+        if (is_null($content)) {
+            throw new \InvalidArgumentException('non-nullable content cannot be null');
+        }
+        $this->container['content'] = $content;
+
+        return $this;
+    }
+
+    /**
+     * Gets severity
+     *
+     * @return string|null
+     */
+    public function getSeverity()
+    {
+        return $this->container['severity'];
+    }
+
+    /**
+     * Sets severity
+     *
+     * @param string|null $severity 严重程度
+     *
+     * @return self
+     */
+    public function setSeverity($severity)
+    {
+        if (is_null($severity)) {
+            throw new \InvalidArgumentException('non-nullable severity cannot be null');
+        }
+        $allowedValues = $this->getSeverityAllowableValues();
+        if (!in_array($severity, $allowedValues, true)) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    "Invalid value '%s' for 'severity', must be one of '%s'",
+                    $severity,
+                    implode("', '", $allowedValues)
+                )
+            );
+        }
+        $this->container['severity'] = $severity;
+
+        return $this;
+    }
+
+    /**
+     * Gets pos_start
+     *
+     * @return int|null
+     */
+    public function getPosStart()
+    {
+        return $this->container['pos_start'];
+    }
+
+    /**
+     * Sets pos_start
+     *
+     * @param int|null $pos_start 批注锚点在句子文本中的起始偏移(只限批注)
+     *
+     * @return self
+     */
+    public function setPosStart($pos_start)
+    {
+        if (is_null($pos_start)) {
+            throw new \InvalidArgumentException('non-nullable pos_start cannot be null');
+        }
+        $this->container['pos_start'] = $pos_start;
+
+        return $this;
+    }
+
+    /**
+     * Gets pos_end
+     *
+     * @return int|null
+     */
+    public function getPosEnd()
+    {
+        return $this->container['pos_end'];
+    }
+
+    /**
+     * Sets pos_end
+     *
+     * @param int|null $pos_end 批注锚点的结束偏移,批注插在这里(只限批注)
+     *
+     * @return self
+     */
+    public function setPosEnd($pos_end)
+    {
+        if (is_null($pos_end)) {
+            throw new \InvalidArgumentException('non-nullable pos_end cannot be null');
+        }
+        $this->container['pos_end'] = $pos_end;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_exact
+     *
+     * @return string|null
+     */
+    public function getQuoteExact()
+    {
+        return $this->container['quote_exact'];
+    }
+
+    /**
+     * Sets quote_exact
+     *
+     * @param string|null $quote_exact 锚定的原文片段(只限批注)
+     *
+     * @return self
+     */
+    public function setQuoteExact($quote_exact)
+    {
+        if (is_null($quote_exact)) {
+            throw new \InvalidArgumentException('non-nullable quote_exact cannot be null');
+        }
+        $this->container['quote_exact'] = $quote_exact;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_prefix
+     *
+     * @return string|null
+     */
+    public function getQuotePrefix()
+    {
+        return $this->container['quote_prefix'];
+    }
+
+    /**
+     * Sets quote_prefix
+     *
+     * @param string|null $quote_prefix 锚定片段前的上下文,用于重新定位(只限批注)
+     *
+     * @return self
+     */
+    public function setQuotePrefix($quote_prefix)
+    {
+        if (is_null($quote_prefix)) {
+            throw new \InvalidArgumentException('non-nullable quote_prefix cannot be null');
+        }
+        $this->container['quote_prefix'] = $quote_prefix;
+
+        return $this;
+    }
+
+    /**
+     * Gets quote_suffix
+     *
+     * @return string|null
+     */
+    public function getQuoteSuffix()
+    {
+        return $this->container['quote_suffix'];
+    }
+
+    /**
+     * Sets quote_suffix
+     *
+     * @param string|null $quote_suffix 锚定片段后的上下文,用于重新定位(只限批注)
+     *
+     * @return self
+     */
+    public function setQuoteSuffix($quote_suffix)
+    {
+        if (is_null($quote_suffix)) {
+            throw new \InvalidArgumentException('non-nullable quote_suffix cannot be null');
+        }
+        $this->container['quote_suffix'] = $quote_suffix;
+
+        return $this;
+    }
+
+    /**
+     * Gets tpl_id
+     *
+     * @return string|null
+     */
+    public function getTplId()
+    {
+        return $this->container['tpl_id'];
+    }
+
+    /**
+     * Sets tpl_id
+     *
+     * @param string|null $tpl_id 话题模板(文章)的 uuid
+     *
+     * @return self
+     */
+    public function setTplId($tpl_id)
+    {
+        if (is_null($tpl_id)) {
+            throw new \InvalidArgumentException('non-nullable tpl_id cannot be null');
+        }
+        $this->container['tpl_id'] = $tpl_id;
+
+        return $this;
+    }
+
+    /**
+     * Gets notify
+     *
+     * @return bool|null
+     */
+    public function getNotify()
+    {
+        return $this->container['notify'];
+    }
+
+    /**
+     * Sets notify
+     *
+     * @param bool|null $notify 是否发通知(批注不发)
+     *
+     * @return self
+     */
+    public function setNotify($notify)
+    {
+        if (is_null($notify)) {
+            throw new \InvalidArgumentException('non-nullable notify cannot be null');
+        }
+        $this->container['notify'] = $notify;
+
+        return $this;
+    }
+    /**
+     * Returns true if offset exists. False otherwise.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return boolean
+     */
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->container[$offset]);
+    }
+
+    /**
+     * Gets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return mixed|null
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet(mixed $offset)
+    {
+        return $this->container[$offset] ?? null;
+    }
+
+    /**
+     * Sets value based on offset.
+     *
+     * @param int|null $offset Offset
+     * @param mixed    $value  Value to be set
+     *
+     * @return void
+     */
+    public function offsetSet($offset, $value): void
+    {
+        if (is_null($offset)) {
+            $this->container[] = $value;
+        } else {
+            $this->container[$offset] = $value;
+        }
+    }
+
+    /**
+     * Unsets offset.
+     *
+     * @param integer|string $offset Offset
+     *
+     * @return void
+     */
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->container[$offset]);
+    }
+
+    /**
+     * Serializes the object to a value that can be serialized natively by json_encode().
+     * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php
+     *
+     * @return mixed Returns data which can be serialized by json_encode(), which is a value
+     * of any type other than a resource.
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+       return ObjectSerializer::sanitizeForSerialization($this);
+    }
+
+    /**
+     * Gets the string presentation of the object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return json_encode(
+            ObjectSerializer::sanitizeForSerialization($this),
+            JSON_PRETTY_PRINT
+        );
+    }
+
+    /**
+     * Gets a header-safe presentation of the object
+     *
+     * @return string
+     */
+    public function toHeaderValue()
+    {
+        return json_encode(ObjectSerializer::sanitizeForSerialization($this));
+    }
+}
+
+

+ 1 - 1
api-test/client/lib/Model/PostApiV3EmailCertificationsRequest.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3Invites200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3Invites200ResponseData.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3InvitesRequest.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3MeReactions200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3MeReactionsRequest.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3PasswordResetsRequest.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3SearchRequest.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3Sessions200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3Sessions200ResponseData.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3SessionsRequest.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3Users200Response.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3Users200ResponseData.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3UsersRequest.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/ProblemDetails.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/ObjectSerializer.php

@@ -15,7 +15,7 @@
  *
  * International Academy Of Pali Tipitaka(国际巴利三藏学院)  由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  *
- * The version of the OpenAPI document: 2026.10.8
+ * The version of the OpenAPI document: 2026.10.15
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 31 - 0
api-test/config.local.example.php

@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * api-test 本机配置模板。复制成 config.local.php 再填(已 gitignore,不要提交):
+ *
+ *   cp config.local.example.php config.local.php
+ *
+ * 优先级:命令行参数 > 环境变量(API_TEST_*)> 本文件 > config.php 的默认值。
+ * 不需要的键删掉或留 null 即可。
+ */
+
+return [
+    // 默认服务器 alias(见 config.php 的 servers),或完整 base URL
+    'default_server' => 'local',
+
+    // 测试账号:调 POST /v3/sessions 换 token。也可以直接给 token(优先)
+    'username' => 'test161',
+    'password' => null,
+    'token' => null,
+
+    // 覆盖 fixtures.php 里的种子数据(换环境时用),只写要改的键
+    'fixtures' => [
+        // 讨论写用例挂的句子:必须在测试账号自己的 channel 里、由测试账号编辑过
+        // 'discussion_sentence' => 'ee5259be-5d1e-4408-a2a6-566a5773eef1',
+        // 'discussion_channel' => '1721b7fa-e471-46fd-b860-64b9e79d370e',
+        // agent 代写用例:归测试账号所有的 AI 模型 uid;不给则 SKIP
+        // 'agent_model' => null,
+        // email-certifications「已注册邮箱」用例;不给则 SKIP
+        // 'registered_email' => 'someone@example.com',
+    ],
+];

+ 16 - 6
api-test/config.php

@@ -11,19 +11,26 @@
  * alias 对齐 openapi/public/assets/protocol/main.yaml 里的 servers。
  * 第一个 `/api` 是同源地址,CLI 客户端用不到,故略。
  *
- * token(测试 /v3/me/* 用)优先级:
+ * 本机的账号、默认服务器、种子数据覆盖写在 config.local.php(模板
+ * config.local.example.php,已 gitignore)。
+ *
+ * token(需登录的用例用)优先级:
  *   1. `php run.php --token=...`
  *   2. 环境变量 `API_TEST_TOKEN`
- *   3. 都没有时,用下面的 username/password 调 `POST /v2/sign-in` 现换一个 token
+ *   3. config.local.php 的 `token`
+ *   4. 都没有时,用下面的 username/password 调 `POST /v3/sessions` 现换一个 token
  *
  * username/password(登录换 token 用)优先级:
  *   1. `php run.php --username=... --password=...`
  *   2. 环境变量 `API_TEST_USERNAME` / `API_TEST_PASSWORD`
+ *   3. config.local.php
  *
  * 写操作(POST/DELETE)在**所有服务器上都真实执行**,包括 prod——会产生垃圾数据,
  * 由测试账号定期清理。
  */
 
+$local = is_file(__DIR__ . '/config.local.php') ? require __DIR__ . '/config.local.php' : [];
+
 return [
     // alias => 完整 base URL(含 /api 前缀)
     'servers' => [
@@ -34,14 +41,17 @@ return [
         'next-cn' => 'https://next.wikipali.cc/api',
         'prod-cn' => 'https://www.wikipali.cc/api',
     ],
-    'default_server' => 'local',
+    'default_server' => $local['default_server'] ?? 'local',
 
     // bearer token;没有则 /v3/me/* 的“带 token”用例会 FAIL(不跳过)。
-    'token' => getenv('API_TEST_TOKEN') ?: null,
+    'token' => getenv('API_TEST_TOKEN') ?: ($local['token'] ?? null),
 
     // 登录凭据:sign-in 用 username(也可以是 email)+ password。
-    'username' => getenv('API_TEST_USERNAME') ?: null,
-    'password' => getenv('API_TEST_PASSWORD') ?: null,
+    'username' => getenv('API_TEST_USERNAME') ?: ($local['username'] ?? null),
+    'password' => getenv('API_TEST_PASSWORD') ?: ($local['password'] ?? null),
+
+    // 覆盖 fixtures.php 的种子数据
+    'fixtures' => $local['fixtures'] ?? [],
 
     'timeout' => 20,
 ];

+ 11 - 1
api-test/fixtures.php

@@ -3,7 +3,8 @@
 /**
  * v3 用例的种子数据。
  *
- * 不同环境(local / staging / prod)数据不同,必要时按环境覆盖。
+ * 不同环境(local / staging / prod)数据不同:换环境时在 config.local.php 的
+ * `fixtures` 里覆盖要改的键,不要改这里。
  * 当前取值来自本地开发库实测 + openapi spec 里的 example。
  */
 
@@ -24,6 +25,15 @@ return [
     'search_q'       => 'dukkha',
     'search_doc_id'  => 'term_f8afdcaa-ad68-4795-b3d8-d5f12bdb3a5f',
 
+    // discussions 写用例挂的句子:放在测试账号自己的 channel 里、由测试账号编辑,
+    // 这样即便有通知也只发给自己(用例本身还会带 notify=false)。换环境时按需覆盖。
+    'discussion_sentence' => 'ee5259be-5d1e-4408-a2a6-566a5773eef1',
+    // discussion_sentence 所在的 channel(agent 授权的范围)
+    'discussion_channel' => '1721b7fa-e471-46fd-b860-64b9e79d370e',
+
+    // agent 代写用例:归测试账号所有的 AI 模型 uid,按环境在 config.local.php 里配,没配则 SKIP
+    'agent_model' => null,
+
     // 一个已注册账号的邮箱(email-certifications 的「已注册」用例;只校验、不发信)。
     // 留空则该用例 SKIP。
     'registered_email' => getenv('API_TEST_REGISTERED_EMAIL') ?: null,

+ 5 - 2
api-test/run.php

@@ -49,8 +49,11 @@ if ($envBase !== null) {
     $config['base_url'] = $servers[$serverArg];
 } elseif ($serverArg !== null && preg_match('#^https?://#', $serverArg)) {
     $config['base_url'] = rtrim($serverArg, '/');
-} else {
+} elseif (isset($servers[$config['default_server']])) {
     $config['base_url'] = $servers[$config['default_server']];
+} else {
+    // config.local.php 的 default_server 也可以直接写完整 base URL
+    $config['base_url'] = rtrim($config['default_server'], '/');
 }
 
 // token 覆盖
@@ -67,7 +70,7 @@ if (is_string($opts['password'] ?? null) && $opts['password'] !== '') {
 }
 
 $GLOBALS['config'] = $config;
-$GLOBALS['fixtures'] = require __DIR__ . '/fixtures.php';
+$GLOBALS['fixtures'] = array_replace(require __DIR__ . '/fixtures.php', $config['fixtures']);
 
 require __DIR__ . '/src/bootstrap.php';
 

+ 3 - 15
api-test/tools/filter-v3.php

@@ -65,21 +65,9 @@ if ($v3Paths === []) {
     exit(2);
 }
 
-// 2) 修正鉴权语义:源 spec 用全局 security 把全部路径标成 bearerAuth,
-//    但 v3 实际只有 /v3/me 与 /v3/me/* 挂 auth.v3。这里逐 operation 写准,并清掉全局 security。
-//    注意 /v3/me 本身(不带斜杠)也要算进去,否则生成的客户端不带 token。
-foreach ($v3Paths as $path => &$item) {
-    foreach ($item as $method => &$op) {
-        if (!is_array($op) || strtolower($method) === 'parameters') {
-            continue;
-        }
-        $op['security'] = ($path === '/v3/me' || str_starts_with((string) $path, '/v3/me/'))
-            ? [['bearerAuth' => []]]
-            : [];
-    }
-    unset($op);
-}
-unset($item);
+// 2) 鉴权:每个 v3 operation 的 security 由生成器按路由中间件写好(auth.v3 → 必须,
+//    否则可选),这里只清掉全局 security,免得没写的 operation 继承成「必须」。
+unset($spec['security']);
 
 // 3) 递归收集引用的组件(含 security scheme)
 $refs = ['#/components/securitySchemes/bearerAuth' => true];

+ 4 - 0
api-v13/app/Console/Commands/ExportDiscussion.php

@@ -52,6 +52,8 @@ class ExportDiscussion extends Command
         $total = DB::table('discussions')
             ->where('editor_uid', $editorUid)
             ->where('status', 'active')
+            // discussions 已改为软删除;裸 DB::table() 不走模型的全局作用域,要自己排除
+            ->whereNull('deleted_at')
             ->count();
 
         if ($total === 0) {
@@ -85,6 +87,8 @@ class ExportDiscussion extends Command
         DB::table('discussions')
             ->where('editor_uid', $editorUid)
             ->where('status', 'active')
+            // discussions 已改为软删除;裸 DB::table() 不走模型的全局作用域,要自己排除
+            ->whereNull('deleted_at')
             ->orderBy('created_at', 'asc')
             ->select(['id', 'res_id', 'res_type', 'content', 'created_at'])
             ->chunk(50, function ($discussions) use ($progressBar) {

+ 48 - 0
api-v13/app/Http/Controllers/V3/AgentGrantController.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace App\Http\Controllers\V3;
+
+use App\Http\Controllers\Concerns\ResolvesCurrentUser;
+use App\Http\Controllers\Controller;
+use App\Http\Requests\V3\StoreAgentGrantRequest;
+use App\Http\Resources\V3\AgentGrantResource;
+use App\Services\V3\AgentGrantService;
+
+/**
+ * agent 写入授权:人类授权自己名下的 AI 模型代写某个 channel。
+ *
+ * 取代 `POST /v2/access-token`(`AccessTokenController@store`)的 agent 用途;v2 那份在
+ * dashboard-v4 下线前原样保留。区别见 {@see AgentGrantService}。
+ */
+class AgentGrantController extends Controller
+{
+    use ResolvesCurrentUser;
+
+    public function __construct(private readonly AgentGrantService $grants) {}
+
+    /**
+     * 签发 agent 写入授权
+     *
+     * 必须用人类的登录 token 调用(模型 token 403);模型必须归当前用户所有,当前用户
+     * 必须能编辑该 channel。授权 7 天有效,只能由指定的模型使用;撤销该模型的 token
+     * (token_version 自增)时一并失效。
+     *
+     * 模型写入时用自己的模型 token 登录,并带请求头 `X-Agent-Grant: <token>`;
+     * 写入的记录 editor 为授权人、co_editor 为模型,权限按授权人判断。
+     *
+     * @responseStatus 201 签发成功
+     * @responseStatus 403 用模型 token 调用、模型不归当前用户,或当前用户不能编辑该 channel
+     *
+     * @bodyParam ai_model string required 被授权的 AI 模型 uid
+     * @bodyParam target_type string required 授权范围的类型。Enum: channel
+     * @bodyParam target_id string required channel 的 uid
+     * @bodyParam book integer 限定 book,0 或不传表示不限。Default: 0
+     */
+    public function store(StoreAgentGrantRequest $request)
+    {
+        return AgentGrantResource::make($this->grants->issue(
+            $this->currentUser($request)['user_uid'],
+            $request->validated(),
+        ))->response()->setStatusCode(201);
+    }
+}

+ 46 - 0
api-v13/app/Http/Controllers/V3/DiscussionBatchCountController.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Http\Controllers\V3;
+
+use App\Http\Controllers\Concerns\ResolvesCurrentUser;
+use App\Http\Controllers\Controller;
+use App\Http\Requests\V3\BatchCountDiscussionRequest;
+use App\Http\Resources\V3\DiscussionCountResource;
+use App\Services\V3\DiscussionService;
+
+/**
+ * 一批对象的讨论角标。
+ *
+ * 取代 `POST /v2/discussion-count` 的讨论部分(`discussions`);那个端点同时返回标签,
+ * 标签部分还没迁,v6 暂时仍调它取 `tags`。v2 那份在 dashboard-v4 下线前原样保留。
+ */
+class DiscussionBatchCountController extends Controller
+{
+    use ResolvesCurrentUser;
+
+    public function __construct(private readonly DiscussionService $discussions) {}
+
+    /**
+     * 批量取讨论角标
+     *
+     * 每个对象返回未关闭的评论线程数(total)与其中当前用户发起的数目(mine,未登录为 0),
+     * 按请求顺序、一个不少(没有讨论的为 0)。只数评论线程(discussion / qa / help / issue),
+     * 批注与已删除的不算;可见性与列表相同:未登录不数协作类话题,basic 角色在无权管理的
+     * 对象上只数自己发起的协作类话题。最多 500 个对象。
+     *
+     * 与 v2 的区别:入参是对象 uid 而不是句子坐标;只给计数不给明细;v2 不分类型、不管可见性。
+     *
+     * @unauthenticated
+     *
+     * @bodyParam targets array required 讨论对象列表,每项 {type, id},最多 500 个
+     * @bodyParam targets.*.type string required 对象类型。Enum: sentence,wbw
+     * @bodyParam targets.*.id string required 对象 uid
+     */
+    public function __invoke(BatchCountDiscussionRequest $request)
+    {
+        return DiscussionCountResource::collection($this->discussions->batchCount(
+            $request->validated('targets'),
+            $this->currentUserUid($request),
+        ));
+    }
+}

+ 183 - 0
api-v13/app/Http/Controllers/V3/DiscussionController.php

@@ -0,0 +1,183 @@
+<?php
+
+namespace App\Http\Controllers\V3;
+
+use App\Http\Controllers\Concerns\ResolvesCurrentUser;
+use App\Http\Controllers\Controller;
+use App\Http\Requests\V3\IndexDiscussionRequest;
+use App\Http\Requests\V3\StoreDiscussionRequest;
+use App\Http\Requests\V3\UpdateDiscussionRequest;
+use App\Http\Resources\V3\DiscussionResource;
+use App\Models\Discussion;
+use App\Services\V3\AgentGrantService;
+use App\Services\V3\DiscussionService;
+use Illuminate\Http\Request;
+
+/**
+ * 评论线程:挂在句子、逐词、文章、术语、任务上的话题与回复;以及挂在句子上的批注
+ * (note 边注、commentary 义注 / 复注对应——巴利注释书,不是普通评论)。
+ *
+ * 取代 `GET/POST /v2/discussion`(view=question / res_id / question-by-topic)、
+ * `GET/PUT/DELETE /v2/discussion/{discussion}`。v2 那份在 dashboard-v4 下线前原样保留。
+ * 回复见 {@see DiscussionReplyController}(取代 view=answer 与带 parent 的 POST)。
+ *
+ * 底层仍是 `discussions` 表。批注会注入阅读页,规则与评论不同,见各方法说明。
+ *
+ * 写操作(新建、修改、删除,以及回复)支持 AI 代写:模型用模型 token 登录,并带请求头
+ * `X-Agent-Grant`(人类经 `POST /v3/agent-grants` 签发)。此时 editor 记授权人、
+ * co_editor 记模型,权限按授权人判断,且只能写授权覆盖的 channel / book 里的句子上的内容。
+ * 模型不带授权写入一律 403。
+ */
+class DiscussionController extends Controller
+{
+    use ResolvesCurrentUser;
+
+    public function __construct(
+        private readonly DiscussionService $discussions,
+        private readonly AgentGrantService $grants,
+    ) {}
+
+    /**
+     * 列出某个对象上的话题
+     *
+     * 只列话题(不含回复),删除了但还有回复的话题以墓碑出现(deleted_at 有值、内容为空)。
+     * 未登录看不到协作类话题(discussion / issue);basic 角色在自己无权管理的对象上只看得到
+     * 自己发起的协作类话题。各状态的数量与「能发起哪些类型」见 `GET /v3/discussions/summary`。
+     * 不传 type 时只列评论线程(discussion / qa / help / issue),批注要显式 type=note / commentary。
+     *
+     * @unauthenticated
+     *
+     * @queryParam target_type string required 挂靠对象的类型。Enum: sentence,wbw,article,term,task
+     * @queryParam target_id string required 挂靠对象的 uuid
+     * @queryParam type string 类型,不传则只列评论线程。Enum: discussion,qa,help,issue,note,commentary
+     * @queryParam status string 话题状态,不传则两种都要。Enum: active,close
+     * @queryParam severity string 严重程度,逗号分隔多选。Example: error,warning
+     * @queryParam sort string 排序,前缀 - 表示倒序。Enum: created_at,-created_at,updated_at,-updated_at Default: -created_at
+     * @queryParam page integer 页码。Default: 1
+     * @queryParam per_page integer 每页数量,最大 200。Default: 15
+     */
+    public function index(IndexDiscussionRequest $request)
+    {
+        return DiscussionResource::collection($this->discussions->listTopics(
+            $request->validated(),
+            $this->currentUserUid($request),
+            $request->integer('per_page', 15),
+        ));
+    }
+
+    /**
+     * 发起话题或新建批注
+     *
+     * discussion / issue 登录即可发;qa / help 只能发在文章上,且只有文章的编辑者能发。
+     * severity 的取值随 type:issue 为 error / warning / suggestion,help 为 urgent / normal,
+     * 其它类型不收。默认会通知对象的作者(站内信 + webhook),`notify=false` 关闭。
+     *
+     * 批注(note / commentary)只能加在句子上,且只有该句所在 channel 的编辑者能加;
+     * 标题可省、正文必填,commentary 的正文必须是义注句子模板 `{{book-para-start-end}}`
+     * (可多句并列);可带锚点 pos_* / quote_*(评论线程传了 422);不发通知。
+     *
+     * @responseStatus 201 创建成功
+     * @responseStatus 403 当前用户不能在这个对象上发起这种类型的话题
+     *
+     * @bodyParam target_type string required 挂靠对象的类型。Enum: sentence,wbw,article,term,task
+     * @bodyParam target_id string required 挂靠对象的 uuid
+     * @bodyParam type string 类型。Enum: discussion,qa,help,issue,note,commentary Default: discussion
+     * @bodyParam title string 标题,最长 256 字符;评论线程必填,批注可省
+     * @bodyParam content string 正文(markdown);批注必填
+     * @bodyParam severity string 严重程度。Enum: error,warning,suggestion,urgent,normal
+     * @bodyParam pos_start integer 批注锚点在句子文本中的起始偏移(只限批注)
+     * @bodyParam pos_end integer 批注锚点的结束偏移,批注插在这里(只限批注)
+     * @bodyParam quote_exact string 锚定的原文片段(只限批注)
+     * @bodyParam quote_prefix string 锚定片段前的上下文,用于重新定位(只限批注)
+     * @bodyParam quote_suffix string 锚定片段后的上下文,用于重新定位(只限批注)
+     * @bodyParam tpl_id string 话题模板(文章)的 uuid
+     * @bodyParam notify boolean 是否发通知(批注不发)。Default: true
+     */
+    public function store(StoreDiscussionRequest $request)
+    {
+        return DiscussionResource::make($this->discussions->createTopic(
+            $this->grants->actor(
+                $this->currentUser($request)['user_uid'],
+                $request->header(AgentGrantService::HEADER),
+            ),
+            $request->validated(),
+        ));
+    }
+
+    /**
+     * 获取单条话题或回复
+     *
+     * 删除了但还有回复的话题返回墓碑;删除了的回复、删除了且没有回复的话题返回 404。
+     * 当前用户看不见的话题同样 404。
+     *
+     * @unauthenticated
+     *
+     * @urlParam discussion string required 话题或回复的 uuid
+     */
+    public function show(Request $request, Discussion $discussion)
+    {
+        return DiscussionResource::make(
+            $this->discussions->show($discussion, $this->currentUserUid($request))
+        );
+    }
+
+    /**
+     * 修改话题或回复(局部更新)
+     *
+     * 只改传了的字段。title / status / type / severity 只对话题有效,回复传了 422。
+     * 作者或对象的管理者(句子、逐词所在 channel 的编辑者,文章的编辑者)可改;
+     * 改 type 时回复跟着改,新类型不收原来的 severity 时 severity 清空。
+     * 批注:没有 status,type 只能在 note / commentary 之间改(不能与评论线程互转),
+     * 正文不能清空,可改锚点。
+     * 改了内容(标题、正文、锚点)时 co_editor 跟着这次写入走:人亲自改则清空,AI 代改则换成该模型;
+     * 只改 status / type / severity 不动 co_editor。
+     *
+     * @responseStatus 403 没有修改权限
+     *
+     * @urlParam discussion string required 话题或回复的 uuid
+     *
+     * @bodyParam title string 标题(只限话题)
+     * @bodyParam content string 正文(markdown)
+     * @bodyParam status string 状态(只限话题)。Enum: active,close
+     * @bodyParam type string 类型(只限话题;评论线程与批注不能互转)。Enum: discussion,qa,help,issue,note,commentary
+     * @bodyParam severity string 严重程度(只限话题),null 表示清空。Enum: error,warning,suggestion,urgent,normal
+     * @bodyParam pos_start integer 锚点起始偏移(只限批注)
+     * @bodyParam pos_end integer 锚点结束偏移(只限批注)
+     * @bodyParam quote_exact string 锚定的原文片段(只限批注)
+     * @bodyParam quote_prefix string 锚定片段前的上下文(只限批注)
+     * @bodyParam quote_suffix string 锚定片段后的上下文(只限批注)
+     */
+    public function update(UpdateDiscussionRequest $request, Discussion $discussion)
+    {
+        return DiscussionResource::make($this->discussions->update(
+            $discussion,
+            $this->grants->actor(
+                $this->currentUser($request)['user_uid'],
+                $request->header(AgentGrantService::HEADER),
+            ),
+            $request->validated(),
+        ));
+    }
+
+    /**
+     * 删除话题或回复(软删除)
+     *
+     * 作者本人或挂靠对象的管理者(句子 / 逐词所在 channel、文章的编辑者)能删;
+     * v2 只允许作者。删话题不动回复:有回复的话题以墓碑形式留在列表里。
+     * 返回删除后的资源(200),不是 204——软删的资源并没有消失。
+     *
+     * @responseStatus 403 既不是作者,也不是挂靠对象的管理者
+     *
+     * @urlParam discussion string required 话题或回复的 uuid
+     */
+    public function destroy(Request $request, Discussion $discussion)
+    {
+        return DiscussionResource::make($this->discussions->delete(
+            $discussion,
+            $this->grants->actor(
+                $this->currentUser($request)['user_uid'],
+                $request->header(AgentGrantService::HEADER),
+            ),
+        ));
+    }
+}

+ 76 - 0
api-v13/app/Http/Controllers/V3/DiscussionReplyController.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace App\Http\Controllers\V3;
+
+use App\Http\Controllers\Concerns\ResolvesCurrentUser;
+use App\Http\Controllers\Controller;
+use App\Http\Requests\V3\IndexDiscussionReplyRequest;
+use App\Http\Requests\V3\StoreDiscussionReplyRequest;
+use App\Http\Resources\V3\DiscussionResource;
+use App\Models\Discussion;
+use App\Services\V3\AgentGrantService;
+use App\Services\V3\DiscussionService;
+
+/**
+ * 话题下的回复。线程只有两层:回复不能再被回复。
+ *
+ * 取代 `GET /v2/discussion?view=answer` 与带 `parent` 的 `POST /v2/discussion`。
+ */
+class DiscussionReplyController extends Controller
+{
+    use ResolvesCurrentUser;
+
+    public function __construct(
+        private readonly DiscussionService $discussions,
+        private readonly AgentGrantService $grants,
+    ) {}
+
+    /**
+     * 列出话题的回复
+     *
+     * 按时间正序。删除了的话题(墓碑)下的回复照样可读。
+     *
+     * @unauthenticated
+     *
+     * @urlParam discussion string required 话题的 uuid
+     *
+     * @queryParam page integer 页码。Default: 1
+     * @queryParam per_page integer 每页数量,最大 200。Default: 50
+     */
+    public function index(IndexDiscussionReplyRequest $request, Discussion $discussion)
+    {
+        return DiscussionResource::collection($this->discussions->listReplies(
+            $discussion,
+            $this->currentUserUid($request),
+            $request->integer('per_page', 50),
+        ));
+    }
+
+    /**
+     * 回复话题
+     *
+     * 类型与挂靠对象继承话题。已关闭的话题不能回复(409),对一条回复再回复 422。
+     * AI 代写同 `POST /v3/discussions`(`X-Agent-Grant` 请求头)。
+     * 文章上的 qa 只有文章的编辑者能回答。默认发通知,`notify=false` 关闭。
+     *
+     * @responseStatus 201 回复成功
+     * @responseStatus 403 当前用户不能回复这个话题
+     * @responseStatus 409 话题已关闭
+     *
+     * @urlParam discussion string required 话题的 uuid
+     *
+     * @bodyParam content string required 正文(markdown)
+     * @bodyParam notify boolean 是否发通知。Default: true
+     */
+    public function store(StoreDiscussionReplyRequest $request, Discussion $discussion)
+    {
+        return DiscussionResource::make($this->discussions->createReply(
+            $discussion,
+            $this->grants->actor(
+                $this->currentUser($request)['user_uid'],
+                $request->header(AgentGrantService::HEADER),
+            ),
+            $request->validated(),
+        ));
+    }
+}

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů