fix(browser): stability enhancements + MCP frontend client
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

S7 Browser Hand:
- Remove dead code: browser/actions.rs (314 lines of unused BrowserAction/ActionResult types)
- Fix browser_scrape_page: log failed selector matches instead of silently swallowing errors
- Fix element_to_info: document known limitation for always-None location/size fields
- Fix browserHandStore: reuse activeSessionId in executeScript/takeScreenshot/executeTemplate
  instead of creating orphan Browser sessions
- Add Browser.connect(sessionId) method for session reuse

MCP Frontend:
- Add desktop/src/lib/mcp-client.ts (77 lines) — typed client for MCP Tauri commands
  (startMcpService, stopMcpService, listMcpServices, callMcpTool)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-04-03 22:16:12 +08:00
parent 943afe3b6b
commit 1c99e5f3a3
8 changed files with 133 additions and 335 deletions

View File

@@ -324,6 +324,14 @@ export class Browser {
return this.sessionId;
}
/**
* Attach to an existing session by ID.
* Use this to reuse a session created elsewhere without spawning a new one.
*/
connect(sessionId: string): void {
this.sessionId = sessionId;
}
/**
* Close browser session
*/

View File

@@ -0,0 +1,77 @@
/**
* MCP (Model Context Protocol) Client for ZCLAW
*
* Thin typed wrapper around the 4 Tauri MCP commands.
* All communication goes through invoke() — no direct HTTP or WebSocket.
*/
import { invoke } from '@tauri-apps/api/core';
import { createLogger } from './logger';
const log = createLogger('mcp-client');
// ============================================================================
// Types
// ============================================================================
export interface McpServiceConfig {
name: string;
command: string;
args?: string[];
env?: Record<string, string>;
cwd?: string;
}
export interface McpToolInfo {
service_name: string;
tool_name: string;
description: string;
input_schema: Record<string, unknown>;
}
export interface McpServiceStatus {
name: string;
tool_count: number;
tools: McpToolInfo[];
}
// ============================================================================
// Functions
// ============================================================================
/**
* Start an MCP service process and discover its tools.
*/
export async function startMcpService(
config: McpServiceConfig
): Promise<McpToolInfo[]> {
log.info('startMcpService', { name: config.name });
return invoke<McpToolInfo[]>('mcp_start_service', { config });
}
/**
* Stop a running MCP service by name.
*/
export async function stopMcpService(name: string): Promise<void> {
log.info('stopMcpService', { name });
return invoke<void>('mcp_stop_service', { name });
}
/**
* List all running MCP services with their discovered tools.
*/
export async function listMcpServices(): Promise<McpServiceStatus[]> {
return invoke<McpServiceStatus[]>('mcp_list_services');
}
/**
* Call a tool exposed by a running MCP service.
*/
export async function callMcpTool(
serviceName: string,
toolName: string,
args: Record<string, unknown>
): Promise<unknown> {
log.info('callMcpTool', { serviceName, toolName });
return invoke<unknown>('mcp_call_tool', { serviceName, toolName, args });
}

View File

@@ -10,6 +10,8 @@ import Browser, {
createSession,
closeSession,
listSessions,
screenshot as screenshotFn,
executeScript as executeScriptFn,
} from '../lib/browser-client';
import {
BUILTIN_TEMPLATES,
@@ -247,14 +249,21 @@ export const useBrowserHandStore = create<BrowserHandState & BrowserHandActions>
},
});
// Create browser instance
// Create browser instance — reuse active session if available
const browser = new Browser();
let createdOwnSession = false;
try {
store.addLog({ level: 'info', message: `开始执行模板: ${template.name}` });
// Start browser session
await browser.start({ headless: true });
// Attach to existing session or start a new one
if (store.activeSessionId) {
browser.connect(store.activeSessionId);
createdOwnSession = false;
} else {
await browser.start({ headless: true });
createdOwnSession = true;
}
// Create execution context
const context = {
@@ -322,7 +331,10 @@ export const useBrowserHandStore = create<BrowserHandState & BrowserHandActions>
throw error;
} finally {
await browser.close();
// Only close the session if we created it (no pre-existing active session)
if (createdOwnSession) {
await browser.close();
}
}
},
@@ -340,10 +352,8 @@ export const useBrowserHandStore = create<BrowserHandState & BrowserHandActions>
});
try {
const browser = new Browser();
await browser.start();
const result = await browser.eval(script, args);
// Use the standalone function with the existing session — no new session created
const result = await executeScriptFn(store.activeSessionId, script, args);
store.updateExecutionState({
isRunning: false,
@@ -399,10 +409,8 @@ export const useBrowserHandStore = create<BrowserHandState & BrowserHandActions>
}
try {
const browser = new Browser();
await browser.start();
const result = await browser.screenshot();
// Use the standalone function with the existing session — no new session created
const result = await screenshotFn(store.activeSessionId);
set((state) => ({
execution: {