Files
zn-ai/electron/gateway/skill-install-shortcut.ts
DEV_DSW 4c61e93c3e Add unit tests for skill capabilities, skill planner, and UV setup
- 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.
2026-04-24 17:02:59 +08:00

257 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}