import logManager from '@electron/service/logger'; import type { SkillInstallRequest, SkillInstallResult } from '@electron/service/skill-install-service'; import { parseGitHubSkillUrl } from '@electron/service/skill-install-service'; import { appendTranscriptLine } from '@electron/utils/token-usage-writer'; import type { RawMessage, ToolResultPayload, ToolStatus } from '@runtime/shared/chat-model'; import { handleSkillsInstall } from './handlers/skills'; import { sessionStore } from './session-store'; import type { GatewayEvent } from './types'; const GITHUB_URL_PATTERN = /https:\/\/github\.com\/[^\s)"',。!?;:]+/giu; const INSTALL_VERB_PATTERN = /(安装|装上|装一下|帮我安装|帮我装|请安装|\binstall\b)/iu; const MARKETPLACE_INSTALL_PATTERNS = [ /^\s*(?:请)?(?:帮我)?(?:安装|装上|装一下)\s+(?:这个\s*)?(?:skill|技能)\s*[::]?\s*["“']?([a-z0-9][a-z0-9._-]*)["”']?\s*$/iu, /^\s*(?:请)?(?:帮我)?(?:安装|装上|装一下)\s+["“']?([a-z0-9][a-z0-9._-]*)["”']?\s*(?:这个)?\s*(?:skill|技能)\s*$/iu, /^\s*install\s+(?:the\s+)?(?:skill\s+)?["']?([a-z0-9][a-z0-9._-]*)["']?\s*$/iu, ] as const; export interface SkillInstallIntent { request: SkillInstallRequest; description: string; } function stripTrailingPunctuation(value: string): string { return value.trim().replace(/[)\]}>.,!?;:'",。!?;:)】》]+$/u, ''); } function extractGitHubSkillUrl(text: string): string | null { const matches = text.match(GITHUB_URL_PATTERN) ?? []; for (const match of matches) { const candidate = stripTrailingPunctuation(match); try { const parsed = parseGitHubSkillUrl(candidate); return parsed.originalUrl; } catch { continue; } } return null; } function describeInstallResult(result: SkillInstallResult): string { return `已安装并启用 skill ${result.slug}(${result.source})。位置:${result.baseDir}`; } function describeInstallFailure(error: unknown): string { return `安装失败:${error instanceof Error ? error.message : String(error)}`; } export function extractSkillInstallIntent(text: string): SkillInstallIntent | null { const trimmed = String(text || '').trim(); if (!trimmed || !INSTALL_VERB_PATTERN.test(trimmed)) { return null; } const githubUrl = extractGitHubSkillUrl(trimmed); if (githubUrl) { const parsed = parseGitHubSkillUrl(githubUrl); return { request: { kind: 'github-url', url: parsed.originalUrl, }, description: parsed.defaultSlug, }; } for (const pattern of MARKETPLACE_INSTALL_PATTERNS) { const match = pattern.exec(trimmed); if (!match?.[1]) { continue; } return { request: { kind: 'marketplace', slug: match[1], }, description: match[1], }; } return null; } async function processSkillInstall( sessionKey: string, runId: string, intent: SkillInstallIntent, signal: AbortSignal, broadcast: (event: GatewayEvent) => void, ) { const toolCallId = `skills.install:${runId}`; const startedAt = Date.now(); let finalToolStatus: ToolStatus | null = null; let finalToolResult: ToolResultPayload | null = null; broadcast({ type: 'tool:status', sessionKey, runId, toolCallId, toolName: 'skills.install', status: 'running', updatedAt: startedAt, summary: `Installing ${intent.description}`, input: intent.request, }); let assistantText = ''; try { const result = await handleSkillsInstall(intent.request, broadcast); if (signal.aborted) { return; } assistantText = describeInstallResult(result); finalToolResult = { ok: true, summary: assistantText, structuredData: result, renderHints: { card: 'skill-install', }, raw: result, }; finalToolStatus = { id: toolCallId, toolCallId, name: 'skills.install', status: 'completed', updatedAt: Date.now(), durationMs: Date.now() - startedAt, summary: assistantText, input: intent.request, result, }; broadcast({ type: 'tool:status', sessionKey, runId, toolCallId, toolName: 'skills.install', 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 = describeInstallFailure(error); finalToolResult = { ok: false, summary: assistantText, error: error instanceof Error ? error.message : String(error), retryable: true, renderHints: { card: 'skill-install', }, raw: { error: error instanceof Error ? error.message : String(error), }, }; finalToolStatus = { id: toolCallId, toolCallId, name: 'skills.install', status: 'error', updatedAt: Date.now(), durationMs: Date.now() - startedAt, summary: assistantText, input: intent.request, result: { error: error instanceof Error ? error.message : String(error), }, }; broadcast({ type: 'tool:status', sessionKey, runId, toolCallId, toolName: 'skills.install', 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: 'skills.install', }, }); broadcast({ type: 'chat:final', sessionKey, runId, message: finalMessage, }); } export function maybeHandleSkillInstallMessage( sessionKey: string, runId: string, message: RawMessage, broadcast: (event: GatewayEvent) => void, ): boolean { const intent = typeof message.content === 'string' ? extractSkillInstallIntent(message.content) : null; if (!intent) { return false; } const abortController = new AbortController(); sessionStore.setActiveRun(sessionKey, runId, abortController); processSkillInstall(sessionKey, runId, intent, abortController.signal, broadcast).catch((error) => { logManager.error('Unexpected error in processSkillInstall:', error); sessionStore.clearActiveRun(sessionKey); broadcast({ type: 'chat:error', sessionKey, runId, error: error instanceof Error ? error.message : String(error), }); }); return true; }