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
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:
@@ -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
|
||||
*/
|
||||
|
||||
77
desktop/src/lib/mcp-client.ts
Normal file
77
desktop/src/lib/mcp-client.ts
Normal 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 });
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user