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

117 lines
4.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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