#!/usr/bin/env node /** * 能力描述检查(任务 A1.6,依据 docs/spec/capability-description.md)。 * * 约定:Provider 的每个能力写成一个 `*.capability.json`,放在 providers// 下。 * 七要素缺一即失败——模型选工具靠的就是这份描述(平台不另建意图识别器)。 * * 用法:node scripts/check-capability-description.mjs [仓库根目录] */ import { existsSync } from 'node:fs'; import { readFile, readdir } from 'node:fs/promises'; import { join, relative, sep } from 'node:path'; import process from 'node:process'; import { pathToFileURL } from 'node:url'; const SCAN_DIRS = ['providers']; const SKIP_DIRS = new Set(['node_modules', 'dist', '.git']); const ID_PATTERN = /^[a-z0-9]+(\.[a-z0-9-]+)+$/; const EFFECTS = new Set(['read', 'draft', 'write']); const PURPOSE_MAX = 40; async function collectCapabilityFiles(dir, acc = []) { let entries; try { entries = await readdir(dir, { withFileTypes: true }); } catch { return acc; } for (const entry of entries) { const full = join(dir, entry.name); if (entry.isDirectory()) { if (SKIP_DIRS.has(entry.name)) continue; await collectCapabilityFiles(full, acc); } else if (entry.name.endsWith('.capability.json')) { acc.push(full); } } return acc; } const nonEmptyString = (value) => typeof value === 'string' && value.trim() !== ''; /** 校验单个能力描述对象,返回问题清单 */ export function validateCapability(description) { const problems = []; if (!nonEmptyString(description.id) || !ID_PATTERN.test(description.id)) { problems.push('id 必须形如 aps.schedule.preview(小写点分)'); } if (!nonEmptyString(description.providerId)) problems.push('缺 providerId'); if (!nonEmptyString(description.title)) problems.push('缺 title'); if (!nonEmptyString(description.purpose)) { problems.push('缺 purpose(一句话用途)'); } else if (description.purpose.length > PURPOSE_MAX) { problems.push(`purpose 超过 ${PURPOSE_MAX} 字`); } if (!nonEmptyString(description.whenNotToUse)) problems.push('缺 whenNotToUse(何时不用)'); if (!EFFECTS.has(description.effect)) problems.push('effect 必须是 read / draft / write'); if (typeof description.inputSchema !== 'object' || description.inputSchema === null) problems.push('缺 inputSchema'); if (!Array.isArray(description.synonyms) || description.synonyms.filter(nonEmptyString).length === 0) { problems.push('synonyms 至少一条(业务黑话与同义词)'); } const example = description.example; if (typeof example !== 'object' || example === null || !nonEmptyString(example.utterance) || !nonEmptyString(example.expects)) { problems.push('example 必须含 utterance 与 expects'); } return problems; } export async function findCapabilityProblems(root) { const findings = []; for (const dir of SCAN_DIRS) { const abs = join(root, dir); if (!existsSync(abs)) continue; for (const file of await collectCapabilityFiles(abs)) { const rel = relative(root, file).split(sep).join('/'); let parsed; try { parsed = JSON.parse(await readFile(file, 'utf8')); } catch (error) { findings.push({ file: rel, problems: [`JSON 解析失败:${error.message}`] }); continue; } const problems = validateCapability(parsed); if (problems.length > 0) findings.push({ file: rel, problems }); } } return findings; } async function main() { const root = process.cwd(); const findings = await findCapabilityProblems(root); if (findings.length === 0) { console.log('能力描述检查通过(当前没有能力描述文件时为正常通过)'); return; } console.error(`能力描述检查发现 ${findings.length} 个文件有问题:`); for (const finding of findings) console.error(` ${finding.file}:${finding.problems.join(';')}`); process.exitCode = 1; } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { await main(); }