aps-agent/scripts/check-capability-descriptio...

117 lines
4.3 KiB
JavaScript
Raw Normal View History

#!/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();
}