- 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.
551 lines
15 KiB
TypeScript
551 lines
15 KiB
TypeScript
export type ToolRegistryEntryKind = 'builtin-tool' | 'skill';
|
|
|
|
export type ToolRiskLevel = 'low' | 'medium' | 'high';
|
|
|
|
export type ToolInputKind =
|
|
| 'text'
|
|
| 'url'
|
|
| 'file'
|
|
| 'attachment'
|
|
| 'history'
|
|
| 'structured';
|
|
|
|
export type ToolOutputKind =
|
|
| 'text'
|
|
| 'json'
|
|
| 'file'
|
|
| 'artifacts'
|
|
| 'browser-state'
|
|
| 'status';
|
|
|
|
export interface ToolRegistryCapabilityInput {
|
|
capabilityKey?: string;
|
|
familyKey?: string;
|
|
toolName?: string;
|
|
skillKey?: string;
|
|
slug?: string;
|
|
name?: string;
|
|
displayName?: string;
|
|
description?: string;
|
|
kind?: ToolRegistryEntryKind;
|
|
aliases?: string[];
|
|
inputKinds?: string[];
|
|
outputKinds?: string[];
|
|
triggerHints?: string[];
|
|
supportedFileTypes?: string[];
|
|
requiresFiles?: boolean;
|
|
requiresExplicitUserIntent?: boolean;
|
|
enabled?: boolean;
|
|
disabled?: boolean;
|
|
riskLevel?: ToolRiskLevel;
|
|
source?: string;
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface ToolRegistryEntry {
|
|
capabilityKey: string;
|
|
familyKey: string;
|
|
toolName: string;
|
|
kind: ToolRegistryEntryKind;
|
|
displayName: string;
|
|
description: string;
|
|
aliases: string[];
|
|
inputKinds: ToolInputKind[];
|
|
outputKinds: ToolOutputKind[];
|
|
triggerHints: string[];
|
|
supportedFileTypes: string[];
|
|
requiresFiles: boolean;
|
|
requiresExplicitUserIntent: boolean;
|
|
riskLevel: ToolRiskLevel;
|
|
enabled: boolean;
|
|
source?: string;
|
|
metadata: Record<string, unknown>;
|
|
}
|
|
|
|
export interface CreateToolRegistryOptions {
|
|
capabilities?: ToolRegistryCapabilityInput[];
|
|
includeBuiltins?: boolean;
|
|
}
|
|
|
|
const SPREADSHEET_FAMILY_KEY = 'spreadsheet.analysis';
|
|
|
|
const SPREADSHEET_EXTENSIONS = ['.xlsx', '.xls', '.csv', '.tsv', '.ods'];
|
|
|
|
const SPREADSHEET_MIME_PATTERNS = [
|
|
'spreadsheet',
|
|
'excel',
|
|
'sheet',
|
|
'csv',
|
|
'tab-separated-values',
|
|
'officedocument.spreadsheetml',
|
|
];
|
|
|
|
const DEFAULT_BUILTIN_ENTRIES: ToolRegistryEntry[] = [
|
|
{
|
|
capabilityKey: 'browser.open_url',
|
|
familyKey: 'browser.open_url',
|
|
toolName: 'browser.open_url',
|
|
kind: 'builtin-tool',
|
|
displayName: 'Browser Open URL',
|
|
description: 'Open an explicit URL in the managed browser when the user clearly asks to open or visit a page.',
|
|
aliases: ['browser.open_url', 'open url', 'open link', 'visit url'],
|
|
inputKinds: ['url'],
|
|
outputKinds: ['browser-state', 'text'],
|
|
triggerHints: ['open this url', 'open the link', 'visit the page', '打开链接', '打开网页', '访问网页'],
|
|
supportedFileTypes: [],
|
|
requiresFiles: false,
|
|
requiresExplicitUserIntent: true,
|
|
riskLevel: 'medium',
|
|
enabled: true,
|
|
metadata: {
|
|
intent: 'open_url',
|
|
sideEffect: 'browser-navigation',
|
|
},
|
|
},
|
|
{
|
|
capabilityKey: 'skills.install',
|
|
familyKey: 'skills.install',
|
|
toolName: 'skills.install',
|
|
kind: 'builtin-tool',
|
|
displayName: 'Skills Install',
|
|
description: 'Install a skill from the marketplace slug or a GitHub skill URL when the user explicitly asks to install one.',
|
|
aliases: ['skills.install', 'install skill', 'skill install', 'add skill'],
|
|
inputKinds: ['text', 'url'],
|
|
outputKinds: ['status', 'text'],
|
|
triggerHints: ['install this skill', 'install skill', 'github skill url', '安装 skill', '安装技能'],
|
|
supportedFileTypes: [],
|
|
requiresFiles: false,
|
|
requiresExplicitUserIntent: true,
|
|
riskLevel: 'medium',
|
|
enabled: true,
|
|
metadata: {
|
|
intent: 'install_skill',
|
|
sideEffect: 'skill-installation',
|
|
},
|
|
},
|
|
];
|
|
|
|
function normalizeTextValue(value: string | undefined | null): string {
|
|
return String(value ?? '').trim();
|
|
}
|
|
|
|
function normalizeLookupValue(value: string | undefined | null): string {
|
|
return normalizeTextValue(value).toLowerCase();
|
|
}
|
|
|
|
function dedupeStrings(values: Array<string | undefined | null>): string[] {
|
|
const seen = new Set<string>();
|
|
const result: string[] = [];
|
|
|
|
for (const value of values) {
|
|
const trimmed = normalizeTextValue(value);
|
|
if (!trimmed) {
|
|
continue;
|
|
}
|
|
const lookup = trimmed.toLowerCase();
|
|
if (seen.has(lookup)) {
|
|
continue;
|
|
}
|
|
seen.add(lookup);
|
|
result.push(trimmed);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function normalizeStringList(values: unknown, fallback: string[] = []): string[] {
|
|
if (!Array.isArray(values)) {
|
|
return [...fallback];
|
|
}
|
|
|
|
return dedupeStrings(values.map((item) => (typeof item === 'string' ? item : '')));
|
|
}
|
|
|
|
function normalizeInputKinds(values: unknown): ToolInputKind[] {
|
|
const allowed: ToolInputKind[] = ['text', 'url', 'file', 'attachment', 'history', 'structured'];
|
|
return normalizeStringList(values)
|
|
.map((value) => value.toLowerCase())
|
|
.filter((value): value is ToolInputKind => allowed.includes(value as ToolInputKind));
|
|
}
|
|
|
|
function normalizeOutputKinds(values: unknown): ToolOutputKind[] {
|
|
const allowed: ToolOutputKind[] = ['text', 'json', 'file', 'artifacts', 'browser-state', 'status'];
|
|
return normalizeStringList(values)
|
|
.map((value) => value.toLowerCase())
|
|
.filter((value): value is ToolOutputKind => allowed.includes(value as ToolOutputKind));
|
|
}
|
|
|
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null;
|
|
}
|
|
|
|
function getMetadataStringArray(
|
|
metadata: Record<string, unknown>,
|
|
...keys: string[]
|
|
): string[] {
|
|
for (const key of keys) {
|
|
const value = metadata[key];
|
|
if (Array.isArray(value)) {
|
|
return normalizeStringList(value);
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function getMetadataBoolean(
|
|
metadata: Record<string, unknown>,
|
|
...keys: string[]
|
|
): boolean | undefined {
|
|
for (const key of keys) {
|
|
const value = metadata[key];
|
|
if (typeof value === 'boolean') {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function inferCapabilityKind(input: ToolRegistryCapabilityInput, capabilityKey: string): ToolRegistryEntryKind {
|
|
if (input.kind === 'builtin-tool' || input.kind === 'skill') {
|
|
return input.kind;
|
|
}
|
|
|
|
if (capabilityKey === 'browser.open_url' || capabilityKey === 'skills.install') {
|
|
return 'builtin-tool';
|
|
}
|
|
|
|
return 'skill';
|
|
}
|
|
|
|
function normalizeRiskLevel(value: string | undefined): ToolRiskLevel {
|
|
switch (normalizeLookupValue(value)) {
|
|
case 'high':
|
|
return 'high';
|
|
case 'medium':
|
|
return 'medium';
|
|
default:
|
|
return 'low';
|
|
}
|
|
}
|
|
|
|
function isSpreadsheetCapability(
|
|
capabilityKey: string,
|
|
displayName: string,
|
|
description: string,
|
|
aliases: string[],
|
|
supportedFileTypes: string[],
|
|
): boolean {
|
|
const haystack = [
|
|
capabilityKey,
|
|
displayName,
|
|
description,
|
|
...aliases,
|
|
...supportedFileTypes,
|
|
].map((item) => item.toLowerCase());
|
|
|
|
if (haystack.some((item) => item === 'minimax-xlsx' || item === 'xlsx')) {
|
|
return true;
|
|
}
|
|
|
|
if (supportedFileTypes.some((item) => isSpreadsheetFileType(item))) {
|
|
return true;
|
|
}
|
|
|
|
return haystack.some((item) =>
|
|
item.includes('spreadsheet')
|
|
|| item.includes('excel')
|
|
|| item.includes('sheet')
|
|
|| item.includes('csv')
|
|
|| item.includes('表格')
|
|
|| item.includes('excel')
|
|
|| item.includes('工作表'),
|
|
);
|
|
}
|
|
|
|
function normalizeSupportedFileTypes(
|
|
input: ToolRegistryCapabilityInput,
|
|
metadata: Record<string, unknown>,
|
|
): string[] {
|
|
const explicit = dedupeStrings([
|
|
...normalizeStringList(input.supportedFileTypes),
|
|
...getMetadataStringArray(metadata, 'supportedFileTypes', 'supported_file_types'),
|
|
]);
|
|
|
|
if (explicit.length > 0) {
|
|
return explicit;
|
|
}
|
|
|
|
const capabilityKey = normalizeLookupValue(
|
|
input.capabilityKey || input.toolName || input.skillKey || input.slug || input.name,
|
|
);
|
|
if (capabilityKey === 'xlsx' || capabilityKey === 'minimax-xlsx') {
|
|
return [...SPREADSHEET_EXTENSIONS];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function normalizeAliases(
|
|
input: ToolRegistryCapabilityInput,
|
|
capabilityKey: string,
|
|
displayName: string,
|
|
toolName: string,
|
|
isSpreadsheet: boolean,
|
|
): string[] {
|
|
return dedupeStrings([
|
|
capabilityKey,
|
|
toolName,
|
|
displayName,
|
|
input.skillKey,
|
|
input.slug,
|
|
input.name,
|
|
...normalizeStringList(input.aliases),
|
|
...(isSpreadsheet ? ['minimax-xlsx', 'xlsx', 'spreadsheet', 'excel analyzer', 'excel analysis'] : []),
|
|
]);
|
|
}
|
|
|
|
function normalizeTriggerHints(
|
|
input: ToolRegistryCapabilityInput,
|
|
metadata: Record<string, unknown>,
|
|
isSpreadsheet: boolean,
|
|
): string[] {
|
|
const hints = dedupeStrings([
|
|
...normalizeStringList(input.triggerHints),
|
|
...getMetadataStringArray(metadata, 'triggerHints', 'trigger_hints'),
|
|
]);
|
|
|
|
if (!isSpreadsheet) {
|
|
return hints;
|
|
}
|
|
|
|
return dedupeStrings([
|
|
...hints,
|
|
'analyze spreadsheet',
|
|
'analyze excel',
|
|
'analyze table file',
|
|
'summarize worksheet',
|
|
'分析表格',
|
|
'分析 excel',
|
|
'分析工作表',
|
|
'分析这个表',
|
|
]);
|
|
}
|
|
|
|
function mergeEntries(base: ToolRegistryEntry, incoming: ToolRegistryEntry): ToolRegistryEntry {
|
|
return {
|
|
...base,
|
|
...incoming,
|
|
aliases: dedupeStrings([...base.aliases, ...incoming.aliases]),
|
|
inputKinds: Array.from(new Set([...base.inputKinds, ...incoming.inputKinds])),
|
|
outputKinds: Array.from(new Set([...base.outputKinds, ...incoming.outputKinds])),
|
|
triggerHints: dedupeStrings([...base.triggerHints, ...incoming.triggerHints]),
|
|
supportedFileTypes: dedupeStrings([...base.supportedFileTypes, ...incoming.supportedFileTypes]),
|
|
metadata: {
|
|
...base.metadata,
|
|
...incoming.metadata,
|
|
},
|
|
enabled: base.enabled || incoming.enabled,
|
|
requiresFiles: base.requiresFiles || incoming.requiresFiles,
|
|
requiresExplicitUserIntent: base.requiresExplicitUserIntent || incoming.requiresExplicitUserIntent,
|
|
};
|
|
}
|
|
|
|
function buildRegistryEntry(input: ToolRegistryCapabilityInput): ToolRegistryEntry | null {
|
|
const metadata = isPlainRecord(input.metadata) ? input.metadata : {};
|
|
const capabilityKey = normalizeTextValue(
|
|
input.capabilityKey || input.toolName || input.skillKey || input.slug || input.name,
|
|
);
|
|
if (!capabilityKey) {
|
|
return null;
|
|
}
|
|
|
|
const toolName = normalizeTextValue(input.toolName || capabilityKey);
|
|
const displayName = normalizeTextValue(input.displayName || input.name || input.skillKey || input.slug || toolName);
|
|
const description = normalizeTextValue(input.description);
|
|
const supportedFileTypes = normalizeSupportedFileTypes(input, metadata);
|
|
const isSpreadsheet = isSpreadsheetCapability(
|
|
capabilityKey,
|
|
displayName,
|
|
description,
|
|
normalizeStringList(input.aliases),
|
|
supportedFileTypes,
|
|
);
|
|
const aliases = normalizeAliases(input, capabilityKey, displayName, toolName, isSpreadsheet);
|
|
const triggerHints = normalizeTriggerHints(input, metadata, isSpreadsheet);
|
|
const inputKinds = normalizeInputKinds(input.inputKinds);
|
|
const outputKinds = normalizeOutputKinds(input.outputKinds);
|
|
const inferredRequiresFiles = isSpreadsheet
|
|
|| supportedFileTypes.length > 0
|
|
|| inputKinds.includes('file')
|
|
|| inputKinds.includes('attachment');
|
|
const requiresFiles = input.requiresFiles
|
|
?? getMetadataBoolean(metadata, 'requiresFiles', 'requires_files')
|
|
?? inferredRequiresFiles;
|
|
const requiresExplicitUserIntent = input.requiresExplicitUserIntent
|
|
?? getMetadataBoolean(metadata, 'requiresExplicitUserIntent', 'requires_explicit_user_intent')
|
|
?? false;
|
|
const familyKey = normalizeTextValue(input.familyKey)
|
|
|| (isSpreadsheet ? SPREADSHEET_FAMILY_KEY : capabilityKey);
|
|
|
|
return {
|
|
capabilityKey,
|
|
familyKey,
|
|
toolName,
|
|
kind: inferCapabilityKind(input, capabilityKey),
|
|
displayName,
|
|
description,
|
|
aliases,
|
|
inputKinds: inputKinds.length > 0 ? inputKinds : (requiresFiles ? ['text', 'file', 'attachment'] : ['text']),
|
|
outputKinds: outputKinds.length > 0 ? outputKinds : ['text'],
|
|
triggerHints,
|
|
supportedFileTypes,
|
|
requiresFiles,
|
|
requiresExplicitUserIntent,
|
|
riskLevel: normalizeRiskLevel(input.riskLevel),
|
|
enabled: input.disabled === true ? false : input.enabled !== false,
|
|
source: normalizeTextValue(input.source) || undefined,
|
|
metadata: {
|
|
...metadata,
|
|
familyKey,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createToolRegistry(options: CreateToolRegistryOptions = {}): ToolRegistryEntry[] {
|
|
const includeBuiltins = options.includeBuiltins !== false;
|
|
const merged = new Map<string, ToolRegistryEntry>();
|
|
|
|
if (includeBuiltins) {
|
|
for (const entry of DEFAULT_BUILTIN_ENTRIES) {
|
|
merged.set(entry.capabilityKey, { ...entry, aliases: [...entry.aliases], triggerHints: [...entry.triggerHints] });
|
|
}
|
|
}
|
|
|
|
for (const capability of options.capabilities || []) {
|
|
if (capability.disabled === true || capability.enabled === false) {
|
|
continue;
|
|
}
|
|
|
|
const entry = buildRegistryEntry(capability);
|
|
if (!entry || entry.enabled === false) {
|
|
continue;
|
|
}
|
|
|
|
const existing = merged.get(entry.capabilityKey);
|
|
merged.set(entry.capabilityKey, existing ? mergeEntries(existing, entry) : entry);
|
|
}
|
|
|
|
return Array.from(merged.values()).sort((left, right) => {
|
|
if (left.kind !== right.kind) {
|
|
return left.kind === 'builtin-tool' ? -1 : 1;
|
|
}
|
|
return left.displayName.localeCompare(right.displayName);
|
|
});
|
|
}
|
|
|
|
export function getRegistryEntryByName(
|
|
registry: ToolRegistryEntry[],
|
|
reference: string,
|
|
): ToolRegistryEntry | undefined {
|
|
const lookup = normalizeLookupValue(reference);
|
|
if (!lookup) {
|
|
return undefined;
|
|
}
|
|
|
|
return registry.find((entry) =>
|
|
normalizeLookupValue(entry.capabilityKey) === lookup
|
|
|| normalizeLookupValue(entry.toolName) === lookup
|
|
|| normalizeLookupValue(entry.familyKey) === lookup
|
|
|| entry.aliases.some((alias) => normalizeLookupValue(alias) === lookup),
|
|
);
|
|
}
|
|
|
|
export function getRegistryEntriesByFamily(
|
|
registry: ToolRegistryEntry[],
|
|
familyKey: string,
|
|
): ToolRegistryEntry[] {
|
|
const lookup = normalizeLookupValue(familyKey);
|
|
if (!lookup) {
|
|
return [];
|
|
}
|
|
|
|
return registry.filter((entry) => normalizeLookupValue(entry.familyKey) === lookup);
|
|
}
|
|
|
|
export function matchRegistryEntriesByAlias(
|
|
registry: ToolRegistryEntry[],
|
|
text: string,
|
|
): Array<{ entry: ToolRegistryEntry; alias: string }> {
|
|
const normalizedText = normalizeLookupValue(text);
|
|
if (!normalizedText) {
|
|
return [];
|
|
}
|
|
|
|
const results: Array<{ entry: ToolRegistryEntry; alias: string }> = [];
|
|
|
|
for (const entry of registry) {
|
|
for (const alias of entry.aliases) {
|
|
const normalizedAlias = normalizeLookupValue(alias);
|
|
if (!normalizedAlias) {
|
|
continue;
|
|
}
|
|
|
|
if (normalizedText.includes(normalizedAlias)) {
|
|
results.push({ entry, alias });
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
export function isSpreadsheetFileType(value: string): boolean {
|
|
const normalized = normalizeLookupValue(value);
|
|
if (!normalized) {
|
|
return false;
|
|
}
|
|
|
|
if (SPREADSHEET_EXTENSIONS.some((ext) => normalized.endsWith(ext))) {
|
|
return true;
|
|
}
|
|
|
|
return SPREADSHEET_MIME_PATTERNS.some((pattern) => normalized.includes(pattern));
|
|
}
|
|
|
|
export function registryEntrySupportsFileType(
|
|
entry: ToolRegistryEntry,
|
|
fileType: string,
|
|
): boolean {
|
|
if (!fileType) {
|
|
return false;
|
|
}
|
|
|
|
if (entry.supportedFileTypes.length === 0) {
|
|
return !entry.requiresFiles;
|
|
}
|
|
|
|
const normalizedFileType = normalizeLookupValue(fileType);
|
|
return entry.supportedFileTypes.some((candidate) => {
|
|
const normalizedCandidate = normalizeLookupValue(candidate);
|
|
if (!normalizedCandidate) {
|
|
return false;
|
|
}
|
|
|
|
if (normalizedCandidate.startsWith('.')) {
|
|
return normalizedFileType.endsWith(normalizedCandidate);
|
|
}
|
|
|
|
return normalizedFileType.includes(normalizedCandidate);
|
|
});
|
|
}
|
|
|
|
export function isSpreadsheetFamilyEntry(entry: ToolRegistryEntry): boolean {
|
|
return normalizeLookupValue(entry.familyKey) === SPREADSHEET_FAMILY_KEY;
|
|
}
|
|
|
|
export function getSpreadsheetFamilyKey(): string {
|
|
return SPREADSHEET_FAMILY_KEY;
|
|
}
|