software_review.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import { tool } from "@opencode-ai/plugin"
  2. import { loadAgentPaaSConfig, requestJSON } from "../lib/agentpaas"
  3. import { startSoftwareReviewPanel } from "../lib/software-review-panel"
  4. import {
  5. reviewPlan,
  6. reviewEdits,
  7. parseReviewProposal,
  8. reviewSummary,
  9. reviewContext,
  10. reviewOntology,
  11. } from "../lib/software-review-contract"
  12. export default tool({
  13. description:
  14. "本体候选审阅流程:analyze 分析服务端仓库;open 返回终端 /refactor 面板入口;dashboard/web 在用户要求可视化时打开同任务只读看板,自动同步;操作仍在 CLI;status 查询;ontology 分页读取架构线索及候选的上游影响、下游依赖、未解析关系;context 读取已选候选与文件索引;source 分页读取实际源码;propose 提交具体计划和补丁等待人工确认。evidence 列出真实证据;artifact 分页读取补丁、测试和日志。此工具不能替用户勾选、批准、质量审查或启动执行。",
  15. args: {
  16. action: tool.schema.enum([
  17. "analyze",
  18. "status",
  19. "open",
  20. "web",
  21. "dashboard",
  22. "context",
  23. "source",
  24. "ontology",
  25. "evidence",
  26. "artifact",
  27. "propose",
  28. ]),
  29. review_id: tool.schema.string().optional(),
  30. artifact_name: tool.schema.string().optional().describe("artifact: evidence 索引中的文件名"),
  31. evidence_token: tool.schema.string().optional().describe("artifact: 当前 evidence.token"),
  32. repository: tool.schema.string().optional().describe("相对于该租户服务端工作区的仓库目录;不是客户端本地路径"),
  33. project: tool.schema.string().optional(),
  34. candidate_id: tool.schema
  35. .string()
  36. .optional()
  37. .describe("ontology 可选:读取该候选已保存的关系证据,按 offset/limit 分页;留意服务端截断提示"),
  38. relation_view: tool.schema
  39. .enum(["impact", "dependencies", "unresolved"])
  40. .optional()
  41. .describe("ontology 的候选关系视图:上游影响(默认)、下游依赖、未解析关系;后两项须带 candidate_id"),
  42. rules_path: tool.schema
  43. .string()
  44. .optional()
  45. .describe(
  46. "analyze 可选:用户提供的仓库相对规则 JSON 路径。规则须引用真实文档原文及源码目标;不可编造规则或默认使用演示规则。",
  47. ),
  48. revision: tool.schema
  49. .number()
  50. .int()
  51. .nonnegative()
  52. .optional()
  53. .describe("propose 必填,独立的顶层参数,使用 context 返回的当前版本"),
  54. plan: reviewPlan.optional().describe("propose 的结构化计划,必须处理全部已选候选;无法处理时先请用户调整选择"),
  55. edits: reviewEdits.optional(),
  56. proposal_json: tool.schema
  57. .string()
  58. .optional()
  59. .describe("旧客户端兼容:{plan,edits} JSON,字段与结构化参数相同;优先使用 plan/edits。revision 必须另传。"),
  60. path: tool.schema.string().optional().describe("source: context.files 中的真实路径"),
  61. start_line: tool.schema.number().int().min(1).optional(),
  62. offset: tool.schema.number().int().min(0).optional().describe("status 候选分页起点,默认 0"),
  63. limit: tool.schema.number().int().min(1).max(80).optional().describe("status 默认 8 项;source 默认 40 行"),
  64. },
  65. async execute(args, context) {
  66. const base = await loadAgentPaaSConfig()
  67. // Optional co-hosted review API for deployments that keep long-running
  68. // agent execution separate. Require the same host before reusing its key.
  69. const url = process.env.AGENTPAAS_SOFTWARE_REVIEW_URL?.replace(/\/+$/, "") || base.url
  70. const target = new URL(url)
  71. const original = new URL(base.url)
  72. if (
  73. target.hostname !== original.hostname ||
  74. target.protocol !== original.protocol ||
  75. target.username ||
  76. target.password ||
  77. target.search ||
  78. target.hash ||
  79. target.pathname !== "/"
  80. )
  81. throw new Error("审阅 API 必须使用同一 PaaS 主机与协议,不向其他主机转发凭证")
  82. const config = { ...base, url }
  83. if (args.action === "analyze") {
  84. if (!args.repository || !args.project) throw new Error("需要服务端仓库相对目录与项目标识")
  85. return JSON.stringify(
  86. reviewSummary(
  87. await requestJSON(config, "/software-reviews", context.abort, {
  88. method: "POST",
  89. headers: { "Content-Type": "application/json" },
  90. body: JSON.stringify({
  91. repository: args.repository,
  92. project: args.project,
  93. ...(args.rules_path ? { rules_path: args.rules_path } : {}),
  94. }),
  95. }),
  96. ),
  97. )
  98. }
  99. if (!args.review_id || !/^[a-f0-9]{32}$/.test(args.review_id)) throw new Error("需要真实审阅编号")
  100. const route = `/software-reviews/${args.review_id}`
  101. if (args.action === "open") {
  102. const review = await requestJSON(config, route, context.abort)
  103. return JSON.stringify({
  104. ...reviewSummary(review),
  105. interface: "cli",
  106. next: "在 OpenCode 输入 /refactor,打开当前审阅,按首页“下一步”继续。四类结果、材料与记录可按需查看;其他操作不是必做步骤。无需打开网页。模型不能代替用户选择、执行确认或质量审查。",
  107. })
  108. }
  109. if (args.action === "web" || args.action === "dashboard") {
  110. await requestJSON(config, route, context.abort)
  111. const panel = startSoftwareReviewPanel(config, args.review_id, 0, "dashboard")
  112. const command =
  113. process.platform === "darwin"
  114. ? ["open", panel.url]
  115. : process.platform === "win32"
  116. ? ["cmd", "/c", "start", "", panel.url]
  117. : ["xdg-open", panel.url]
  118. const processResult = Bun.spawn(command, { stdout: "ignore", stderr: "ignore" })
  119. const code = await processResult.exited
  120. return JSON.stringify({
  121. review_id: args.review_id,
  122. url: panel.url,
  123. opened: code === 0,
  124. next: "看板只读并自动同步该审阅任务。选择候选、确认执行与质量审查继续在 OpenCode /refactor 内完成;不需要重新分析。",
  125. })
  126. }
  127. if (args.action === "evidence") return JSON.stringify(await requestJSON(config, route + "/evidence", context.abort))
  128. if (args.action === "artifact") {
  129. if (!args.artifact_name || !args.evidence_token) throw new Error("先读取 evidence,使用真实文件名和 token")
  130. return JSON.stringify(
  131. await requestJSON(
  132. config,
  133. route +
  134. `/evidence-text?name=${encodeURIComponent(args.artifact_name)}&token=${encodeURIComponent(args.evidence_token)}&offset=${args.offset || 0}`,
  135. context.abort,
  136. ),
  137. )
  138. }
  139. if (args.action === "ontology")
  140. return JSON.stringify(
  141. reviewOntology(
  142. await requestJSON(config, route, context.abort),
  143. args.candidate_id,
  144. args.offset,
  145. args.limit,
  146. args.relation_view,
  147. ),
  148. )
  149. if (args.action === "propose") {
  150. const proposal = parseReviewProposal(args)
  151. const selected = await requestJSON(config, route + "/context?include_sources=false", context.abort)
  152. if (selected.revision !== args.revision) throw new Error("revision 已变化;重新读取 context,不重跑分析。")
  153. const ids = (selected.selected as { id: string }[]).map((c) => c.id).sort()
  154. if (JSON.stringify([...proposal.plan.candidate_ids].sort()) !== JSON.stringify(ids))
  155. throw new Error(
  156. `提案未发送:必须覆盖用户全部已选候选 ${ids.join(", ")}。不能自行略去缺测试的候选;解释缺口并请用户在终端 /refactor 面板调整选择,再读取 context。`,
  157. )
  158. return JSON.stringify(
  159. reviewSummary(
  160. await requestJSON(config, route + "/proposal", context.abort, {
  161. method: "POST",
  162. headers: { "Content-Type": "application/json" },
  163. body: JSON.stringify({ revision: args.revision, plan: proposal.plan, edits: proposal.edits }),
  164. }),
  165. ),
  166. )
  167. }
  168. if (args.action === "context")
  169. return JSON.stringify(
  170. reviewContext(await requestJSON(config, route + "/context?include_sources=false", context.abort)),
  171. )
  172. if (args.action === "source") {
  173. if (!args.path) throw new Error("source 需要 context.files 中的 path")
  174. return JSON.stringify(
  175. await requestJSON(
  176. config,
  177. route +
  178. `/source?path=${encodeURIComponent(args.path)}&start_line=${args.start_line || 1}&limit=${args.limit || 40}`,
  179. context.abort,
  180. ),
  181. )
  182. }
  183. return JSON.stringify(reviewSummary(await requestJSON(config, route, context.abort), args.offset, args.limit))
  184. },
  185. })