Files
zn-ai/electron/gateway/skill-install-shortcut.ts
duanshuwen df600272d6 feat: add tool status management and localization for skill installation
- Updated chat message types to include tool statuses.
- Enhanced localization files for English, Thai, and Chinese to support new tool status messages.
- Modified HomePage and SkillsPage components to handle tool statuses in chat messages.
- Implemented tool status merging and updating logic in the chat store.
- Added handling for tool status events in the gateway event processing.
- Created tests for chat message rendering with tool statuses and skill installation shortcuts.
- Improved gateway event dispatching for tool lifecycle events.
2026-04-23 20:27:54 +08:00

234 lines
6.4 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, 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;
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);
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);
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(),
_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;
}