import logManager from '@electron/service/logger'; import { extractBrowserOpenIntent, openUrlInBrowser } from '@electron/service/browser-open-service'; import { appendTranscriptLine } from '@electron/utils/token-usage-writer'; import type { RawMessage, ToolStatus } from '@runtime/shared/chat-model'; import { sessionStore } from './session-store'; import type { GatewayEvent } from './types'; function buildBrowserOpenResponseText(result: { pageUrl: string; title?: string }): string { const suffix = result.title ? `(${result.title})` : ''; return `已为你打开 ${result.pageUrl}${suffix}`; } function buildBrowserOpenErrorText(error: unknown): string { return `打开失败:${error instanceof Error ? error.message : String(error)}`; } async function processBrowserOpen( sessionKey: string, runId: string, url: string, signal: AbortSignal, broadcast: (event: GatewayEvent) => void, ) { let assistantText = ''; const toolCallId = `browser.open_url:${runId}`; const startedAt = Date.now(); let finalToolStatus: ToolStatus | null = null; broadcast({ type: 'tool:status', sessionKey, runId, toolCallId, toolName: 'browser.open_url', status: 'running', updatedAt: startedAt, summary: `Opening ${url}`, input: { url }, }); try { const result = await openUrlInBrowser(url, { signal }); if (signal.aborted) { return; } assistantText = buildBrowserOpenResponseText(result); finalToolStatus = { id: toolCallId, toolCallId, name: 'browser.open_url', status: 'completed', updatedAt: Date.now(), durationMs: Date.now() - startedAt, summary: assistantText, input: { url }, result, }; broadcast({ type: 'tool:status', sessionKey, runId, toolCallId, toolName: 'browser.open_url', status: finalToolStatus.status, updatedAt: finalToolStatus.updatedAt, durationMs: finalToolStatus.durationMs, summary: finalToolStatus.summary, input: finalToolStatus.input, result: finalToolStatus.result, }); } catch (error) { if (signal.aborted) { return; } assistantText = buildBrowserOpenErrorText(error); finalToolStatus = { id: toolCallId, toolCallId, name: 'browser.open_url', status: 'error', updatedAt: Date.now(), durationMs: Date.now() - startedAt, summary: assistantText, input: { url }, result: { error: error instanceof Error ? error.message : String(error), }, }; broadcast({ type: 'tool:status', sessionKey, runId, toolCallId, toolName: 'browser.open_url', status: finalToolStatus.status, updatedAt: finalToolStatus.updatedAt, durationMs: finalToolStatus.durationMs, summary: finalToolStatus.summary, input: finalToolStatus.input, result: finalToolStatus.result, }); } sessionStore.clearActiveRun(sessionKey); const finalMessage: RawMessage = { role: 'assistant', content: assistantText, timestamp: Date.now(), _toolStatuses: finalToolStatus ? [finalToolStatus] : undefined, }; sessionStore.appendMessage(sessionKey, finalMessage); appendTranscriptLine(sessionKey, { type: 'message', timestamp: new Date().toISOString(), message: { role: 'assistant', content: assistantText, tool: 'browser.open_url', }, }); broadcast({ type: 'chat:final', sessionKey, runId, message: finalMessage, }); } export function maybeHandleBrowserOpenMessage( sessionKey: string, runId: string, message: RawMessage, broadcast: (event: GatewayEvent) => void, ): boolean { const browserIntent = typeof message.content === 'string' ? extractBrowserOpenIntent(message.content) : null; if (!browserIntent) { return false; } const abortController = new AbortController(); sessionStore.setActiveRun(sessionKey, runId, abortController); processBrowserOpen(sessionKey, runId, browserIntent.url, abortController.signal, broadcast).catch( (error) => { logManager.error('Unexpected error in processBrowserOpen:', error); sessionStore.clearActiveRun(sessionKey); broadcast({ type: 'chat:error', sessionKey, runId, error: error instanceof Error ? error.message : String(error), }); }, ); return true; }