feat: 增加共享 Agent Browser 调试能力
需求:在 AI 编程会话中让用户与 Agent 共享同一浏览器页面,并查看控制台与网络信息。 实现:新增沙箱浏览器内核、Host API/渲染器面板、OpenCode 工具接入及安全边界测试。
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
} from '../opencode/playwright-mcp';
|
||||
import { resolveOpencodeRuntimePaths } from '../opencode/paths';
|
||||
import {
|
||||
resolveBundledAgentBrowserPluginPath,
|
||||
resolveBundledCourseSkillsDir,
|
||||
resolveBundledSuperpowersDir,
|
||||
} from '../opencode/superpowers';
|
||||
@@ -61,6 +62,7 @@ import { acquireProcessInstanceFileLock } from './process-instance-lock';
|
||||
|
||||
import { getHostApiToken, startHostApiServer } from '../api/server';
|
||||
import { HostEventBus } from '../api/event-bus';
|
||||
import { AgentBrowserModule, ElectronAgentBrowserAdapter } from '../agent-browser';
|
||||
import { browserOAuthManager } from '../utils/browser-oauth';
|
||||
import { createProjectProgressSync } from '../services/project-progress-sync';
|
||||
import { createWorksCloudDeployment } from '../services/works-cloud-deployment';
|
||||
@@ -148,6 +150,7 @@ let hostEventBus!: HostEventBus;
|
||||
let hostApiServer: Server | null = null;
|
||||
let projectProgressSync: ReturnType<typeof createProjectProgressSync> | null = null;
|
||||
let worksCloudDeployment: ReturnType<typeof createWorksCloudDeployment> | null = null;
|
||||
let agentBrowser: AgentBrowserModule | null = null;
|
||||
const mainWindowFocusState = createMainWindowFocusState();
|
||||
const quitLifecycleState = createQuitLifecycleState();
|
||||
const launchDeepLinkUrl = findNianCodeDeepLinkUrl(process.argv);
|
||||
@@ -309,6 +312,23 @@ function registerMakeloreProtocolClient(): void {
|
||||
function createMainWindow(): BrowserWindow {
|
||||
const win = createWindow();
|
||||
|
||||
const closeAgentBrowserForHostRenderer = (reason: string): void => {
|
||||
void agentBrowser?.close().catch((error) => {
|
||||
logger.warn(`Failed to close Agent Browser after ${reason}:`, error);
|
||||
});
|
||||
};
|
||||
win.webContents.on('render-process-gone', () => {
|
||||
closeAgentBrowserForHostRenderer('the main renderer exited');
|
||||
});
|
||||
win.webContents.on(
|
||||
'did-start-navigation',
|
||||
(_event, _url, isInPlace, isMainFrame) => {
|
||||
if (isMainFrame && !isInPlace) {
|
||||
closeAgentBrowserForHostRenderer('the main renderer started navigation');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
win.once('ready-to-show', () => {
|
||||
if (mainWindow !== win) {
|
||||
return;
|
||||
@@ -331,6 +351,11 @@ function createMainWindow(): BrowserWindow {
|
||||
});
|
||||
|
||||
win.on('closed', () => {
|
||||
const browser = agentBrowser;
|
||||
agentBrowser = null;
|
||||
void browser?.dispose().catch((error) => {
|
||||
logger.warn('Failed to dispose Agent Browser after the main window closed:', error);
|
||||
});
|
||||
if (mainWindow === win) {
|
||||
mainWindow = null;
|
||||
}
|
||||
@@ -403,6 +428,7 @@ async function initialize(): Promise<void> {
|
||||
|
||||
// Create the main window
|
||||
const window = createMainWindow();
|
||||
agentBrowser = new AgentBrowserModule(new ElectronAgentBrowserAdapter(window));
|
||||
|
||||
// Create system tray
|
||||
if (!isE2EMode) {
|
||||
@@ -417,6 +443,7 @@ async function initialize(): Promise<void> {
|
||||
opencodeProjectStore,
|
||||
eventBus: hostEventBus,
|
||||
mainWindow: window,
|
||||
agentBrowser,
|
||||
worksCloudDeployment: worksCloudDeployment ?? undefined,
|
||||
imageWorkspace,
|
||||
});
|
||||
@@ -505,12 +532,18 @@ if (gotTheLock) {
|
||||
resourcesPath: process.resourcesPath,
|
||||
appPath: app.getAppPath(),
|
||||
});
|
||||
const bundledAgentBrowserPluginPath = resolveBundledAgentBrowserPluginPath({
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
appPath: app.getAppPath(),
|
||||
});
|
||||
opencodeManager = new OpencodeManager({
|
||||
port: 4096,
|
||||
binPath: opencodePaths.binPath,
|
||||
userDataDir: app.getPath('userData'),
|
||||
bundledSuperpowersDir,
|
||||
bundledCourseSkillsDir,
|
||||
bundledAgentBrowserPluginPath,
|
||||
pythonRuntime,
|
||||
runtimeConfigProvider: async () => {
|
||||
const runtime = await buildOpencodeRuntimeConfigFromNianCodeProviders({
|
||||
@@ -608,7 +641,13 @@ if (gotTheLock) {
|
||||
const stopOpencodePromise = opencodeManager.stop().catch((err) => {
|
||||
logger.warn('opencodeManager.stop() error during quit:', err);
|
||||
});
|
||||
const stopPromise = Promise.allSettled([stopOpencodePromise]);
|
||||
const stopAgentBrowserPromise = agentBrowser?.dispose().catch((err) => {
|
||||
logger.warn('agentBrowser.dispose() error during quit:', err);
|
||||
}) ?? Promise.resolve();
|
||||
const stopPromise = Promise.allSettled([
|
||||
stopOpencodePromise,
|
||||
stopAgentBrowserPromise,
|
||||
]);
|
||||
const timeoutPromise = new Promise<'timeout'>((resolve) => {
|
||||
setTimeout(() => resolve('timeout'), 5000);
|
||||
});
|
||||
@@ -629,6 +668,11 @@ if (gotTheLock) {
|
||||
logger.error(`${reason}:`, error);
|
||||
projectProgressSync?.stop();
|
||||
worksCloudDeployment?.stop();
|
||||
try {
|
||||
void agentBrowser?.dispose().catch(() => { /* ignore */ });
|
||||
} catch {
|
||||
// ignore — dispose() may not be callable if state is corrupted
|
||||
}
|
||||
try {
|
||||
void opencodeManager?.stop().catch(() => { /* ignore */ });
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user