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.
This commit is contained in:
233
electron/gateway/skill-install-shortcut.ts
Normal file
233
electron/gateway/skill-install-shortcut.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user