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

feat(openapi): 规格加入 /v3/discussions;v3 按路由中间件写 security

- 重新生成规格:新增 /v3/discussions(列表、summary、单条、replies);
  v2 discussion 两个端点经 overrides 标为 deprecated
- tagOf() 剥掉 me/ 前缀,/v3/me/* 不再全挤进生成客户端的 MeApi
- v3 每个 operation 按中间件写 security:挂 auth.v3 → 必须 bearer,
  否则可选([{}, bearerAuth]);v2 仍用全局 security,不变
- 分路径文件同样把 bearerAuth: {  } 修成 []

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
visuddhinanda 5 дней назад
Родитель
Сommit
af76d9bd5f
70 измененных файлов с 1268 добавлено и 362 удалено
  1. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/01-list.png
  2. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/02-tab-0.png
  3. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/02-tab-1.png
  4. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/02-tab-2.png
  5. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/03-search.png
  6. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/06-create.png
  7. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/07-show.png
  8. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/08-chapter.png
  9. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/08-term.png
  10. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/09-share.png
  11. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/10-setting.png
  12. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/11-webhooks.png
  13. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/12-bad-id.png
  14. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/13-create-done.png
  15. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/13-create-filled.png
  16. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/14-edit-filled.png
  17. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/14-edit-saved.png
  18. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/15-menu.png
  19. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/15-transfer.png
  20. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/16-delete-confirm.png
  21. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/16-delete-done.png
  22. BIN
      dashboard-test/documents/testlog/assets/channel_20260907_034904/17-deleted-detail.png
  23. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/01-signup-welcome.png
  24. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/02-signup-code-sent.png
  25. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/03-signup-wrong-code.png
  26. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/04-signup-account.png
  27. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/05-signup-client-validation.png
  28. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/06-signup-server-422.png
  29. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/07-signup-done.png
  30. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/08-invite-invalid.png
  31. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/10-invite-done.png
  32. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/12-forgot-sent.png
  33. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/13-reset-invalid.png
  34. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/15-reset-done.png
  35. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/16-signin-wrong.png
  36. BIN
      dashboard-test/documents/testlog/assets/users_20261008_072502/18-signin-restored.png
  37. 0 209
      dashboard-test/documents/testlog/channel_20260907_034904.md
  38. 0 107
      dashboard-test/documents/testlog/users_20261008_072502.md
  39. 9 1
      openapi/public/assets/protocol/main.yaml
  40. 18 0
      openapi/public/assets/protocol/overrides/v2-discussion-discussion.yaml
  41. 14 0
      openapi/public/assets/protocol/overrides/v2-discussion.yaml
  42. 13 25
      openapi/public/assets/protocol/resources/auto/v2-discussion-discussion.yaml
  43. 11 16
      openapi/public/assets/protocol/resources/auto/v2-discussion.yaml
  44. 299 0
      openapi/public/assets/protocol/resources/auto/v3-discussions-discussion-replies.yaml
  45. 340 0
      openapi/public/assets/protocol/resources/auto/v3-discussions-discussion.yaml
  46. 69 0
      openapi/public/assets/protocol/resources/auto/v3-discussions-summary.yaml
  47. 378 0
      openapi/public/assets/protocol/resources/auto/v3-discussions.yaml
  48. 4 0
      openapi/public/assets/protocol/resources/auto/v3-email-certifications.yaml
  49. 4 0
      openapi/public/assets/protocol/resources/auto/v3-heartbeat.yaml
  50. 4 0
      openapi/public/assets/protocol/resources/auto/v3-invites-invite.yaml
  51. 4 0
      openapi/public/assets/protocol/resources/auto/v3-invites.yaml
  52. 4 1
      openapi/public/assets/protocol/resources/auto/v3-me-reactions-reaction.yaml
  53. 8 2
      openapi/public/assets/protocol/resources/auto/v3-me-reactions.yaml
  54. 3 0
      openapi/public/assets/protocol/resources/auto/v3-me.yaml
  55. 8 0
      openapi/public/assets/protocol/resources/auto/v3-password-resets-token.yaml
  56. 4 0
      openapi/public/assets/protocol/resources/auto/v3-password-resets.yaml
  57. 4 0
      openapi/public/assets/protocol/resources/auto/v3-progress.yaml
  58. 4 0
      openapi/public/assets/protocol/resources/auto/v3-reactions-tally.yaml
  59. 4 0
      openapi/public/assets/protocol/resources/auto/v3-reactions.yaml
  60. 4 0
      openapi/public/assets/protocol/resources/auto/v3-search-search.yaml
  61. 4 0
      openapi/public/assets/protocol/resources/auto/v3-search-suggest.yaml
  62. 8 0
      openapi/public/assets/protocol/resources/auto/v3-search.yaml
  63. 4 0
      openapi/public/assets/protocol/resources/auto/v3-sessions.yaml
  64. 4 0
      openapi/public/assets/protocol/resources/auto/v3-tipitaka-reading-channel.yaml
  65. 4 0
      openapi/public/assets/protocol/resources/auto/v3-tipitaka-related-paragraphs-aggregate-book-name.yaml
  66. 4 0
      openapi/public/assets/protocol/resources/auto/v3-tipitaka-related-paragraphs-aggregate.yaml
  67. 4 0
      openapi/public/assets/protocol/resources/auto/v3-tipitaka-related-paragraphs.yaml
  68. 4 0
      openapi/public/assets/protocol/resources/auto/v3-upgrade.yaml
  69. 4 0
      openapi/public/assets/protocol/resources/auto/v3-users.yaml
  70. 18 1
      openapi/scripts/generate.php

BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/01-list.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/02-tab-0.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/02-tab-1.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/02-tab-2.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/03-search.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/06-create.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/07-show.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/08-chapter.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/08-term.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/09-share.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/10-setting.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/11-webhooks.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/12-bad-id.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/13-create-done.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/13-create-filled.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/14-edit-filled.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/14-edit-saved.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/15-menu.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/15-transfer.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/16-delete-confirm.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/16-delete-done.png


BIN
dashboard-test/documents/testlog/assets/channel_20260907_034904/17-deleted-detail.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/01-signup-welcome.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/02-signup-code-sent.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/03-signup-wrong-code.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/04-signup-account.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/05-signup-client-validation.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/06-signup-server-422.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/07-signup-done.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/08-invite-invalid.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/10-invite-done.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/12-forgot-sent.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/13-reset-invalid.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/15-reset-done.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/16-signin-wrong.png


BIN
dashboard-test/documents/testlog/assets/users_20261008_072502/18-signin-restored.png


+ 0 - 209
dashboard-test/documents/testlog/channel_20260907_034904.md

@@ -1,209 +0,0 @@
-# 测试日志 · /workspace/channel
-
-| 项目     | 内容                                                        |
-| -------- | ----------------------------------------------------------- |
-| 路由     | `/workspace/channel`(含 `:channelId`、`:channelId/setting`)|
-| 时间     | 2026-09-07 03:49                                            |
-| 账号     | `test161`                                                   |
-| 环境     | dashboard-v6 `:4000` · api-v13 `:8000` · 本地容器            |
-| 脚本     | [`src/tests/channel.spec.ts`](../../src/tests/channel.spec.ts)         |
-| 截图     | `assets/channel_20260907_034904/`                           |
-| 复跑     | `E2E_SHOT=documents/testlog/assets/channel_20260907_034904 npm run test:channel` |
-
----
-
-## 结论
-
-17 个测试步骤(含破坏性操作),**通过 13 项,发现 2 个功能性缺陷 + 5 个体验/告警问题**。
-其中 Webhooks 页签完全不可达(白屏),chapter 列表永远为空。
-
-频道的增删改(CRUD)全链路正常,数据可持久化,删除后列表与详情页均正确失效。
-
----
-
-## 一、通过的功能
-
-| # | 功能                        | 结果                          |
-| - | --------------------------- | ----------------------------- |
-| 1 | 列表页加载                  | 3 条,无报错                  |
-| 2 | 页签 此工作室 / 协作 / 社区 | 3 / 1 / 20 条,切换正常       |
-| 3 | 关键词搜索 + 清空           | 搜 `test` → 1 条;清空 → 3 条 |
-| 4 | 类型列筛选                  | 译文 / Nissaya / 注疏 / 原文  |
-| 5 | 创建时间排序                | 正常                          |
-| 6 | 新建频道表单                | 名称 / 类型 / 语言 正常弹出   |
-| 7 | 频道详情页                  | 标题、面包屑正常              |
-| 8 | 详情页 term 页签            | 正常                          |
-| 9 | 分享弹窗                    | 正常打开                      |
-| 10| 设置页 - 基本信息           | 表单正常渲染                  |
-| 11| **新建频道(真实提交)**    | rows 3 → 4,提示「操作成功」  |
-| 12| **编辑基本信息(真实保存)**| 提示「操作成功」,重载后 summary 已持久化 |
-| 13| **删除频道(真实删除)**    | 确认弹窗文案正确,rows 4 → 3  |
-| 14| **删除后访问原详情页**      | 正确失效(接口 400)          |
-
-### 列表页
-
-![列表页](assets/channel_20260907_034904/01-list.png)
-
-### 新建频道
-
-![新建](assets/channel_20260907_034904/06-create.png)
-
-### 频道详情
-
-![详情](assets/channel_20260907_034904/07-show.png)
-
-### 设置页
-
-![设置](assets/channel_20260907_034904/10-setting.png)
-
-### 破坏性操作
-
-新建频道(填入临时名称 `e2e-tmp-<timestamp>`):
-
-![新建填写](assets/channel_20260907_034904/13-create-filled.png)
-
-编辑基本信息并保存,重载后确认 summary 已写库:
-
-![编辑保存](assets/channel_20260907_034904/14-edit-saved.png)
-
-行操作菜单(分享 / 转让 / 删除)与删除确认弹窗:
-
-![删除确认](assets/channel_20260907_034904/16-delete-confirm.png)
-
-> 转让弹窗文案:「将 e2e-tmp-… 转让下面的用户。操作后需要等待对方确认后,资源才会被转移。」
-> 因需要接收方账号,**未实际提交**,仅确认弹窗与用户选择器正常渲染。
->
-> ![转让弹窗](assets/channel_20260907_034904/15-transfer.png)
-
----
-
-## 二、待修复
-
-### - [ ] BUG-1 🔴 Webhooks 页签跳转到不存在的路由,整页白屏
-
-点设置页 **Webhooks** 标签后跳到
-`/studio/test161/channel/{id}/setting/webhooks` → **404 Unexpected Application Error**。
-
-```
-[warning] No routes matched location "/studio/test161/channel/.../setting/webhooks"
-[error]   Error handled by React Router default ErrorBoundary: ErrorResponseImpl
-```
-
-![webhooks 白屏](assets/channel_20260907_034904/11-webhooks.png)
-
-**原因**:`src/pages/workspace/channel/setting.tsx:55` 仍使用 v4 旧路径:
-
-```js
-navigate(`/studio/${studioName}/channel/${channelId}/setting/${activeKey}`);
-```
-
-而 `src/routes/channelRoutes.ts` 中 setting 的实际路径是
-`/workspace/channel/:channelId/setting`。
-
-**附带问题**:`setting.tsx:20-22` 读取 `useParams()` 的 `type` 和 `id`,
-但路由未定义这两个参数,恒为 `undefined`,导致页签状态无法从 URL 恢复、
-WebhookEdit 分支(`id === "new"` / 编辑既有 webhook)永远进不去。
-
-**修复方向**:路由补 `setting/:type?/:id?` 子路由,`navigate` 改为 `/workspace/channel/...`。
-
----
-
-### - [ ] BUG-2 🔴 chapter 列表接口 400,且异常未捕获
-
-```
-[HTTP 400]   /api/v2/progress?view=chapter&channel=...&progress=0.01&offset=0
-[PAGEERROR]  HttpError: no data
-```
-
-详情页 chapter 页签恒为「暂无数据」。
-
-![chapter 空](assets/channel_20260907_034904/08-chapter.png)
-
-**原因**:后端 `api-v13/app/Http/Controllers/ProgressController.php@index`
-的 switch 只实现了 `view=channel`,其余分支一律返回 `error('invalid view', 400)`。
-前端 `src/components/channel/ChapterInChannelList.tsx:177` 传的是 `view=chapter`。
-
-**同类问题**:`src/components/tipitaka/PaliChapterChannelList.tsx:23`
-传的 `view=chapter_channels` 后端同样未实现(本次未测,待验证)。
-
-**附带问题**:前端未 catch,直接冒泡成 pageerror。
-
-> ⚠️ `ProgressController.php` 当前在工作区有未提交改动,可能正在重构中。
-
----
-
-### - [ ] BUG-3 🟡 详情页 chapter 表出现两个「公开性」列
-
-`ChapterInChannelList.tsx` 的 `progress` 列(:96)和 `view` 列(:110)
-都使用了 `forms.fields.publicity.label`,表头连续出现两个「公开性」。
-应分别改为「进度」和「视图」对应的 i18n key。
-
----
-
-### - [ ] BUG-4 🟡 不存在的 channelId 导致白屏
-
-`channelLoader` 抛 `HttpError: no res` 后落到 React Router 默认 ErrorBoundary。
-
-![非法 id](assets/channel_20260907_034904/12-bad-id.png)
-
-**修复方向**:给 channel 路由加 `errorElement`,展示「频道不存在」而非堆栈。
-
----
-
-### - [ ] BUG-5 🟡 删除后工具栏徽标计数不刷新
-
-删除频道后列表已刷新为空,但左上角徽标仍显示「此工作室 **4**」,实际只剩 3 条;
-需整页刷新才恢复正确。
-
-![删除后徽标未更新](assets/channel_20260907_034904/16-delete-done.png)
-
-**原因**:`src/components/channel/ChannelTable.tsx` 中拉取
-`/api/v2/channel-my-number` 的 `useEffect` 依赖数组只有 `[studioName]`,
-新建 / 删除后不会重新请求。
-
-**修复方向**:把计数请求抽成函数,在 `showDeleteConfirm` 成功回调和
-`ChannelCreate` 的 `onSuccess` 里与 `ref.current?.reload()` 一起调用。
-
-同样问题在**新建**后也存在(rows 变 4 但徽标仍为 3)。
-
----
-
-### - [ ] BUG-6 🟢 社区页签头像 404
-
-```
-[HTTP 404] /storage/attachments-staging/2062811b-....jpg
-[HTTP 404] /storage/attachments-staging/44f57bab-....jpg
-```
-
-可能仅为本地环境缺少附件文件,需在其他环境复核。
-
----
-
-### - [ ] BUG-7 🟢 React 开发态告警
-
-| 告警                                                                      | 位置                     |
-| ------------------------------------------------------------------------- | ------------------------ |
-| `Cannot update a component (ProTable) while rendering a different component (HeaderMenu)` | 切换工具栏页签时 setState in render |
-| `Form already set 'initialValues' with path 'type' / 'source_type'`        | 设置页 `channel/Edit`,initialValues 与字段 initialValue 冲突 |
-| `No HydrateFallback element provided to render during initial hydration`   | 路由配置                 |
-
----
-
-## 三、未覆盖 / 待确认
-
-| 项 | 说明 |
-| --- | --- |
-| 转让频道提交 | 需要接收方账号,仅验证到弹窗渲染。补测需准备第二个测试账号 |
-| 分享弹窗授权提交 | 同上,需要目标用户 |
-| Webhooks 增删改 | 被 BUG-1 阻塞,路由修复后再测 |
-| 删除成功提示 | 删除后 1.2s 未捕获到 `message.success("删除成功")`,创建/保存的提示均能捕获。存疑,待人工确认是否真的没弹 |
-
-### 测试数据
-
-破坏性测试使用临时频道 `e2e-tmp-<timestamp>`,**测试结束已删除,未留残留数据**。
-脚本可用 `E2E_DESTRUCTIVE=0` 跳过这部分。
-
-### 回归断言
-
-BUG-1、BUG-4 已写成 `src/tests/channel.spec.ts` 里的断言,修复前 `npm test`
-会稳定报这两个失败(4 通过 / 2 失败),修复后自动转绿。

+ 0 - 107
dashboard-test/documents/testlog/users_20261008_072502.md

@@ -1,107 +0,0 @@
-# 测试日志 · /anonymous 账号页面
-
-| 项目     | 内容                                                                                  |
-| -------- | ------------------------------------------------------------------------------------- |
-| 路由     | `/anonymous/sign-in`、`/anonymous/sign-up`、`/anonymous/sign-up/:token`、`/anonymous/forgot-password`、`/anonymous/reset-password/:token` |
-| 时间     | 2026-10-08 07:25                                                                      |
-| 账号     | 匿名(不复用登录态)                                                                  |
-| 环境     | dashboard-v6 `:4000` · api-v13 `:8000` · 本地容器                                     |
-| 脚本     | [`src/tests/users.spec.ts`](../../src/tests/users.spec.ts)                            |
-| 截图     | `assets/users_20261008_072502/`                                                       |
-| 复跑     | `E2E_SHOT=documents/testlog/assets/users_20261008_072502 npm run test:users`          |
-
----
-
-## 结论
-
-登录、注册、找回密码迁到 v3(`sessions` / `me` / `email-certifications` / `invites` / `users` /
-`password-resets`)后的首次 e2e。21 个步骤 + 1 条已知缺陷断言:**6 个用例通过,1 个按设计保持红灯(BUG-5)**。
-
-会发邮件或建账号的请求(发验证码、换 invite、建号、设新密码)用 `page.route` 拦截,
-验证的是前端流程、错误展示与**前端发出的请求体**;没有副作用的请求走真实后端。
-本次无数据残留:真实后端只收到了查询、校验失败与测试账号 `test161` 的登录请求,
-未建任何账号、未发任何邮件。
-
----
-
-## 一、通过的功能
-
-| #  | 功能                                        | 后端   | 结果 |
-| -- | ------------------------------------------- | ------ | ---- |
-| 1  | 注册第一步:未勾选「已了解」时下一步禁用    | —      | 正常;功能清单图标不依赖 emoji 字体 |
-| 2  | 获取验证码                                  | 拦截   | 请求体 `{email, lang:"zh-Hans"}`;提示「验证码已发送」,按钮进入倒计时 |
-| 3  | 错误验证码                                  | 真实 422 | 错误挂在验证码输入框下(文案为英文,见 BUG-5) |
-| 4  | 正确验证码 → 账号信息,邮箱只读展示         | 拦截   | 请求体 `{email, code}` |
-| 5  | 前端校验:用户名非法字符、两次密码不一致    | —      | 两条错误各自挂在对应字段下 |
-| 6  | 后端 422 字段错误                           | 拦截   | `errors.username` 挂到用户名输入框下 |
-| 7  | 建号成功 → 完成页 → 去登录                  | 拦截   | 请求体含 `invite`、**不含 `email`**;跳到 `/anonymous/sign-in` |
-| 8  | 无效邀请                                    | 真实 404 | 「邀请无效或已被使用」 |
-| 9  | v2 邀请邮件旧路径 `/anonymous/users/sign-up/:token` | 真实 404 | 重定向到 `/anonymous/sign-up/:token` |
-| 10 | 有效邀请 → 显示邮箱 → 建号成功              | 拦截   | 请求体含 `invite`、`nickname` |
-| 11 | 找回密码:邮箱格式错                        | —      | 前端拦下,未发请求 |
-| 12 | 找回密码:未注册邮箱                        | 真实 204 | 「如果该邮箱已注册……60 分钟内」——不泄露是否注册 |
-| 13 | 无效重置链接 → 「忘记密码」入口             | 真实 404 | 跳回 `/anonymous/forgot-password` |
-| 14 | 重置页:两次密码不一致                      | 拦截 GET | 显示账号名;错误挂在确认密码下 |
-| 15 | 设新密码 → 成功页 → 去登录                  | 拦截 PATCH 204 | 请求体 `{password, password_confirmation}` |
-| 16 | 登录:密码错误                              | 真实 422 | 表单上方「用户名或密码错误」 |
-| 17 | 登录:只发一次请求                          | 真实 201 | 只有 `POST /api/v3/sessions`,不再调 `/v2/sign-in`、`/v2/auth/current` |
-| 18 | 刷新页面恢复登录态                          | 真实 200 | `GET /api/v3/me`,停留在 `/workspace` |
-| 19 | `?url=` 同源地址                            | 回放真实登录结果 | 登录后跳回该地址 |
-| 20 | `?url=` 外站地址                            | 回放真实登录结果 | 不跳,进 `/workspace`(原先是开放重定向) |
-| 21 | `?url=` `javascript:`                       | 回放真实登录结果 | 不执行,进 `/workspace`(原先是 XSS) |
-
-![注册第一步](assets/users_20261008_072502/01-signup-welcome.png)
-![验证码已发送](assets/users_20261008_072502/02-signup-code-sent.png)
-![后端 422 挂到字段](assets/users_20261008_072502/06-signup-server-422.png)
-![注册完成](assets/users_20261008_072502/07-signup-done.png)
-![无效邀请](assets/users_20261008_072502/08-invite-invalid.png)
-![找回密码已提交](assets/users_20261008_072502/12-forgot-sent.png)
-![无效重置链接](assets/users_20261008_072502/13-reset-invalid.png)
-![重置成功](assets/users_20261008_072502/15-reset-done.png)
-![登录失败](assets/users_20261008_072502/16-signin-wrong.png)
-![刷新后仍登录](assets/users_20261008_072502/18-signin-restored.png)
-
----
-
-## 二、待修复
-
-- [x] **BUG-5 v3 接口的错误文案没有本地化,中文界面里显示英文**(同日修复:后端 v3 路由组挂
-  `V3\NegotiateLocale` 按 Accept-Language 选语言,`client.ts` 按界面语言带上这个头;
-  复跑 `npm run test:users` 7/7 通过,断言移到「回归」组保留)
-
-  ```text
-  Expected: "验证码不正确或已过期。"
-  Received: "The verification code is incorrect or has expired."
-  ```
-
-  ![英文错误文案](assets/users_20261008_072502/06-signup-server-422.png)
-
-  原因:`api-v13/bootstrap/app.php` 只把 `SetLocale` 挂在 web 组,api 组没有语言协商,
-  `__()` 永远取 `en`(v3-resource skill 第 11 条已记录);`dashboard-v6/src/api/client.ts`
-  也没有把界面语言告诉后端。翻译文件本身是齐的(`resources/lang/zh-Hans/messages.php`)。
-
-  修复方向:v3 路由组挂一个只认 `Accept-Language` 的语言协商中间件(不要动 v2 的 api 组);
-  `client.ts` 的中间件按 `locales.get()` 设置 `Accept-Language`。两边同一个 commit。
-
----
-
-## 三、未覆盖 / 待确认
-
-- **真实收信的端到端流程未覆盖**:发验证码、设新密码的成功路径会真的发邮件(开发机是真实 SMTP),
-  建号没有删除接口。这部分由 api-v13 的 Pest 测试覆盖(`SignUpV3Test` 22 条、
-  `PasswordResetV3Test` 14 条,Mail 是 fake 的)。要做真实端到端,需要一个能收信的测试邮箱
-  或把开发机的 `MAIL_MAILER` 换成 `log`。
-- **同一条错误显示两次**:字段级 422 时,`unwrap()` 先弹一条全局提示,表单再把同一条错误挂到字段下
-  (见截图 06 顶部)。是 `src/api/client.ts` 的既定行为;表单场景要不要只挂字段、不弹提示,待定。
-- **204 响应被记成 `[REQFAIL] net::ERR_ABORTED`**:`password-resets`、`email-certifications`
-  返回 204 时 Chromium 报请求中止,但页面行为正确(测试通过)。疑为 openapi-fetch 对空响应体的处理,
-  不影响功能;暂不加进 `collect.ts` 的噪音过滤,以免掩盖真正的中止。
-- **开发态重复请求**:邀请页、重置页的 GET 各发了两次,是 React StrictMode 在开发态双调 effect,生产构建不会。
-- **限流会让测试互相干扰**:登录按「账号 + IP」每分钟 5 次,`password-resets` /
-  `email-certifications` 按 IP 每分钟 10 次。本 spec 一次用 test161 登录 4 次
-  (setup 1 + 16、17 + 用例二开头 1,19–21 回放这份结果、不再真登),api-test 一次 2 次。
-  两套测试连着跑要隔一分钟,否则表现为「Too Many Attempts.」。
-- **mobile 仍走 v2 登录**:wikipali-mobile 的 `src/api/auth.ts` 还在调 `/v2/sign-in`、
-  `/v2/auth/current`,不在本测试范围。
-- **`AnonymousLayout` 仍是占位**:页面顶部是 `anonymous header`、底部是 `anonymous layout footer`,
-  没有 v4 的语言切换和样式。

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

@@ -7,7 +7,7 @@ info:
 
     由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。
     响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
-  version: 2026.10.8
+  version: 2026.10.15
 servers:
   -
     url: /api
@@ -681,6 +681,14 @@ paths:
     $ref: ./resources/auto/v2-webhook.yaml
   '/v2/webhook/{webhook}':
     $ref: ./resources/auto/v2-webhook-webhook.yaml
+  /v3/discussions:
+    $ref: ./resources/auto/v3-discussions.yaml
+  /v3/discussions/summary:
+    $ref: ./resources/auto/v3-discussions-summary.yaml
+  '/v3/discussions/{discussion}':
+    $ref: ./resources/auto/v3-discussions-discussion.yaml
+  '/v3/discussions/{discussion}/replies':
+    $ref: ./resources/auto/v3-discussions-discussion-replies.yaml
   /v3/email-certifications:
     $ref: ./resources/auto/v3-email-certifications.yaml
   /v3/heartbeat:

+ 18 - 0
openapi/public/assets/protocol/overrides/v2-discussion-discussion.yaml

@@ -0,0 +1,18 @@
+# 手写补充:v2 discussion 单条读写已被 v3 取代(2026-10-15)。
+get:
+  deprecated: true
+  description: |
+    已被 `GET /v3/discussions/{discussion}` 取代。保留仅为 dashboard-v4 兼容,待 v4 下线后删除。
+put:
+  deprecated: true
+  description: |
+    已被 `PATCH /v3/discussions/{discussion}`(局部更新)取代。保留仅为 dashboard-v4 兼容,待 v4 下线后删除。
+patch:
+  deprecated: true
+  description: |
+    已被 `PATCH /v3/discussions/{discussion}`(局部更新)取代。保留仅为 dashboard-v4 兼容,待 v4 下线后删除。
+delete:
+  deprecated: true
+  description: |
+    已被 `DELETE /v3/discussions/{discussion}` 取代。2026-10-15 起两边都是软删除。
+    保留仅为 dashboard-v4 兼容,待 v4 下线后删除。

+ 14 - 0
openapi/public/assets/protocol/overrides/v2-discussion.yaml

@@ -0,0 +1,14 @@
+# 手写补充:v2 discussion 列表 / 新建已被 v3 取代(2026-10-15)。
+get:
+  deprecated: true
+  description: |
+    已被 v3 取代,保留仅为 dashboard-v4 兼容,待 v4 下线后删除:
+    - `view=question` / `view=res_id` / `view=question-by-topic` → `GET /v3/discussions?target_type=&target_id=`
+    - `view=answer` → `GET /v3/discussions/{discussion}/replies`
+    - 响应里的 `active` / `close` / `can_create` → `GET /v3/discussions/summary`
+    - `view=topic-by-user`、`view=all` 与课程参数(`course` / `show_student`)在 v3 暂无对应端点
+post:
+  deprecated: true
+  description: |
+    已被 v3 取代,保留仅为 dashboard-v4 兼容,待 v4 下线后删除:
+    不带 `parent` → `POST /v3/discussions`;带 `parent` → `POST /v3/discussions/{discussion}/replies`。

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

@@ -4,10 +4,8 @@ get:
   tags:
     - discussion
   operationId: get_api_v2_discussion_discussion_
-  description: |-
-    按路径参数做路由模型绑定并直接返回,不做登录与权限校验。
-
-    实现:`DiscussionController@show`
+  description: |
+    已被 `GET /v3/discussions/{discussion}` 取代。保留仅为 dashboard-v4 兼容,待 v4 下线后删除。
   parameters:
     -
       name: discussion
@@ -77,19 +75,14 @@ get:
       $ref: '../../main.yaml#/components/responses/Unauthorized'
     422:
       $ref: '../../main.yaml#/components/responses/ValidationError'
+  deprecated: true
 put:
   summary: '修改讨论 / 批注'
   tags:
     - discussion
   operationId: put_api_v2_discussion_discussion_
-  description: |-
-    必须登录,未登录返回 403。作者本人可改;非作者则要求是资源所属 channel 的可编辑者
-    (res_type=sentence 取 sentence.channel_uid,res_type=wbw 经 wbw block 取 channel_uid),
-    两者都不满足返回 403。
-    title、content、status 按请求覆盖,未提交时分别回落为 null、null、active;
-    type 与五个锚点字段为增量更新,只改请求里出现的字段,未提交的原样保留。
-
-    实现:`DiscussionController@update`
+  description: |
+    已被 `PATCH /v3/discussions/{discussion}`(局部更新)取代。保留仅为 dashboard-v4 兼容,待 v4 下线后删除。
   parameters:
     -
       name: discussion
@@ -203,19 +196,14 @@ put:
       $ref: '../../main.yaml#/components/responses/Unauthorized'
     422:
       $ref: '../../main.yaml#/components/responses/ValidationError'
+  deprecated: true
 patch:
   summary: '修改讨论 / 批注'
   tags:
     - discussion
   operationId: patch_api_v2_discussion_discussion_
-  description: |-
-    必须登录,未登录返回 403。作者本人可改;非作者则要求是资源所属 channel 的可编辑者
-    (res_type=sentence 取 sentence.channel_uid,res_type=wbw 经 wbw block 取 channel_uid),
-    两者都不满足返回 403。
-    title、content、status 按请求覆盖,未提交时分别回落为 null、null、active;
-    type 与五个锚点字段为增量更新,只改请求里出现的字段,未提交的原样保留。
-
-    实现:`DiscussionController@update`
+  description: |
+    已被 `PATCH /v3/discussions/{discussion}`(局部更新)取代。保留仅为 dashboard-v4 兼容,待 v4 下线后删除。
   parameters:
     -
       name: discussion
@@ -329,16 +317,15 @@ patch:
       $ref: '../../main.yaml#/components/responses/Unauthorized'
     422:
       $ref: '../../main.yaml#/components/responses/ValidationError'
+  deprecated: true
 delete:
   summary: '删除讨论 / 批注'
   tags:
     - discussion
   operationId: delete_api_v2_discussion_discussion_
-  description: |-
-    必须登录,未登录返回 401。目前只允许作者本人删除,其他人返回 403。
-    不会级联删除子回复,也不会回退父节点的 children_count。
-
-    实现:`DiscussionController@destroy`
+  description: |
+    已被 `DELETE /v3/discussions/{discussion}` 取代。2026-10-15 起两边都是软删除。
+    保留仅为 dashboard-v4 兼容,待 v4 下线后删除。
   parameters:
     -
       name: discussion
@@ -365,3 +352,4 @@ delete:
       $ref: '../../main.yaml#/components/responses/Unauthorized'
     422:
       $ref: '../../main.yaml#/components/responses/ValidationError'
+  deprecated: true

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

@@ -4,14 +4,12 @@ get:
   tags:
     - discussion
   operationId: get_api_v2_discussion
-  description: |-
-    按 view 指定的口径返回 discussion 列表,支持状态过滤、分页与排序。
-    权限:未登录用户请求 type=discussion 时直接返回空结果;basic 角色用户在自己无编辑权限的
-    channel 上只能看到自己发表的 discussion;view=topic-by-user 必须登录,否则返回 403。
-    返回 rows、count(当前口径总数)、active/close(顶级节点的活跃与关闭数)、
-    can_create/can_reply(当前用户能否发起与回复,按 type 与 res_type 计算)。
-
-    实现:`DiscussionController@index`
+  description: |
+    已被 v3 取代,保留仅为 dashboard-v4 兼容,待 v4 下线后删除:
+    - `view=question` / `view=res_id` / `view=question-by-topic` → `GET /v3/discussions?target_type=&target_id=`
+    - `view=answer` → `GET /v3/discussions/{discussion}/replies`
+    - 响应里的 `active` / `close` / `can_create` → `GET /v3/discussions/summary`
+    - `view=topic-by-user`、`view=all` 与课程参数(`course` / `show_student`)在 v3 暂无对应端点
   parameters:
     -
       name: view
@@ -145,19 +143,15 @@ get:
       $ref: '../../main.yaml#/components/responses/Unauthorized'
     422:
       $ref: '../../main.yaml#/components/responses/ValidationError'
+  deprecated: true
 post:
   summary: '发表讨论 / 批注或回复'
   tags:
     - discussion
   operationId: post_api_v2_discussion
-  description: |-
-    必须登录,未登录返回 401。传了 parent 即为回复:res_id 与 res_type 继承自父节点,
-    无需再传,父节点不存在返回 no record,保存后父节点 children_count 自增;
-    不传 parent 为顶级节点,此时 res_id、res_type、title 均为必填。
-    锚点字段(pos_start/pos_end/quote_exact/quote_prefix/quote_suffix)两种情况都可选。
-    默认会向消息队列 discussion 推送通知,可用 notification=false 关闭。
-
-    实现:`DiscussionController@store`
+  description: |
+    已被 v3 取代,保留仅为 dashboard-v4 兼容,待 v4 下线后删除:
+    不带 `parent` → `POST /v3/discussions`;带 `parent` → `POST /v3/discussions/{discussion}/replies`。
   requestBody:
     required: true
     content:
@@ -284,3 +278,4 @@ post:
       $ref: '../../main.yaml#/components/responses/Unauthorized'
     422:
       $ref: '../../main.yaml#/components/responses/ValidationError'
+  deprecated: true

+ 299 - 0
openapi/public/assets/protocol/resources/auto/v3-discussions-discussion-replies.yaml

@@ -0,0 +1,299 @@
+# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-discussions-discussion-replies.yaml
+get:
+  summary: 列出话题的回复
+  tags:
+    - discussions
+  operationId: get_api_v3_discussions_discussion_replies
+  description: |-
+    按时间正序。删除了的话题(墓碑)下的回复照样可读。
+
+    实现:`V3\DiscussionReplyController@index`
+  security:
+    - {  }
+    -
+      bearerAuth: []
+  parameters:
+    -
+      name: discussion
+      in: path
+      required: true
+      schema:
+        type: string
+      description: '话题的 uuid'
+    -
+      name: per_page
+      in: query
+      schema:
+        type: integer
+        default: 50
+      description: '每页数量,最大 200'
+    -
+      name: page
+      in: query
+      schema:
+        type: integer
+        default: 1
+      description: 页码
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    id:
+                      type: string
+                    target_type:
+                      type: string
+                    target_id:
+                      type: string
+                    parent_id:
+                      type: string
+                      nullable: true
+                    type:
+                      type: string
+                    severity:
+                      type: string
+                      nullable: true
+                    status:
+                      type: string
+                    title:
+                      type: string
+                      nullable: true
+                    content:
+                      type: string
+                      nullable: true
+                    content_type:
+                      type: string
+                    html:
+                      type: string
+                      description: 仅在部分查询口径下返回。
+                    summary:
+                      type: string
+                      description: 仅在部分查询口径下返回。
+                    tpl_id:
+                      type: string
+                      nullable: true
+                    reply_count:
+                      type: integer
+                      description: 仅在部分查询口径下返回。
+                    editor:
+                      type: object
+                      properties: { id: { type: string }, nickName: { type: string }, userName: { type: string }, realName: { type: string }, sn: { type: integer }, avatar: { type: string }, roles: { type: array, items: { type: string } } }
+                      nullable: true
+                    abilities:
+                      type: object
+                      properties: { update: { type: boolean }, delete: { type: boolean }, close: { type: boolean }, reply: { type: boolean } }
+                    created_at:
+                      type: string
+                    updated_at:
+                      type: string
+                    deleted_at:
+                      type: string
+                      nullable: true
+              meta:
+                $ref: '../../main.yaml#/components/schemas/PaginationMeta'
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'
+post:
+  summary: 回复话题
+  tags:
+    - discussions
+  operationId: post_api_v3_discussions_discussion_replies
+  description: |-
+    类型与挂靠对象继承话题。已关闭的话题不能回复(409),对一条回复再回复 422。
+    文章上的 qa 只有文章的编辑者能回答。默认发通知,`notify=false` 关闭。
+
+    实现:`V3\DiscussionReplyController@store`
+  security:
+    -
+      bearerAuth: []
+  parameters:
+    -
+      name: discussion
+      in: path
+      required: true
+      schema:
+        type: string
+      description: '话题的 uuid'
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            content:
+              type: string
+              description: 正文(markdown)
+            notify:
+              type: boolean
+              description: 是否发通知
+              default: true
+          required:
+            - content
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: object
+                properties:
+                  id:
+                    type: string
+                  target_type:
+                    type: string
+                  target_id:
+                    type: string
+                  parent_id:
+                    type: string
+                    nullable: true
+                  type:
+                    type: string
+                  severity:
+                    type: string
+                    nullable: true
+                  status:
+                    type: string
+                  title:
+                    type: string
+                    nullable: true
+                  content:
+                    type: string
+                    nullable: true
+                  content_type:
+                    type: string
+                  html:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  summary:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  tpl_id:
+                    type: string
+                    nullable: true
+                  reply_count:
+                    type: integer
+                    description: 仅在部分查询口径下返回。
+                  editor:
+                    type: object
+                    properties:
+                      id: { type: string }
+                      nickName: { type: string }
+                      userName: { type: string }
+                      realName: { type: string }
+                      sn: { type: integer }
+                      avatar: { type: string }
+                      roles: { type: array, items: { type: string } }
+                    nullable: true
+                  abilities:
+                    type: object
+                    properties:
+                      update: { type: boolean }
+                      delete: { type: boolean }
+                      close: { type: boolean }
+                      reply: { type: boolean }
+                  created_at:
+                    type: string
+                  updated_at:
+                    type: string
+                  deleted_at:
+                    type: string
+                    nullable: true
+    201:
+      description: 回复成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: object
+                properties:
+                  id:
+                    type: string
+                  target_type:
+                    type: string
+                  target_id:
+                    type: string
+                  parent_id:
+                    type: string
+                    nullable: true
+                  type:
+                    type: string
+                  severity:
+                    type: string
+                    nullable: true
+                  status:
+                    type: string
+                  title:
+                    type: string
+                    nullable: true
+                  content:
+                    type: string
+                    nullable: true
+                  content_type:
+                    type: string
+                  html:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  summary:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  tpl_id:
+                    type: string
+                    nullable: true
+                  reply_count:
+                    type: integer
+                    description: 仅在部分查询口径下返回。
+                  editor:
+                    type: object
+                    properties:
+                      id: { type: string }
+                      nickName: { type: string }
+                      userName: { type: string }
+                      realName: { type: string }
+                      sn: { type: integer }
+                      avatar: { type: string }
+                      roles: { type: array, items: { type: string } }
+                    nullable: true
+                  abilities:
+                    type: object
+                    properties:
+                      update: { type: boolean }
+                      delete: { type: boolean }
+                      close: { type: boolean }
+                      reply: { type: boolean }
+                  created_at:
+                    type: string
+                  updated_at:
+                    type: string
+                  deleted_at:
+                    type: string
+                    nullable: true
+    401:
+      $ref: '../../main.yaml#/components/responses/ProblemUnauthorized'
+    403:
+      description: 当前用户不能回复这个话题
+      content:
+        application/problem+json:
+          schema:
+            $ref: '../../main.yaml#/components/schemas/ProblemDetails'
+    409:
+      description: 话题已关闭
+      content:
+        application/problem+json:
+          schema:
+            $ref: '../../main.yaml#/components/schemas/ProblemDetails'
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 340 - 0
openapi/public/assets/protocol/resources/auto/v3-discussions-discussion.yaml

@@ -0,0 +1,340 @@
+# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-discussions-discussion.yaml
+get:
+  summary: 获取单条话题或回复
+  tags:
+    - discussions
+  operationId: get_api_v3_discussions_discussion_
+  description: |-
+    删除了但还有回复的话题返回墓碑;删除了的回复、删除了且没有回复的话题返回 404。
+    当前用户看不见的话题同样 404。
+
+    实现:`V3\DiscussionController@show`
+  security:
+    - {  }
+    -
+      bearerAuth: []
+  parameters:
+    -
+      name: discussion
+      in: path
+      required: true
+      schema:
+        type: string
+      description: '话题或回复的 uuid'
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: object
+                properties:
+                  id:
+                    type: string
+                  target_type:
+                    type: string
+                  target_id:
+                    type: string
+                  parent_id:
+                    type: string
+                    nullable: true
+                  type:
+                    type: string
+                  severity:
+                    type: string
+                    nullable: true
+                  status:
+                    type: string
+                  title:
+                    type: string
+                    nullable: true
+                  content:
+                    type: string
+                    nullable: true
+                  content_type:
+                    type: string
+                  html:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  summary:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  tpl_id:
+                    type: string
+                    nullable: true
+                  reply_count:
+                    type: integer
+                    description: 仅在部分查询口径下返回。
+                  editor:
+                    type: object
+                    properties:
+                      id: { type: string }
+                      nickName: { type: string }
+                      userName: { type: string }
+                      realName: { type: string }
+                      sn: { type: integer }
+                      avatar: { type: string }
+                      roles: { type: array, items: { type: string } }
+                    nullable: true
+                  abilities:
+                    type: object
+                    properties:
+                      update: { type: boolean }
+                      delete: { type: boolean }
+                      close: { type: boolean }
+                      reply: { type: boolean }
+                  created_at:
+                    type: string
+                  updated_at:
+                    type: string
+                  deleted_at:
+                    type: string
+                    nullable: true
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'
+delete:
+  summary: 删除话题或回复(软删除)
+  tags:
+    - discussions
+  operationId: delete_api_v3_discussions_discussion_
+  description: |-
+    目前只有作者本人能删。删话题不动回复:有回复的话题以墓碑形式留在列表里。
+    返回删除后的资源(200),不是 204——软删的资源并没有消失。
+
+    实现:`V3\DiscussionController@destroy`
+  security:
+    -
+      bearerAuth: []
+  parameters:
+    -
+      name: discussion
+      in: path
+      required: true
+      schema:
+        type: string
+      description: '话题或回复的 uuid'
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: object
+                properties:
+                  id:
+                    type: string
+                  target_type:
+                    type: string
+                  target_id:
+                    type: string
+                  parent_id:
+                    type: string
+                    nullable: true
+                  type:
+                    type: string
+                  severity:
+                    type: string
+                    nullable: true
+                  status:
+                    type: string
+                  title:
+                    type: string
+                    nullable: true
+                  content:
+                    type: string
+                    nullable: true
+                  content_type:
+                    type: string
+                  html:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  summary:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  tpl_id:
+                    type: string
+                    nullable: true
+                  reply_count:
+                    type: integer
+                    description: 仅在部分查询口径下返回。
+                  editor:
+                    type: object
+                    properties:
+                      id: { type: string }
+                      nickName: { type: string }
+                      userName: { type: string }
+                      realName: { type: string }
+                      sn: { type: integer }
+                      avatar: { type: string }
+                      roles: { type: array, items: { type: string } }
+                    nullable: true
+                  abilities:
+                    type: object
+                    properties:
+                      update: { type: boolean }
+                      delete: { type: boolean }
+                      close: { type: boolean }
+                      reply: { type: boolean }
+                  created_at:
+                    type: string
+                  updated_at:
+                    type: string
+                  deleted_at:
+                    type: string
+                    nullable: true
+    401:
+      $ref: '../../main.yaml#/components/responses/ProblemUnauthorized'
+    403:
+      description: 只能删除自己发表的内容
+      content:
+        application/problem+json:
+          schema:
+            $ref: '../../main.yaml#/components/schemas/ProblemDetails'
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'
+patch:
+  summary: 修改话题或回复(局部更新)
+  tags:
+    - discussions
+  operationId: patch_api_v3_discussions_discussion_
+  description: |-
+    只改传了的字段。title / status / type / severity 只对话题有效,回复传了 422。
+    作者或对象的管理者(句子、逐词所在 channel 的编辑者,文章的编辑者)可改;
+    改 type 时回复跟着改,新类型不收原来的 severity 时 severity 清空。
+
+    实现:`V3\DiscussionController@update`
+  security:
+    -
+      bearerAuth: []
+  parameters:
+    -
+      name: discussion
+      in: path
+      required: true
+      schema:
+        type: string
+      description: '话题或回复的 uuid'
+  requestBody:
+    required: false
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            title:
+              type: string
+              description: 标题(只限话题)
+            content:
+              type: string
+              description: 正文(markdown)
+            status:
+              type: string
+              description: 状态(只限话题)
+              enum:
+                - active
+                - close
+            type:
+              type: string
+              description: 话题类型(只限话题)
+              enum:
+                - discussion
+                - qa
+                - help
+                - issue
+            severity:
+              type: string
+              description: '严重程度(只限话题),null 表示清空'
+              enum:
+                - error
+                - warning
+                - info
+                - urgent
+                - normal
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: object
+                properties:
+                  id:
+                    type: string
+                  target_type:
+                    type: string
+                  target_id:
+                    type: string
+                  parent_id:
+                    type: string
+                    nullable: true
+                  type:
+                    type: string
+                  severity:
+                    type: string
+                    nullable: true
+                  status:
+                    type: string
+                  title:
+                    type: string
+                    nullable: true
+                  content:
+                    type: string
+                    nullable: true
+                  content_type:
+                    type: string
+                  html:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  summary:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  tpl_id:
+                    type: string
+                    nullable: true
+                  reply_count:
+                    type: integer
+                    description: 仅在部分查询口径下返回。
+                  editor:
+                    type: object
+                    properties:
+                      id: { type: string }
+                      nickName: { type: string }
+                      userName: { type: string }
+                      realName: { type: string }
+                      sn: { type: integer }
+                      avatar: { type: string }
+                      roles: { type: array, items: { type: string } }
+                    nullable: true
+                  abilities:
+                    type: object
+                    properties:
+                      update: { type: boolean }
+                      delete: { type: boolean }
+                      close: { type: boolean }
+                      reply: { type: boolean }
+                  created_at:
+                    type: string
+                  updated_at:
+                    type: string
+                  deleted_at:
+                    type: string
+                    nullable: true
+    401:
+      $ref: '../../main.yaml#/components/responses/ProblemUnauthorized'
+    403:
+      description: 没有修改权限
+      content:
+        application/problem+json:
+          schema:
+            $ref: '../../main.yaml#/components/schemas/ProblemDetails'
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 69 - 0
openapi/public/assets/protocol/resources/auto/v3-discussions-summary.yaml

@@ -0,0 +1,69 @@
+# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-discussions-summary.yaml
+get:
+  summary: 话题概况
+  tags:
+    - discussions
+  operationId: get_api_v3_discussions_summary
+  description: |-
+    各状态的话题数(只数没删除的、当前用户看得见的),以及当前用户能在这个对象上
+    发起哪些类型的话题(未登录为空数组)。
+
+    实现:`V3\DiscussionSummaryController@__invoke`
+  security:
+    - {  }
+    -
+      bearerAuth: []
+  parameters:
+    -
+      name: target_type
+      in: query
+      schema:
+        type: string
+        enum:
+          - sentence
+          - wbw
+          - article
+          - term
+          - task
+      description: 挂靠对象的类型
+      required: true
+    -
+      name: target_id
+      in: query
+      schema:
+        type: string
+      description: '挂靠对象的 uuid'
+      required: true
+    -
+      name: type
+      in: query
+      schema:
+        type: string
+        enum:
+          - discussion
+          - qa
+          - help
+          - issue
+      description: 只数这种类型的话题
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: object
+                properties:
+                  status_counts:
+                    type: object
+                    properties:
+                      active: { type: integer }
+                      close: { type: integer }
+                  creatable_types:
+                    type: array
+                    items:
+                      type: string
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 378 - 0
openapi/public/assets/protocol/resources/auto/v3-discussions.yaml

@@ -0,0 +1,378 @@
+# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/v3-discussions.yaml
+get:
+  summary: 列出某个对象上的话题
+  tags:
+    - discussions
+  operationId: get_api_v3_discussions
+  description: |-
+    只列话题(不含回复),删除了但还有回复的话题以墓碑出现(deleted_at 有值、内容为空)。
+    未登录看不到协作类话题(discussion / issue);basic 角色在自己无权管理的对象上只看得到
+    自己发起的协作类话题。各状态的数量与「能发起哪些类型」见 `GET /v3/discussions/summary`。
+
+    实现:`V3\DiscussionController@index`
+  security:
+    - {  }
+    -
+      bearerAuth: []
+  parameters:
+    -
+      name: per_page
+      in: query
+      schema:
+        type: integer
+        default: 15
+      description: '每页数量,最大 200'
+    -
+      name: target_type
+      in: query
+      schema:
+        type: string
+        enum:
+          - sentence
+          - wbw
+          - article
+          - term
+          - task
+      description: 挂靠对象的类型
+      required: true
+    -
+      name: target_id
+      in: query
+      schema:
+        type: string
+      description: '挂靠对象的 uuid'
+      required: true
+    -
+      name: type
+      in: query
+      schema:
+        type: string
+        enum:
+          - discussion
+          - qa
+          - help
+          - issue
+      description: 话题类型
+    -
+      name: status
+      in: query
+      schema:
+        type: string
+        enum:
+          - active
+          - close
+      description: 话题状态,不传则两种都要
+    -
+      name: severity
+      in: query
+      schema:
+        type: string
+      description: 严重程度,逗号分隔多选
+      example: 'error,warning'
+    -
+      name: sort
+      in: query
+      schema:
+        type: string
+        enum:
+          - created_at
+          - '-created_at'
+          - updated_at
+          - '-updated_at'
+        default: '-created_at'
+      description: '排序,前缀 - 表示倒序'
+    -
+      name: page
+      in: query
+      schema:
+        type: integer
+        default: 1
+      description: 页码
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: array
+                items:
+                  type: object
+                  properties:
+                    id:
+                      type: string
+                    target_type:
+                      type: string
+                    target_id:
+                      type: string
+                    parent_id:
+                      type: string
+                      nullable: true
+                    type:
+                      type: string
+                    severity:
+                      type: string
+                      nullable: true
+                    status:
+                      type: string
+                    title:
+                      type: string
+                      nullable: true
+                    content:
+                      type: string
+                      nullable: true
+                    content_type:
+                      type: string
+                    html:
+                      type: string
+                      description: 仅在部分查询口径下返回。
+                    summary:
+                      type: string
+                      description: 仅在部分查询口径下返回。
+                    tpl_id:
+                      type: string
+                      nullable: true
+                    reply_count:
+                      type: integer
+                      description: 仅在部分查询口径下返回。
+                    editor:
+                      type: object
+                      properties: { id: { type: string }, nickName: { type: string }, userName: { type: string }, realName: { type: string }, sn: { type: integer }, avatar: { type: string }, roles: { type: array, items: { type: string } } }
+                      nullable: true
+                    abilities:
+                      type: object
+                      properties: { update: { type: boolean }, delete: { type: boolean }, close: { type: boolean }, reply: { type: boolean } }
+                    created_at:
+                      type: string
+                    updated_at:
+                      type: string
+                    deleted_at:
+                      type: string
+                      nullable: true
+              meta:
+                $ref: '../../main.yaml#/components/schemas/PaginationMeta'
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'
+post:
+  summary: 发起话题
+  tags:
+    - discussions
+  operationId: post_api_v3_discussions
+  description: |-
+    discussion / issue 登录即可发;qa / help 只能发在文章上,且只有文章的编辑者能发。
+    severity 的取值随 type:issue 为 error / warning / info,help 为 urgent / normal,
+    其它类型不收。默认会通知对象的作者(站内信 + webhook),`notify=false` 关闭。
+
+    实现:`V3\DiscussionController@store`
+  security:
+    -
+      bearerAuth: []
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            target_type:
+              type: string
+              description: 挂靠对象的类型
+              enum:
+                - sentence
+                - wbw
+                - article
+                - term
+                - task
+            target_id:
+              type: string
+              description: '挂靠对象的 uuid'
+            type:
+              type: string
+              description: 话题类型
+              enum:
+                - discussion
+                - qa
+                - help
+                - issue
+              default: discussion
+            title:
+              type: string
+              description: '标题,最长 256 字符'
+            content:
+              type: string
+              description: 正文(markdown)
+            severity:
+              type: string
+              description: 严重程度
+              enum:
+                - error
+                - warning
+                - info
+                - urgent
+                - normal
+            tpl_id:
+              type: string
+              description: '话题模板(文章)的 uuid'
+            notify:
+              type: boolean
+              description: 是否发通知
+              default: true
+          required:
+            - target_type
+            - target_id
+            - title
+  responses:
+    200:
+      description: 成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: object
+                properties:
+                  id:
+                    type: string
+                  target_type:
+                    type: string
+                  target_id:
+                    type: string
+                  parent_id:
+                    type: string
+                    nullable: true
+                  type:
+                    type: string
+                  severity:
+                    type: string
+                    nullable: true
+                  status:
+                    type: string
+                  title:
+                    type: string
+                    nullable: true
+                  content:
+                    type: string
+                    nullable: true
+                  content_type:
+                    type: string
+                  html:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  summary:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  tpl_id:
+                    type: string
+                    nullable: true
+                  reply_count:
+                    type: integer
+                    description: 仅在部分查询口径下返回。
+                  editor:
+                    type: object
+                    properties:
+                      id: { type: string }
+                      nickName: { type: string }
+                      userName: { type: string }
+                      realName: { type: string }
+                      sn: { type: integer }
+                      avatar: { type: string }
+                      roles: { type: array, items: { type: string } }
+                    nullable: true
+                  abilities:
+                    type: object
+                    properties:
+                      update: { type: boolean }
+                      delete: { type: boolean }
+                      close: { type: boolean }
+                      reply: { type: boolean }
+                  created_at:
+                    type: string
+                  updated_at:
+                    type: string
+                  deleted_at:
+                    type: string
+                    nullable: true
+    201:
+      description: 创建成功
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: object
+                properties:
+                  id:
+                    type: string
+                  target_type:
+                    type: string
+                  target_id:
+                    type: string
+                  parent_id:
+                    type: string
+                    nullable: true
+                  type:
+                    type: string
+                  severity:
+                    type: string
+                    nullable: true
+                  status:
+                    type: string
+                  title:
+                    type: string
+                    nullable: true
+                  content:
+                    type: string
+                    nullable: true
+                  content_type:
+                    type: string
+                  html:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  summary:
+                    type: string
+                    description: 仅在部分查询口径下返回。
+                  tpl_id:
+                    type: string
+                    nullable: true
+                  reply_count:
+                    type: integer
+                    description: 仅在部分查询口径下返回。
+                  editor:
+                    type: object
+                    properties:
+                      id: { type: string }
+                      nickName: { type: string }
+                      userName: { type: string }
+                      realName: { type: string }
+                      sn: { type: integer }
+                      avatar: { type: string }
+                      roles: { type: array, items: { type: string } }
+                    nullable: true
+                  abilities:
+                    type: object
+                    properties:
+                      update: { type: boolean }
+                      delete: { type: boolean }
+                      close: { type: boolean }
+                      reply: { type: boolean }
+                  created_at:
+                    type: string
+                  updated_at:
+                    type: string
+                  deleted_at:
+                    type: string
+                    nullable: true
+    401:
+      $ref: '../../main.yaml#/components/responses/ProblemUnauthorized'
+    403:
+      description: 当前用户不能在这个对象上发起这种类型的话题
+      content:
+        application/problem+json:
+          schema:
+            $ref: '../../main.yaml#/components/schemas/ProblemDetails'
+    422:
+      $ref: '../../main.yaml#/components/responses/ProblemValidation'

+ 4 - 0
openapi/public/assets/protocol/resources/auto/v3-email-certifications.yaml

@@ -9,6 +9,10 @@ post:
     邮箱已注册时返回 422。按 IP 与邮箱限流,超出返回 429。
 
     实现:`V3\EmailCertificationController@store`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   requestBody:
     required: true
     content:

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

@@ -16,6 +16,10 @@ get:
     RFC 9457 Problem Details 而非正常结构。
 
     实现:`V3\HeartbeatController@show`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   responses:
     200:
       description: 成功

+ 4 - 0
openapi/public/assets/protocol/resources/auto/v3-invites-invite.yaml

@@ -8,6 +8,10 @@ get:
     邀请注册页用它显示邮箱。不存在或已被使用都返回 404。
 
     实现:`V3\InviteController@show`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   parameters:
     -
       name: invite

+ 4 - 0
openapi/public/assets/protocol/resources/auto/v3-invites.yaml

@@ -9,6 +9,10 @@ post:
     验证码错误、过期或试错 5 次后返回 422(errors.code)。
 
     实现:`V3\InviteController@store`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   requestBody:
     required: true
     content:

+ 4 - 1
openapi/public/assets/protocol/resources/auto/v3-me-reactions-reaction.yaml

@@ -2,7 +2,7 @@
 delete:
   summary: '删除我自己的 reaction'
   tags:
-    - me
+    - reactions
   operationId: delete_api_v3_me_reactions_reaction_
   description: |-
     只能删自己那条(user_id 与当前用户不符返回 403)。
@@ -10,6 +10,9 @@ delete:
     (见 v3-resource skill 硬规范第 2 条)。
 
     实现:`V3\MeReactionController@destroy`
+  security:
+    -
+      bearerAuth: []
   parameters:
     -
       name: reaction

+ 8 - 2
openapi/public/assets/protocol/resources/auto/v3-me-reactions.yaml

@@ -2,9 +2,12 @@
 get:
   summary: '列出我自己的 reactions'
   tags:
-    - me
+    - reactions
   operationId: get_api_v3_me_reactions
   description: 实现:`V3\MeReactionController@index`
+  security:
+    -
+      bearerAuth: []
   parameters:
     -
       name: per_page
@@ -86,7 +89,7 @@ get:
 post:
   summary: '添加一条我自己的 reaction(幂等)'
   tags:
-    - me
+    - reactions
   operationId: post_api_v3_me_reactions
   description: |-
     唯一键是 (type, target_id, user_id):重复提交不会产生第二条记录,返回同一条。
@@ -99,6 +102,9 @@ post:
     下次读 tally 时校正。见 v3-resource skill 硬规范第 2 条。
 
     实现:`V3\MeReactionController@store`
+  security:
+    -
+      bearerAuth: []
   requestBody:
     required: true
     content:

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

@@ -9,6 +9,9 @@ get:
     不回传 token(v2 的 auth/current 会把请求里的 token 原样回显)。
 
     实现:`V3\MeController@show`
+  security:
+    -
+      bearerAuth: []
   responses:
     200:
       description: 成功

+ 8 - 0
openapi/public/assets/protocol/resources/auto/v3-password-resets-token.yaml

@@ -8,6 +8,10 @@ get:
     重置页用它显示「正在为哪个账号设置新密码」。token 不存在或已过期一律 404。
 
     实现:`V3\PasswordResetController@show`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   parameters:
     -
       name: token
@@ -48,6 +52,10 @@ patch:
     成功后 token 作废,返回 204 空体;之后用新密码走登录。
 
     实现:`V3\PasswordResetController@update`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   parameters:
     -
       name: token

+ 4 - 0
openapi/public/assets/protocol/resources/auto/v3-password-resets.yaml

@@ -11,6 +11,10 @@ post:
     按 IP 与邮箱限流,超出返回 429。
 
     实现:`V3\PasswordResetController@store`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   requestBody:
     required: true
     content:

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

@@ -10,6 +10,10 @@ get:
     那不是业务路径开关而是噪音,已去掉(硬规范第 1 条)。
 
     实现:`V3\ProgressController@index`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   parameters:
     -
       name: channels

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

@@ -8,6 +8,10 @@ get:
     一次聚合查询出各 type 的 count;登录时再查一次当前用户的选择,补 selected / id。
 
     实现:`V3\ReactionTallyController@index`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   parameters:
     -
       name: target_id

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

@@ -8,6 +8,10 @@ get:
     按 target 聚合的公共列表,不要求登录。分页由 Laravel paginator 提供。
 
     实现:`V3\ReactionController@index`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   parameters:
     -
       name: per_page

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

@@ -8,6 +8,10 @@ get:
     直接从 OpenSearch 按文档 id 取回,不走检索。取不到返回 404。
 
     实现:`V3\SearchPlusController@show`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   parameters:
     -
       name: search

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

@@ -9,6 +9,10 @@ get:
     q 为空时返回 400。
 
     实现:`V3\SearchSuggestController@index`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   parameters:
     -
       name: q

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

@@ -9,6 +9,10 @@ get:
     同样的参数也可以用 POST /v3/search 提交(参数多、URL 放不下时用)。
 
     实现:`V3\SearchPlusController@index`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   parameters:
     -
       name: q
@@ -155,6 +159,10 @@ post:
     尤其是 page_refs / related_id 这类数组时,URL 放不下。
 
     实现:`V3\SearchPlusController@store`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   requestBody:
     required: true
     content:

+ 4 - 0
openapi/public/assets/protocol/resources/auto/v3-sessions.yaml

@@ -10,6 +10,10 @@ post:
     按 IP 与账号限流,超出返回 429。
 
     实现:`V3\SessionController@store`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   requestBody:
     required: true
     content:

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

@@ -15,6 +15,10 @@ get:
     无过滤时用 `next_cursor` 是否为 null 判断结束,不要指望百分比。
 
     实现:`V3\TipitakaReadingController@__invoke`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   parameters:
     -
       name: channel

+ 4 - 0
openapi/public/assets/protocol/resources/auto/v3-tipitaka-related-paragraphs-aggregate-book-name.yaml

@@ -8,6 +8,10 @@ get:
     data 是去重、>0 的 cs_para(升序),每条形如 {cs_para},分页由 Laravel paginator 提供。
 
     实现:`V3\TipitakaRelatedParagraphAggregateController@show`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   parameters:
     -
       name: book_name

+ 4 - 0
openapi/public/assets/protocol/resources/auto/v3-tipitaka-related-paragraphs-aggregate.yaml

@@ -8,6 +8,10 @@ get:
     data 是去空、去重的 SC 缩写,每条形如 {book_name},分页由 Laravel paginator 提供。
 
     实现:`V3\TipitakaRelatedParagraphAggregateController@index`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   parameters:
     -
       name: file

+ 4 - 0
openapi/public/assets/protocol/resources/auto/v3-tipitaka-related-paragraphs.yaml

@@ -9,6 +9,10 @@ get:
     没有关联段落时返回空集(这是正常结果,不是错误)。
 
     实现:`V3\TipitakaRelatedParagraphController@index`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   parameters:
     -
       name: book

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

@@ -8,6 +8,10 @@ get:
     目前只回报服务可用,尚未接入版本比对逻辑。
 
     实现:`V3\UpgradeController@index`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   responses:
     200:
       description: 成功

+ 4 - 0
openapi/public/assets/protocol/resources/auto/v3-users.yaml

@@ -10,6 +10,10 @@ post:
     注册不等于登录:返回 201 与新账号,之后用用户名/邮箱 + 密码走登录。
 
     实现:`V3\UserController@store`
+  security:
+    - {  }
+    -
+      bearerAuth: []
   requestBody:
     required: true
     content:

+ 18 - 1
openapi/scripts/generate.php

@@ -688,6 +688,15 @@ function buildOperation(array $route, string $httpMethod): array
     if (! empty($info['tags']['deprecated'])) {
         $op['deprecated'] = true;
     }
+    // v3 的鉴权按路由中间件逐个写准(v2 沿用全局 security,不动):
+    // 挂了 auth.v3 → 必须带 token;没挂 → 匿名可用,带了 token 就按登录用户处理
+    // (讨论列表的可见性、tally 的「我点过没有」),所以是「可选」。生成的客户端据此
+    // 决定发不发 Authorization——同一路径读公开、写要登录(middlewareFor)也能分清。
+    if (str_starts_with($route['uri'], 'api/v3/')) {
+        $op['security'] = in_array('auth.v3', $route['middleware'] ?? [], true)
+            ? [['bearerAuth' => []]]
+            : [[], ['bearerAuth' => []]];
+    }
     $documented = $info['tags']['params'] ?? [];
 
     $params = [];
@@ -950,9 +959,16 @@ function defaultSummary(string $method, string $uri): string
     };
 }
 
+/**
+ * 取路径第一段当 tag。openapi-generator 按第一个 tag 分 Api 类,
+ * 所以 tag 必须是资源名:`me/` 只是「当前用户」这个主体,要剥掉,
+ * 否则 /v3/me/reactions、/v3/me/discussions 会全挤进一个 MeApi。
+ * 单独的 /v3/me(当前用户档案)剥完为空,仍归 me。
+ */
 function tagOf(string $uri): string
 {
     $path = preg_replace('#^api/(v2|v3|ops)/#', '', $uri);
+    $path = preg_replace('#^me/(?=.)#', '', $path);
     $first = explode('/', $path)[0];
 
     return $first === '' ? 'misc' : $first;
@@ -1033,7 +1049,8 @@ foreach ($paths as $path => $ops) {
     file_put_contents(
         $autoDir . '/' . $name,
         "# 由 openapi/scripts/generate.php 生成,请勿手改;补充写到 overrides/{$name}\n" .
-            Yaml::dump($ops, 12, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)
+            // 同 main.yaml:security requirement 的值必须是数组,空数组会被 dump 成 {  }
+            str_replace('bearerAuth: {  }', 'bearerAuth: []', Yaml::dump($ops, 12, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK))
     );
     $refs[$path] = ['$ref' => "./resources/auto/{$name}"];
 }