| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- import { tool } from "@opencode-ai/plugin"
- import { loadAgentPaaSConfig, requestJSON } from "../lib/agentpaas"
- import { startSoftwareReviewPanel } from "../lib/software-review-panel"
- import {
- reviewPlan,
- reviewEdits,
- parseReviewProposal,
- reviewSummary,
- reviewContext,
- reviewOntology,
- } from "../lib/software-review-contract"
- export default tool({
- description:
- "本体候选审阅流程:analyze 分析服务端仓库;open 返回终端 /refactor 面板入口;dashboard/web 在用户要求可视化时打开同任务只读看板,自动同步;操作仍在 CLI;status 查询;ontology 分页读取架构线索及候选的上游影响、下游依赖、未解析关系;context 读取已选候选与文件索引;source 分页读取实际源码;propose 提交具体计划和补丁等待人工确认。evidence 列出真实证据;artifact 分页读取补丁、测试和日志。此工具不能替用户勾选、批准、质量审查或启动执行。",
- args: {
- action: tool.schema.enum([
- "analyze",
- "status",
- "open",
- "web",
- "dashboard",
- "context",
- "source",
- "ontology",
- "evidence",
- "artifact",
- "propose",
- ]),
- review_id: tool.schema.string().optional(),
- artifact_name: tool.schema.string().optional().describe("artifact: evidence 索引中的文件名"),
- evidence_token: tool.schema.string().optional().describe("artifact: 当前 evidence.token"),
- repository: tool.schema.string().optional().describe("相对于该租户服务端工作区的仓库目录;不是客户端本地路径"),
- project: tool.schema.string().optional(),
- candidate_id: tool.schema
- .string()
- .optional()
- .describe("ontology 可选:读取该候选已保存的关系证据,按 offset/limit 分页;留意服务端截断提示"),
- relation_view: tool.schema
- .enum(["impact", "dependencies", "unresolved"])
- .optional()
- .describe("ontology 的候选关系视图:上游影响(默认)、下游依赖、未解析关系;后两项须带 candidate_id"),
- rules_path: tool.schema
- .string()
- .optional()
- .describe(
- "analyze 可选:用户提供的仓库相对规则 JSON 路径。规则须引用真实文档原文及源码目标;不可编造规则或默认使用演示规则。",
- ),
- revision: tool.schema
- .number()
- .int()
- .nonnegative()
- .optional()
- .describe("propose 必填,独立的顶层参数,使用 context 返回的当前版本"),
- plan: reviewPlan.optional().describe("propose 的结构化计划,必须处理全部已选候选;无法处理时先请用户调整选择"),
- edits: reviewEdits.optional(),
- proposal_json: tool.schema
- .string()
- .optional()
- .describe("旧客户端兼容:{plan,edits} JSON,字段与结构化参数相同;优先使用 plan/edits。revision 必须另传。"),
- path: tool.schema.string().optional().describe("source: context.files 中的真实路径"),
- start_line: tool.schema.number().int().min(1).optional(),
- offset: tool.schema.number().int().min(0).optional().describe("status 候选分页起点,默认 0"),
- limit: tool.schema.number().int().min(1).max(80).optional().describe("status 默认 8 项;source 默认 40 行"),
- },
- async execute(args, context) {
- const base = await loadAgentPaaSConfig()
- // Optional co-hosted review API for deployments that keep long-running
- // agent execution separate. Require the same host before reusing its key.
- const url = process.env.AGENTPAAS_SOFTWARE_REVIEW_URL?.replace(/\/+$/, "") || base.url
- const target = new URL(url)
- const original = new URL(base.url)
- if (
- target.hostname !== original.hostname ||
- target.protocol !== original.protocol ||
- target.username ||
- target.password ||
- target.search ||
- target.hash ||
- target.pathname !== "/"
- )
- throw new Error("审阅 API 必须使用同一 PaaS 主机与协议,不向其他主机转发凭证")
- const config = { ...base, url }
- if (args.action === "analyze") {
- if (!args.repository || !args.project) throw new Error("需要服务端仓库相对目录与项目标识")
- return JSON.stringify(
- reviewSummary(
- await requestJSON(config, "/software-reviews", context.abort, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- repository: args.repository,
- project: args.project,
- ...(args.rules_path ? { rules_path: args.rules_path } : {}),
- }),
- }),
- ),
- )
- }
- if (!args.review_id || !/^[a-f0-9]{32}$/.test(args.review_id)) throw new Error("需要真实审阅编号")
- const route = `/software-reviews/${args.review_id}`
- if (args.action === "open") {
- const review = await requestJSON(config, route, context.abort)
- return JSON.stringify({
- ...reviewSummary(review),
- interface: "cli",
- next: "在 OpenCode 输入 /refactor,打开当前审阅,按首页“下一步”继续。四类结果、材料与记录可按需查看;其他操作不是必做步骤。无需打开网页。模型不能代替用户选择、执行确认或质量审查。",
- })
- }
- if (args.action === "web" || args.action === "dashboard") {
- await requestJSON(config, route, context.abort)
- const panel = startSoftwareReviewPanel(config, args.review_id, 0, "dashboard")
- const command =
- process.platform === "darwin"
- ? ["open", panel.url]
- : process.platform === "win32"
- ? ["cmd", "/c", "start", "", panel.url]
- : ["xdg-open", panel.url]
- const processResult = Bun.spawn(command, { stdout: "ignore", stderr: "ignore" })
- const code = await processResult.exited
- return JSON.stringify({
- review_id: args.review_id,
- url: panel.url,
- opened: code === 0,
- next: "看板只读并自动同步该审阅任务。选择候选、确认执行与质量审查继续在 OpenCode /refactor 内完成;不需要重新分析。",
- })
- }
- if (args.action === "evidence") return JSON.stringify(await requestJSON(config, route + "/evidence", context.abort))
- if (args.action === "artifact") {
- if (!args.artifact_name || !args.evidence_token) throw new Error("先读取 evidence,使用真实文件名和 token")
- return JSON.stringify(
- await requestJSON(
- config,
- route +
- `/evidence-text?name=${encodeURIComponent(args.artifact_name)}&token=${encodeURIComponent(args.evidence_token)}&offset=${args.offset || 0}`,
- context.abort,
- ),
- )
- }
- if (args.action === "ontology")
- return JSON.stringify(
- reviewOntology(
- await requestJSON(config, route, context.abort),
- args.candidate_id,
- args.offset,
- args.limit,
- args.relation_view,
- ),
- )
- if (args.action === "propose") {
- const proposal = parseReviewProposal(args)
- const selected = await requestJSON(config, route + "/context?include_sources=false", context.abort)
- if (selected.revision !== args.revision) throw new Error("revision 已变化;重新读取 context,不重跑分析。")
- const ids = (selected.selected as { id: string }[]).map((c) => c.id).sort()
- if (JSON.stringify([...proposal.plan.candidate_ids].sort()) !== JSON.stringify(ids))
- throw new Error(
- `提案未发送:必须覆盖用户全部已选候选 ${ids.join(", ")}。不能自行略去缺测试的候选;解释缺口并请用户在终端 /refactor 面板调整选择,再读取 context。`,
- )
- return JSON.stringify(
- reviewSummary(
- await requestJSON(config, route + "/proposal", context.abort, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ revision: args.revision, plan: proposal.plan, edits: proposal.edits }),
- }),
- ),
- )
- }
- if (args.action === "context")
- return JSON.stringify(
- reviewContext(await requestJSON(config, route + "/context?include_sources=false", context.abort)),
- )
- if (args.action === "source") {
- if (!args.path) throw new Error("source 需要 context.files 中的 path")
- return JSON.stringify(
- await requestJSON(
- config,
- route +
- `/source?path=${encodeURIComponent(args.path)}&start_line=${args.start_line || 1}&limit=${args.limit || 40}`,
- context.abort,
- ),
- )
- }
- return JSON.stringify(reviewSummary(await requestJSON(config, route, context.abort), args.offset, args.limit))
- },
- })
|