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; } 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; } 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[] { const seen = new Set(); 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 { return typeof value === 'object' && value !== null; } function getMetadataStringArray( metadata: Record, ...keys: string[] ): string[] { for (const key of keys) { const value = metadata[key]; if (Array.isArray(value)) { return normalizeStringList(value); } } return []; } function getMetadataBoolean( metadata: Record, ...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[] { 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, 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(); 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; }