refactor(plugin)!: 插件改名 studio、新增 writer、统一本机 stdio 管道
CI / contract (push) Has been cancelled

命名结构(品牌=市场,产品=插件):市场名保持 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。
This commit is contained in:
ringo
2026-09-20 11:56:23 +08:00
parent 80fbd162d3
commit 7d3f9e2933
28 changed files with 1354 additions and 418 deletions
+163
View File
@@ -0,0 +1,163 @@
#!/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);