- Implement tests for random ID generation, ensuring preference for crypto.randomUUID. - Create tests for runtime context capabilities, validating the injection of enabled skill capabilities. - Add tests for skill capability parsing, including classification and command example extraction. - Introduce tests for the skill planner, verifying tool call planning based on user requests and attachment requirements. - Establish tests for UV setup, ensuring proper handling of Python installation scenarios and environment checks.
187 lines
4.9 KiB
TypeScript
187 lines
4.9 KiB
TypeScript
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, ToolResultPayload, 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;
|
||
let finalToolResult: ToolResultPayload | 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);
|
||
finalToolResult = {
|
||
ok: true,
|
||
summary: assistantText,
|
||
structuredData: result,
|
||
renderHints: {
|
||
card: 'browser-step',
|
||
},
|
||
raw: 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);
|
||
finalToolResult = {
|
||
ok: false,
|
||
summary: assistantText,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
retryable: true,
|
||
renderHints: {
|
||
card: 'browser-step',
|
||
},
|
||
raw: {
|
||
error: error instanceof Error ? error.message : String(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(),
|
||
toolResult: finalToolResult,
|
||
_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;
|
||
}
|