// plugins/agent-browser-tools.ts import { tool } from "@opencode-ai/plugin/tool"; function hostApiBaseUrl() { const value = process.env.NIANCODE_HOST_API_BASE_URL?.trim(); if (!value) throw new Error("Makelore \u5F00\u53D1\u6D4F\u89C8\u5668\u670D\u52A1\u5C1A\u672A\u542F\u52A8\u3002"); return value.replace(/\/+$/, ""); } function hostApiToken() { const value = process.env.NIANCODE_HOST_API_TOKEN?.trim(); if (!value) throw new Error("Makelore \u5F00\u53D1\u6D4F\u89C8\u5668\u51ED\u8BC1\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u542F\u5BA2\u6237\u7AEF\u540E\u518D\u8BD5\u3002"); return value; } function browserError(payload, status) { const code = typeof payload.code === "string" ? payload.code : ""; const message = typeof payload.message === "string" ? payload.message : typeof payload.error === "string" ? payload.error : ""; const fallback = { PROJECT_NOT_ACTIVE: "\u8BF7\u5148\u5728 Makelore \u4E2D\u6253\u5F00\u8981\u8C03\u8BD5\u7684\u9879\u76EE\u3002", PROJECT_MISMATCH: "\u667A\u80FD\u4F53\u53EA\u80FD\u8C03\u8BD5\u5F53\u524D\u6253\u5F00\u7684\u9879\u76EE\u3002", BROWSER_NOT_OPEN: "\u5F00\u53D1\u6D4F\u89C8\u5668\u5C1A\u672A\u6253\u5F00\uFF0C\u8BF7\u5148\u8C03\u7528 browser_context \u7684 open\u3002", VIEWPORT_NOT_READY: "\u5F00\u53D1\u6D4F\u89C8\u5668\u663E\u793A\u533A\u57DF\u5C1A\u672A\u51C6\u5907\u597D\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", DEBUGGER_BUSY: "\u5F00\u53D1\u6D4F\u89C8\u5668\u8C03\u8BD5\u5668\u6B63\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", ATTACH_FAILED: "\u65E0\u6CD5\u8FDE\u63A5\u5F00\u53D1\u6D4F\u89C8\u5668\u8C03\u8BD5\u5668\uFF0C\u8BF7\u5173\u95ED\u6D4F\u89C8\u5668\u9762\u677F\u540E\u91CD\u65B0\u6253\u5F00\u3002", DEVTOOLS_CONFLICT: "\u5F53\u524D\u9875\u9762\u7684\u539F\u751F\u5F00\u53D1\u8005\u5DE5\u5177\u5DF2\u6253\u5F00\uFF1B\u5173\u95ED\u5B83\u540E\u667A\u80FD\u4F53\u624D\u80FD\u7EE7\u7EED\u8C03\u8BD5\u3002", CDP_METHOD_BLOCKED: "\u8BE5 CDP \u547D\u4EE4\u4F1A\u5F71\u54CD Makelore \u5BA2\u6237\u7AEF\u6216\u5176\u4ED6\u9875\u9762\uFF0C\u5DF2\u88AB\u963B\u6B62\u3002", TARGET_DENIED: "\u4E0D\u80FD\u8BBF\u95EE\u5F53\u524D\u9879\u76EE\u6D4F\u89C8\u5668\u4E4B\u5916\u7684\u9875\u9762\u76EE\u6807\u3002", CDP_TIMEOUT: "CDP \u547D\u4EE4\u6267\u884C\u8D85\u65F6\u4E14\u7ED3\u679C\u672A\u77E5\uFF1B\u5F00\u53D1\u6D4F\u89C8\u5668\u5DF2\u5173\u95ED\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u9875\u9762\u5E76\u6838\u5BF9\u72B6\u6001\u3002", CURSOR_EXPIRED: "\u90E8\u5206\u8F83\u65E9\u7684\u6D4F\u89C8\u5668\u4E8B\u4EF6\u5DF2\u88AB\u6E05\u7406\uFF0C\u8BF7\u4ECE\u8FD4\u56DE\u7684\u65B0\u6E38\u6807\u7EE7\u7EED\u8BFB\u53D6\u3002", PAYLOAD_NOT_FOUND: "\u6D4F\u89C8\u5668\u6570\u636E\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u4EA7\u751F\u8BE5\u6570\u636E\u7684\u547D\u4EE4\u3002", TARGET_GONE: "\u9875\u9762\u76EE\u6807\u5DF2\u7ECF\u5173\u95ED\u6216\u5237\u65B0\uFF0C\u8BF7\u91CD\u65B0\u83B7\u53D6\u9875\u9762\u72B6\u6001\u3002", RENDERER_CRASHED: "\u5F00\u53D1\u6D4F\u89C8\u5668\u9875\u9762\u5DF2\u5D29\u6E83\uFF0C\u8BF7\u5173\u95ED\u540E\u91CD\u65B0\u6253\u5F00\u3002" }; if (code && fallback[code]) return fallback[code]; if (message) return message; if (status === 401) return "\u5F00\u53D1\u6D4F\u89C8\u5668\u51ED\u8BC1\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u542F Makelore \u540E\u518D\u8BD5\u3002"; return `\u5F00\u53D1\u6D4F\u89C8\u5668\u64CD\u4F5C\u5931\u8D25\uFF08HTTP ${status}\uFF09\u3002`; } async function hostRequest(path, options, signal) { let response; try { response = await fetch(`${hostApiBaseUrl()}${path}`, { method: options.method ?? "POST", headers: { Authorization: `Bearer ${hostApiToken()}`, Accept: "application/json", ...options.body ? { "Content-Type": "application/json" } : {} }, ...options.body ? { body: JSON.stringify(options.body) } : {}, signal }); } catch (error) { if (signal.aborted) throw new Error("\u5F00\u53D1\u6D4F\u89C8\u5668\u64CD\u4F5C\u5DF2\u53D6\u6D88\u3002", { cause: error }); const detail = error instanceof Error ? error.message : String(error); throw new Error(`\u65E0\u6CD5\u8FDE\u63A5 Makelore \u5F00\u53D1\u6D4F\u89C8\u5668\u670D\u52A1\uFF1A${detail}`, { cause: error }); } const payload = await response.json().catch(() => ({})); if (!response.ok || payload.success !== true) { throw new Error(browserError(payload, response.status)); } return payload; } function result(title, payload) { return { title, output: JSON.stringify(payload, null, 2), metadata: { source: "niancode-agent-browser" } }; } var NianCodeAgentBrowserPlugin = async () => ({ tool: { browser_context: tool({ description: "Open, inspect, close, or reset the shared Makelore development browser for the current project. The browser is visible to the user and shares its page state with the agent. Use open with a local or public development URL before other browser tools.", args: { action: tool.schema.enum(["open", "status", "close", "reset_profile"]).describe("Browser lifecycle action"), url: tool.schema.string().optional().describe("URL to open. Required only for action=open.") }, async execute(args, context) { const projectPath = context.directory; if (args.action === "status") { const query = new URLSearchParams({ project_path: projectPath }); const payload2 = await hostRequest( `/api/agent-browser/state?${query.toString()}`, { method: "GET" }, context.abort ); return result("\u5F00\u53D1\u6D4F\u89C8\u5668\u72B6\u6001", payload2.browser); } if (args.action === "open") { const url = args.url?.trim(); if (!url) throw new Error("\u6253\u5F00\u5F00\u53D1\u6D4F\u89C8\u5668\u65F6\u5FC5\u987B\u63D0\u4F9B url\u3002"); const payload2 = await hostRequest("/api/agent-browser/open", { body: { project_path: projectPath, url } }, context.abort); return result("\u5DF2\u6253\u5F00\u5F00\u53D1\u6D4F\u89C8\u5668", payload2.browser); } const path = args.action === "close" ? "/api/agent-browser/close" : "/api/agent-browser/reset-profile"; const payload = await hostRequest(path, { body: { project_path: projectPath } }, context.abort); return result( args.action === "close" ? "\u5DF2\u5173\u95ED\u5F00\u53D1\u6D4F\u89C8\u5668" : "\u5DF2\u6E05\u7A7A\u5F00\u53D1\u6D4F\u89C8\u5668\u6570\u636E", payload.browser ); } }), browser_navigate: tool({ description: "Navigate the shared development browser for the current project. Supports opening a URL, back, forward, and reload.", args: { action: tool.schema.enum(["url", "back", "forward", "reload"]).describe("Navigation action"), url: tool.schema.string().optional().describe("Destination URL. Required only for action=url.") }, async execute(args, context) { const url = args.url?.trim(); if (args.action === "url" && !url) { throw new Error("\u8DF3\u8F6C\u7F51\u9875\u65F6\u5FC5\u987B\u63D0\u4F9B url\u3002"); } const payload = await hostRequest("/api/agent-browser/navigate", { body: { project_path: context.directory, action: args.action, ...url ? { url } : {} } }, context.abort); return result("\u5F00\u53D1\u6D4F\u89C8\u5668\u5DF2\u5BFC\u822A", payload.browser); } }), browser_cdp: tool({ description: "Use Full Chrome DevTools Protocol on the current project browser. send forwards one CDP method and params without summarizing the result; read_events returns raw buffered CDP events after a cursor; read_payload reads a chunk referenced by a previous result.", args: { action: tool.schema.enum(["send", "read_events", "read_payload"]).describe("CDP operation"), method: tool.schema.string().optional().describe("Full CDP method name, required for send (for example Runtime.evaluate)"), params: tool.schema.object({}).passthrough().optional().describe("Raw CDP params for send"), session_ref: tool.schema.string().optional().describe("Child-target session reference returned by this browser"), timeout_ms: tool.schema.number().int().positive().optional().describe("Optional CDP command timeout"), after: tool.schema.number().int().nonnegative().optional().describe("Read events with sequence greater than this cursor"), methods: tool.schema.array(tool.schema.string()).optional().describe("Optional exact CDP event method filters"), limit: tool.schema.number().int().positive().optional().describe("Maximum events to return"), wait_ms: tool.schema.number().int().nonnegative().optional().describe("Optional event long-poll duration"), handle: tool.schema.string().optional().describe("Payload handle, required for read_payload"), offset: tool.schema.number().int().nonnegative().optional().describe("Payload byte offset"), max_bytes: tool.schema.number().int().positive().optional().describe("Maximum payload bytes in this chunk") }, async execute(args, context) { if (args.action === "send") { const method = args.method?.trim(); if (!method) throw new Error("\u53D1\u9001 CDP \u547D\u4EE4\u65F6\u5FC5\u987B\u63D0\u4F9B method\u3002"); const payload2 = await hostRequest("/api/agent-browser/cdp/send", { body: { project_path: context.directory, method, ...args.params ? { params: args.params } : {}, ...args.session_ref ? { session_ref: args.session_ref } : {}, ...args.timeout_ms !== void 0 ? { timeout_ms: args.timeout_ms } : {} } }, context.abort); return result(`CDP ${method}`, payload2.result); } if (args.action === "read_events") { const payload2 = await hostRequest("/api/agent-browser/cdp/events", { body: { project_path: context.directory, ...args.after !== void 0 ? { after: args.after } : {}, ...args.methods ? { methods: args.methods } : {}, ...args.limit !== void 0 ? { limit: args.limit } : {}, ...args.wait_ms !== void 0 ? { wait_ms: args.wait_ms } : {} } }, context.abort); return result("CDP \u4E8B\u4EF6", payload2.page); } const handle = args.handle?.trim(); if (!handle) throw new Error("\u8BFB\u53D6\u5927\u578B CDP \u6570\u636E\u65F6\u5FC5\u987B\u63D0\u4F9B handle\u3002"); const payload = await hostRequest("/api/agent-browser/payload/read", { body: { project_path: context.directory, handle, ...args.offset !== void 0 ? { offset: args.offset } : {}, ...args.max_bytes !== void 0 ? { max_bytes: args.max_bytes } : {} } }, context.abort); return result("CDP \u6570\u636E\u5206\u5757", payload.chunk); } }) } }); export { NianCodeAgentBrowserPlugin };