فهرست منبع

feat: add agentpaas_run bridge tool + research-67 subagent facades

- .opencode/tool/agentpaas_run.ts: 桥接工具, 把任务转发给远端 AgentPaaS 智能体执行, SSE 解析后返回 done 事件结果
- .opencode/agent/research-67-*.md: 10 个 subagent 门面 (mode=subagent, tools={*:false, agentpaas_run:true}, model=opencode-go/glm-5.2)
  agent_id 留 TODO_REPLACE_AFTER_CREATE 占位, 待 Mac 上 agentpaas agent create 重注册后回填实际 ag_*
  (opencode 是 github 公开 fork, 不含任何 API key)

Per Mac迁移改动清单.md 第四部分. opencode package.json tree-sitter 改动跳过 (本机 Xcode 已装).
遗留待联调: opencode-go/glm-5.2 provider 在 Mac bun dev 下能否用未验证, 若不行门面 model 换 deepseek 或删行.
Jinchen Zhang 1 ماه پیش
والد
کامیت
ca42b17fcd

+ 20 - 0
.opencode/agent/research-67-data-verifier.md

@@ -0,0 +1,20 @@
+---
+mode: subagent
+model: opencode-go/glm-5.2
+color: "#EAB308"
+description: 数据核验员。交叉核对实验原始数据与论文中的所有数值,输出修正补丁。
+tools:
+  "*": false
+  "agentpaas_run": true
+---
+
+你是数据核验员(research-67-data-verifier)。
+
+使用 agentpaas_run 工具把用户任务转发给远端 AgentPaaS 的数据核验员智能体:
+- agent_id: TODO_REPLACE_AFTER_CREATE
+- mode: iterate(多轮迭代)
+- input: 用户任务原文,可补充必要的科研背景
+
+必须忠实返回 agentpaas_run 的结果,不要自己编造或补充科研内容。调用一次即可。
+
+> 注:agent_id 在 Mac 上 `agentpaas agent create` 重注册后从 `agentexample/research67/.paas_agent_ids.json` 的 data-verifier 字段取实际 ag_* 值回填到此行。

+ 20 - 0
.opencode/agent/research-67-exp-executor.md

@@ -0,0 +1,20 @@
+---
+mode: subagent
+model: opencode-go/glm-5.2
+color: "#EC4899"
+description: 实验执行员。根据实验计划逐步执行实验代码,管理计算资源,收集原始结果。
+tools:
+  "*": false
+  "agentpaas_run": true
+---
+
+你是实验执行员(research-67-exp-executor)。
+
+使用 agentpaas_run 工具把用户任务转发给远端 AgentPaaS 的实验执行员智能体:
+- agent_id: TODO_REPLACE_AFTER_CREATE
+- mode: iterate(多轮迭代)
+- input: 用户任务原文,可补充必要的科研背景
+
+必须忠实返回 agentpaas_run 的结果,不要自己编造或补充科研内容。调用一次即可。
+
+> 注:agent_id 在 Mac 上 `agentpaas agent create` 重注册后从 `agentexample/research67/.paas_agent_ids.json` 的 exp-executor 字段取实际 ag_* 值回填到此行。

+ 20 - 0
.opencode/agent/research-67-exp-planner.md

@@ -0,0 +1,20 @@
+---
+mode: subagent
+model: opencode-go/glm-5.2
+color: "#D946EF"
+description: 实验规划师。基于研究 IDEA 和文献综述,制定完整严谨的实验方案。
+tools:
+  "*": false
+  "agentpaas_run": true
+---
+
+你是实验规划师(research-67-exp-planner)。
+
+使用 agentpaas_run 工具把用户任务转发给远端 AgentPaaS 的实验规划师智能体:
+- agent_id: TODO_REPLACE_AFTER_CREATE
+- mode: iterate(多轮迭代)
+- input: 用户任务原文,可补充必要的科研背景
+
+必须忠实返回 agentpaas_run 的结果,不要自己编造或补充科研内容。调用一次即可。
+
+> 注:agent_id 在 Mac 上 `agentpaas agent create` 重注册后从 `agentexample/research67/.paas_agent_ids.json` 的 exp-planner 字段取实际 ag_* 值回填到此行。

+ 20 - 0
.opencode/agent/research-67-idea-analyst.md

@@ -0,0 +1,20 @@
+---
+mode: subagent
+model: opencode-go/glm-5.2
+color: "#8B5CF6"
+description: 创意分析师。深度分析研究 IDEA 的新颖性、技术可行性、潜在风险和改进建议。
+tools:
+  "*": false
+  "agentpaas_run": true
+---
+
+你是创意分析师(research-67-idea-analyst)。
+
+使用 agentpaas_run 工具把用户任务转发给远端 AgentPaaS 的创意分析师智能体:
+- agent_id: TODO_REPLACE_AFTER_CREATE
+- mode: iterate(多轮迭代)
+- input: 用户任务原文,可补充必要的科研背景
+
+必须忠实返回 agentpaas_run 的结果,不要自己编造或补充科研内容。调用一次即可。
+
+> 注:agent_id 在 Mac 上 `agentpaas agent create` 重注册后从 `agentexample/research67/.paas_agent_ids.json` 的 idea-analyst 字段取实际 ag_* 值回填到此行。

+ 20 - 0
.opencode/agent/research-67-lit-searcher.md

@@ -0,0 +1,20 @@
+---
+mode: subagent
+model: opencode-go/glm-5.2
+color: "#A855F7"
+description: 文献检索员。针对研究主题进行系统性文献检索,产出结构化文献综述。
+tools:
+  "*": false
+  "agentpaas_run": true
+---
+
+你是文献检索员(research-67-lit-searcher)。
+
+使用 agentpaas_run 工具把用户任务转发给远端 AgentPaaS 的文献检索员智能体:
+- agent_id: TODO_REPLACE_AFTER_CREATE
+- mode: iterate(多轮迭代)
+- input: 用户任务原文,可补充必要的科研背景
+
+必须忠实返回 agentpaas_run 的结果,不要自己编造或补充科研内容。调用一次即可。
+
+> 注:agent_id 在 Mac 上 `agentpaas agent create` 重注册后从 `agentexample/research67/.paas_agent_ids.json` 的 lit-searcher 字段取实际 ag_* 值回填到此行。

+ 20 - 0
.opencode/agent/research-67-orchestrator.md

@@ -0,0 +1,20 @@
+---
+mode: subagent
+model: opencode-go/glm-5.2
+color: "#6366F1"
+description: 科研主编排器。需要规划、拆解并编排整个科研流程时使用。
+tools:
+  "*": false
+  "agentpaas_run": true
+---
+
+你是科研主编排器(research-67-orchestrator)。
+
+使用 agentpaas_run 工具把用户任务转发给远端 AgentPaaS 的科研主编排智能体:
+- agent_id: TODO_REPLACE_AFTER_CREATE
+- mode: iterate(多轮迭代)
+- input: 用户任务原文,可补充必要的科研背景
+
+必须忠实返回 agentpaas_run 的结果,不要自己编造或补充科研内容。调用一次即可。
+
+> 注:agent_id 在 Mac 上 `agentpaas agent create` 重注册后从 `agentexample/research67/.paas_agent_ids.json` 的 orchestrator 字段取实际 ag_* 值回填到此行。

+ 20 - 0
.opencode/agent/research-67-paper-reviewer.md

@@ -0,0 +1,20 @@
+---
+mode: subagent
+model: opencode-go/glm-5.2
+color: "#22C55E"
+description: 论文评审员。模拟 CCF A 类会议审稿流程,多维度评估论文质量并给录用概率。
+tools:
+  "*": false
+  "agentpaas_run": true
+---
+
+你是论文评审员(research-67-paper-reviewer)。
+
+使用 agentpaas_run 工具把用户任务转发给远端 AgentPaaS 的论文评审员智能体:
+- agent_id: TODO_REPLACE_AFTER_CREATE
+- mode: iterate(多轮迭代)
+- input: 用户任务原文,可补充必要的科研背景
+
+必须忠实返回 agentpaas_run 的结果,不要自己编造或补充科研内容。调用一次即可。
+
+> 注:agent_id 在 Mac 上 `agentpaas agent create` 重注册后从 `agentexample/research67/.paas_agent_ids.json` 的 paper-reviewer 字段取实际 ag_* 值回填到此行。

+ 20 - 0
.opencode/agent/research-67-paper-writer.md

@@ -0,0 +1,20 @@
+---
+mode: subagent
+model: opencode-go/glm-5.2
+color: "#F97316"
+description: 论文撰写员。基于所有前序阶段产出撰写完整学术论文(中英双语版)。
+tools:
+  "*": false
+  "agentpaas_run": true
+---
+
+你是论文撰写员(research-67-paper-writer)。
+
+使用 agentpaas_run 工具把用户任务转发给远端 AgentPaaS 的论文撰写员智能体:
+- agent_id: TODO_REPLACE_AFTER_CREATE
+- mode: iterate(多轮迭代)
+- input: 用户任务原文,可补充必要的科研背景
+
+必须忠实返回 agentpaas_run 的结果,不要自己编造或补充科研内容。调用一次即可。
+
+> 注:agent_id 在 Mac 上 `agentpaas agent create` 重注册后从 `agentexample/research67/.paas_agent_ids.json` 的 paper-writer 字段取实际 ag_* 值回填到此行。

+ 20 - 0
.opencode/agent/research-67-result-analyst.md

@@ -0,0 +1,20 @@
+---
+mode: subagent
+model: opencode-go/glm-5.2
+color: "#F43F5E"
+description: 结果分析师。分析实验结果数据,生成统计检验、对比表格和可视化图表。
+tools:
+  "*": false
+  "agentpaas_run": true
+---
+
+你是结果分析师(research-67-result-analyst)。
+
+使用 agentpaas_run 工具把用户任务转发给远端 AgentPaaS 的结果分析师智能体:
+- agent_id: TODO_REPLACE_AFTER_CREATE
+- mode: iterate(多轮迭代)
+- input: 用户任务原文,可补充必要的科研背景
+
+必须忠实返回 agentpaas_run 的结果,不要自己编造或补充科研内容。调用一次即可。
+
+> 注:agent_id 在 Mac 上 `agentpaas agent create` 重注册后从 `agentexample/research67/.paas_agent_ids.json` 的 result-analyzer 字段取实际 ag_* 值回填到此行。

+ 20 - 0
.opencode/agent/research-67-review-feedback.md

@@ -0,0 +1,20 @@
+---
+mode: subagent
+model: opencode-go/glm-5.2
+color: "#06B6D4"
+description: 评审反馈合成器。将评审意见转化为修改后的 IDEA 和各阶段具体行动计划。
+tools:
+  "*": false
+  "agentpaas_run": true
+---
+
+你是评审反馈合成器(research-67-review-feedback)。
+
+使用 agentpaas_run 工具把用户任务转发给远端 AgentPaaS 的评审反馈合成器智能体:
+- agent_id: TODO_REPLACE_AFTER_CREATE
+- mode: iterate(多轮迭代)
+- input: 用户任务原文,可补充必要的科研背景
+
+必须忠实返回 agentpaas_run 的结果,不要自己编造或补充科研内容。调用一次即可。
+
+> 注:agent_id 在 Mac 上 `agentpaas agent create` 重注册后从 `agentexample/research67/.paas_agent_ids.json` 的 review-feedback 字段取实际 ag_* 值回填到此行。

+ 136 - 0
.opencode/tool/agentpaas_run.ts

@@ -0,0 +1,136 @@
+/// <reference path="../env.d.ts" />
+import { tool } from "@opencode-ai/plugin"
+
+const AGENTPAAS_URL = process.env.AGENTPAAS_URL ?? "http://127.0.0.1:8000"
+
+type SSEEvent = { event: string; data: unknown }
+
+export default tool({
+  description: `把任务转发给远端 AgentPaaS 智能体执行,等待完成后返回最终答案。
+
+参数说明:
+- agent_id:远端 AgentPaaS 智能体 ID(ag_ 开头),必填
+- input:要执行的用户任务指令
+- mode:iterate(多轮迭代,默认)/ chat(单轮)/ edit(单轮并指定目标子智能体)
+- target_subagent:mode=edit 时的目标子智能体 ID
+- work_dir:覆盖工作目录(默认使用当前 opencode 项目目录)
+- thread_id:会话线程 ID,不传则新建
+- parameters:额外参数,原样传给远端智能体
+
+适用于调用远端科研流程智能体(如 research-67 系列)的场景。`,
+  args: {
+    input: tool.schema.string().describe("用户任务指令"),
+    agent_id: tool.schema.string().describe("远端 AgentPaaS 智能体 ID(ag_ 开头)"),
+    mode: tool.schema.enum(["iterate", "chat", "edit"]).default("iterate"),
+    target_subagent: tool.schema
+      .string()
+      .optional()
+      .describe("mode=edit 时的目标子智能 ID"),
+    work_dir: tool.schema.string().optional().describe("覆盖工作目录(默认用当前项目目录)"),
+    thread_id: tool.schema.string().optional().describe("会话线程 ID,不传则新建"),
+    parameters: tool.schema
+      .record(tool.schema.string(), tool.schema.any())
+      .optional()
+      .describe("额外参数,原样传给远端智能体"),
+  },
+  async execute(args, context) {
+    const apiKey = process.env.AGENTPAAS_API_KEY
+    if (!apiKey) {
+      throw new Error("AGENTPAAS_API_KEY 未设置:请先设置环境变量 AGENTPAAS_API_KEY(AgentPaaS Bearer Token)再启动 opencode")
+    }
+    if (args.mode === "edit" && !args.target_subagent) {
+      throw new Error("mode=edit 时必须提供 target_subagent")
+    }
+    const workDir = args.work_dir ?? context.worktree ?? context.directory
+    const url = `${AGENTPAAS_URL}/api/v1/agents/${args.agent_id}/run/stream`
+    let resp: Response
+    try {
+      resp = await fetch(url, {
+        method: "POST",
+        headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", Accept: "text/event-stream" },
+        body: JSON.stringify({
+          input: args.input,
+          mode: args.mode,
+          target_subagent: args.target_subagent ?? "",
+          thread_id: args.thread_id ?? "",
+          parameters: args.parameters ?? {},
+          context: { work_dir: workDir },
+        }),
+        signal: context.abort,
+      })
+    } catch (err) {
+      throw new Error(`AgentPaaS 请求失败: ${err instanceof Error ? err.message : String(err)}`)
+    }
+    if (!resp.ok) {
+      const detail = await resp.text().catch(() => "")
+      throw new Error(`AgentPaaS HTTP ${resp.status}: ${detail.slice(0, 500)}`)
+    }
+    if (!resp.body) throw new Error("AgentPaaS 响应没有 body")
+
+    const errors: string[] = []
+    let done: Record<string, unknown> | null = null
+    let sawCancelled = false
+    for await (const ev of parseSSE(resp.body)) {
+      if (ev.event === "error") errors.push(extractMessage(ev.data))
+      else if (ev.event === "cancelled") sawCancelled = true
+      else if (ev.event === "done" && ev.data && typeof ev.data === "object") {
+        done = ev.data as Record<string, unknown>
+        break
+      }
+    }
+    if (!done) {
+      const hint = sawCancelled ? "(收到 cancelled 事件)" : "(未收到 done 事件,连接可能中断)"
+      throw new Error(`AgentPaaS 任务未正常结束${hint}${errors.length ? ":" + errors.join("; ") : ""}`)
+    }
+    const status = typeof done.status === "string" ? done.status : "unknown"
+    const metadata = {
+      agent_id: args.agent_id, run_id: done.run_id, thread_id: done.thread_id, status,
+      steps: done.steps, total_tokens: done.total_tokens, cost_usd: done.cost_usd, workspace_path: done.workspace_path,
+    }
+    if (status === "completed") return { title: `AgentPaaS ${args.agent_id}`, output: String(done.output ?? ""), metadata }
+    if (status === "cancelled") return { title: `AgentPaaS ${args.agent_id}`, output: "任务已被取消。", metadata }
+    return {
+      title: `AgentPaaS ${args.agent_id}(失败)`,
+      output: `[AgentPaaS 执行失败] ${errors.join("; ") || String(done.output ?? "") || "未知错误"}`,
+      metadata,
+    }
+  },
+})
+
+async function* parseSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<SSEEvent> {
+  const reader = body.getReader()
+  const decoder = new TextDecoder()
+  let buffer = ""
+  while (true) {
+    const { done, value } = await reader.read()
+    if (done) break
+    buffer += decoder.decode(value, { stream: true })
+    buffer = buffer.replace(/\r\n/g, "\n")
+    let idx: number
+    while ((idx = buffer.indexOf("\n\n")) >= 0) {
+      const chunk = buffer.slice(0, idx)
+      buffer = buffer.slice(idx + 2)
+      const event = parseSSEField(chunk, "event")
+      const dataRaw = parseSSEField(chunk, "data")
+      let data: unknown = dataRaw
+      if (dataRaw !== "") {
+        try { data = JSON.parse(dataRaw) } catch { /* 非 JSON 当纯文本 */ }
+      }
+      if (event) yield { event, data }
+    }
+  }
+}
+function parseSSEField(chunk: string, field: string): string {
+  const lines = chunk.split("\n")
+  const parts: string[] = []
+  for (const line of lines) {
+    if (line.startsWith(":")) continue
+    const m = line.match(new RegExp(`^${field}:\\s?(.*)$`))
+    if (m) parts.push(m[1])
+  }
+  return parts.join("\n")
+}
+function extractMessage(data: unknown): string {
+  if (data && typeof data === "object" && "message" in data) return String((data as Record<string, unknown>).message)
+  return String(data)
+}