117 lines
4.3 KiB
JavaScript
117 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 能力描述检查(任务 A1.6,依据 docs/spec/capability-description.md)。
|
||
*
|
||
* 约定:Provider 的每个能力写成一个 `*.capability.json`,放在 providers/<id>/ 下。
|
||
* 七要素缺一即失败——模型选工具靠的就是这份描述(平台不另建意图识别器)。
|
||
*
|
||
* 用法: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 = [];
|
||
let scanned = 0;
|
||
for (const dir of SCAN_DIRS) {
|
||
const abs = join(root, dir);
|
||
if (!existsSync(abs)) continue;
|
||
for (const file of await collectCapabilityFiles(abs)) {
|
||
scanned += 1;
|
||
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;
|
||
}
|
||
|
||
export async function countCapabilityFiles(root) {
|
||
let count = 0;
|
||
for (const dir of SCAN_DIRS) {
|
||
const abs = join(root, dir);
|
||
if (existsSync(abs)) count += (await collectCapabilityFiles(abs)).length;
|
||
}
|
||
return count;
|
||
}
|
||
|
||
async function main() {
|
||
const root = process.cwd();
|
||
const findings = await findCapabilityProblems(root);
|
||
const scanned = await countCapabilityFiles(root);
|
||
if (findings.length === 0) {
|
||
console.log(`能力描述检查通过:校验 ${scanned} 个能力描述文件`);
|
||
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();
|
||
}
|