- 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.
257 lines
7.0 KiB
TypeScript
257 lines
7.0 KiB
TypeScript
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;
|
||
}
|