Files
deta-agent-plugins/scripts/check-plugin-contract.mjs
T
ringo 7d3f9e2933
CI / contract (push) Has been cancelled
refactor(plugin)!: 插件改名 studio、新增 writer、统一本机 stdio 管道
命名结构(品牌=市场,产品=插件):市场名保持 deta;插件 deta → studio(displayName DETA Studio),新增 writer(DETA Writer)。安装标识 studio@deta / writer@deta;MCP 服务名 studio_mcp / writer_mcp,可同时安装。plugins/deta → plugins/studio(git mv 保留历史),新增 plugins/writer。

连接形态统一为单一本机 stdio 管道:.mcp.json 与 cordis.patch.yml 均声明 stdio → bin/deta-mcp.sh --profile <产品>,并按官方插件写法补 cwd: "."(缺它时 Cwd=- ,相对命令按宿主进程 cwd 解析会静默起不来);启动器新增 --profile 档位(工作区 ~/.deta/<产品>/workspace、工具面缺省 full),运行时按 DETA_SERVER_ROOT → ~/.deta/runtime → 开发检出解析,入口优先打包产物;DETA_TOKEN 可选注入(缺省时云类工具返回 auth_required)。原远程端点声明(含 OAuth/401 段落)从插件移除。

文档与治理:根 README 与两个插件 README 改为单一管道口径;技能 aml-design 对齐本机全量面;新增 scripts/check-plugin-contract.mjs(E1 禁工具清单 / E2 必须给出 list_capabilities / E3 禁解释内部实现 / E4 市场条目一致性 / W1 开发检出依赖)与 .gitea/workflows/ci.yml。版本:studio 0.3.1、writer 0.1.0。
2026-09-20 11:56:23 +08:00

164 lines
7.0 KiB
JavaScript
Raw 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
/**
* 插件仓契约校验(仓库职责章程的机器化部分)
*
* 章程:本仓是**宿主适配与分发层**,零逻辑、不承载契约。
* 能力(工具实现 / MCP 协议 / 启动形态 / 工具面裁剪 / 领域技能)唯一来源在 ant-copilot;
* 本仓只保留「宿主清单 + 连接声明 + 启动器 + 宿主侧投影」。
*
* 规则:
* E1 技能文档不得出现「工具一览 / 工具清单」类章节标题
* —— 工具面随部署形态裁剪(engine / full),任何手写清单必然过期;
* 取清单的唯一途径是运行时调 `list_capabilities`。
* E2 技能文档必须给出「取当前形态工具名」的途径(出现 `list_capabilities`)
* —— 正面要求,零误报:只要技能不许自带清单、又必须教模型去哪查,
* 工具面增删就与本文档解耦。(曾试过"段落内枚举 ≥5 个标识符"的启发式,
* 误报率高——流程散文里正常提及工具名会被误判,已弃用。)
* E3 插件 README 不得解释领域服务内部常量
* —— 那是 ant-copilot 的实现细节,文档越界即漂移源。
* W1 不得出现开发检出路径(~/Dev/ant/ant-copilot)
* —— 本机引擎包落地后升级为错误(见章程 M2)。
*
* 用法:node scripts/check-plugin-contract.mjs [--json]
* 退出码:有 error 时 1,否则 0(warning 不失败)。
*/
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
import { join, relative, basename, resolve } from 'node:path';
const ROOT = new URL('..', import.meta.url).pathname.replace(/\/$/, '');
const PLUGINS = join(ROOT, 'plugins');
/** 本目录是工具自身,不参与内容校验(其注释里必然出现被禁模式) */
const SKIP_DIRS = new Set(['node_modules', '.git', 'scripts']);
/** 遍历目录,返回全部文件绝对路径 */
function walk(dir) {
const out = [];
for (const name of readdirSync(dir)) {
if (SKIP_DIRS.has(name)) continue;
const p = join(dir, name);
const st = statSync(p);
if (st.isDirectory()) out.push(...walk(p));
else out.push(p);
}
return out;
}
const RULES = {
E1: {
level: 'error',
what: '技能文档出现「工具一览 / 工具清单」类章节标题',
hint: '删掉清单,改为「不确定有哪些工具时调 list_capabilities」——工具面按形态裁剪,手写清单必然过期',
},
E2: {
level: 'error',
what: '技能文档没有给出取当前形态工具名的途径(缺 list_capabilities 指引)',
hint: '在技能开头加一句:不确定有哪些工具时先调 list_capabilities 拿完整工具名,再按名字直接调用',
},
E3: {
level: 'error',
what: '插件 README 解释领域服务内部常量',
hint: 'ant-copilot 的实现细节不进本仓文档;本仓只写安装 / 连接 / 排障',
},
E4: {
level: 'error',
what: '市场条目与实际插件目录不一致(悬空条目或名称不匹配)',
hint: '条目 name 必须等于该目录 .codex-plugin/plugin.json 的 name,path 必须真实存在——改名/加产品时同步改市场文件',
},
W1: {
level: 'warning',
what: '出现开发检出路径(~/Dev/ant/ant-copilot)',
hint: '启动器应随包分发运行时;本机引擎包落地后本规则升级为 error',
},
};
const HEADING_BAN = /工具一览|工具清单|工具列表|工具汇总|可用工具|能力一览|能力清单/;
/**
* 只认「赋值/缺省」形态(`VAR=` 开头)——那才是"解释内部语义"的信号;
* 裸写常量名的行常常是章程里在说"不要解释它",或启动器在转发自己的用户级旋钮,
* 一律放行(宁可漏报,不要把自己的章程判成违规)。
*/
const INTERNAL_CONST = /\b(?:DETA_TOOL_SURFACE|DETA_MCP_PORT|DETA_GSA_URL)\s*=|dist\/src\/headless|dist\/deta-mcp/;
const DEV_PATH = /(?:\$HOME|~|\/Users\/[^/\s]+)\/Dev\/ant\/ant-copilot/;
const findings = [];
const add = (rule, file, line, evidence) =>
findings.push({ rule, level: RULES[rule].level, file: relative(ROOT, file), line, evidence: evidence.trim() });
const files = walk(ROOT);
for (const file of files) {
const isSkill = file.includes(`${join('skills', '')}`) && basename(file) === 'SKILL.md';
const isReadme = basename(file).toLowerCase().startsWith('readme');
const isText = /\.(md|ya?ml|json|mjs|sh|ts)$/.test(file);
let text;
try {
text = readFileSync(file, 'utf8');
} catch {
continue;
}
const lines = text.split('\n');
lines.forEach((line, i) => {
const n = i + 1;
// E1:技能文档的章节标题
if (isSkill && /^#{2,6}\s/.test(line) && HEADING_BAN.test(line)) add('E1', file, n, line);
// E3:README 里的领域服务内部常量
if (isReadme && INTERNAL_CONST.test(line)) add('E3', file, n, line);
// W1:开发检出路径
if (isText && DEV_PATH.test(line)) add('W1', file, n, line);
});
// E2:技能必须教模型去哪取工具名(正面要求,零误报)
if (isSkill && !text.includes('list_capabilities')) {
add('E2', file, 1, '全文未出现 list_capabilities');
}
}
// E4:市场条目 ↔ 插件目录一致性(改名 / 加产品时防悬空条目)
for (const rel of ['.agents/plugins/marketplace.json', '.agents/plugins/api_marketplace.json']) {
const abs = join(ROOT, rel);
if (!existsSync(abs)) continue;
let market;
try {
market = JSON.parse(readFileSync(abs, 'utf8'));
} catch (e) {
add('E4', abs, 1, `市场文件不是合法 JSON:${e.message}`);
continue;
}
for (const entry of market.plugins ?? []) {
const relPath = String(entry?.source?.path ?? '');
const manifest = join(resolve(ROOT, relPath), '.codex-plugin', 'plugin.json');
if (!relPath || !existsSync(manifest)) {
add('E4', abs, 1, `条目 name=${entry?.name ?? '?'} 指向的插件目录不存在或缺插件清单:${relPath || '(空 path)'}`);
continue;
}
let name = '';
try {
name = String(JSON.parse(readFileSync(manifest, 'utf8')).name ?? '');
} catch {
// 清单本身不合法由 validate_plugin.py 报,这里不重复
}
if (name !== entry.name) {
add('E4', abs, 1, `市场条目 name=${entry.name} 与插件清单 name=${name} 不一致`);
}
}
}
if (process.argv.includes('--json')) {
console.log(JSON.stringify({ findings, rules: RULES }, null, 2));
} else {
for (const f of findings) {
const mark = f.level === 'error' ? '✗' : '!';
console.log(`${mark} [${f.rule}] ${f.file}:${f.line}`);
console.log(` ${f.evidence.slice(0, 140)}`);
if (f.level === 'error') console.log(` → ${RULES[f.rule].hint}`);
}
const errs = findings.filter((f) => f.level === 'error').length;
const warns = findings.length - errs;
console.log(`\n契约校验:${errs} 个错误 / ${warns} 个警告`);
if (!findings.length) console.log('✓ 全部通过');
}
process.exit(findings.some((f) => f.level === 'error') ? 1 : 0);