Просмотр исходного кода

Merge pull request #2490 from visuddhinanda/development

Development
visuddhinanda 2 недель назад
Родитель
Сommit
fcf33daea1
100 измененных файлов с 682 добавлено и 256 удалено
  1. 42 3
      .claude/skills/v3-resource/SKILL.md
  2. 7 2
      CLAUDE.md
  3. 53 0
      api-test/cases/07_discussions.php
  4. 1 1
      api-test/client/lib/Api/AgentGrantsApi.php
  5. 10 10
      api-test/client/lib/Api/DiscussionsApi.php
  6. 1 1
      api-test/client/lib/Api/EmailCertificationsApi.php
  7. 1 1
      api-test/client/lib/Api/HeartbeatApi.php
  8. 1 1
      api-test/client/lib/Api/InvitesApi.php
  9. 1 1
      api-test/client/lib/Api/MeApi.php
  10. 1 1
      api-test/client/lib/Api/PasswordResetsApi.php
  11. 1 1
      api-test/client/lib/Api/ProgressApi.php
  12. 1 1
      api-test/client/lib/Api/ReactionsApi.php
  13. 1 1
      api-test/client/lib/Api/SearchApi.php
  14. 1 1
      api-test/client/lib/Api/SearchSuggestApi.php
  15. 1 1
      api-test/client/lib/Api/SessionsApi.php
  16. 6 6
      api-test/client/lib/Api/TipitakaDiscussionsApi.php
  17. 1 1
      api-test/client/lib/Api/TipitakaReadingApi.php
  18. 1 1
      api-test/client/lib/Api/TipitakaRelatedParagraphsApi.php
  19. 1 1
      api-test/client/lib/Api/UpgradeApi.php
  20. 1 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. 1 1
      api-test/client/lib/Model/GetApiV3Discussions200Response.php
  26. 42 1
      api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInner.php
  27. 1 1
      api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInnerAbilities.php
  28. 1 1
      api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInnerEditor.php
  29. 1 1
      api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInnerSentence.php
  30. 1 1
      api-test/client/lib/Model/GetApiV3DiscussionsSummary200Response.php
  31. 1 1
      api-test/client/lib/Model/GetApiV3DiscussionsSummary200ResponseData.php
  32. 1 1
      api-test/client/lib/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCounts.php
  33. 1 1
      api-test/client/lib/Model/GetApiV3Heartbeat200Response.php
  34. 1 1
      api-test/client/lib/Model/GetApiV3Heartbeat200ResponseData.php
  35. 1 1
      api-test/client/lib/Model/GetApiV3Me200Response.php
  36. 1 1
      api-test/client/lib/Model/GetApiV3Me200ResponseData.php
  37. 1 1
      api-test/client/lib/Model/GetApiV3MeReactions200Response.php
  38. 1 1
      api-test/client/lib/Model/GetApiV3MeReactions200ResponseDataInner.php
  39. 1 1
      api-test/client/lib/Model/GetApiV3PasswordResetsToken200Response.php
  40. 1 1
      api-test/client/lib/Model/GetApiV3PasswordResetsToken200ResponseData.php
  41. 1 1
      api-test/client/lib/Model/GetApiV3Progress200Response.php
  42. 1 1
      api-test/client/lib/Model/GetApiV3Progress200ResponseDataInner.php
  43. 1 1
      api-test/client/lib/Model/GetApiV3ReactionsTally200Response.php
  44. 1 1
      api-test/client/lib/Model/GetApiV3ReactionsTally200ResponseDataInner.php
  45. 1 1
      api-test/client/lib/Model/GetApiV3Search200Response.php
  46. 1 1
      api-test/client/lib/Model/GetApiV3SearchSearch200Response.php
  47. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaReadingChannel200Response.php
  48. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaReadingChannel200ResponseDataInner.php
  49. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaReadingChannel200ResponseMeta.php
  50. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200Response.php
  51. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200ResponseDataInner.php
  52. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200ResponseDataInnerPathInner.php
  53. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphs200ResponseMeta.php
  54. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregate200Response.php
  55. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregate200ResponseDataInner.php
  56. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200Response.php
  57. 1 1
      api-test/client/lib/Model/GetApiV3TipitakaRelatedParagraphsAggregateBookName200ResponseDataInner.php
  58. 1 1
      api-test/client/lib/Model/ModelInterface.php
  59. 1 1
      api-test/client/lib/Model/PaginationMeta.php
  60. 38 42
      api-test/client/lib/Model/PatchApiV3DiscussionsDiscussionRequest.php
  61. 1 1
      api-test/client/lib/Model/PatchApiV3PasswordResetsTokenRequest.php
  62. 1 1
      api-test/client/lib/Model/PostApiV3AgentGrants200Response.php
  63. 1 1
      api-test/client/lib/Model/PostApiV3AgentGrants200ResponseData.php
  64. 1 1
      api-test/client/lib/Model/PostApiV3AgentGrantsRequest.php
  65. 1 1
      api-test/client/lib/Model/PostApiV3Discussions200Response.php
  66. 1 1
      api-test/client/lib/Model/PostApiV3DiscussionsBatchCount200Response.php
  67. 1 1
      api-test/client/lib/Model/PostApiV3DiscussionsBatchCount200ResponseDataInner.php
  68. 1 1
      api-test/client/lib/Model/PostApiV3DiscussionsBatchCountRequest.php
  69. 1 1
      api-test/client/lib/Model/PostApiV3DiscussionsBatchCountRequestTargetsInner.php
  70. 1 1
      api-test/client/lib/Model/PostApiV3DiscussionsDiscussionRepliesRequest.php
  71. 40 44
      api-test/client/lib/Model/PostApiV3DiscussionsRequest.php
  72. 1 1
      api-test/client/lib/Model/PostApiV3EmailCertificationsRequest.php
  73. 1 1
      api-test/client/lib/Model/PostApiV3Invites200Response.php
  74. 1 1
      api-test/client/lib/Model/PostApiV3Invites200ResponseData.php
  75. 1 1
      api-test/client/lib/Model/PostApiV3InvitesRequest.php
  76. 1 1
      api-test/client/lib/Model/PostApiV3MeReactions200Response.php
  77. 1 1
      api-test/client/lib/Model/PostApiV3MeReactionsRequest.php
  78. 1 1
      api-test/client/lib/Model/PostApiV3PasswordResetsRequest.php
  79. 1 1
      api-test/client/lib/Model/PostApiV3SearchRequest.php
  80. 1 1
      api-test/client/lib/Model/PostApiV3Sessions200Response.php
  81. 1 1
      api-test/client/lib/Model/PostApiV3Sessions200ResponseData.php
  82. 1 1
      api-test/client/lib/Model/PostApiV3SessionsRequest.php
  83. 1 1
      api-test/client/lib/Model/PostApiV3Users200Response.php
  84. 1 1
      api-test/client/lib/Model/PostApiV3Users200ResponseData.php
  85. 1 1
      api-test/client/lib/Model/PostApiV3UsersRequest.php
  86. 1 1
      api-test/client/lib/Model/ProblemDetails.php
  87. 1 1
      api-test/client/lib/ObjectSerializer.php
  88. 21 11
      api-v13/app/Http/Controllers/V3/DiscussionController.php
  89. 1 1
      api-v13/app/Http/Controllers/V3/DiscussionSummaryController.php
  90. 3 3
      api-v13/app/Http/Controllers/V3/TipitakaDiscussionController.php
  91. 17 4
      api-v13/app/Http/Requests/V3/StoreDiscussionRequest.php
  92. 2 1
      api-v13/app/Http/Requests/V3/UpdateDiscussionRequest.php
  93. 4 1
      api-v13/app/Http/Resources/V3/DiscussionResource.php
  94. 27 7
      api-v13/app/Models/Discussion.php
  95. 48 3
      api-v13/app/Services/AnnotationLayout.php
  96. 33 7
      api-v13/app/Services/PaliContentService.php
  97. 74 13
      api-v13/app/Services/V3/DiscussionService.php
  98. 29 0
      api-v13/database/migrations/2026_10_22_100000_add_score_in_discussions.php
  99. 38 0
      api-v13/resources/css/modules/reader-content.css
  100. 67 17
      api-v13/resources/js/modules/issue-popover.js

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

@@ -840,7 +840,10 @@ Route::prefix('v3')->as('v3.')->group(function () {
 index 用 `spatie/laravel-query-builder` 把 scope 暴露成 `filter[]`,
 scope 和 filter 的组装写在 Service 里(硬规范第 5 条),控制器只负责调它。
 
-## 迁移九步
+## 迁移步骤
+
+0–9 每次都做;10–12 **只在改动涉及它们时才做**(判据写在各步开头),不涉及就跳过,
+但收尾报告里要说明跳过了哪几步、为什么不涉及。
 
 ### 0. 先盘点,列给用户确认
 
@@ -1106,7 +1109,8 @@ php api-test/run.php --username=test161 --password=12345 --filter=<资源>
 
 - 一个资源一个 `cases/NN_<资源>.php`,调用走生成的 `<Tag>Api` 类(tag 规则见生成器 `tagOf()`)
 - 至少覆盖:公开读 200、缺必填 422、无 token 写 401、带 token 的「建 → 改 → 删」完整一轮
-- 写操作**会在所有服务器上真实执行**,只用自建的临时数据并测完即删
+- 写操作**会在所有服务器上真实执行**,只用自建的临时数据并测完即删;能做成幂等的(如「一句只留一条、
+  再写就覆盖」的资源)可以不删,跑多少次都不累积。**本地开发库上跑完不要另外清理**——用户要看测试结果
 - 与 Pest 的分工:Pest 覆盖权限矩阵与边界;api-test 验证**部署出来的服务器**与规格一致
 
 ### 9. 更新 dashboard-test(只测 v6)
@@ -1124,6 +1128,40 @@ cd dashboard-test && npx playwright test <资源>
 - 迁移让某个页面「能链接到但没有路由」的(如复制链接),**在 v6 补上那个页面**再测
 - 正式跑一次就在 `dashboard-test/testlog/` 写测试日志(gitignore,只留本机)
 
+### 10. 按需:Laravel library 阅读器(api-v13 自带的阅读页)
+
+**涉及时才做**:改动影响阅读页正文的 HTML(`PaliContentService` 的渲染 / 注入、`AnnotationLayout`
+的新标记或新 class),或 library 页面(`routes/web.php` 的 `library.*`)要用到这个资源。
+
+- 代码在 api-v13 里:`resources/views/library/**`、`resources/js/modules/*.js`(如 `issue-popover.js`)、
+  `resources/css/modules/reader-content.css`,文案在 `resources/lang/{locale}/library.php`(至少 en 与 zh-Hans),
+  经 blade 的 `data-i18n` 传给 JS
+- 新的正文标记要有样式与交互(点击 → 取 `/api/v3/...`,桌面 popover、手机抽屉),分档、配色等
+  规则以后端输出的 class / `data-*` 为准,JS 不重复阈值
+- `npm run build`(`public/build` 不进版本库),用本地开发库的数据打开页面,
+  Playwright 截图核对桌面与手机两种宽度,确认没有 JS 报错
+
+### 11. 按需:wikipali-mobile(`/home/deploy/workspace/wikipali-mobile`,独立仓库)
+
+**涉及时才做**:mobile 调用的 v3 端点改了(`npx tsc --noEmit` 会报出来),或阅读正文的 HTML 变了
+(mobile 的阅读 WebView 直接渲染服务端 HTML,新标记不加样式就是裸的)。
+
+- 第 5 步已把 `schema.d.ts` 生成到 mobile;`npx tsc --noEmit` 必须过
+- 阅读正文:`src/screens/ReaderLayerPane.tsx` 的 WebView CSS 与点击上报(WebView 没有 tabler 字体,
+  图标用 SVG mask 画),弹层在 `src/components/`;配色唯一来源写在组件里,WebView CSS 变量从它生成
+- 文案 8 个 locale 都要补(缺 key `tsc` 报错),按 messages / labels / buttons 归类
+- 设计文档 `docs/<主题>.md` 补一节;遵守它自己的 CLAUDE.md(不提交、emoji 提交信息)
+
+### 12. 按需:wikipali-plugins(`/home/deploy/workspace/wikipali-plugins`,独立仓库)
+
+**涉及时才做**:这个资源是 agent 经插件读写的(discussions、sentences、术语、agent-grants……),
+或改动改变了插件在调的接口(入参、返回、状态码)。
+
+- 命令在 `plugins/wikipali/lib/cmd_*.py` + `lib/cli.py`;`./release-check.sh` 必须过
+- **它的 CLAUDE.md 要求同步 `docs/wikipali-api-calls.md`**(接口清单),另外 `references/api-write.md`、
+  `skills/write/SKILL.md`(含 description 的触发词)也要跟上
+- 版本号不自己定:提交前问用户要不要升、升到多少
+
 ## 验收:CI 三道闸
 
 ```bash
@@ -1151,5 +1189,6 @@ cd dashboard-v6 && npm run build                                  # 前端类型
 ## 收尾报告
 
 用一句话说清:迁了哪个资源、v2 的哪几个端点/view 映射成了 v3 的哪几个端点、
-补了几条 Pest 测试、api-test 与 dashboard-test 各加了什么、三道闸的结果。**改了名的话,提醒用户往 CLAUDE.md 的
+补了几条 Pest 测试、api-test 与 dashboard-test 各加了什么、三道闸的结果;
+第 10–12 步做了哪几步(没做的说明为什么不涉及)。**改了名的话,提醒用户往 CLAUDE.md 的
 「v2 → v3 对照表」加一行。**

+ 7 - 2
CLAUDE.md

@@ -350,7 +350,7 @@ Resource / FormRequest / 测试同理;URL 只换版本前缀
 | `related-paragraph` / `RelatedParagraphController` | `tipitaka-related-paragraphs` (+`/aggregate`) / `V3\TipitakaRelatedParagraphController` + `V3\TipitakaRelatedParagraphAggregateController` | 字段改名 `book_title_pali`→`title`、`cs6_para`→`cs_para`;响应 `{ok,…}`→原生 Resource;消除逐行 N+1(批量 whereIn);新增 `book_name`/`cs_para` 过滤器与独立 `/aggregate` 聚合端点(`group_by=book_id`→book_name 列表 / `group_by=book_name`→cs_para 列表,分页) |
 | `auth/forgot-password` + `auth/reset-password` / `ForgotPasswordController` + `ResetPasswordController` | `password-resets` (+`/{token}`) / `V3\PasswordResetController` | 三条并成一个资源:POST 发信、GET 看账号、PATCH 设密码(204)。库里只存 token 的 sha256、60 分钟过期、邮箱未注册也 204、不收 `dashboard` 参数(链接用 `mint.server.dashboard_v6_base_path` 拼到 v6 的 `/anonymous/reset-password/{token}`)、限流 |
 | `email-certification` + `invite/{id}`(读)+ `sign-up` / `EmailCertificationController` + `InviteController@show` + `SignUpController@store` | `email-certifications` + `invites` (+`/{invite}`) + `users` / `V3\EmailCertificationController` + `V3\InviteController` + `V3\UserController` | 注册。验证码改在服务端比对:v2 的 `GET email-certification/{id}`(把码交给前端)在 v3 没有对应端点,换成 `POST invites {email, code}` 签发 invite;自助与邀请注册都凭 invite 走 `POST users`。验证码 6 位、30 分钟、试错 5 次作废;验证通过前不写 invites 表。studio 发邀请(`POST /v2/invite`)不在此列,将来迁成 `studios/{studio}/invites` |
-| `discussion`(列表、单条、新建、修改、删除)/ `DiscussionController` | `discussions` (+`/summary`、`/{d}/replies`) / `V3\DiscussionController` + `V3\DiscussionSummaryController` + `V3\DiscussionReplyController` | 管评论线程(type `discussion` / `qa` / `help` / `issue`)与批注(`note` 边注、`commentary` 义注 / 复注对应——巴利注释书,不是普通评论)。批注只挂句子、只有该句管理者能建(AI 凭授权代写按授权人判)、带锚点 `pos_*`/`quote_*`、可回复、无开关、新建不发通知;列表与 summary 不带 `type` 时只算评论线程。写入支持 agent 代写(`X-Agent-Grant`),新增 `co_editor`(人亲自改内容时清空)。字段改名 `res_type`/`res_id`→`target_type`/`target_id`、`parent`→`parent_id`、`children_count`→`reply_count`;`view=question/res_id/question-by-topic`→列表,`view=answer`+带 parent 的 POST→`replies`,响应里的 `active`/`close`/`can_create`→`summary`;每条带 `abilities`;只有 PATCH;软删除(2026-10-15 起 v2 也是),有回复的话题留作墓碑。`view=topic-by-user`/`all`、课程参数、`discussion-anchor`、`sent-discussion-tree` 未迁;`discussion-count` 拆开:讨论角标迁成 `POST discussions/batch-count`(只给计数,入参改为对象 uid,套 v3 可见性与类型口径),标签部分仍走 v2、待迁标签时处理;课程模式的「学生的讨论」待课程迁移后补(TODO 在 `DiscussionService::batchCount`) |
+| `discussion`(列表、单条、新建、修改、删除)/ `DiscussionController` | `discussions` (+`/summary`、`/{d}/replies`) / `V3\DiscussionController` + `V3\DiscussionSummaryController` + `V3\DiscussionReplyController` | 管评论线程(type `discussion` / `qa` / `help` / `issue`)与批注(`note` 边注、`commentary` 义注 / 复注对应——巴利注释书,不是普通评论),2026-10-22 起还有译文评估(`assessment`:`score` 0–100、`severity` 存评估方法、只挂句子且只有该句管理者能建、一句只留一条再评估就覆盖、注入阅读页句尾按分数着色的对勾)。批注只挂句子、只有该句管理者能建(AI 凭授权代写按授权人判)、带锚点 `pos_*`/`quote_*`、可回复、无开关、新建不发通知;列表与 summary 不带 `type` 时只算评论线程。写入支持 agent 代写(`X-Agent-Grant`),新增 `co_editor`(人亲自改内容时清空)。字段改名 `res_type`/`res_id`→`target_type`/`target_id`、`parent`→`parent_id`、`children_count`→`reply_count`;`view=question/res_id/question-by-topic`→列表,`view=answer`+带 parent 的 POST→`replies`,响应里的 `active`/`close`/`can_create`→`summary`;每条带 `abilities`;只有 PATCH;软删除(2026-10-15 起 v2 也是),有回复的话题留作墓碑。`view=topic-by-user`/`all`、课程参数、`discussion-anchor`、`sent-discussion-tree` 未迁;`discussion-count` 拆开:讨论角标迁成 `POST discussions/batch-count`(只给计数,入参改为对象 uid,套 v3 可见性与类型口径),标签部分仍走 v2、待迁标签时处理;课程模式的「学生的讨论」待课程迁移后补(TODO 在 `DiscussionService::batchCount`) |
 | `access-token`(agent 用途)/ `AccessTokenController@store` | `agent-grants` / `V3\AgentGrantController` | 人类为自己名下的 AI 模型签 channel 写入授权。载荷由服务端决定(v2 原样签客户端 payload)、绑定模型(act)与签发人(grantor)、带模型 token_version、独立密钥;写入时放 `X-Agent-Grant` 头而不是请求体 |
 | `sent-discussion-tree`(部分)/ `DiscussionController@discussion_tree` | `tipitaka-discussions/{channel}` / `V3\TipitakaDiscussionController` | 新能力为主:按经文范围(channel 必填;`book` + `chapter` 或 `para`/`to`,不给 book 即整个 channel)列挂在句子上的话题,每条带 `sentence` 坐标,按经文顺序排;`type` 可逗号多选,默认只列话题,可见性同 `/v3/discussions`。v2 那条要调用方逐句列出坐标、只回标题与回复数,未迁也不迁(只有 v4 在用)。与 `/v3/discussions`(单个对象)必填参数不同,按规范第 1 条分成两个端点 |
 | `sign-in` + `auth/current` / `AuthController@signIn` + `@getUserInfoByToken` | `sessions` + `me` / `V3\SessionController` + `V3\MeController` | 登录一次返回 `{token, user}`,不再二次查询;`login` 字段收用户名或邮箱(邮箱不区分大小写),失败统一 422 `errors.login`;按账号 + IP 限流。`me` 的字段沿用 v2 的驼峰用户摘要(`UserService::profile()`),`roles` 恒为数组,另加 `email`,不再回显 token。token 与 v2 完全相同、两边互认 |
@@ -363,7 +363,7 @@ v3 这条只服务单 channel 的只读渲染。前端 `dashboard-v6/src/api/pal
 补偿手段要写进代码:v3 控制器的类注释写明「取代 `GET /v2/xxx`」,模型注释写明底层表名
 (如 `Reaction` 写明仍是 `likes` 表),这样从任一端都能找到另一端。
 
-**做迁移时使用 `v3-resource` skill**,里面有完整的九步链路与模板。
+**做迁移时使用 `v3-resource` skill**,里面有完整链路(0–9 步必做,10–12 步按需)与模板。
 
 **每迁一个资源,两套外部测试都要跟着更新**(skill 第 8、9 步),没更新就不算迁完:
 
@@ -371,6 +371,11 @@ v3 这条只服务单 channel 的只读渲染。前端 `dashboard-v6/src/api/pal
    用例(公开读、422、无 token 401、带 token 的建改删一轮),`php api-test/run.php` 跑绿。
 2. **`dashboard-test/`(只测 v6)**:给切过去的页面加 `src/tests/<资源>.spec.ts`,
    `npx playwright test <资源>` 跑绿。迁移牵出的「有链接没页面」在 v6 补上页面一起测。
+
+另有三处**只在涉及时**才改(skill 第 10–12 步):Laravel library 阅读器(改了阅读页正文 HTML)、
+`wikipali-mobile`(改了它调的 v3 端点或阅读正文 HTML)、`wikipali-plugins`(agent 经插件读写的资源)。
+不涉及就不动,但收尾报告里要说明。本地开发库上测试写入的数据跑完不要删,用户要看结果。
+
 一次只迁一个资源。试点顺序:先 A 类 3 条(零线上风险,用来打磨样板与 CI 三道闸),
 再从 `channel` 开始啃 B 类。
 

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

@@ -269,3 +269,56 @@ test('glossary 一轮:带锚点建 → 公开可读 → 不收锚点的类型
     }
     assert_status(200, $del, '删 glossary');
 });
+
+/* ---- 译文评估(assessment) ---- */
+
+/*
+ * 评估一句只留一条,再 POST 就覆盖,所以这里不删:测试句上始终至多一条评估,跑多少次都不会累积,
+ * 留着还能在阅读页上看到句尾的对勾(本地开发库的测试数据要保留,见 mint 的 memory)。
+ */
+test('assessment 一轮:建或覆盖 → 再评估覆盖同一条 → 公开可读 → 分数 / 方法校验 422 → PATCH 改分', function () {
+    $token = required_token();
+    $stamp = 'api-test-assessment-' . date('Ymd-His');
+    $post = fn (array $body) => raw('POST', '/v3/discussions', null, [
+        'target_type' => 'sentence', 'target_id' => discussion_target(), 'type' => 'assessment', ...$body,
+    ], $token);
+
+    // 第一次:句上没有评估就 201,已有就 200 覆盖
+    $first = $post(['score' => 55, 'severity' => 'api-test', 'content' => $stamp]);
+    if (! in_array($first['status'], [200, 201], true)) {
+        assert_status(201, $first, '建 assessment');
+    }
+    $id = $first['body']['data']['id'] ?? null;
+    if (! $id) {
+        fail('建 assessment:应返回 id');
+    }
+    assert_eq(55, $first['body']['data']['score'] ?? null, 'assessment score');
+    assert_eq('api-test', $first['body']['data']['severity'] ?? null, 'assessment 方法存在 severity');
+
+    // 再评估:覆盖同一条,200,id 不变
+    $again = $post(['score' => 82, 'severity' => 'api-test', 'title' => '复评', 'content' => $stamp]);
+    assert_status(200, $again, '再评估');
+    assert_eq($id, $again['body']['data']['id'] ?? null, '再评估覆盖同一条');
+    assert_eq(82, $again['body']['data']['score'] ?? null, '覆盖后的 score');
+
+    // 公开:匿名也读得到;不带 type 的列表不列评估
+    assert_status(200, raw('GET', "/v3/discussions/{$id}"), '匿名读 assessment');
+    $listed = raw('GET', '/v3/discussions', ['target_type' => 'sentence', 'target_id' => discussion_target(), 'type' => 'assessment']);
+    assert_eq([$id], array_column($listed['body']['data'] ?? [], 'id'), 'type=assessment 列出这一条');
+
+    // 校验:缺分数、超范围、带小数、方法格式不对、其它类型带分数
+    assert_field_error('score', $post(['severity' => 'api-test']), 'assessment 缺 score');
+    assert_field_error('score', $post(['score' => 101, 'severity' => 'api-test']), 'assessment score 超范围');
+    assert_field_error('score', $post(['score' => 80.5, 'severity' => 'api-test']), 'assessment score 只收整数');
+    assert_field_error('severity', $post(['score' => 50, 'severity' => 'bad method']), 'assessment 方法格式');
+    assert_field_error('score', raw('POST', '/v3/discussions', null, [
+        'target_type' => 'sentence', 'target_id' => discussion_target(), 'type' => 'issue',
+        'title' => $stamp, 'score' => 50, 'notify' => false,
+    ], $token), 'issue 带 score');
+
+    // PATCH 改分;评估没有开关
+    $patched = raw('PATCH', "/v3/discussions/{$id}", null, ['score' => 91], $token);
+    assert_status(200, $patched, 'PATCH assessment score');
+    assert_eq(91, $patched['body']['data']['score'] ?? null, 'PATCH 后的 score');
+    assert_field_error('status', raw('PATCH', "/v3/discussions/{$id}", null, ['status' => 'close'], $token), 'assessment 不能关闭');
+});

+ 1 - 1
api-test/client/lib/Api/AgentGrantsApi.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 10 - 10
api-test/client/lib/Api/DiscussionsApi.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -477,7 +477,7 @@ class DiscussionsApi
      * @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 $severity 严重程度(assessment 为评估方法),逗号分隔多选 (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
@@ -502,7 +502,7 @@ class DiscussionsApi
      * @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 $severity 严重程度(assessment 为评估方法),逗号分隔多选 (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
@@ -608,7 +608,7 @@ class DiscussionsApi
      * @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 $severity 严重程度(assessment 为评估方法),逗号分隔多选 (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
@@ -636,7 +636,7 @@ class DiscussionsApi
      * @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 $severity 严重程度(assessment 为评估方法),逗号分隔多选 (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
@@ -693,7 +693,7 @@ class DiscussionsApi
      * @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 $severity 严重程度(assessment 为评估方法),逗号分隔多选 (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
@@ -2144,7 +2144,7 @@ class DiscussionsApi
     /**
      * 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
@@ -2162,7 +2162,7 @@ class DiscussionsApi
     /**
      * 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
@@ -2303,7 +2303,7 @@ class DiscussionsApi
     /**
      * 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
@@ -2324,7 +2324,7 @@ class DiscussionsApi
     /**
      * 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

+ 1 - 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Api/ReactionsApi.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 6 - 6
api-test/client/lib/Api/TipitakaDiscussionsApi.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -138,7 +138,7 @@ class TipitakaDiscussionsApi
      * @param  int|null $to 结束段落号(含),缺省同 para (optional)
      * @param  string|null $type 类型,逗号分隔多选;不传则只列评论线程 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
-     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  string|null $severity 严重程度(assessment 为评估方法),逗号分隔多选 (optional)
      * @param  int|null $page 页码 (optional, default to 1)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3TipitakaDiscussionsChannel'] to see the possible values for this operation
      *
@@ -165,7 +165,7 @@ class TipitakaDiscussionsApi
      * @param  int|null $to 结束段落号(含),缺省同 para (optional)
      * @param  string|null $type 类型,逗号分隔多选;不传则只列评论线程 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
-     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  string|null $severity 严重程度(assessment 为评估方法),逗号分隔多选 (optional)
      * @param  int|null $page 页码 (optional, default to 1)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3TipitakaDiscussionsChannel'] to see the possible values for this operation
      *
@@ -287,7 +287,7 @@ class TipitakaDiscussionsApi
      * @param  int|null $to 结束段落号(含),缺省同 para (optional)
      * @param  string|null $type 类型,逗号分隔多选;不传则只列评论线程 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
-     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  string|null $severity 严重程度(assessment 为评估方法),逗号分隔多选 (optional)
      * @param  int|null $page 页码 (optional, default to 1)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3TipitakaDiscussionsChannel'] to see the possible values for this operation
      *
@@ -317,7 +317,7 @@ class TipitakaDiscussionsApi
      * @param  int|null $to 结束段落号(含),缺省同 para (optional)
      * @param  string|null $type 类型,逗号分隔多选;不传则只列评论线程 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
-     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  string|null $severity 严重程度(assessment 为评估方法),逗号分隔多选 (optional)
      * @param  int|null $page 页码 (optional, default to 1)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3TipitakaDiscussionsChannel'] to see the possible values for this operation
      *
@@ -376,7 +376,7 @@ class TipitakaDiscussionsApi
      * @param  int|null $to 结束段落号(含),缺省同 para (optional)
      * @param  string|null $type 类型,逗号分隔多选;不传则只列评论线程 (optional)
      * @param  string|null $status 话题状态,不传则两种都要 (optional)
-     * @param  string|null $severity 严重程度,逗号分隔多选 (optional)
+     * @param  string|null $severity 严重程度(assessment 为评估方法),逗号分隔多选 (optional)
      * @param  int|null $page 页码 (optional, default to 1)
      * @param  string $contentType The value for the Content-Type header. Check self::contentTypes['getApiV3TipitakaDiscussionsChannel'] to see the possible values for this operation
      *

+ 1 - 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21' . PHP_EOL;
+        $report .= '    The version of the OpenAPI document: 2026.10.23' . 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3Discussions200Response.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 42 - 1
api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInner.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -63,6 +63,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'parent_id' => 'string',
         'type' => 'string',
         'severity' => 'string',
+        'score' => 'int',
         'status' => 'string',
         'title' => 'string',
         'content' => 'string',
@@ -99,6 +100,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'parent_id' => null,
         'type' => null,
         'severity' => null,
+        'score' => null,
         'status' => null,
         'title' => null,
         'content' => null,
@@ -133,6 +135,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'parent_id' => true,
         'type' => false,
         'severity' => true,
+        'score' => true,
         'status' => false,
         'title' => true,
         'content' => true,
@@ -247,6 +250,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'parent_id' => 'parent_id',
         'type' => 'type',
         'severity' => 'severity',
+        'score' => 'score',
         'status' => 'status',
         'title' => 'title',
         'content' => 'content',
@@ -281,6 +285,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'parent_id' => 'setParentId',
         'type' => 'setType',
         'severity' => 'setSeverity',
+        'score' => 'setScore',
         'status' => 'setStatus',
         'title' => 'setTitle',
         'content' => 'setContent',
@@ -315,6 +320,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         'parent_id' => 'getParentId',
         'type' => 'getType',
         'severity' => 'getSeverity',
+        'score' => 'getScore',
         'status' => 'getStatus',
         'title' => 'getTitle',
         'content' => 'getContent',
@@ -400,6 +406,7 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         $this->setIfExists('parent_id', $data ?? [], null);
         $this->setIfExists('type', $data ?? [], null);
         $this->setIfExists('severity', $data ?? [], null);
+        $this->setIfExists('score', $data ?? [], null);
         $this->setIfExists('status', $data ?? [], null);
         $this->setIfExists('title', $data ?? [], null);
         $this->setIfExists('content', $data ?? [], null);
@@ -640,6 +647,40 @@ class GetApiV3Discussions200ResponseDataInner implements ModelInterface, ArrayAc
         return $this;
     }
 
+    /**
+     * Gets score
+     *
+     * @return int|null
+     */
+    public function getScore()
+    {
+        return $this->container['score'];
+    }
+
+    /**
+     * Sets score
+     *
+     * @param int|null $score score
+     *
+     * @return self
+     */
+    public function setScore($score)
+    {
+        if (is_null($score)) {
+            array_push($this->openAPINullablesSetToNull, 'score');
+        } else {
+            $nullablesSetToNull = $this->getOpenAPINullablesSetToNull();
+            $index = array_search('score', $nullablesSetToNull);
+            if ($index !== FALSE) {
+                unset($nullablesSetToNull[$index]);
+                $this->setOpenAPINullablesSetToNull($nullablesSetToNull);
+            }
+        }
+        $this->container['score'] = $score;
+
+        return $this;
+    }
+
     /**
      * Gets status
      *

+ 1 - 1
api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInnerAbilities.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInnerEditor.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3Discussions200ResponseDataInnerSentence.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3DiscussionsSummary200Response.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3DiscussionsSummary200ResponseData.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/GetApiV3DiscussionsSummary200ResponseDataStatusCounts.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 38 - 42
api-test/client/lib/Model/PatchApiV3DiscussionsDiscussionRequest.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -62,6 +62,7 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         'status' => 'string',
         'type' => 'string',
         'severity' => 'string',
+        'score' => 'int',
         'pos_start' => 'int',
         'pos_end' => 'int',
         'quote_exact' => 'string',
@@ -82,6 +83,7 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         'status' => null,
         'type' => null,
         'severity' => null,
+        'score' => null,
         'pos_start' => null,
         'pos_end' => null,
         'quote_exact' => null,
@@ -100,6 +102,7 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         'status' => false,
         'type' => false,
         'severity' => false,
+        'score' => false,
         'pos_start' => false,
         'pos_end' => false,
         'quote_exact' => false,
@@ -198,6 +201,7 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         'status' => 'status',
         'type' => 'type',
         'severity' => 'severity',
+        'score' => 'score',
         'pos_start' => 'pos_start',
         'pos_end' => 'pos_end',
         'quote_exact' => 'quote_exact',
@@ -216,6 +220,7 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         'status' => 'setStatus',
         'type' => 'setType',
         'severity' => 'setSeverity',
+        'score' => 'setScore',
         'pos_start' => 'setPosStart',
         'pos_end' => 'setPosEnd',
         'quote_exact' => 'setQuoteExact',
@@ -234,6 +239,7 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         'status' => 'getStatus',
         'type' => 'getType',
         'severity' => 'getSeverity',
+        'score' => 'getScore',
         'pos_start' => 'getPosStart',
         'pos_end' => 'getPosEnd',
         'quote_exact' => 'getQuoteExact',
@@ -291,11 +297,7 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
     public const TYPE_GLOSSARY = 'glossary';
     public const TYPE_NOTE = 'note';
     public const TYPE_COMMENTARY = 'commentary';
-    public const SEVERITY_ERROR = 'error';
-    public const SEVERITY_WARNING = 'warning';
-    public const SEVERITY_SUGGESTION = 'suggestion';
-    public const SEVERITY_URGENT = 'urgent';
-    public const SEVERITY_NORMAL = 'normal';
+    public const TYPE_ASSESSMENT = 'assessment';
 
     /**
      * Gets allowable values of the enum
@@ -325,22 +327,7 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
             self::TYPE_GLOSSARY,
             self::TYPE_NOTE,
             self::TYPE_COMMENTARY,
-        ];
-    }
-
-    /**
-     * Gets allowable values of the enum
-     *
-     * @return string[]
-     */
-    public function getSeverityAllowableValues()
-    {
-        return [
-            self::SEVERITY_ERROR,
-            self::SEVERITY_WARNING,
-            self::SEVERITY_SUGGESTION,
-            self::SEVERITY_URGENT,
-            self::SEVERITY_NORMAL,
+            self::TYPE_ASSESSMENT,
         ];
     }
 
@@ -364,6 +351,7 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         $this->setIfExists('status', $data ?? [], null);
         $this->setIfExists('type', $data ?? [], null);
         $this->setIfExists('severity', $data ?? [], null);
+        $this->setIfExists('score', $data ?? [], null);
         $this->setIfExists('pos_start', $data ?? [], null);
         $this->setIfExists('pos_end', $data ?? [], null);
         $this->setIfExists('quote_exact', $data ?? [], null);
@@ -416,15 +404,6 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
             );
         }
 
-        $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;
     }
 
@@ -581,7 +560,7 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
     /**
      * Sets severity
      *
-     * @param string|null $severity 严重程度(只限话题),null 表示清空
+     * @param string|null $severity 严重程度(只限话题),null 表示清空;assessment 为评估方法,不能清空
      *
      * @return self
      */
@@ -590,21 +569,38 @@ class PatchApiV3DiscussionsDiscussionRequest implements ModelInterface, ArrayAcc
         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 score
+     *
+     * @return int|null
+     */
+    public function getScore()
+    {
+        return $this->container['score'];
+    }
+
+    /**
+     * Sets score
+     *
+     * @param int|null $score 评估分数,0–100 的整数(只限 assessment)
+     *
+     * @return self
+     */
+    public function setScore($score)
+    {
+        if (is_null($score)) {
+            throw new \InvalidArgumentException('non-nullable score cannot be null');
+        }
+        $this->container['score'] = $score;
+
+        return $this;
+    }
+
     /**
      * Gets pos_start
      *

+ 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3AgentGrants200Response.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3AgentGrants200ResponseData.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3AgentGrantsRequest.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3Discussions200Response.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3DiscussionsBatchCount200Response.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3DiscussionsBatchCount200ResponseDataInner.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3DiscussionsBatchCountRequest.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3DiscussionsBatchCountRequestTargetsInner.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 1 - 1
api-test/client/lib/Model/PostApiV3DiscussionsDiscussionRepliesRequest.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 40 - 44
api-test/client/lib/Model/PostApiV3DiscussionsRequest.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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */
@@ -63,6 +63,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         'title' => 'string',
         'content' => 'string',
         'severity' => 'string',
+        'score' => 'int',
         'pos_start' => 'int',
         'pos_end' => 'int',
         'quote_exact' => 'string',
@@ -86,6 +87,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         'title' => null,
         'content' => null,
         'severity' => null,
+        'score' => null,
         'pos_start' => null,
         'pos_end' => null,
         'quote_exact' => null,
@@ -107,6 +109,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         'title' => false,
         'content' => false,
         'severity' => false,
+        'score' => false,
         'pos_start' => false,
         'pos_end' => false,
         'quote_exact' => false,
@@ -208,6 +211,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         'title' => 'title',
         'content' => 'content',
         'severity' => 'severity',
+        'score' => 'score',
         'pos_start' => 'pos_start',
         'pos_end' => 'pos_end',
         'quote_exact' => 'quote_exact',
@@ -229,6 +233,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         'title' => 'setTitle',
         'content' => 'setContent',
         'severity' => 'setSeverity',
+        'score' => 'setScore',
         'pos_start' => 'setPosStart',
         'pos_end' => 'setPosEnd',
         'quote_exact' => 'setQuoteExact',
@@ -250,6 +255,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         'title' => 'getTitle',
         'content' => 'getContent',
         'severity' => 'getSeverity',
+        'score' => 'getScore',
         'pos_start' => 'getPosStart',
         'pos_end' => 'getPosEnd',
         'quote_exact' => 'getQuoteExact',
@@ -312,11 +318,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
     public const TYPE_GLOSSARY = 'glossary';
     public const TYPE_NOTE = 'note';
     public const TYPE_COMMENTARY = 'commentary';
-    public const SEVERITY_ERROR = 'error';
-    public const SEVERITY_WARNING = 'warning';
-    public const SEVERITY_SUGGESTION = 'suggestion';
-    public const SEVERITY_URGENT = 'urgent';
-    public const SEVERITY_NORMAL = 'normal';
+    public const TYPE_ASSESSMENT = 'assessment';
 
     /**
      * Gets allowable values of the enum
@@ -349,22 +351,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
             self::TYPE_GLOSSARY,
             self::TYPE_NOTE,
             self::TYPE_COMMENTARY,
-        ];
-    }
-
-    /**
-     * Gets allowable values of the enum
-     *
-     * @return string[]
-     */
-    public function getSeverityAllowableValues()
-    {
-        return [
-            self::SEVERITY_ERROR,
-            self::SEVERITY_WARNING,
-            self::SEVERITY_SUGGESTION,
-            self::SEVERITY_URGENT,
-            self::SEVERITY_NORMAL,
+            self::TYPE_ASSESSMENT,
         ];
     }
 
@@ -389,6 +376,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         $this->setIfExists('title', $data ?? [], null);
         $this->setIfExists('content', $data ?? [], null);
         $this->setIfExists('severity', $data ?? [], null);
+        $this->setIfExists('score', $data ?? [], null);
         $this->setIfExists('pos_start', $data ?? [], null);
         $this->setIfExists('pos_end', $data ?? [], null);
         $this->setIfExists('quote_exact', $data ?? [], null);
@@ -449,15 +437,6 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
             );
         }
 
-        $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;
     }
 
@@ -587,7 +566,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
     /**
      * Sets title
      *
-     * @param string|null $title 标题,最长 256 字符;评论线程必填(glossary 填术语词头),批注可省
+     * @param string|null $title 标题,最长 256 字符;评论线程必填(glossary 填术语词头),批注与评估可省
      *
      * @return self
      */
@@ -614,7 +593,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
     /**
      * Sets content
      *
-     * @param string|null $content 正文(markdown);批注必填
+     * @param string|null $content 正文(markdown);批注必填,评估写打分理由
      *
      * @return self
      */
@@ -641,7 +620,7 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
     /**
      * Sets severity
      *
-     * @param string|null $severity 严重程度
+     * @param string|null $severity 严重程度:issue 为 error / warning / suggestion,help 为 urgent / normal;assessment 必填,填评估方法(1–16 位字母数字 . _ -)
      *
      * @return self
      */
@@ -650,21 +629,38 @@ class PostApiV3DiscussionsRequest implements ModelInterface, ArrayAccess, \JsonS
         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 score
+     *
+     * @return int|null
+     */
+    public function getScore()
+    {
+        return $this->container['score'];
+    }
+
+    /**
+     * Sets score
+     *
+     * @param int|null $score 评估分数,0–100 的整数(只限且必填于 assessment)
+     *
+     * @return self
+     */
+    public function setScore($score)
+    {
+        if (is_null($score)) {
+            throw new \InvalidArgumentException('non-nullable score cannot be null');
+        }
+        $this->container['score'] = $score;
+
+        return $this;
+    }
+
     /**
      * Gets pos_start
      *

+ 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * 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.21
+ * The version of the OpenAPI document: 2026.10.23
  * Generated by: https://openapi-generator.tech
  * Generator version: 7.25.0
  */

+ 21 - 11
api-v13/app/Http/Controllers/V3/DiscussionController.php

@@ -21,7 +21,7 @@ use Illuminate\Http\Request;
  * `GET/PUT/DELETE /v2/discussion/{discussion}`。v2 那份在 dashboard-v4 下线前原样保留。
  * 回复见 {@see DiscussionReplyController}(取代 view=answer 与带 parent 的 POST)。
  *
- * 底层仍是 `discussions` 表。批注会注入阅读页,规则与评论不同,见各方法说明。
+ * 底层仍是 `discussions` 表。批注与评估(assessment)会注入阅读页,规则与评论不同,见各方法说明。
  *
  * 写操作(新建、修改、删除,以及回复)支持 AI 代写:模型用模型 token 登录,并带请求头
  * `X-Agent-Grant`(人类经 `POST /v3/agent-grants` 签发)。此时 editor 记授权人、
@@ -43,16 +43,17 @@ class DiscussionController extends Controller
      * 只列话题(不含回复),删除了但还有回复的话题以墓碑出现(deleted_at 有值、内容为空)。
      * 未登录看不到不公开的话题(discussion);basic 角色在自己无权管理的对象上只看得到
      * 自己发起的不公开话题。各状态的数量与「能发起哪些类型」见 `GET /v3/discussions/summary`。
-     * 不传 type 时只列评论线程(discussion / qa / help / issue / glossary),批注要显式 type=note / commentary。
+     * 不传 type 时只列评论线程(discussion / qa / help / issue / glossary),批注与评估要显式
+     * type=note / commentary / assessment。
      * 要一段经文(一章、一段段落区间)里的全部话题,用 `GET /v3/tipitaka-discussions/{channel}`。
      *
      * @unauthenticated
      *
      * @queryParam target_type string required 挂靠对象的类型。Enum: sentence,wbw,article,term,task
      * @queryParam target_id string required 挂靠对象的 uuid
-     * @queryParam type string 类型,不传则只列评论线程。Enum: discussion,qa,help,issue,glossary,note,commentary
+     * @queryParam type string 类型,不传则只列评论线程。Enum: discussion,qa,help,issue,glossary,note,commentary,assessment
      * @queryParam status string 话题状态,不传则两种都要。Enum: active,close
-     * @queryParam severity string 严重程度,逗号分隔多选。Example: error,warning
+     * @queryParam severity string 严重程度(assessment 为评估方法),逗号分隔多选。Example: error,warning
      * @queryParam sort string 排序,前缀 - 表示倒序。Enum: created_at,-created_at,updated_at,-updated_at Default: -created_at
      * @queryParam page integer 页码。Default: 1
      * @queryParam per_page integer 每页数量,最大 200。Default: 15
@@ -67,7 +68,7 @@ class DiscussionController extends Controller
     }
 
     /**
-     * 发起话题或新建批注
+     * 发起话题、新建批注或评估
      *
      * discussion / issue 登录即可发;qa / help 只能发在文章上,且只有文章的编辑者能发。
      * severity 的取值随 type:issue 为 error / warning / suggestion,help 为 urgent / normal,
@@ -77,19 +78,26 @@ class DiscussionController extends Controller
      * 标题可省、正文必填,commentary 的正文必须是义注句子模板 `{{book-para-start-end}}`
      * (可多句并列);可带锚点 pos_* / quote_*;不发通知。
      *
+     * assessment(译文评估:用某种方法给这句译文打分)只能加在句子上,且只有该句所在 channel 的编辑者能加
+     * (AI 凭授权代写);score(0–100 的整数)必填,severity 必填、填评估方法(1–16 位字母数字 . _ -),
+     * 标题可省,理由写 content;不收锚点、不发通知。**一句只留一条**:已有评估时就地覆盖
+     * (分数、方法、理由、署名都换成这次的,回复保留),返回 200;没有才新建,返回 201。
+     *
      * glossary(外挂术语:不改译文,给锚点处的译文挂上术语)是评论线程,title 必填、填术语词头;
      * 与批注一样只能挂在句子上、只有该句所在 channel 的编辑者能发,对所有人公开可见。
      * 锚点只有 note / commentary / issue / glossary 收,其它类型传了 422。
      *
      * @responseStatus 201 创建成功
+     * @responseStatus 200 覆盖了该句已有的评估(只限 assessment)
      * @responseStatus 403 当前用户不能在这个对象上发起这种类型的话题
      *
      * @bodyParam target_type string required 挂靠对象的类型。Enum: sentence,wbw,article,term,task
      * @bodyParam target_id string required 挂靠对象的 uuid
-     * @bodyParam type string 类型。Enum: discussion,qa,help,issue,glossary,note,commentary Default: discussion
-     * @bodyParam title string 标题,最长 256 字符;评论线程必填(glossary 填术语词头),批注可省
-     * @bodyParam content string 正文(markdown);批注必填
-     * @bodyParam severity string 严重程度。Enum: error,warning,suggestion,urgent,normal
+     * @bodyParam type string 类型。Enum: discussion,qa,help,issue,glossary,note,commentary,assessment Default: discussion
+     * @bodyParam title string 标题,最长 256 字符;评论线程必填(glossary 填术语词头),批注与评估可省
+     * @bodyParam content string 正文(markdown);批注必填,评估写打分理由
+     * @bodyParam severity string 严重程度:issue 为 error / warning / suggestion,help 为 urgent / normal;assessment 必填,填评估方法(1–16 位字母数字 . _ -)。Example: error
+     * @bodyParam score integer 评估分数,0–100 的整数(只限且必填于 assessment)。Example: 85
      * @bodyParam pos_start integer 锚点在句子文本中的起始偏移(只限 note / commentary / issue / glossary)
      * @bodyParam pos_end integer 锚点的结束偏移,批注插在这里(只限 note / commentary / issue / glossary)
      * @bodyParam quote_exact string 锚定的原文片段(只限 note / commentary / issue / glossary)
@@ -137,6 +145,7 @@ class DiscussionController extends Controller
      * 正文不能清空,可改锚点。
      * 改了内容(标题、正文、锚点)时 co_editor 跟着这次写入走:人亲自改则清空,AI 代改则换成该模型;
      * 只改 status / type / severity 不动 co_editor。
+     * 评估:可改 score、severity(评估方法,不能清空)、标题、正文;没有 status,不能与其它类型互转。
      *
      * @responseStatus 403 没有修改权限
      *
@@ -145,8 +154,9 @@ class DiscussionController extends Controller
      * @bodyParam title string 标题(只限话题)
      * @bodyParam content string 正文(markdown)
      * @bodyParam status string 状态(只限话题)。Enum: active,close
-     * @bodyParam type string 类型(只限话题;评论线程与批注不能互转)。Enum: discussion,qa,help,issue,glossary,note,commentary
-     * @bodyParam severity string 严重程度(只限话题),null 表示清空。Enum: error,warning,suggestion,urgent,normal
+     * @bodyParam type string 类型(只限话题;评论线程与批注不能互转)。Enum: discussion,qa,help,issue,glossary,note,commentary,assessment
+     * @bodyParam severity string 严重程度(只限话题),null 表示清空;assessment 为评估方法,不能清空。Example: warning
+     * @bodyParam score integer 评估分数,0–100 的整数(只限 assessment)。Example: 85
      * @bodyParam pos_start integer 锚点起始偏移(只限 note / commentary / issue / glossary)
      * @bodyParam pos_end integer 锚点结束偏移(只限 note / commentary / issue / glossary)
      * @bodyParam quote_exact string 锚定的原文片段(只限 note / commentary / issue / glossary)

+ 1 - 1
api-v13/app/Http/Controllers/V3/DiscussionSummaryController.php

@@ -30,7 +30,7 @@ class DiscussionSummaryController extends Controller
      *
      * @queryParam target_type string required 挂靠对象的类型。Enum: sentence,wbw,article,term,task
      * @queryParam target_id string required 挂靠对象的 uuid
-     * @queryParam type string 只数这种类型,不传则只数评论线程。Enum: discussion,qa,help,issue,glossary,note,commentary
+     * @queryParam type string 只数这种类型,不传则只数评论线程。Enum: discussion,qa,help,issue,glossary,note,commentary,assessment
      */
     public function __invoke(SummaryDiscussionRequest $request)
     {

+ 3 - 3
api-v13/app/Http/Controllers/V3/TipitakaDiscussionController.php

@@ -28,8 +28,8 @@ class TipitakaDiscussionController extends Controller
      *
      * 只列挂在该 channel 句子上的话题(不含回复),按经文顺序(书、段、句)排列,同一句里按时间;
      * 每条带 `sentence`(所在句子的坐标)。删除了但还有回复的话题以墓碑出现。
-     * 不传 type 时只列评论线程(discussion / qa / help / issue / glossary),批注要显式
-     * type=note / commentary。可见性同 `GET /v3/discussions`:未登录看不到 discussion;
+     * 不传 type 时只列评论线程(discussion / qa / help / issue / glossary),批注与评估要显式
+     * type=note / commentary / assessment。可见性同 `GET /v3/discussions`:未登录看不到 discussion;
      * basic 角色在自己不能编辑的 channel 上只看得到自己发起的 discussion。
      * channel 不存在或范围内没有讨论都返回空列表;chapter 不存在 404。
      *
@@ -45,7 +45,7 @@ class TipitakaDiscussionController extends Controller
      * @queryParam to integer 结束段落号(含),缺省同 para。Example: 896
      * @queryParam type string 类型,逗号分隔多选;不传则只列评论线程。Example: issue,glossary
      * @queryParam status string 话题状态,不传则两种都要。Enum: active,close
-     * @queryParam severity string 严重程度,逗号分隔多选。Example: error,warning
+     * @queryParam severity string 严重程度(assessment 为评估方法),逗号分隔多选。Example: error,warning
      * @queryParam page integer 页码。Default: 1
      * @queryParam per_page integer 每页数量,最大 200。Default: 15
      */

+ 17 - 4
api-v13/app/Http/Requests/V3/StoreDiscussionRequest.php

@@ -15,6 +15,9 @@ use Illuminate\Validation\Rule;
  * 批注(note / commentary)与评论线程的规则不同:只能挂在句子上、标题可省、正文必填,
  * commentary 的正文必须是义注句子模板。glossary 也只能挂在句子上。
  * 锚点(pos_* / quote_*)只有 {@see Discussion::ANCHORED_TYPES} 收。
+ *
+ * assessment(译文评估):score(0–100 的整数)与 severity(评估方法)必填,标题可省,理由写 content;
+ * 其它类型传 score 就 422。
  */
 class StoreDiscussionRequest extends FormRequest
 {
@@ -32,6 +35,7 @@ class StoreDiscussionRequest extends FormRequest
     {
         $type = $this->input('type', 'discussion');
         $severities = Discussion::SEVERITIES[$type] ?? [];
+        $isAssessment = $type === Discussion::TYPE_ASSESSMENT;
         $isAnnotation = in_array($type, Discussion::INJECTED_TYPES, true);
         $sentenceOnly = in_array($type, Discussion::SENTENCE_ONLY_TYPES, true);
         // 不收锚点的类型传了 422,免得静默丢掉
@@ -43,7 +47,7 @@ class StoreDiscussionRequest extends FormRequest
             'target_type' => ['required', 'string', Rule::in($sentenceOnly ? ['sentence'] : Discussion::TARGET_TYPES)],
             'target_id' => ['required', 'uuid'],
             'type' => ['sometimes', 'string', Rule::in(Discussion::COMMENT_TYPES)],
-            'title' => $isAnnotation
+            'title' => $isAnnotation || $isAssessment
                 ? ['sometimes', 'nullable', 'string', 'max:256']
                 : ['required', 'string', 'max:256'],
             'content' => match ($type) {
@@ -51,9 +55,14 @@ class StoreDiscussionRequest extends FormRequest
                 Discussion::TYPE_NOTE => ['required', 'string', 'max:20000'],
                 default => ['sometimes', 'nullable', 'string', 'max:20000'],
             },
-            'severity' => $severities === []
-                ? ['prohibited']
-                : ['sometimes', 'nullable', 'string', Rule::in($severities)],
+            'severity' => match (true) {
+                $isAssessment => ['required', 'string', 'regex:'.Discussion::ASSESSMENT_METHOD_PATTERN],
+                $severities === [] => ['prohibited'],
+                default => ['sometimes', 'nullable', 'string', Rule::in($severities)],
+            },
+            'score' => $isAssessment
+                ? ['required', 'integer', 'between:0,100']
+                : ['prohibited'],
             'pos_start' => $anchor(['integer', 'min:0']),
             'pos_end' => $anchor(['integer', 'min:0', 'gte:pos_start']),
             'quote_exact' => $anchor(['string', 'max:1024']),
@@ -72,11 +81,15 @@ class StoreDiscussionRequest extends FormRequest
         $messages = [
             'content.regex' => __('messages.discussion_commentary_format'),
             'severity.prohibited' => __('messages.discussion_severity_not_allowed'),
+            'severity.regex' => __('messages.discussion_assessment_method'),
+            'score.prohibited' => __('messages.discussion_score_not_allowed'),
         ];
         if (in_array($this->input('type'), Discussion::INJECTED_TYPES, true)) {
             $messages['target_type.in'] = __('messages.discussion_annotation_target');
         } elseif ($this->input('type') === Discussion::TYPE_GLOSSARY) {
             $messages['target_type.in'] = __('messages.discussion_glossary_target');
+        } elseif ($this->input('type') === Discussion::TYPE_ASSESSMENT) {
+            $messages['target_type.in'] = __('messages.discussion_assessment_target');
         }
         foreach (self::ANCHOR_FIELDS as $field) {
             $messages["{$field}.prohibited"] = __('messages.discussion_anchor_thread');

+ 2 - 1
api-v13/app/Http/Requests/V3/UpdateDiscussionRequest.php

@@ -12,7 +12,7 @@ use Illuminate\Validation\Rule;
  * title / status / type / severity 只对话题有效,回复传了 422——这一条与
  * severity 是否匹配最终 type,都要看库里那条记录,在 Service 里判。
  * 批注相关的规则(锚点只有可锚定的类型收、commentary 正文格式、不能与评论线程互转、没有 status)
- * 同样取决于那条记录的类型,也在 Service 里判。
+ * 同样取决于那条记录的类型,也在 Service 里判。score 只有 assessment 收,也在 Service 里判。
  */
 class UpdateDiscussionRequest extends FormRequest
 {
@@ -32,6 +32,7 @@ class UpdateDiscussionRequest extends FormRequest
             'status' => ['sometimes', 'string', Rule::in(Discussion::STATUSES)],
             'type' => ['sometimes', 'string', Rule::in(Discussion::COMMENT_TYPES)],
             'severity' => ['sometimes', 'nullable', 'string'],
+            'score' => ['sometimes', 'integer', 'between:0,100'],
             'pos_start' => ['sometimes', 'nullable', 'integer', 'min:0'],
             'pos_end' => ['sometimes', 'nullable', 'integer', 'min:0'],
             'quote_exact' => ['sometimes', 'nullable', 'string', 'max:1024'],

+ 4 - 1
api-v13/app/Http/Resources/V3/DiscussionResource.php

@@ -8,7 +8,8 @@ use App\Services\V3\DiscussionService;
 use Illuminate\Http\Request;
 
 /**
- * 一条评论:话题(parent_id 为 null)或回复;或一条批注(note / commentary)。
+ * 一条评论:话题(parent_id 为 null)或回复;或一条批注(note / commentary);或一条译文评估
+ * (assessment:score 是 0–100 的分数,severity 是评估方法,content 是理由)。
  *
  * 批注与 issue / glossary 可带锚点(pos_start / pos_end / quote_*),指明它挂在句子文本的
  * 哪一段;其它评论线程的这几个字段为 null。glossary 的 title 是术语词头。
@@ -43,6 +44,7 @@ class DiscussionResource extends BaseResource
      *     parent_id: string|null,
      *     type: string,
      *     severity: string|null,
+     *     score: int|null,
      *     status: string,
      *     title: string|null,
      *     content: string|null,
@@ -76,6 +78,7 @@ class DiscussionResource extends BaseResource
             'parent_id' => $this->parent,
             'type' => $this->type,
             'severity' => $this->severity,
+            'score' => $this->score,
             'status' => $this->status,
             'title' => $tombstone ? null : $this->title,
             'content' => $tombstone ? null : $this->content,

+ 27 - 7
api-v13/app/Models/Discussion.php

@@ -36,12 +36,14 @@ class Discussion extends Model
         'id' => 'string',
         'pos_start' => 'integer',
         'pos_end' => 'integer',
+        'score' => 'integer',
     ];
 
     // 批量填充
     protected $fillable = [
         'res_id',
         'res_type',
+        'score',
         'type',
         'tpl_id',
         'title',
@@ -77,6 +79,20 @@ class Discussion extends Model
      */
     public const TYPE_GLOSSARY = 'glossary';
 
+    /**
+     * 译文评估:用某种方法给一句译文打分。score 是归一到 0–100 的整数分数,severity 存评估方法
+     * ({@see self::ASSESSMENT_METHOD_PATTERN}),content 写理由,title 可省。
+     * 只能挂在句子上、只有该句的管理者能建(AI 凭授权代写);一句只留一条,重新评估覆盖旧的。
+     * 没有开关状态、不能与其它类型互转、不带锚点;注入阅读页,在句尾显示按分数着色的对勾。
+     */
+    public const TYPE_ASSESSMENT = 'assessment';
+
+    /** 评估方法(存在 severity 列,varchar(16)):字母数字开头,可含 . _ - */
+    public const ASSESSMENT_METHOD_PATTERN = '/^[A-Za-z0-9][A-Za-z0-9._-]{0,15}$/';
+
+    /** 没有开关状态的类型:批注 + 评估 */
+    public const STATELESS_TYPES = [self::TYPE_COMMENTARY, self::TYPE_NOTE, self::TYPE_ASSESSMENT];
+
     /** 评论线程:话题 + 一层回复,有开关状态 */
     public const THREAD_TYPES = ['discussion', 'qa', 'help', 'issue', self::TYPE_GLOSSARY];
 
@@ -85,20 +101,24 @@ class Discussion extends Model
 
     /**
      * 会被注入阅读页的全部类型({@see PaliContentService::injectAnnotationNotes()}):
-     * 批注插边注,glossary 换成术语,issue 高亮。issue / glossary 只注入未关闭的。
+     * 批注插边注,glossary 换成术语,issue 高亮,assessment 句尾对勾。issue / glossary 只注入未关闭的。
      */
-    public const READING_TYPES = [self::TYPE_COMMENTARY, self::TYPE_NOTE, 'issue', self::TYPE_GLOSSARY];
+    public const READING_TYPES = [self::TYPE_COMMENTARY, self::TYPE_NOTE, 'issue', self::TYPE_GLOSSARY, self::TYPE_ASSESSMENT];
 
-    /** 只能挂在句子上的类型:批注 + glossary */
-    public const SENTENCE_ONLY_TYPES = [self::TYPE_COMMENTARY, self::TYPE_NOTE, self::TYPE_GLOSSARY];
+    /** 只能挂在句子上的类型:批注 + glossary + assessment */
+    public const SENTENCE_ONLY_TYPES = [self::TYPE_COMMENTARY, self::TYPE_NOTE, self::TYPE_GLOSSARY, self::TYPE_ASSESSMENT];
 
     /**
      * `/v3/discussions` 管的全部类型:评论线程({@see self::THREAD_TYPES})+ 批注
-     * ({@see self::INJECTED_TYPES})。批注锚定在句子的某个位置、注入阅读页,没有回复和开关。
+     * ({@see self::INJECTED_TYPES})+ 评估({@see self::TYPE_ASSESSMENT})。
+     * 批注与评估都注入阅读页,可以回复,没有开关。
      */
-    public const COMMENT_TYPES = ['discussion', 'qa', 'help', 'issue', 'glossary', 'note', 'commentary'];
+    public const COMMENT_TYPES = ['discussion', 'qa', 'help', 'issue', 'glossary', 'note', 'commentary', 'assessment'];
 
-    /** 各类型允许的 severity 取值;不在表里的类型不允许设 severity */
+    /**
+     * 各类型允许的 severity 取值;不在表里的类型不允许设 severity。
+     * assessment 例外:severity 存评估方法,按 {@see self::ASSESSMENT_METHOD_PATTERN} 校验,必填。
+     */
     public const SEVERITIES = [
         'issue' => ['error', 'warning', 'suggestion'],
         'help' => ['urgent', 'normal'],

+ 48 - 3
api-v13/app/Services/AnnotationLayout.php

@@ -12,7 +12,8 @@ namespace App\Services;
  * - glossary:区间 [start, end) 的原文换成 `[[title#原文]]`,MdRender 渲染成术语;
  * - issue:区间外包一层 `<span class="anno-issue …" data-discussion-ids="…">`(高亮,点击看内容);
  *   没有位置的 issue 合成一个三角叹号图标,放在句尾;
- * - note / commentary:在插入点插入调用方预先渲染好的 `{{note|…}}` 模板。
+ * - note / commentary:在插入点插入调用方预先渲染好的 `{{note|…}}` 模板;
+ * - assessment:句子最末尾一个对勾图标(在 issue 的三角之后),颜色按分数分档({@see self::assessmentLevel()})。
  *
  * 交叠的处理(核心约束:任何时候都不能切断一个标记):
  * - 受保护区间:content 里本来就有的标记(`[[…]]`、`{{…}}`、`` `…` ``、`**…**`、HTML 标签),
@@ -32,6 +33,9 @@ class AnnotationLayout
     /** 严重程度由轻到重;合并时取最重的,没有 severity 的最轻 */
     private const SEVERITY_RANK = ['suggestion' => 1, 'warning' => 2, 'error' => 3];
 
+    /** 评估分档的下限(含):由高到低,都不够就是 low */
+    private const ASSESSMENT_LEVELS = ['high' => 80, 'medium' => 60];
+
     /** glossary 的原文或词头里不能有这些字符,否则 `[[title#原文]]` 会被拆坏 */
     private const GLOSSARY_FORBIDDEN = '/[\[\]{}|#\r\n]/u';
 
@@ -99,13 +103,14 @@ class AnnotationLayout
      * @param  list<array{id: string, start: int, end: int, title: string}>  $glossaries  已定位的术语
      * @param  list<array{id: string, start: int|null, end: int|null, severity: string|null}>  $issues  start/end 为 null 表示没有位置
      * @param  list<array{id: string, at: int, order: array<int, int>, html: string}>  $notes  at 是插入点,order 是同一插入点上的先后
-     * @param  bool  $withIssues  是否输出 issue 的高亮与图标(只有 html 格式需要)
+     * @param  bool  $withIssues  是否输出 issue 的高亮与图标、评估的对勾(只有 html 格式需要)
+     * @param  array{id: string, score: int}|null  $assessment  这句的评估,没有为 null
      * @return array{content: string, notes: list<string>, glossaries: list<string>, issues: list<array{ids: list<string>, severity: string|null, start: int|null, end: int|null}>}
      *
      * 返回值里 notes 是 note id 的阅读顺序,glossaries 是实际替换了的术语 id,
      * issues 是合并后的高亮(没有位置的合成一条,start / end 为 null)。
      */
-    public static function apply(string $content, array $glossaries, array $issues, array $notes, bool $withIssues = true): array
+    public static function apply(string $content, array $glossaries, array $issues, array $notes, bool $withIssues = true, ?array $assessment = null): array
     {
         $len = mb_strlen($content, 'UTF-8');
         $protected = self::protectedRanges($content);
@@ -226,6 +231,9 @@ class AnnotationLayout
                 self::maxSeverity(array_column($unanchored, 'severity')),
             );
         }
+        if ($withIssues && $assessment !== null) {
+            $out .= self::assessmentMark($assessment['id'], $assessment['score']);
+        }
 
         $report = array_map(fn ($s) => [
             'ids' => $s['ids'], 'severity' => $s['severity'], 'start' => $s['start'], 'end' => $s['end'],
@@ -342,6 +350,43 @@ class AnnotationLayout
             .' data-severity="'.e($severity ?? '').'">';
     }
 
+    /**
+     * 评估分数的档位:high(≥80)/ medium(≥60)/ low。阅读器按档位着色(绿 / 橙 / 红)。
+     */
+    public static function assessmentLevel(int $score): string
+    {
+        foreach (self::ASSESSMENT_LEVELS as $level => $min) {
+            if ($score >= $min) {
+                return $level;
+            }
+        }
+
+        return 'low';
+    }
+
+    /**
+     * 没有评估的句子:灰色对勾,不能点(没有 discussion id)。只在同段有别的句子评估过时由
+     * {@see PaliContentService::renderReadSentences()} 补上,标出句子的边界。
+     */
+    public static function unassessedMark(): string
+    {
+        return '<span class="anno-assessment-mark anno-assessment-none" data-level="none" aria-hidden="true">'
+            .'<i class="ti ti-circle-check"></i></span>';
+    }
+
+    /**
+     * 评估:句尾一个对勾图标,颜色按分数档位,点击看分数与理由。
+     */
+    private static function assessmentMark(string $id, int $score): string
+    {
+        $level = self::assessmentLevel($score);
+
+        return '<span class="anno-assessment-mark anno-assessment-'.$level.'"'
+            .' data-discussion-ids="'.e($id).'"'
+            .' data-score="'.$score.'" data-level="'.$level.'" role="button" tabindex="0">'
+            .'<i class="ti ti-circle-check"></i></span>';
+    }
+
     /**
      * 没有位置的 issue:句尾一个三角叹号图标,颜色取最重的 severity。
      *

+ 33 - 7
api-v13/app/Services/PaliContentService.php

@@ -752,22 +752,25 @@ class PaliContentService
      * - `glossary`(外挂术语):锚点处的原文换成 `[[title#原文]]`,渲染成术语(title 是词头);
      * - `issue`:锚点处的原文包一层高亮 span,带 discussion id,阅读器点击看内容;
      *   没有位置的合成一个三角叹号图标放在句尾。只在 html 格式输出。
-     * issue / glossary 只注入未关闭的;批注没有开关状态。
+     * - `assessment`(译文评估):句尾一个对勾图标,颜色按分数分档,带 discussion id,
+     *   阅读器点击看分数与理由。一句只有一条(有并发残留时取最近更新的)。只在 html 格式输出。
+     *   同段里其它没有评估的句子由 {@see self::renderReadSentences()} 补灰色对勾。
+     * issue / glossary 只注入未关闭的;批注与评估没有开关状态。
      *
      * 位置按 pos_* 定位,与 quote_exact 对不上(正文改过)时按摘录重新定位。
      *
-     * @return array{content: string, notes: list<array{fnId: string, noteHtml: string, citeHtml: string}>}
+     * @return array{content: string, notes: list<array{fnId: string, noteHtml: string, citeHtml: string}>, assessed: bool}
      */
     protected function injectAnnotationNotes(Sentence $row, string $channelType, string $format = 'html'): array
     {
         $content = (string) $row->content;
-        $result = ['content' => $content, 'notes' => []];
+        $result = ['content' => $content, 'notes' => [], 'assessed' => false];
         $records = Discussion::where('res_type', 'sentence')
             ->where('res_id', $row->uid)
             ->whereIn('type', Discussion::READING_TYPES)
             // 只注入话题本身;回复(类型跟随话题)不进阅读页
             ->whereNull('parent')
-            ->where(fn ($q) => $q->whereIn('type', Discussion::INJECTED_TYPES)->orWhere('status', 'active'))
+            ->where(fn ($q) => $q->whereIn('type', Discussion::STATELESS_TYPES)->orWhere('status', 'active'))
             ->orderBy('created_at')
             ->get();
 
@@ -783,7 +786,15 @@ class PaliContentService
         $issues = [];
         $notes = [];
         $footnotes = [];
+        $assessment = null;
         foreach ($records as $record) {
+            if ($record->type === Discussion::TYPE_ASSESSMENT) {
+                if ($record->score !== null && ($assessment === null || $record->updated_at >= $assessment['updated_at'])) {
+                    $assessment = ['id' => $record->id, 'score' => (int) $record->score, 'updated_at' => $record->updated_at];
+                }
+
+                continue;
+            }
             if ($record->type === Discussion::TYPE_GLOSSARY) {
                 $range = $locate($record);
                 if ($range !== null) {
@@ -816,9 +827,17 @@ class PaliContentService
             ];
         }
 
-        $layout = AnnotationLayout::apply($content, $glossaries, $issues, $notes, $format === 'html');
+        $layout = AnnotationLayout::apply(
+            $content,
+            $glossaries,
+            $issues,
+            $notes,
+            $format === 'html',
+            $assessment === null ? null : ['id' => $assessment['id'], 'score' => $assessment['score']],
+        );
         $result['content'] = $layout['content'];
         $result['notes'] = array_map(fn ($id) => $footnotes[$id], $layout['notes']);
+        $result['assessed'] = $assessment !== null;
 
         Log::info('reading-annotations: inject', [
             'sid' => "{$row->book_id}-{$row->paragraph}-{$row->word_start}-{$row->word_end}",
@@ -950,9 +969,16 @@ class PaliContentService
             ->get();
 
         $allNotes = [];
-        foreach ($records as $row) {
-            $injected = $this->injectAnnotationNotes($row, $channelType, $format);
+        $injections = $records->map(fn (Sentence $row) => $this->injectAnnotationNotes($row, $channelType, $format));
+        // 段里有句子评估过,其余没评估的句子在句尾补灰色对勾:不然读者会把它与下一句的
+        // 对勾连起来,误以为是一句。整段都没评估就都不补(段落本身有分隔)。
+        $markUnassessed = $format === 'html' && $injections->contains('assessed', true);
+        foreach ($records as $i => $row) {
+            $injected = $injections[$i];
             $content = $injected['content'];
+            if ($markUnassessed && ! $injected['assessed'] && trim($content) !== '') {
+                $content .= AnnotationLayout::unassessedMark();
+            }
             $allNotes = array_merge($allNotes, $injected['notes']);
             $html = MdRender::render(
                 $content,

+ 74 - 13
api-v13/app/Services/V3/DiscussionService.php

@@ -42,6 +42,10 @@ use Illuminate\Validation\ValidationException;
  * glossary(外挂术语)是评论线程(有回复和开关),但像批注一样只能挂在句子上、只有该句的管理者能发起,
  * 对所有人公开可见。锚点除批注外,issue 与 glossary 也收({@see Discussion::ANCHORED_TYPES})。
  *
+ * assessment(译文评估)像批注一样:只挂句子、只有该句的管理者能建(AI 凭授权代写)、公开、
+ * 可回复、没有开关、不发通知、列表不带 type 时不列。一句只留一条,再评估就覆盖
+ * ({@see self::saveAssessment()});分数在 score,评估方法在 severity。
+ *
  * 写入方(`$actor`)由 {@see AgentGrantService::actor()} 给出:人类直接写,或 AI 模型凭人类
  * 授权代写。代写时 editor 是授权人、co_editor 是模型,权限一律按 editor 判,
  * 而且只能写授权覆盖的 channel / book 里的东西。
@@ -332,10 +336,13 @@ class DiscussionService
     }
 
     /**
-     * 发起话题,或新建一条批注(批注不发通知)。
+     * 发起话题,或新建一条批注 / 评估(批注与评估不发通知)。
+     *
+     * 评估(assessment)一句只留一条:该句已有评估就就地覆盖(分数、方法、理由、标题、署名),
+     * 回复保留,返回 200;没有才新建,返回 201。
      *
      * @param  array{editor: string, co_editor: string|null, scope: array{channel: string, book: int}|null}  $actor
-     * @param  array{target_type: string, target_id: string, type?: string, title?: string|null, content?: string|null, severity?: string|null, pos_start?: int|null, pos_end?: int|null, quote_exact?: string|null, quote_prefix?: string|null, quote_suffix?: string|null, tpl_id?: string|null, notify?: bool}  $data
+     * @param  array{target_type: string, target_id: string, type?: string, title?: string|null, content?: string|null, severity?: string|null, score?: int, pos_start?: int|null, pos_end?: int|null, quote_exact?: string|null, quote_prefix?: string|null, quote_suffix?: string|null, tpl_id?: string|null, notify?: bool}  $data
      */
     public function createTopic(array $actor, array $data): Discussion
     {
@@ -349,6 +356,9 @@ class DiscussionService
         if (! in_array($type, $this->creatableTypes($userUid, $data['target_type'], $data['target_id']), true)) {
             abort(403, __('messages.discussion_forbidden'));
         }
+        if ($type === Discussion::TYPE_ASSESSMENT) {
+            return $this->fresh($this->saveAssessment($actor, $data), $userUid);
+        }
 
         $topic = new Discussion;
         $topic->res_type = $data['target_type'];
@@ -371,6 +381,40 @@ class DiscussionService
         return $this->fresh($topic, $userUid);
     }
 
+    /**
+     * 新建或覆盖一句的评估。锁住句子行,免得并发评估同一句时各建一条。
+     * 覆盖时署名换成这次评估的人(及代写的模型),created_at 不变。
+     *
+     * @param  array{editor: string, co_editor: string|null, scope: array{channel: string, book: int}|null}  $actor
+     * @param  array{target_id: string, title?: string|null, content?: string|null, severity?: string|null, score?: int, tpl_id?: string|null}  $data
+     */
+    private function saveAssessment(array $actor, array $data): Discussion
+    {
+        return DB::transaction(function () use ($actor, $data) {
+            Sentence::where('uid', $data['target_id'])->lockForUpdate()->first(['uid']);
+            $assessment = Discussion::where('res_type', 'sentence')
+                ->where('res_id', $data['target_id'])
+                ->where('type', Discussion::TYPE_ASSESSMENT)
+                ->whereNull('parent')
+                ->latest('updated_at')
+                ->first() ?? new Discussion;
+            $assessment->res_type = 'sentence';
+            $assessment->res_id = $data['target_id'];
+            $assessment->type = Discussion::TYPE_ASSESSMENT;
+            $assessment->title = $data['title'] ?? null;
+            $assessment->content = $data['content'] ?? null;
+            $assessment->content_type = 'markdown';
+            $assessment->severity = $data['severity'];
+            $assessment->score = (int) $data['score'];
+            $assessment->tpl_id = $data['tpl_id'] ?? null;
+            $assessment->editor_uid = $actor['editor'];
+            $assessment->co_editor_uid = $actor['co_editor'];
+            $assessment->save();
+
+            return $assessment;
+        });
+    }
+
     /**
      * 回复话题。话题必须没关闭;不能回复一条回复。
      *
@@ -426,12 +470,13 @@ class DiscussionService
      * 改成 glossary 须挂在句子上、且当前用户是该句的管理者(同新建)。
      * 批注:锚点可改,没有 status,type 只能在 note / commentary 之间改,
      * 正文不能清空,commentary 的正文必须是义注句子模板(按改后的 type 与正文判)。
+     * 评估:可改 score、severity(评估方法,不能清空)、标题、正文;没有 status,不能与其它类型互转。
      *
      * 改了内容(标题、正文、锚点)时 co_editor 改成这次写入的协作者:人亲自改则清空,
-     * AI 代改则换成该模型。只改 status / type / severity 不动 co_editor。
+     * AI 代改则换成该模型。只改 status / type / severity 不动 co_editor(评估的 score 算内容)。
      *
      * @param  array{editor: string, co_editor: string|null, scope: array{channel: string, book: int}|null}  $actor
-     * @param  array{title?: string, content?: string|null, status?: string, type?: string, severity?: string|null, pos_start?: int|null, pos_end?: int|null, quote_exact?: string|null, quote_prefix?: string|null, quote_suffix?: string|null}  $data
+     * @param  array{title?: string, content?: string|null, status?: string, type?: string, severity?: string|null, score?: int, pos_start?: int|null, pos_end?: int|null, quote_exact?: string|null, quote_prefix?: string|null, quote_suffix?: string|null}  $data
      */
     public function update(Discussion $discussion, array $actor, array $data): Discussion
     {
@@ -444,7 +489,7 @@ class DiscussionService
             abort(403, __('messages.discussion_forbidden'));
         }
         $isTopic = $discussion->parent === null;
-        $topicOnly = array_intersect_key($data, array_flip(['title', 'status', 'type', 'severity']));
+        $topicOnly = array_intersect_key($data, array_flip(['title', 'status', 'type', 'severity', 'score']));
         if (! $isTopic && $topicOnly !== []) {
             throw ValidationException::withMessages(
                 array_fill_keys(array_keys($topicOnly), __('messages.discussion_topic_only'))
@@ -459,7 +504,13 @@ class DiscussionService
         if ($type === Discussion::TYPE_GLOSSARY && $discussion->type !== Discussion::TYPE_GLOSSARY) {
             $this->assertCanBecomeGlossary($discussion, $userUid);
         }
-        if (array_key_exists('severity', $data)) {
+        if ($type === Discussion::TYPE_ASSESSMENT) {
+            // 评估的 severity 是评估方法:不能清空,格式见 ASSESSMENT_METHOD_PATTERN
+            if (array_key_exists('severity', $data)
+                && ! preg_match(Discussion::ASSESSMENT_METHOD_PATTERN, (string) $data['severity'])) {
+                throw ValidationException::withMessages(['severity' => __('messages.discussion_assessment_method')]);
+            }
+        } elseif (array_key_exists('severity', $data)) {
             if ($data['severity'] !== null && ! in_array($data['severity'], Discussion::SEVERITIES[$type] ?? [], true)) {
                 throw ValidationException::withMessages(['severity' => __('messages.discussion_severity_not_allowed')]);
             }
@@ -469,7 +520,7 @@ class DiscussionService
 
         DB::transaction(function () use ($discussion, $data, $actor) {
             $typeChanged = array_key_exists('type', $data) && $data['type'] !== $discussion->type;
-            $authored = array_intersect_key($data, array_flip(['title', 'content', ...self::ANCHOR_FIELDS]));
+            $authored = array_intersect_key($data, array_flip(['title', 'content', 'score', ...self::ANCHOR_FIELDS]));
             $discussion->fill($authored);
             // 改了内容,co_editor 就跟着这次写的人走:人亲自改 → 清空,AI 代改 → 换成这个模型。
             // 只改状态 / 类型 / severity 是管理动作,不动内容的署名。
@@ -590,18 +641,28 @@ class DiscussionService
     }
 
     /**
-     * 批注与评论线程在 PATCH 上的差异,见 {@see self::update()}。
+     * 批注、评估与评论线程在 PATCH 上的差异,见 {@see self::update()}。
      *
      * @param  array<string, mixed>  $data
      */
     private function assertAnnotationUpdate(Discussion $discussion, array $data): void
     {
         $isAnnotation = $this->isAnnotation($discussion);
+        $isAssessment = $discussion->type === Discussion::TYPE_ASSESSMENT;
         $errors = [];
         if (array_key_exists('type', $data)
+            && ($data['type'] === Discussion::TYPE_ASSESSMENT) !== $isAssessment) {
+            $errors['type'] = __('messages.discussion_assessment_type');
+        } elseif (array_key_exists('type', $data)
             && in_array($data['type'], Discussion::INJECTED_TYPES, true) !== $isAnnotation) {
             $errors['type'] = __('messages.discussion_type_group');
         }
+        if (array_key_exists('score', $data) && ! $isAssessment) {
+            $errors['score'] = __('messages.discussion_score_not_allowed');
+        }
+        if ($isAssessment && array_key_exists('status', $data)) {
+            $errors['status'] = __('messages.discussion_assessment_no_status');
+        }
         if (! in_array($data['type'] ?? $discussion->type, Discussion::ANCHORED_TYPES, true)) {
             foreach (array_intersect(self::ANCHOR_FIELDS, array_keys($data)) as $field) {
                 $errors[$field] = __('messages.discussion_anchor_thread');
@@ -707,7 +768,7 @@ class DiscussionService
      * 当前用户能在这个对象上发起哪些类型的话题。
      *
      * 协作类(discussion / issue)登录即可;qa / help 沿用 v2 `can_create` 的口径:
-     * 只能发在文章上,而且只有文章的编辑者能发;批注(note / commentary)与 glossary 只能加在
+     * 只能发在文章上,而且只有文章的编辑者能发;批注(note / commentary)、glossary 与 assessment 只能加在
      * 句子上,而且只有该句的管理者能加(v2 登录即可,v3 收紧)。
      *
      * @return list<string>
@@ -721,9 +782,9 @@ class DiscussionService
         if ($targetType === 'article' && $this->canManageTarget($userUid, $targetType, $targetId)) {
             array_push($types, 'qa', 'help');
         }
-        // 批注注入阅读页、所有读者可见,只有该句的管理者能加
+        // 批注与评估注入阅读页、所有读者可见,只有该句的管理者能加
         if ($targetType === 'sentence' && $this->canManageTarget($userUid, $targetType, $targetId)) {
-            array_push($types, Discussion::TYPE_GLOSSARY, Discussion::TYPE_NOTE, Discussion::TYPE_COMMENTARY);
+            array_push($types, Discussion::TYPE_GLOSSARY, Discussion::TYPE_NOTE, Discussion::TYPE_COMMENTARY, Discussion::TYPE_ASSESSMENT);
         }
 
         return $types;
@@ -743,8 +804,8 @@ class DiscussionService
         $isAuthor = $discussion->editor_uid === $viewerUid;
         $canManage = $isAuthor || $this->canManageTarget($viewerUid, $discussion->res_type, $discussion->res_id);
         $isTopic = $discussion->parent === null;
-        if ($this->isAnnotation($discussion)) {
-            // 批注没有开关;回复与评论线程一样,登录即可
+        if (in_array($discussion->type, Discussion::STATELESS_TYPES, true)) {
+            // 批注与评估没有开关;回复与评论线程一样,登录即可
             return [
                 'update' => $canManage,
                 'delete' => $canManage,

+ 29 - 0
api-v13/database/migrations/2026_10_22_100000_add_score_in_discussions.php

@@ -0,0 +1,29 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * 给 discussions 增加评估分数 score,只有 type=assessment(译文评估)有值。
+     * 统一归一到 0–100 的整数:译文质量是模糊评估,小数没有意义。评估方法记在 severity 里。
+     *
+     * nullable:存量数据与 dashboard-v4(v2 写入)都不会带这个值。
+     */
+    public function up(): void
+    {
+        Schema::table('discussions', function (Blueprint $table) {
+            $table->smallInteger('score')->nullable()->index()
+                ->comment('评估分数 0–100,只有 type=assessment 有值');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('discussions', function (Blueprint $table) {
+            $table->dropColumn('score');
+        });
+    }
+};

+ 38 - 0
api-v13/resources/css/modules/reader-content.css

@@ -500,3 +500,41 @@ article.reader-body span.anno-issue-mark.is-active {
 .anno-issue-item.anno-issue-error      { --anno-issue-color: var(--tblr-danger); }
 .anno-issue-item.anno-issue-warning    { --anno-issue-color: var(--tblr-warning); }
 .anno-issue-item.anno-issue-suggestion { --anno-issue-color: var(--tblr-info); }
+
+/* ══════════════════════════════════════════
+   九、译文评估标注
+   PaliContentService::injectAnnotationNotes 注入:句尾的 span.anno-assessment-mark(对勾图标),
+   颜色按分数档位(AnnotationLayout::assessmentLevel):high(≥80)绿、medium(≥60)橙、low 红;
+   同段有句子评估过时,没评估的句子是灰色对勾(anno-assessment-none,不能点)。
+   点击由 issue-popover.js 弹出分数、方法与理由,弹层条目复用 .anno-issue-item。
+   ══════════════════════════════════════════ */
+
+.anno-assessment-high   { --anno-assessment-color: var(--tblr-success); }
+.anno-assessment-medium { --anno-assessment-color: var(--tblr-warning); }
+.anno-assessment-low    { --anno-assessment-color: var(--tblr-danger); }
+/* 同段有句子评估过时,没评估的句子补的灰色对勾:只标句子边界,不能点 */
+.anno-assessment-none   { --anno-assessment-color: var(--tblr-gray-400, #adb5bd); }
+
+article.reader-body span.anno-assessment-mark {
+    cursor: pointer;
+    margin-left: 0.15em;
+    color: var(--anno-assessment-color);
+    font-size: 0.95em;
+    vertical-align: -0.05em;
+}
+article.reader-body span.anno-assessment-mark:hover,
+article.reader-body span.anno-assessment-mark.is-active {
+    filter: brightness(0.85);
+}
+article.reader-body span.anno-assessment-mark.anno-assessment-none {
+    cursor: default;
+    filter: none;
+}
+
+.anno-assessment-item .anno-issue-item-head .ti {
+    color: var(--anno-assessment-color);
+}
+.anno-assessment-score {
+    color: var(--anno-assessment-color);
+    font-variant-numeric: tabular-nums;
+}

+ 67 - 17
api-v13/resources/js/modules/issue-popover.js

@@ -1,15 +1,17 @@
 // resources/js/modules/issue-popover.js
 //
-// 阅读页 issue 标注的点击查看。正文里的标注由后端注入
+// 阅读页 issue 与译文评估标注的点击查看。正文里的标注由后端注入
 // (PaliContentService::injectAnnotationNotes → AnnotationLayout):
-//   span.anno-issue       有位置的 issue,包住被指出的原文(交叠的已合并成一个 span)
-//   span.anno-issue-mark  没有位置的 issue,句尾的三角叹号图标
-// 两者都带 data-discussion-ids="id1,id2"。点击后逐条取 GET /api/v3/discussions/{id}
-// (issue 对匿名公开),桌面端用 Bootstrap Popover,手机端用 #annoIssueDrawer 抽屉。
+//   span.anno-issue            有位置的 issue,包住被指出的原文(交叠的已合并成一个 span)
+//   span.anno-issue-mark       没有位置的 issue,句尾的三角叹号图标
+//   span.anno-assessment-mark  译文评估,句尾的对勾图标,颜色按分数档位(high / medium / low)
+// 都带 data-discussion-ids="id1,id2"。点击后逐条取 GET /api/v3/discussions/{id}
+// (issue 与评估对匿名公开),桌面端用 Bootstrap Popover,手机端用 #annoIssueDrawer 抽屉。
 
 import * as bootstrap from 'bootstrap';
 
 const isMobile = () => window.innerWidth < 768;
+const MARKERS = '.anno-issue, .anno-issue-mark, .anno-assessment-mark';
 const cache = new Map();
 
 function escapeHtml(text) {
@@ -48,11 +50,48 @@ async function fetchIssue(id) {
     }
 }
 
-function renderIssue(issue, i18n) {
+// level 取自标注的 data-level(后端 AnnotationLayout::assessmentLevel 定档),前端不重复分档阈值
+function renderAssessment(assessment, i18n, level) {
+    const score = Number(assessment.score ?? 0).toString();
+    const editor = assessment.editor?.nickName ?? '';
+    const coEditor = assessment.co_editor?.nickName ?? '';
+    const date = assessment.updated_at
+        ? new Date(assessment.updated_at).toLocaleDateString()
+        : '';
+    const replies = assessment.reply_count
+        ? i18n.replies.replace(':count', assessment.reply_count)
+        : '';
+    const body = (assessment.summary ?? assessment.content ?? '').trim();
+    const meta = [
+        i18n.method.replace(':method', assessment.severity ?? ''),
+        coEditor ? `${editor} + ${coEditor}` : editor,
+        date,
+        replies,
+    ]
+        .filter(Boolean)
+        .join(' · ');
+
+    return `
+        <div class="anno-issue-item anno-assessment-item anno-assessment-${level}">
+            <div class="anno-issue-item-head">
+                <i class="ti ti-circle-check"></i>
+                <span class="anno-assessment-score">${escapeHtml(score)}</span>
+                <span>${escapeHtml(assessment.title ?? '')}</span>
+            </div>
+            ${body ? `<div class="anno-issue-item-body">${escapeHtml(body)}</div>` : ''}
+            <div class="anno-issue-item-meta">${escapeHtml(meta)}</div>
+        </div>`;
+}
+
+function renderIssue(issue, i18n, level) {
     if (!issue) {
         return `<div class="anno-issue-item text-muted">${escapeHtml(i18n.notFound)}</div>`;
     }
 
+    if (issue.type === 'assessment') {
+        return renderAssessment(issue, i18n, level ?? 'none');
+    }
+
     const severity = issue.severity ?? '';
     const severityLabel = i18n.severity?.[severity] ?? '';
     const editor = issue.editor?.nickName ?? '';
@@ -79,13 +118,13 @@ function renderIssue(issue, i18n) {
         </div>`;
 }
 
-async function renderList(ids, i18n) {
+async function renderList(ids, i18n, level) {
     const results = await Promise.allSettled(ids.map(fetchIssue));
 
     return results
         .map((r) =>
             r.status === 'fulfilled'
-                ? renderIssue(r.value, i18n)
+                ? renderIssue(r.value, i18n, level)
                 : `<div class="anno-issue-item text-danger">${escapeHtml(i18n.failed)}</div>`,
         )
         .join('');
@@ -95,7 +134,7 @@ export function initIssueMarkers() {
     const article = document.querySelector('article.reader-body');
     const drawerEl = document.getElementById('annoIssueDrawer');
 
-    if (!article || !article.querySelector('.anno-issue, .anno-issue-mark')) {
+    if (!article || !article.querySelector(MARKERS)) {
         return;
     }
 
@@ -105,6 +144,8 @@ export function initIssueMarkers() {
         notFound: '',
         replies: ':count',
         severity: {},
+        assessmentTitle: '',
+        method: ':method',
     };
 
     try {
@@ -140,8 +181,17 @@ export function initIssueMarkers() {
 
         activeEl = el;
         el.classList.add('is-active');
+        const title = el.classList.contains('anno-assessment-mark')
+            ? i18n.assessmentTitle
+            : i18n.title;
 
         if (isMobile() && drawerEl) {
+            const heading = drawerEl.querySelector('.offcanvas-title');
+
+            if (heading) {
+                heading.textContent = title;
+            }
+
             const body = drawerEl.querySelector('.offcanvas-body');
             body.innerHTML = loadingHtml;
             const drawer = bootstrap.Offcanvas.getOrCreateInstance(drawerEl);
@@ -149,7 +199,7 @@ export function initIssueMarkers() {
                 once: true,
             });
             drawer.show();
-            body.innerHTML = await renderList(ids, i18n);
+            body.innerHTML = await renderList(ids, i18n, el.dataset.level);
 
             return;
         }
@@ -161,12 +211,12 @@ export function initIssueMarkers() {
             placement: 'bottom',
             fallbackPlacements: ['top', 'bottom'],
             customClass: 'anno-issue-popover',
-            title: escapeHtml(i18n.title),
+            title: escapeHtml(title),
             content: `<div class="anno-issue-list">${loadingHtml}</div>`,
         });
         activePopover = popover;
         popover.show();
-        const html = await renderList(ids, i18n);
+        const html = await renderList(ids, i18n, el.dataset.level);
 
         if (activePopover !== popover) {
             return; // 等待期间已经切到别的标注
@@ -195,7 +245,7 @@ export function initIssueMarkers() {
             return;
         }
 
-        const el = e.target.closest('.anno-issue, .anno-issue-mark');
+        const el = e.target.closest(MARKERS);
 
         if (!el) {
             return;
@@ -206,7 +256,9 @@ export function initIssueMarkers() {
     });
 
     article.addEventListener('keydown', (e) => {
-        const el = e.target.closest?.('.anno-issue-mark');
+        const el = e.target.closest?.(
+            '.anno-issue-mark, .anno-assessment-mark',
+        );
 
         if (el && (e.key === 'Enter' || e.key === ' ')) {
             e.preventDefault();
@@ -218,9 +270,7 @@ export function initIssueMarkers() {
     document.addEventListener('click', (e) => {
         if (
             activePopover &&
-            !e.target.closest(
-                '.anno-issue, .anno-issue-mark, .anno-issue-popover',
-            )
+            !e.target.closest(`${MARKERS}, .anno-issue-popover`)
         ) {
             deactivate();
         }

Некоторые файлы не были показаны из-за большого количества измененных файлов