|
|
@@ -1,13 +1,14 @@
|
|
|
/// <reference path="../env.d.ts" />
|
|
|
import { tool } from "@opencode-ai/plugin"
|
|
|
+import { mkdir } from "node:fs/promises"
|
|
|
import { homedir } from "node:os"
|
|
|
import path from "node:path"
|
|
|
|
|
|
-const AGENTPAAS_CONFIG_PATH = path.join(homedir(), ".agentpaas", "config.json")
|
|
|
const DEFAULT_AGENTPAAS_URL = "http://127.0.0.1:8000"
|
|
|
|
|
|
type SSEEvent = { event: string; data: unknown }
|
|
|
type AgentListItem = { id: string; name: string }
|
|
|
+type ConversationState = { threadID: string; runID: string }
|
|
|
type ProgressStatus = "completed" | "running" | "pending" | "failed"
|
|
|
type ProgressMilestone = {
|
|
|
id: string
|
|
|
@@ -32,8 +33,9 @@ export default tool({
|
|
|
- input:要执行的用户任务指令
|
|
|
- mode:iterate(多轮迭代,默认)/ chat(单轮)/ edit(单轮并指定目标子智能体)
|
|
|
- target_subagent:mode=edit 时的目标子智能体 ID
|
|
|
-- work_dir:覆盖工作目录(默认使用当前 opencode 项目目录)
|
|
|
-- thread_id:会话线程 ID,不传则新建
|
|
|
+- work_dir:可选的服务端可见工作目录;留空时由 AgentPaaS 创建隔离 workspace
|
|
|
+- conversation:continue(默认,复用本 OpenCode session 的远端 thread/workspace)/ new(新课题)
|
|
|
+- thread_id:可选的远端会话线程 ID;显式值优先于自动映射
|
|
|
- parameters:额外参数,原样传给远端智能体
|
|
|
|
|
|
适用于调用远端科研流程智能体(如 research-67 系列)的场景。`,
|
|
|
@@ -42,8 +44,9 @@ export default tool({
|
|
|
agent_name: tool.schema.string().describe("远端 AgentPaaS 智能体名称"),
|
|
|
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,不传则新建"),
|
|
|
+ work_dir: tool.schema.string().optional().describe("服务端可见工作目录;通常留空,由 AgentPaaS 创建"),
|
|
|
+ thread_id: tool.schema.string().optional().describe("远端会话线程 ID;通常留空,由 OpenCode session 自动维护"),
|
|
|
+ conversation: tool.schema.enum(["continue", "new"]).default("continue").describe("复用当前对话或开始新课题"),
|
|
|
parameters: tool.schema
|
|
|
.record(tool.schema.string(), tool.schema.any())
|
|
|
.optional()
|
|
|
@@ -55,7 +58,19 @@ export default tool({
|
|
|
throw new Error("mode=edit 时必须提供 target_subagent")
|
|
|
}
|
|
|
const agentID = await resolveAgentID(config, args.agent_name, context.abort)
|
|
|
- const workDir = args.work_dir ?? context.worktree ?? context.directory
|
|
|
+ const conversation = args.conversation ?? "continue"
|
|
|
+ const conversationKey = `${config.url}\n${agentID}`
|
|
|
+ const savedConversation =
|
|
|
+ conversation === "new" ? undefined : await loadConversationState(context.sessionID, conversationKey)
|
|
|
+ const threadID = args.thread_id?.trim() || savedConversation?.threadID || ""
|
|
|
+ const continueRunID =
|
|
|
+ conversation === "continue" &&
|
|
|
+ (!args.thread_id?.trim() || args.thread_id.trim() === savedConversation?.threadID)
|
|
|
+ ? savedConversation?.runID || ""
|
|
|
+ : ""
|
|
|
+ // OpenCode 的宿主机目录不会自动挂载进 AgentPaaS Docker。只有调用方
|
|
|
+ // 明确给出服务端可见路径时才覆盖,否则让 PaaS 创建并隔离 run workspace。
|
|
|
+ const workDir = args.work_dir
|
|
|
const url = `${config.url}/api/v1/agents/${encodeURIComponent(agentID)}/run/stream`
|
|
|
let resp: Response
|
|
|
try {
|
|
|
@@ -70,9 +85,12 @@ export default tool({
|
|
|
input: args.input,
|
|
|
mode: args.mode,
|
|
|
target_subagent: args.target_subagent ?? "",
|
|
|
- thread_id: args.thread_id ?? "",
|
|
|
+ thread_id: threadID,
|
|
|
parameters: args.parameters ?? {},
|
|
|
- context: { work_dir: workDir },
|
|
|
+ context: {
|
|
|
+ ...(workDir ? { work_dir: workDir } : {}),
|
|
|
+ ...(continueRunID ? { run_id: continueRunID } : {}),
|
|
|
+ },
|
|
|
}),
|
|
|
signal: context.abort,
|
|
|
})
|
|
|
@@ -114,6 +132,13 @@ export default tool({
|
|
|
const data = asRecord(ev.data)
|
|
|
if (ev.event === "started") {
|
|
|
startedRunID = getString(data?.run_id)
|
|
|
+ const remoteThreadID = getString(data?.thread_id)
|
|
|
+ if (remoteThreadID && startedRunID) {
|
|
|
+ await saveConversationState(context.sessionID, conversationKey, {
|
|
|
+ threadID: remoteThreadID,
|
|
|
+ runID: startedRunID,
|
|
|
+ })
|
|
|
+ }
|
|
|
if (context.abort.aborted) cancelOnAbort()
|
|
|
updateMetadata("Research67 正在运行", {
|
|
|
run_id: data?.run_id,
|
|
|
@@ -320,8 +345,60 @@ async function resolveAgentID(config: { url: string; apiKey: string }, agentName
|
|
|
return matches[0].id
|
|
|
}
|
|
|
|
|
|
+const conversationCache = new Map<string, ConversationState>()
|
|
|
+
|
|
|
+async function loadConversationState(sessionID: string, key: string): Promise<ConversationState | undefined> {
|
|
|
+ const cacheKey = `${sessionID}\n${key}`
|
|
|
+ const cached = conversationCache.get(cacheKey)
|
|
|
+ if (cached) return cached
|
|
|
+
|
|
|
+ const file = Bun.file(conversationStatePath(sessionID))
|
|
|
+ if (!(await file.exists())) return undefined
|
|
|
+ try {
|
|
|
+ const parsed: unknown = await file.json()
|
|
|
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined
|
|
|
+ const value = (parsed as Record<string, unknown>)[key]
|
|
|
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined
|
|
|
+ const threadID = getString((value as Record<string, unknown>).thread_id)
|
|
|
+ const runID = getString((value as Record<string, unknown>).run_id)
|
|
|
+ if (!threadID || !runID) return undefined
|
|
|
+ const state = { threadID, runID }
|
|
|
+ conversationCache.set(cacheKey, state)
|
|
|
+ return state
|
|
|
+ } catch {
|
|
|
+ return undefined
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function saveConversationState(sessionID: string, key: string, state: ConversationState) {
|
|
|
+ conversationCache.set(`${sessionID}\n${key}`, state)
|
|
|
+ const filePath = conversationStatePath(sessionID)
|
|
|
+ const file = Bun.file(filePath)
|
|
|
+ let saved: Record<string, unknown> = {}
|
|
|
+ if (await file.exists()) {
|
|
|
+ try {
|
|
|
+ const parsed: unknown = await file.json()
|
|
|
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) saved = parsed as Record<string, unknown>
|
|
|
+ } catch {
|
|
|
+ saved = {}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ saved[key] = { thread_id: state.threadID, run_id: state.runID }
|
|
|
+ try {
|
|
|
+ await mkdir(agentPaaSConversationDir(), { recursive: true, mode: 0o700 })
|
|
|
+ await Bun.write(filePath, `${JSON.stringify(saved, null, 2)}\n`)
|
|
|
+ } catch {
|
|
|
+ // The in-memory mapping still preserves continuity for this CLI process.
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function conversationStatePath(sessionID: string) {
|
|
|
+ return path.join(agentPaaSConversationDir(), `${sessionID.replace(/[^A-Za-z0-9._-]/g, "_")}.json`)
|
|
|
+}
|
|
|
+
|
|
|
async function loadAgentPaaSConfig() {
|
|
|
- const file = Bun.file(AGENTPAAS_CONFIG_PATH)
|
|
|
+ const configPath = agentPaaSConfigPath()
|
|
|
+ const file = Bun.file(configPath)
|
|
|
let saved: Record<string, unknown> = {}
|
|
|
if (await file.exists()) {
|
|
|
try {
|
|
|
@@ -330,7 +407,7 @@ async function loadAgentPaaSConfig() {
|
|
|
saved = parsed as Record<string, unknown>
|
|
|
} catch (err) {
|
|
|
throw new Error(
|
|
|
- `AgentPaaS 配置文件无效:${AGENTPAAS_CONFIG_PATH}\n${err instanceof Error ? err.message : String(err)}`,
|
|
|
+ `AgentPaaS 配置文件无效:${configPath}\n${err instanceof Error ? err.message : String(err)}`,
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
@@ -341,7 +418,7 @@ async function loadAgentPaaSConfig() {
|
|
|
const apiKey = process.env.AGENTPAAS_API_KEY?.trim() || savedApiKey
|
|
|
if (apiKey) return { url, apiKey }
|
|
|
|
|
|
- throw new Error(`AgentPaaS 尚未配置。请创建 ${AGENTPAAS_CONFIG_PATH}:
|
|
|
+ throw new Error(`AgentPaaS 尚未配置。请创建 ${configPath}:
|
|
|
{
|
|
|
"server": "${DEFAULT_AGENTPAAS_URL}",
|
|
|
"api_key": "你的 AgentPaaS API Key"
|
|
|
@@ -349,6 +426,18 @@ async function loadAgentPaaSConfig() {
|
|
|
也可以通过 AGENTPAAS_URL 和 AGENTPAAS_API_KEY 环境变量临时覆盖。`)
|
|
|
}
|
|
|
|
|
|
+function agentPaaSHome() {
|
|
|
+ return process.env.HOME?.trim() || homedir()
|
|
|
+}
|
|
|
+
|
|
|
+function agentPaaSConfigPath() {
|
|
|
+ return path.join(agentPaaSHome(), ".agentpaas", "config.json")
|
|
|
+}
|
|
|
+
|
|
|
+function agentPaaSConversationDir() {
|
|
|
+ return path.join(agentPaaSHome(), ".agentpaas", "opencode-conversations")
|
|
|
+}
|
|
|
+
|
|
|
async function resolveConfirmation(
|
|
|
config: { url: string; apiKey: string },
|
|
|
runID: string,
|