DiscussionController.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. <?php
  2. namespace App\Http\Controllers\V3;
  3. use App\Http\Controllers\Concerns\ResolvesCurrentUser;
  4. use App\Http\Controllers\Controller;
  5. use App\Http\Requests\V3\IndexDiscussionRequest;
  6. use App\Http\Requests\V3\StoreDiscussionRequest;
  7. use App\Http\Requests\V3\UpdateDiscussionRequest;
  8. use App\Http\Resources\V3\DiscussionResource;
  9. use App\Models\Discussion;
  10. use App\Services\V3\AgentGrantService;
  11. use App\Services\V3\DiscussionService;
  12. use Illuminate\Http\Request;
  13. /**
  14. * 评论线程:挂在句子、逐词、文章、术语、任务上的话题与回复;以及挂在句子上的批注
  15. * (note 边注、commentary 义注 / 复注对应——巴利注释书,不是普通评论)。
  16. *
  17. * 取代 `GET/POST /v2/discussion`(view=question / res_id / question-by-topic)、
  18. * `GET/PUT/DELETE /v2/discussion/{discussion}`。v2 那份在 dashboard-v4 下线前原样保留。
  19. * 回复见 {@see DiscussionReplyController}(取代 view=answer 与带 parent 的 POST)。
  20. *
  21. * 底层仍是 `discussions` 表。批注与评估(assessment)会注入阅读页,规则与评论不同,见各方法说明。
  22. *
  23. * 写操作(新建、修改、删除,以及回复)支持 AI 代写:模型用模型 token 登录,并带请求头
  24. * `X-Agent-Grant`(人类经 `POST /v3/agent-grants` 签发)。此时 editor 记授权人、
  25. * co_editor 记模型,权限按授权人判断,且只能写授权覆盖的 channel / book 里的句子上的内容。
  26. * 模型不带授权写入一律 403。
  27. */
  28. class DiscussionController extends Controller
  29. {
  30. use ResolvesCurrentUser;
  31. public function __construct(
  32. private readonly DiscussionService $discussions,
  33. private readonly AgentGrantService $grants,
  34. ) {}
  35. /**
  36. * 列出某个对象上的话题
  37. *
  38. * 只列话题(不含回复),删除了但还有回复的话题以墓碑出现(deleted_at 有值、内容为空)。
  39. * 未登录看不到不公开的话题(discussion);basic 角色在自己无权管理的对象上只看得到
  40. * 自己发起的不公开话题。各状态的数量与「能发起哪些类型」见 `GET /v3/discussions/summary`。
  41. * 不传 type 时只列评论线程(discussion / qa / help / issue / glossary),批注与评估要显式
  42. * type=note / commentary / assessment。
  43. * 要一段经文(一章、一段段落区间)里的全部话题,用 `GET /v3/tipitaka-discussions/{channel}`。
  44. *
  45. * @unauthenticated
  46. *
  47. * @queryParam target_type string required 挂靠对象的类型。Enum: sentence,wbw,article,term,task
  48. * @queryParam target_id string required 挂靠对象的 uuid
  49. * @queryParam type string 类型,不传则只列评论线程。Enum: discussion,qa,help,issue,glossary,note,commentary,assessment
  50. * @queryParam status string 话题状态,不传则两种都要。Enum: active,close
  51. * @queryParam severity string 严重程度(assessment 为评估方法),逗号分隔多选。Example: error,warning
  52. * @queryParam sort string 排序,前缀 - 表示倒序。Enum: created_at,-created_at,updated_at,-updated_at Default: -created_at
  53. * @queryParam page integer 页码。Default: 1
  54. * @queryParam per_page integer 每页数量,最大 200。Default: 15
  55. */
  56. public function index(IndexDiscussionRequest $request)
  57. {
  58. return DiscussionResource::collection($this->discussions->listTopics(
  59. $request->validated(),
  60. $this->currentUserUid($request),
  61. $request->integer('per_page', 15),
  62. ));
  63. }
  64. /**
  65. * 发起话题、新建批注或评估
  66. *
  67. * discussion / issue 登录即可发;qa / help 只能发在文章上,且只有文章的编辑者能发。
  68. * severity 的取值随 type:issue 为 error / warning / suggestion,help 为 urgent / normal,
  69. * 其它类型不收。默认会通知对象的作者(站内信 + webhook),`notify=false` 关闭。
  70. *
  71. * 批注(note / commentary)只能加在句子上,且只有该句所在 channel 的编辑者能加;
  72. * 标题可省、正文必填,commentary 的正文必须是义注句子模板 `{{book-para-start-end}}`
  73. * (可多句并列);可带锚点 pos_* / quote_*;不发通知。
  74. *
  75. * assessment(译文评估:用某种方法给这句译文打分)只能加在句子上,且只有该句所在 channel 的编辑者能加
  76. * (AI 凭授权代写);score(0–100 的整数)必填,severity 必填、填评估方法(1–16 位字母数字 . _ -),
  77. * 标题可省,理由写 content;不收锚点、不发通知。**一句只留一条**:已有评估时就地覆盖
  78. * (分数、方法、理由、署名都换成这次的,回复保留),返回 200;没有才新建,返回 201。
  79. *
  80. * glossary(外挂术语:不改译文,给锚点处的译文挂上术语)是评论线程,title 必填、填术语词头;
  81. * 与批注一样只能挂在句子上、只有该句所在 channel 的编辑者能发,对所有人公开可见。
  82. * 锚点只有 note / commentary / issue / glossary 收,其它类型传了 422。
  83. *
  84. * @responseStatus 201 创建成功
  85. * @responseStatus 200 覆盖了该句已有的评估(只限 assessment)
  86. * @responseStatus 403 当前用户不能在这个对象上发起这种类型的话题
  87. *
  88. * @bodyParam target_type string required 挂靠对象的类型。Enum: sentence,wbw,article,term,task
  89. * @bodyParam target_id string required 挂靠对象的 uuid
  90. * @bodyParam type string 类型。Enum: discussion,qa,help,issue,glossary,note,commentary,assessment Default: discussion
  91. * @bodyParam title string 标题,最长 256 字符;评论线程必填(glossary 填术语词头),批注与评估可省
  92. * @bodyParam content string 正文(markdown);批注必填,评估写打分理由
  93. * @bodyParam severity string 严重程度:issue 为 error / warning / suggestion,help 为 urgent / normal;assessment 必填,填评估方法(1–16 位字母数字 . _ -)。Example: error
  94. * @bodyParam score integer 评估分数,0–100 的整数(只限且必填于 assessment)。Example: 85
  95. * @bodyParam pos_start integer 锚点在句子文本中的起始偏移(只限 note / commentary / issue / glossary)
  96. * @bodyParam pos_end integer 锚点的结束偏移,批注插在这里(只限 note / commentary / issue / glossary)
  97. * @bodyParam quote_exact string 锚定的原文片段(只限 note / commentary / issue / glossary)
  98. * @bodyParam quote_prefix string 锚定片段前的上下文,用于重新定位(只限 note / commentary / issue / glossary)
  99. * @bodyParam quote_suffix string 锚定片段后的上下文,用于重新定位(只限 note / commentary / issue / glossary)
  100. * @bodyParam tpl_id string 话题模板(文章)的 uuid
  101. * @bodyParam notify boolean 是否发通知(批注不发)。Default: true
  102. */
  103. public function store(StoreDiscussionRequest $request)
  104. {
  105. return DiscussionResource::make($this->discussions->createTopic(
  106. $this->grants->actor(
  107. $this->currentUser($request)['user_uid'],
  108. $request->header(AgentGrantService::HEADER),
  109. ),
  110. $request->validated(),
  111. ));
  112. }
  113. /**
  114. * 获取单条话题或回复
  115. *
  116. * 删除了但还有回复的话题返回墓碑;删除了的回复、删除了且没有回复的话题返回 404。
  117. * 当前用户看不见的话题同样 404。
  118. *
  119. * @unauthenticated
  120. *
  121. * @urlParam discussion string required 话题或回复的 uuid
  122. */
  123. public function show(Request $request, Discussion $discussion)
  124. {
  125. return DiscussionResource::make(
  126. $this->discussions->show($discussion, $this->currentUserUid($request))
  127. );
  128. }
  129. /**
  130. * 修改话题或回复(局部更新)
  131. *
  132. * 只改传了的字段。title / status / type / severity 只对话题有效,回复传了 422。
  133. * 作者或对象的管理者(句子、逐词所在 channel 的编辑者,文章的编辑者)可改;
  134. * 改 type 时回复跟着改,新类型不收原来的 severity 时 severity 清空(锚点保留,改回可带锚点的类型时恢复);
  135. * 改成 glossary 须挂在句子上、且是该句的管理者。
  136. * 批注:没有 status,type 只能在 note / commentary 之间改(不能与评论线程互转),
  137. * 正文不能清空,可改锚点。
  138. * 改了内容(标题、正文、锚点)时 co_editor 跟着这次写入走:人亲自改则清空,AI 代改则换成该模型;
  139. * 只改 status / type / severity 不动 co_editor。
  140. * 评估:可改 score、severity(评估方法,不能清空)、标题、正文;没有 status,不能与其它类型互转。
  141. *
  142. * @responseStatus 403 没有修改权限
  143. *
  144. * @urlParam discussion string required 话题或回复的 uuid
  145. *
  146. * @bodyParam title string 标题(只限话题)
  147. * @bodyParam content string 正文(markdown)
  148. * @bodyParam status string 状态(只限话题)。Enum: active,close
  149. * @bodyParam type string 类型(只限话题;评论线程与批注不能互转)。Enum: discussion,qa,help,issue,glossary,note,commentary,assessment
  150. * @bodyParam severity string 严重程度(只限话题),null 表示清空;assessment 为评估方法,不能清空。Example: warning
  151. * @bodyParam score integer 评估分数,0–100 的整数(只限 assessment)。Example: 85
  152. * @bodyParam pos_start integer 锚点起始偏移(只限 note / commentary / issue / glossary)
  153. * @bodyParam pos_end integer 锚点结束偏移(只限 note / commentary / issue / glossary)
  154. * @bodyParam quote_exact string 锚定的原文片段(只限 note / commentary / issue / glossary)
  155. * @bodyParam quote_prefix string 锚定片段前的上下文(只限 note / commentary / issue / glossary)
  156. * @bodyParam quote_suffix string 锚定片段后的上下文(只限 note / commentary / issue / glossary)
  157. */
  158. public function update(UpdateDiscussionRequest $request, Discussion $discussion)
  159. {
  160. return DiscussionResource::make($this->discussions->update(
  161. $discussion,
  162. $this->grants->actor(
  163. $this->currentUser($request)['user_uid'],
  164. $request->header(AgentGrantService::HEADER),
  165. ),
  166. $request->validated(),
  167. ));
  168. }
  169. /**
  170. * 删除话题或回复(软删除)
  171. *
  172. * 作者本人或挂靠对象的管理者(句子 / 逐词所在 channel、文章的编辑者)能删;
  173. * v2 只允许作者。删话题不动回复:有回复的话题以墓碑形式留在列表里。
  174. * 返回删除后的资源(200),不是 204——软删的资源并没有消失。
  175. *
  176. * @responseStatus 403 既不是作者,也不是挂靠对象的管理者
  177. *
  178. * @urlParam discussion string required 话题或回复的 uuid
  179. */
  180. public function destroy(Request $request, Discussion $discussion)
  181. {
  182. return DiscussionResource::make($this->discussions->delete(
  183. $discussion,
  184. $this->grants->actor(
  185. $this->currentUser($request)['user_uid'],
  186. $request->header(AgentGrantService::HEADER),
  187. ),
  188. ));
  189. }
  190. }