Jelajahi Sumber

test(api-test): 新增 discussions 用例;本机配置改为 config.local.php

- cases/07_discussions.php:读、422/404/405、无 token 401、带 token 建→回复→
  关闭后回复 409→重开→改类型→删(软删)一整轮,失败也会清理
- 鉴权以生成器写的 security 为准,filter-v3.php 不再猜「只有 /v3/me* 要 token」
- 账号、默认服务器、种子数据覆盖放 config.local.php(模板 config.local.example.php,
  gitignore);优先级 命令行 > 环境变量 > config.local.php > config.php
- 03_reactions 改用 ReactionsApi;客户端按新规格重新生成

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
visuddhinanda 4 hari lalu
induk
melakukan
9b6ef82071
84 mengubah file dengan 10003 tambahan dan 1118 penghapusan
  1. 3 0
      api-test/.gitignore
  2. 47 28
      api-test/README.md
  3. 8 9
      api-test/cases/03_reactions.php
  4. 193 0
      api-test/cases/07_discussions.php
  5. 36 3
      api-test/client/.openapi-generator/FILES
  6. 2914 0
      api-test/client/lib/Api/DiscussionsApi.php
  7. 5 1
      api-test/client/lib/Api/EmailCertificationsApi.php
  8. 5 1
      api-test/client/lib/Api/HeartbeatApi.php
  9. 9 1
      api-test/client/lib/Api/InvitesApi.php
  10. 1 925
      api-test/client/lib/Api/MeApi.php
  11. 13 1
      api-test/client/lib/Api/PasswordResetsApi.php
  12. 5 1
      api-test/client/lib/Api/ProgressApi.php
  13. 993 60
      api-test/client/lib/Api/ReactionsApi.php
  14. 13 1
      api-test/client/lib/Api/SearchApi.php
  15. 5 1
      api-test/client/lib/Api/SearchSuggestApi.php
  16. 5 1
      api-test/client/lib/Api/SessionsApi.php
  17. 5 1
      api-test/client/lib/Api/TipitakaReadingApi.php
  18. 13 1
      api-test/client/lib/Api/TipitakaRelatedParagraphsApi.php
  19. 5 1
      api-test/client/lib/Api/UpgradeApi.php
  20. 5 1
      api-test/client/lib/Api/UsersApi.php
  21. 1 1
      api-test/client/lib/ApiException.php
  22. 2 2
      api-test/client/lib/Configuration.php
  23. 1 1
      api-test/client/lib/FormDataProcessor.php
  24. 1 1
      api-test/client/lib/HeaderSelector.php
  25. 443 0
      api-test/client/lib/Model/GetApiV3Discussions200Response.php
  26. 1070 0
      api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInner.php
  27. 511 0
      api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInnerAbilities.php
  28. 5 5
      api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInnerEditor.php
  29. 409 0
      api-test/client/lib/Model/GetApiV3DiscussionsSummary200Response.php
  30. 443 0
      api-test/client/lib/Model/GetApiV3DiscussionsSummary200ResponseData.php
  31. 443 0
      api-test/client/lib/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCounts.php
  32. 1 1
      api-test/client/lib/Model/GetApiV3Heartbeat200Response.php
  33. 1 1
      api-test/client/lib/Model/GetApiV3Heartbeat200ResponseData.php
  34. 1 1
      api-test/client/lib/Model/GetApiV3Me200Response.php
  35. 1 1
      api-test/client/lib/Model/GetApiV3Me200ResponseData.php
  36. 1 1
      api-test/client/lib/Model/GetApiV3MeReactions200Response.php
  37. 4 4
      api-test/client/lib/Model/GetApiV3MeReactions200ResponseDataInner.php
  38. 1 1
      api-test/client/lib/Model/GetApiV3PasswordResetsToken200Response.php
  39. 1 1
      api-test/client/lib/Model/GetApiV3PasswordResetsToken200ResponseData.php
  40. 1 1
      api-test/client/lib/Model/GetApiV3Progress200Response.php
  41. 1 1
      api-test/client/lib/Model/GetApiV3Progress200ResponseDataInner.php
  42. 1 1
      api-test/client/lib/Model/GetApiV3ReactionsTally200Response.php
  43. 1 1
      api-test/client/lib/Model/GetApiV3ReactionsTally200ResponseDataInner.php
  44. 1 1
      api-test/client/lib/Model/GetApiV3Search200Response.php
  45. 1 1
      api-test/client/lib/Model/GetApiV3SearchSearch200Response.php
  46. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaReadingChannel200Response.php
  47. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaReadingChannel200ResponseDataInner.php
  48. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaReadingChannel200ResponseMeta.php
  49. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200Response.php
  50. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200ResponseDataInner.php
  51. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200ResponseDataInnerPathInner.php
  52. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200ResponseMeta.php
  53. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregate200Response.php
  54. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregate200ResponseDataInner.php
  55. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200Response.php
  56. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200ResponseDataInner.php
  57. 1 1
      api-test/client/lib/Model/ModelInterface.php
  58. 1 1
      api-test/client/lib/Model/PaginationMeta.php
  59. 657 0
      api-test/client/lib/Model/PatchApiV3DiscussionsDiscussionRequest.php
  60. 1 1
      api-test/client/lib/Model/PatchApiV3PasswordResetsTokenRequest.php
  61. 409 0
      api-test/client/lib/Model/PostApiV3Discussions200Response.php
  62. 446 0
      api-test/client/lib/Model/PostApiV3DiscussionsDiscussionRepliesRequest.php
  63. 774 0
      api-test/client/lib/Model/PostApiV3DiscussionsRequest.php
  64. 1 1
      api-test/client/lib/Model/PostApiV3EmailCertificationsRequest.php
  65. 1 1
      api-test/client/lib/Model/PostApiV3Invites200Response.php
  66. 1 1
      api-test/client/lib/Model/PostApiV3Invites200ResponseData.php
  67. 1 1
      api-test/client/lib/Model/PostApiV3InvitesRequest.php
  68. 1 1
      api-test/client/lib/Model/PostApiV3MeReactions200Response.php
  69. 1 1
      api-test/client/lib/Model/PostApiV3MeReactionsRequest.php
  70. 1 1
      api-test/client/lib/Model/PostApiV3PasswordResetsRequest.php
  71. 1 1
      api-test/client/lib/Model/PostApiV3SearchRequest.php
  72. 1 1
      api-test/client/lib/Model/PostApiV3Sessions200Response.php
  73. 1 1
      api-test/client/lib/Model/PostApiV3Sessions200ResponseData.php
  74. 1 1
      api-test/client/lib/Model/PostApiV3SessionsRequest.php
  75. 1 1
      api-test/client/lib/Model/PostApiV3Users200Response.php
  76. 1 1
      api-test/client/lib/Model/PostApiV3Users200ResponseData.php
  77. 1 1
      api-test/client/lib/Model/PostApiV3UsersRequest.php
  78. 1 1
      api-test/client/lib/Model/ProblemDetails.php
  79. 1 1
      api-test/client/lib/ObjectSerializer.php
  80. 28 0
      api-test/config.local.example.php
  81. 16 6
      api-test/config.php
  82. 6 1
      api-test/fixtures.php
  83. 5 2
      api-test/run.php
  84. 3 15
      api-test/tools/filter-v3.php

+ 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');
 });

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

@@ -0,0 +1,193 @@
+<?php
+
+use \WikipaliApi\Api\DiscussionsApi;
+use \WikipaliApi\Model\PatchApiV3DiscussionsDiscussionRequest;
+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');
+});
+
+/* ---- 无 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');
+
+        // 列表里查得到
+        $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, '删后再查');
+});

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

@@ -4,6 +4,7 @@
 .travis.yml
 README.md
 composer.json
+docs/Api/DiscussionsApi.md
 docs/Api/EmailCertificationsApi.md
 docs/Api/HeartbeatApi.md
 docs/Api/InvitesApi.md
@@ -18,13 +19,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 +52,11 @@ 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/PostApiV3Discussions200Response.md
+docs/Model/PostApiV3DiscussionsDiscussionRepliesRequest.md
+docs/Model/PostApiV3DiscussionsRequest.md
 docs/Model/PostApiV3EmailCertificationsRequest.md
 docs/Model/PostApiV3Invites200Response.md
 docs/Model/PostApiV3Invites200ResponseData.md
@@ -62,6 +73,7 @@ docs/Model/PostApiV3Users200ResponseData.md
 docs/Model/PostApiV3UsersRequest.md
 docs/Model/ProblemDetails.md
 git_push.sh
+lib/Api/DiscussionsApi.php
 lib/Api/EmailCertificationsApi.php
 lib/Api/HeartbeatApi.php
 lib/Api/InvitesApi.php
@@ -80,13 +92,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 +126,11 @@ 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/PostApiV3Discussions200Response.php
+lib/Model/PostApiV3DiscussionsDiscussionRepliesRequest.php
+lib/Model/PostApiV3DiscussionsRequest.php
 lib/Model/PostApiV3EmailCertificationsRequest.php
 lib/Model/PostApiV3Invites200Response.php
 lib/Model/PostApiV3Invites200ResponseData.php
@@ -126,6 +148,7 @@ lib/Model/PostApiV3UsersRequest.php
 lib/Model/ProblemDetails.php
 lib/ObjectSerializer.php
 phpunit.xml.dist
+test/Api/DiscussionsApiTest.php
 test/Api/EmailCertificationsApiTest.php
 test/Api/HeartbeatApiTest.php
 test/Api/InvitesApiTest.php
@@ -140,12 +163,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 +196,11 @@ 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/PostApiV3Discussions200ResponseTest.php
+test/Model/PostApiV3DiscussionsDiscussionRepliesRequestTest.php
+test/Model/PostApiV3DiscussionsRequestTest.php
 test/Model/PostApiV3EmailCertificationsRequestTest.php
 test/Model/PostApiV3Invites200ResponseDataTest.php
 test/Model/PostApiV3Invites200ResponseTest.php

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

@@ -0,0 +1,2914 @@
+<?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',
+        ],
+        '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 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()) {

File diff ditekan karena terlalu besar
+ 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));
+    }
+}
+
+

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

@@ -0,0 +1,1070 @@
+<?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',
+        'reply_count' => 'int',
+        '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,
+        'reply_count' => null,
+        '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,
+        'reply_count' => false,
+        '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',
+        'reply_count' => 'reply_count',
+        'editor' => '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',
+        'reply_count' => 'setReplyCount',
+        'editor' => 'setEditor',
+        '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',
+        'reply_count' => 'getReplyCount',
+        'editor' => 'getEditor',
+        '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('reply_count', $data ?? [], null);
+        $this->setIfExists('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 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 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
  */

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

@@ -0,0 +1,657 @@
+<?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'
+    ];
+
+    /**
+     * 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
+    ];
+
+    /**
+     * Array of nullable properties. Used for (de)serialization
+     *
+     * @var boolean[]
+     */
+    protected static array $openAPINullables = [
+        'title' => false,
+        'content' => false,
+        'status' => false,
+        'type' => false,
+        'severity' => 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'
+    ];
+
+    /**
+     * 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'
+    ];
+
+    /**
+     * 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'
+    ];
+
+    /**
+     * 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 SEVERITY_ERROR = 'error';
+    public const SEVERITY_WARNING = 'warning';
+    public const SEVERITY_INFO = 'info';
+    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,
+        ];
+    }
+
+    /**
+     * Gets allowable values of the enum
+     *
+     * @return string[]
+     */
+    public function getSeverityAllowableValues()
+    {
+        return [
+            self::SEVERITY_ERROR,
+            self::SEVERITY_WARNING,
+            self::SEVERITY_INFO,
+            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);
+    }
+
+    /**
+     * 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;
+    }
+    /**
+     * 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/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));
+    }
+}
+
+

+ 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));
+    }
+}
+
+

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

@@ -0,0 +1,774 @@
+<?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',
+        '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,
+        '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,
+        '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',
+        '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',
+        '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',
+        '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 SEVERITY_ERROR = 'error';
+    public const SEVERITY_WARNING = 'warning';
+    public const SEVERITY_INFO = 'info';
+    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,
+        ];
+    }
+
+    /**
+     * Gets allowable values of the enum
+     *
+     * @return string[]
+     */
+    public function getSeverityAllowableValues()
+    {
+        return [
+            self::SEVERITY_ERROR,
+            self::SEVERITY_WARNING,
+            self::SEVERITY_INFO,
+            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('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)
+            );
+        }
+
+        if ($this->container['title'] === null) {
+            $invalidProperties[] = "'title' can't be null";
+        }
+        $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
+     */
+    public function getTitle()
+    {
+        return $this->container['title'];
+    }
+
+    /**
+     * Sets title
+     *
+     * @param string $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 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
  */

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

@@ -0,0 +1,28 @@
+<?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',
+        // 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,
 ];

+ 6 - 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,10 @@ return [
     'search_q'       => 'dukkha',
     'search_doc_id'  => 'term_f8afdcaa-ad68-4795-b3d8-d5f12bdb3a5f',
 
+    // discussions 写用例挂的句子:放在测试账号自己的 channel 里、由测试账号编辑,
+    // 这样即便有通知也只发给自己(用例本身还会带 notify=false)。换环境时按需覆盖。
+    'discussion_sentence' => 'ee5259be-5d1e-4408-a2a6-566a5773eef1',
+
     // 一个已注册账号的邮箱(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];

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini