feat: 增加共享 Agent Browser 调试能力
需求:在 AI 编程会话中让用户与 Agent 共享同一浏览器页面,并查看控制台与网络信息。 实现:新增沙箱浏览器内核、Host API/渲染器面板、OpenCode 工具接入及安全边界测试。
This commit is contained in:
223
.opencode/plugins/agent-browser-tools.ts
Normal file
223
.opencode/plugins/agent-browser-tools.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { tool } from '@opencode-ai/plugin/tool';
|
||||
|
||||
type HostPayload = Record<string, unknown>;
|
||||
|
||||
function hostApiBaseUrl(): string {
|
||||
const value = process.env.NIANCODE_HOST_API_BASE_URL?.trim();
|
||||
if (!value) throw new Error('Makelore 开发浏览器服务尚未启动。');
|
||||
return value.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function hostApiToken(): string {
|
||||
const value = process.env.NIANCODE_HOST_API_TOKEN?.trim();
|
||||
if (!value) throw new Error('Makelore 开发浏览器凭证不可用,请重启客户端后再试。');
|
||||
return value;
|
||||
}
|
||||
|
||||
function browserError(payload: HostPayload, status: number): string {
|
||||
const code = typeof payload.code === 'string' ? payload.code : '';
|
||||
const message = typeof payload.message === 'string'
|
||||
? payload.message
|
||||
: typeof payload.error === 'string'
|
||||
? payload.error
|
||||
: '';
|
||||
const fallback: Record<string, string> = {
|
||||
PROJECT_NOT_ACTIVE: '请先在 Makelore 中打开要调试的项目。',
|
||||
PROJECT_MISMATCH: '智能体只能调试当前打开的项目。',
|
||||
BROWSER_NOT_OPEN: '开发浏览器尚未打开,请先调用 browser_context 的 open。',
|
||||
VIEWPORT_NOT_READY: '开发浏览器显示区域尚未准备好,请稍后重试。',
|
||||
DEBUGGER_BUSY: '开发浏览器调试器正忙,请稍后重试。',
|
||||
ATTACH_FAILED: '无法连接开发浏览器调试器,请关闭浏览器面板后重新打开。',
|
||||
DEVTOOLS_CONFLICT: '当前页面的原生开发者工具已打开;关闭它后智能体才能继续调试。',
|
||||
CDP_METHOD_BLOCKED: '该 CDP 命令会影响 Makelore 客户端或其他页面,已被阻止。',
|
||||
TARGET_DENIED: '不能访问当前项目浏览器之外的页面目标。',
|
||||
CDP_TIMEOUT: 'CDP 命令执行超时且结果未知;开发浏览器已关闭,请重新打开页面并核对状态。',
|
||||
CURSOR_EXPIRED: '部分较早的浏览器事件已被清理,请从返回的新游标继续读取。',
|
||||
PAYLOAD_NOT_FOUND: '浏览器数据已过期,请重新执行产生该数据的命令。',
|
||||
TARGET_GONE: '页面目标已经关闭或刷新,请重新获取页面状态。',
|
||||
RENDERER_CRASHED: '开发浏览器页面已崩溃,请关闭后重新打开。',
|
||||
};
|
||||
if (code && fallback[code]) return fallback[code];
|
||||
if (message) return message;
|
||||
if (status === 401) return '开发浏览器凭证已失效,请重启 Makelore 后再试。';
|
||||
return `开发浏览器操作失败(HTTP ${status})。`;
|
||||
}
|
||||
|
||||
async function hostRequest(
|
||||
path: string,
|
||||
options: { method?: 'GET' | 'POST'; body?: HostPayload },
|
||||
signal: AbortSignal,
|
||||
): Promise<HostPayload> {
|
||||
let response: 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('开发浏览器操作已取消。', { cause: error });
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`无法连接 Makelore 开发浏览器服务:${detail}`, { cause: error });
|
||||
}
|
||||
|
||||
const payload = await response.json().catch(() => ({})) as HostPayload;
|
||||
if (!response.ok || payload.success !== true) {
|
||||
throw new Error(browserError(payload, response.status));
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function result(title: string, payload: unknown) {
|
||||
return {
|
||||
title,
|
||||
output: JSON.stringify(payload, null, 2),
|
||||
metadata: { source: 'niancode-agent-browser' },
|
||||
};
|
||||
}
|
||||
|
||||
export const 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 payload = await hostRequest(
|
||||
`/api/agent-browser/state?${query.toString()}`,
|
||||
{ method: 'GET' },
|
||||
context.abort,
|
||||
);
|
||||
return result('开发浏览器状态', payload.browser);
|
||||
}
|
||||
|
||||
if (args.action === 'open') {
|
||||
const url = args.url?.trim();
|
||||
if (!url) throw new Error('打开开发浏览器时必须提供 url。');
|
||||
const payload = await hostRequest('/api/agent-browser/open', {
|
||||
body: { project_path: projectPath, url },
|
||||
}, context.abort);
|
||||
return result('已打开开发浏览器', payload.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' ? '已关闭开发浏览器' : '已清空开发浏览器数据',
|
||||
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('跳转网页时必须提供 url。');
|
||||
}
|
||||
const payload = await hostRequest('/api/agent-browser/navigate', {
|
||||
body: {
|
||||
project_path: context.directory,
|
||||
action: args.action,
|
||||
...(url ? { url } : {}),
|
||||
},
|
||||
}, context.abort);
|
||||
return result('开发浏览器已导航', 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('发送 CDP 命令时必须提供 method。');
|
||||
const payload = 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 !== undefined ? { timeout_ms: args.timeout_ms } : {}),
|
||||
},
|
||||
}, context.abort);
|
||||
return result(`CDP ${method}`, payload.result);
|
||||
}
|
||||
|
||||
if (args.action === 'read_events') {
|
||||
const payload = await hostRequest('/api/agent-browser/cdp/events', {
|
||||
body: {
|
||||
project_path: context.directory,
|
||||
...(args.after !== undefined ? { after: args.after } : {}),
|
||||
...(args.methods ? { methods: args.methods } : {}),
|
||||
...(args.limit !== undefined ? { limit: args.limit } : {}),
|
||||
...(args.wait_ms !== undefined ? { wait_ms: args.wait_ms } : {}),
|
||||
},
|
||||
}, context.abort);
|
||||
return result('CDP 事件', payload.page);
|
||||
}
|
||||
|
||||
const handle = args.handle?.trim();
|
||||
if (!handle) throw new Error('读取大型 CDP 数据时必须提供 handle。');
|
||||
const payload = await hostRequest('/api/agent-browser/payload/read', {
|
||||
body: {
|
||||
project_path: context.directory,
|
||||
handle,
|
||||
...(args.offset !== undefined ? { offset: args.offset } : {}),
|
||||
...(args.max_bytes !== undefined ? { max_bytes: args.max_bytes } : {}),
|
||||
},
|
||||
}, context.abort);
|
||||
return result('CDP 数据分块', payload.chunk);
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
// 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
|
||||
};
|
||||
35
.opencode/skills/agent-browser/SKILL.md
Normal file
35
.opencode/skills/agent-browser/SKILL.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: agent-browser
|
||||
description: 当需要打开、查看或调试当前 Makelore 项目的本地或公网网页,读取 Console、Network、DOM、样式、性能信息,执行页面脚本或刷新验证修改时使用。
|
||||
---
|
||||
|
||||
# Makelore 开发浏览器
|
||||
|
||||
开发浏览器是用户和智能体共享的项目调试界面。它只用于开发过程,不代表作品已经发布。
|
||||
|
||||
## 基本流程
|
||||
|
||||
1. 确认项目的开发服务已经启动,并取得本地网页地址。
|
||||
2. 调用 `browser_context` 的 `open` 打开该地址。
|
||||
3. 调用 `browser_cdp` 的 `read_events` 读取 Console 与 Network 事件。
|
||||
4. 需要主动检查页面时,使用 `browser_cdp` 的 `send` 调用标准 CDP 方法。
|
||||
5. 修改代码后调用 `browser_navigate` 的 `reload`,再读取新事件验证结果。
|
||||
|
||||
## 工具选择
|
||||
|
||||
- `browser_context`:打开、查看状态、关闭浏览器或清空当前项目的浏览器数据。
|
||||
- `browser_navigate`:跳转、后退、前进或刷新共享页面。
|
||||
- `browser_cdp`:
|
||||
- `send` 原样执行当前项目页面的 CDP 命令;
|
||||
- `read_events` 按游标读取原始事件,不会消费其他读取者的数据;
|
||||
- `read_payload` 分块读取响应体、截图等大型结果。
|
||||
|
||||
## 约束
|
||||
|
||||
- 不要传递或猜测 `projectId`、`tabId`、`targetId`、`webContentsId`;项目身份由当前会话目录自动绑定。
|
||||
- 不要通过远程调试端口、`curl` 或另一个浏览器绕过这些工具。
|
||||
- 页面刷新或调试器重连后,旧的 DOM node、RemoteObject 和子 Target 引用可能失效,应重新获取。
|
||||
- 开发浏览器面板收起或被模态框遮挡时,智能体调试会暂停;等待用户重新显示页面后再继续。
|
||||
- `CDP_TIMEOUT` 表示结果未知,客户端会关闭该浏览器以清理悬挂命令;不要盲目重复可能产生副作用的命令,先重新打开页面并核对状态。
|
||||
- 原生 DevTools 与智能体调试冲突时,请用户关闭该页面的原生 DevTools 后再继续。
|
||||
- Console、请求头、响应体、Cookie 和页面内容可能包含敏感信息,只在完成当前调试所必需时读取和展示。
|
||||
Reference in New Issue
Block a user