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:
duanshuwen
2026-04-23 20:27:54 +08:00
parent 979fb0a0f6
commit df600272d6
29 changed files with 2041 additions and 384 deletions

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