根因:启动器的运行时解析第三顺位是开发检出 ~/Dev/ant/ant-copilot,同事机器没有该目录 → 插件装上、MCP 起不来;而私有 registry 匿名读返回 401,走 npm 安装等于要求每台机器都配 Gitea token。 改法:① 打包产物随插件分发(plugins/<产品>/runtime/,3.96MB 自包含单文件 + manifest 记录来源版本/提交/大小/sha256);② 启动器每次拉起比对 sha256,缺失或被改动即从插件包安装到 ~/.deta/runtime(首次启动零操作、升级自动同步);③ 新增 deta install-runtime(显式安装 + 补装可选依赖 @resvg/resvg-js + 装完立刻做一次真实 MCP 握手自证),status/doctor 显示运行时来源与预览图依赖状态;④ 契约校验加 E6:运行时包必须自洽(sha256/字节数/manifest.pluginVersion 与插件版本一致),拦住“升版本忘重跑 sync-runtime”。 实测:全新 HOME(模拟同事机器、无开发检出、无凭据)下供给探针 10/10 通过 —— 首次启动自动安装、启动行指向沙箱运行时、install-runtime 握手 39 个工具、重复安装幂等、运行时被改动自动修复、插件包缺运行时两处都给打包缺陷指引;已装副本 0.3.14 独立验证(含 npm 自动补装 @resvg 2 包)。studio 0.3.14 / writer 0.1.5。
This commit is contained in:
@@ -74,14 +74,18 @@ codex plugin marketplace add https://git.deta.city/deta/deta-agent-plugins.git
|
||||
|
||||
codex plugin list
|
||||
codex plugin add studio@deta
|
||||
|
||||
# 准备领域服务运行时(二选一):本机引擎包(~/.deta/runtime)或源码构建
|
||||
# 源码构建:在领域服务仓库执行 npm run build:mcp
|
||||
# 冒烟(应等待 stdin/stdout)
|
||||
bash plugins/studio/bin/deta-mcp.sh
|
||||
```
|
||||
|
||||
装完即用,**没有 OAuth 登录步骤、也不需要终端**。需要云侧能力(项目列表 / 新建项目 / 项目同步 /
|
||||
领域服务运行时随插件包分发,启动器首次拉起时自动安装到 `~/.deta/runtime`,插件升级后自动同步
|
||||
(只比对哈希,命中就跳过)。需要显式重装、或补装候选预览图的栅格化依赖时:
|
||||
|
||||
```bash
|
||||
~/.deta/bin/deta install-runtime # 安装/更新运行时 + 预览图依赖,并立刻做一次真实 MCP 握手
|
||||
~/.deta/bin/deta status # 运行时来源(版本 / 提交 / 哈希)、凭据来源、工作区
|
||||
~/.deta/bin/deta doctor # 端到端自检:运行时 / 凭据 / 编译服务 / MCP 握手
|
||||
```
|
||||
|
||||
**装完即用**:没有额外安装步骤,也不需要终端。需要云侧能力(项目列表 / 新建项目 / 项目同步 /
|
||||
打开 Studio)时:让助手调 `login` 工具 → 浏览器打开授权地址(首次输一次账号密码或邮箱验证码)→
|
||||
点「登录并授权」;令牌自动写入 `~/.deta` 并即时生效,过期先静默续期。
|
||||
|
||||
@@ -104,6 +108,21 @@ codex plugin add studio@deta
|
||||
|
||||
(版本号形如 `0.1.0+codex.20260910020424`;重装后需开新会话才能加载新技能。)
|
||||
|
||||
开发态要跑**工作树**(而不是插件包里那份打包运行时)时,显式指定运行时根:
|
||||
|
||||
```bash
|
||||
DETA_SERVER_ROOT=~/Dev/ant/ant-copilot bash plugins/studio/bin/deta-mcp.sh --profile studio
|
||||
```
|
||||
|
||||
领域服务改动后,要把新产物同步进插件包(否则同事与你自己装的都是旧运行时),
|
||||
并顺手升插件版本(契约校验会拦下版本与运行时不一致的情况):
|
||||
|
||||
```bash
|
||||
node scripts/sync-runtime.mjs
|
||||
node scripts/check-plugin-contract.mjs
|
||||
bash scripts/verify-runtime-provision.sh # 运行时供给探针(全新 HOME 下 10 项断言)
|
||||
```
|
||||
|
||||
校验清单:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "studio",
|
||||
"version": "0.3.13",
|
||||
"version": "0.3.14",
|
||||
"description": "DETA Studio 插件:以 AML 为唯一事实源编辑建筑模型,编译为建筑模型并执行规则校验。",
|
||||
"author": {
|
||||
"name": "DETA",
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
* 纪律:本文件**永不打印令牌**,只报告来源与长度;密码/验证码一律由 deta-login.mjs
|
||||
* 在用户自己的终端里交互读取(不回显)。
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join, resolve, basename } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -64,13 +65,135 @@ function cmdLogin(argv) {
|
||||
child.on('exit', (code) => process.exit(code ?? 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时供给:插件包自带打包产物 → 安装到 `~/.deta/runtime`。
|
||||
*
|
||||
* 为什么随插件分发而不是 npm 安装:`@ringoarch/ant-copilot` 在私有 registry 上,匿名读 401,
|
||||
* 走 npm 等于要求每台同事机器都配 token。打包产物是自包含单文件,插件包带上即可「装插件就能用」。
|
||||
* 启动器每次拉起都会做同样的幂等检查(缺失/不一致就补)——本命令是它的**显式版 + 自证版**,
|
||||
* 用于首次核对、补装可选依赖、以及排障(装完立刻做一次真实 MCP 握手)。
|
||||
*/
|
||||
|
||||
/** 插件包内的运行时位置 */
|
||||
function bundledRuntime() {
|
||||
const dir = join(HERE, '..', 'runtime');
|
||||
return { dir, entry: join(dir, 'dist', 'deta-mcp.mjs'), manifestPath: join(dir, 'manifest.json') };
|
||||
}
|
||||
|
||||
function sha256File(p) {
|
||||
try {
|
||||
return createHash('sha256').update(readFileSync(p)).digest('hex');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function readJson(p) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(p, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 运行时溯源描述(给 status / doctor 用;永不打印凭据) */
|
||||
function runtimeProvenance(root) {
|
||||
const m = readJson(join(root, 'manifest.json'));
|
||||
if (!m) return '';
|
||||
const sha = sha256File(join(root, 'dist', 'deta-mcp.mjs'));
|
||||
const verdict = sha && sha === m.sha256 ? 'sha 一致' : 'sha 不一致(运行时被改动或拷贝不完整,跑 deta install-runtime --force 重装)';
|
||||
const size = typeof m.bytes === 'number' ? ` · ${(m.bytes / 1048576).toFixed(2)}MB` : '';
|
||||
return `${m.source ?? '?'}@${m.version ?? '?'} (${m.gitSha ?? '?'})${size} · ${verdict}`;
|
||||
}
|
||||
|
||||
/** 候选预览图的栅格化依赖(可选):缺失只影响候选预览图 */
|
||||
function resvgReady(root) {
|
||||
return existsSync(join(root, 'node_modules', '@resvg', 'resvg-js', 'package.json'));
|
||||
}
|
||||
|
||||
async function cmdInstallRuntime(argv) {
|
||||
const force = argv.includes('--force');
|
||||
const noResvg = argv.includes('--no-resvg');
|
||||
const b = bundledRuntime();
|
||||
|
||||
if (!existsSync(b.entry)) {
|
||||
console.error(`✗ 插件包内没有运行时:${b.entry}`);
|
||||
console.error(' 这是打包缺陷(插件应由 scripts/sync-runtime.mjs 带上运行时)。');
|
||||
console.error(' 处置:重装插件(codex plugin remove <插件>@deta && codex plugin add <插件>@deta);');
|
||||
console.error(' 开发态则在插件仓执行 node scripts/sync-runtime.mjs。');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const manifest = readJson(b.manifestPath) ?? {};
|
||||
const srcSha = sha256File(b.entry);
|
||||
if (manifest.sha256 && srcSha !== manifest.sha256) {
|
||||
console.error(`✗ 插件包内运行时与 manifest 不一致(包内 sha ${String(srcSha).slice(0, 12)}… ≠ manifest ${String(manifest.sha256).slice(0, 12)}…)`);
|
||||
console.error(' 处置:在插件仓重跑 node scripts/sync-runtime.mjs 并重装插件。');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const target = join(DETA_DIR, 'runtime');
|
||||
const targetEntry = join(target, 'dist', 'deta-mcp.mjs');
|
||||
const installedSha = sha256File(targetEntry);
|
||||
const same = installedSha && installedSha === srcSha;
|
||||
const sizeMb = (statSync(b.entry).size / 1048576).toFixed(2);
|
||||
|
||||
if (same && !force) {
|
||||
console.log(`✓ 运行时已是最新:${target}(${sizeMb}MB · ${b.manifestPath ? `来自 ${manifest.source ?? '?'}@${manifest.version ?? '?'} (${manifest.gitSha ?? '?'})` : ''})`);
|
||||
} else {
|
||||
mkdirSync(join(target, 'dist'), { recursive: true });
|
||||
copyFileSync(b.entry, targetEntry);
|
||||
if (existsSync(b.manifestPath)) copyFileSync(b.manifestPath, join(target, 'manifest.json'));
|
||||
const verify = sha256File(targetEntry);
|
||||
if (verify !== srcSha) {
|
||||
console.error(`✗ 安装后校验失败:${targetEntry} 与源不一致`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✓ 已安装运行时到 ${target}(${sizeMb}MB · sha ${srcSha.slice(0, 12)}… · 来自 ${manifest.source ?? '?'}@${manifest.version ?? '?'} (${manifest.gitSha ?? '?'}))`);
|
||||
}
|
||||
|
||||
// 可选依赖:候选预览图的 SVG 栅格化(缺了只是没有预览图,不影响其他能力)
|
||||
if (noResvg) {
|
||||
console.log('· 已按 --no-resvg 跳过 @resvg/resvg-js(候选预览图不可用)');
|
||||
} else if (resvgReady(target) && !force) {
|
||||
console.log('✓ 候选预览图依赖 @resvg/resvg-js 已就绪');
|
||||
} else {
|
||||
console.log('· 正在安装候选预览图依赖 @resvg/resvg-js(可选,需要联网访问公共 npm)…');
|
||||
const r = spawnSync('npm', ['i', '--prefix', target, '--no-save', '--no-audit', '--no-fund', '--loglevel=error', '@resvg/resvg-js@^2'], { stdio: 'inherit' });
|
||||
if (r.error || r.status !== 0) {
|
||||
console.log('! @resvg/resvg-js 未装上(自动安装失败)——其余能力不受影响,仅候选预览图不可用。');
|
||||
console.log(` 手动补装:npm i --prefix "${target}" @resvg/resvg-js@^2`);
|
||||
if (r.error) console.log(` 原因:${r.error.message}`);
|
||||
} else {
|
||||
console.log('✓ 候选预览图依赖 @resvg/resvg-js 已就绪');
|
||||
}
|
||||
}
|
||||
|
||||
// 装完立刻做一次真实 MCP 握手——「装上了」不等于「起得来」
|
||||
const launcher = join(HERE, 'deta-mcp.sh');
|
||||
const r = await smokeMcp(launcher, PROFILE);
|
||||
if (r.ok) {
|
||||
console.log(`✓ MCP 握手通过(--profile ${PROFILE}):${r.tools} 个工具${r.tokenLine ? ` · ${r.tokenLine.trim()}` : ''}`);
|
||||
console.log('\n宿主里的下一步:退出并重启宿主应用(或开新会话)即可看到工具。');
|
||||
} else {
|
||||
console.error(`✗ MCP 握手失败:${r.detail}`);
|
||||
console.error(' 排障:~/.deta/bin/deta doctor;仍不行时看启动器输出的运行时路径是否正确。');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function cmdStatus() {
|
||||
const { source, token, meta } = readToken();
|
||||
console.log(`凭据:${token ? `已存在(来源 ${source},长度 ${token.length})` : '未登录'}`);
|
||||
if (meta?.username) console.log(`账号:${meta.username}(登录于 ${meta.loginAt ?? '未知'})`);
|
||||
console.log(`工作区:${process.env.DETA_WORKSPACE || join(DETA_DIR, PROFILE, 'workspace')}`);
|
||||
const runtime = resolveRuntime();
|
||||
console.log(`运行时:${runtime || '未找到(需要本机引擎包或 DETA_SERVER_ROOT)'}`);
|
||||
console.log(`运行时:${runtime || '未找到(插件包自带运行时,正常应由启动器自动安装到 ~/.deta/runtime)'}`);
|
||||
if (runtime) {
|
||||
const prov = runtimeProvenance(runtime);
|
||||
console.log(` · 来源:${prov || '开发检出(非打包运行时,仅开发机可见)'}`);
|
||||
console.log(` · 候选预览图依赖 @resvg/resvg-js:${resvgReady(runtime) ? '已就绪' : '缺失(跑 deta install-runtime 补装;不影响其他能力)'}`);
|
||||
}
|
||||
if (!token) console.log('\n未登录时云类工具(项目列表 / 同步 / 打开 Studio)会返回 auth_required;跑 `deta login` 即可。');
|
||||
}
|
||||
|
||||
@@ -118,7 +241,12 @@ async function cmdDoctor() {
|
||||
const say = (ok, label, detail) => { console.log(`${ok ? '✓' : '✗'} ${label}${detail ? ` —— ${detail}` : ''}`); if (!ok) failed += 1; };
|
||||
|
||||
const runtime = resolveRuntime();
|
||||
say(Boolean(runtime), '领域服务运行时', runtime || '未找到:装本机引擎包到 ~/.deta/runtime,或设 DETA_SERVER_ROOT');
|
||||
say(Boolean(runtime), '领域服务运行时', runtime || '未找到:插件包自带时应由启动器自动装到 ~/.deta/runtime;否则设 DETA_SERVER_ROOT');
|
||||
if (runtime) {
|
||||
console.log(` · 来源:${runtimeProvenance(runtime) || '开发检出(非打包运行时)'}`);
|
||||
const hasResvg = resvgReady(runtime);
|
||||
console.log(` · 候选预览图依赖 @resvg/resvg-js:${hasResvg ? '已就绪' : '缺失 —— 跑 deta install-runtime 补装(只影响候选预览图)'}`);
|
||||
}
|
||||
|
||||
const { source, token } = readToken();
|
||||
say(Boolean(token), '云凭据', token ? `已存在(来源 ${source},长度 ${token.length})` : '未登录:跑 `~/.deta/bin/deta login`');
|
||||
@@ -147,18 +275,21 @@ switch (cmd) {
|
||||
case 'login': cmdLogin(rest); break;
|
||||
case 'status': cmdStatus(); break;
|
||||
case 'doctor': await cmdDoctor(); break;
|
||||
case 'install-runtime': await cmdInstallRuntime(rest); break;
|
||||
case 'help':
|
||||
case '--help':
|
||||
case '-h':
|
||||
console.log(`DETA 命令行(插件自动注册为 ~/.deta/bin/deta)
|
||||
|
||||
deta login [--identifier <账号>] [--email-code <邮箱>] 登录 deta.city(凭据写入 ~/.deta/)
|
||||
deta install-runtime [--force] [--no-resvg] 安装/更新领域服务运行时 + 可选预览图依赖
|
||||
deta status 查看凭据来源 / 运行时 / 工作区
|
||||
deta doctor 端到端自检(运行时、凭据、GSA、MCP 握手)
|
||||
|
||||
运行时通常无需手动安装:启动器会在插件升级后自动同步到 ~/.deta/runtime。
|
||||
登录后开新会话即可让宿主读到凭据(无需 source / launchctl / 重启应用)。`);
|
||||
break;
|
||||
default:
|
||||
console.error(`未知命令:${cmd}(可用:login / status / doctor)`);
|
||||
console.error(`未知命令:${cmd}(可用:login / install-runtime / status / doctor)`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,30 @@ while [ $# -gt 0 ]; do
|
||||
esac
|
||||
done
|
||||
|
||||
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
# ── 运行时自备:插件包内自带打包产物(同事机器没有开发检出,也拿不到私有 registry)──
|
||||
# 缺失或与插件包不一致时自动安装到 $HOME/.deta/runtime;只比 sha256,命中即跳过(4MB 哈希约 20ms)。
|
||||
# 为什么随插件分发:私有 registry 匿名读 401,走 npm 会让每个同事机器都要配 token。
|
||||
RUNTIME_DIR="$HOME/.deta/runtime"
|
||||
BUNDLED="$PLUGIN_ROOT/runtime/dist/deta-mcp.mjs"
|
||||
if [ -f "$BUNDLED" ]; then
|
||||
WANT_SHA="$(shasum -a 256 "$BUNDLED" 2>/dev/null | cut -d' ' -f1)"
|
||||
HAVE_SHA=""
|
||||
if [ -f "$RUNTIME_DIR/dist/deta-mcp.mjs" ]; then
|
||||
HAVE_SHA="$(shasum -a 256 "$RUNTIME_DIR/dist/deta-mcp.mjs" 2>/dev/null | cut -d' ' -f1)"
|
||||
fi
|
||||
if [ -n "$WANT_SHA" ] && [ "$WANT_SHA" != "$HAVE_SHA" ]; then
|
||||
mkdir -p "$RUNTIME_DIR/dist" 2>/dev/null || true
|
||||
if cp "$BUNDLED" "$RUNTIME_DIR/dist/deta-mcp.mjs" 2>/dev/null; then
|
||||
if [ -f "$PLUGIN_ROOT/runtime/manifest.json" ]; then
|
||||
cp "$PLUGIN_ROOT/runtime/manifest.json" "$RUNTIME_DIR/manifest.json" 2>/dev/null || true
|
||||
fi
|
||||
echo "[deta] 已从插件包安装运行时到 ${RUNTIME_DIR}" >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 运行时解析:显式 DETA_SERVER_ROOT → 本机引擎包安装位置 → 本机开发检出(打印提示)──
|
||||
ROOT="${DETA_SERVER_ROOT:-}"
|
||||
if [ -z "$ROOT" ]; then
|
||||
@@ -70,7 +94,6 @@ export DETA_TOOL_SURFACE="${DETA_SURFACE:-full}"
|
||||
|
||||
# ── 稳定入口:把命令行门面注册为 ~/.deta/bin/deta(幂等,路径随插件更新自动刷新)──
|
||||
# 插件装在宿主缓存目录,用户看不见这个路径;而用户与工具回执都需要一条可复制的命令。
|
||||
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
BIN_DIR="$HOME/.deta/bin"
|
||||
SHIM="$BIN_DIR/deta"
|
||||
SHIM_BODY="#!/bin/sh
|
||||
@@ -123,4 +146,9 @@ fi
|
||||
|
||||
echo "[deta] 档位=${PROFILE} 运行时=${ROOT} 工作区=${WS} 工具面=${DETA_TOOL_SURFACE} 云令牌=${CLOUD_STATE} 网关桥=${BRIDGE_STATE}" >&2
|
||||
|
||||
# ── 可选依赖提示:缺 @resvg/resvg-js 只影响候选预览图,其余能力不受影响 ──
|
||||
if [ ! -f "$ROOT/node_modules/@resvg/resvg-js/package.json" ]; then
|
||||
echo "[deta] 提示:候选预览图需要 @resvg/resvg-js;运行 ~/.deta/bin/deta install-runtime 补装(其余能力不受影响)" >&2
|
||||
fi
|
||||
|
||||
exec node "$ENTRY" "$@"
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "deta-studio-plugin",
|
||||
"version": "0.3.13",
|
||||
"version": "0.3.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "deta-studio-plugin",
|
||||
"version": "0.3.13",
|
||||
"version": "0.3.14",
|
||||
"license": "Proprietary"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "deta-studio-plugin",
|
||||
"version": "0.3.13",
|
||||
"version": "0.3.14",
|
||||
"description": "DETA Studio:AML 建筑模型读写与编译校验(Codex 插件 / dsh 组合包 双壳)",
|
||||
"type": "module",
|
||||
"license": "Proprietary",
|
||||
|
||||
+96086
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"source": "@ringoarch/ant-copilot",
|
||||
"version": "0.2.2",
|
||||
"gitSha": "984a816",
|
||||
"builtAt": "2026-09-20T08:38:38.048Z",
|
||||
"entry": "dist/deta-mcp.mjs",
|
||||
"sha256": "db05fa93049977fe69846b296e51ee6b9245552e8d96928589911c0051e8ee59",
|
||||
"bytes": 4153494,
|
||||
"note": "由 scripts/sync-runtime.mjs 生成;勿手工编辑。安装到 ~/.deta/runtime 由启动器/install-runtime 完成。",
|
||||
"plugin": "studio",
|
||||
"pluginVersion": "0.3.14"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "writer",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"description": "DETA Writer 插件:读取建筑模型与模拟结果,产出报告 / 文档内容。",
|
||||
"author": {
|
||||
"name": "DETA",
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
* 纪律:本文件**永不打印令牌**,只报告来源与长度;密码/验证码一律由 deta-login.mjs
|
||||
* 在用户自己的终端里交互读取(不回显)。
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join, resolve, basename } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -64,13 +65,135 @@ function cmdLogin(argv) {
|
||||
child.on('exit', (code) => process.exit(code ?? 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时供给:插件包自带打包产物 → 安装到 `~/.deta/runtime`。
|
||||
*
|
||||
* 为什么随插件分发而不是 npm 安装:`@ringoarch/ant-copilot` 在私有 registry 上,匿名读 401,
|
||||
* 走 npm 等于要求每台同事机器都配 token。打包产物是自包含单文件,插件包带上即可「装插件就能用」。
|
||||
* 启动器每次拉起都会做同样的幂等检查(缺失/不一致就补)——本命令是它的**显式版 + 自证版**,
|
||||
* 用于首次核对、补装可选依赖、以及排障(装完立刻做一次真实 MCP 握手)。
|
||||
*/
|
||||
|
||||
/** 插件包内的运行时位置 */
|
||||
function bundledRuntime() {
|
||||
const dir = join(HERE, '..', 'runtime');
|
||||
return { dir, entry: join(dir, 'dist', 'deta-mcp.mjs'), manifestPath: join(dir, 'manifest.json') };
|
||||
}
|
||||
|
||||
function sha256File(p) {
|
||||
try {
|
||||
return createHash('sha256').update(readFileSync(p)).digest('hex');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function readJson(p) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(p, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 运行时溯源描述(给 status / doctor 用;永不打印凭据) */
|
||||
function runtimeProvenance(root) {
|
||||
const m = readJson(join(root, 'manifest.json'));
|
||||
if (!m) return '';
|
||||
const sha = sha256File(join(root, 'dist', 'deta-mcp.mjs'));
|
||||
const verdict = sha && sha === m.sha256 ? 'sha 一致' : 'sha 不一致(运行时被改动或拷贝不完整,跑 deta install-runtime --force 重装)';
|
||||
const size = typeof m.bytes === 'number' ? ` · ${(m.bytes / 1048576).toFixed(2)}MB` : '';
|
||||
return `${m.source ?? '?'}@${m.version ?? '?'} (${m.gitSha ?? '?'})${size} · ${verdict}`;
|
||||
}
|
||||
|
||||
/** 候选预览图的栅格化依赖(可选):缺失只影响候选预览图 */
|
||||
function resvgReady(root) {
|
||||
return existsSync(join(root, 'node_modules', '@resvg', 'resvg-js', 'package.json'));
|
||||
}
|
||||
|
||||
async function cmdInstallRuntime(argv) {
|
||||
const force = argv.includes('--force');
|
||||
const noResvg = argv.includes('--no-resvg');
|
||||
const b = bundledRuntime();
|
||||
|
||||
if (!existsSync(b.entry)) {
|
||||
console.error(`✗ 插件包内没有运行时:${b.entry}`);
|
||||
console.error(' 这是打包缺陷(插件应由 scripts/sync-runtime.mjs 带上运行时)。');
|
||||
console.error(' 处置:重装插件(codex plugin remove <插件>@deta && codex plugin add <插件>@deta);');
|
||||
console.error(' 开发态则在插件仓执行 node scripts/sync-runtime.mjs。');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const manifest = readJson(b.manifestPath) ?? {};
|
||||
const srcSha = sha256File(b.entry);
|
||||
if (manifest.sha256 && srcSha !== manifest.sha256) {
|
||||
console.error(`✗ 插件包内运行时与 manifest 不一致(包内 sha ${String(srcSha).slice(0, 12)}… ≠ manifest ${String(manifest.sha256).slice(0, 12)}…)`);
|
||||
console.error(' 处置:在插件仓重跑 node scripts/sync-runtime.mjs 并重装插件。');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const target = join(DETA_DIR, 'runtime');
|
||||
const targetEntry = join(target, 'dist', 'deta-mcp.mjs');
|
||||
const installedSha = sha256File(targetEntry);
|
||||
const same = installedSha && installedSha === srcSha;
|
||||
const sizeMb = (statSync(b.entry).size / 1048576).toFixed(2);
|
||||
|
||||
if (same && !force) {
|
||||
console.log(`✓ 运行时已是最新:${target}(${sizeMb}MB · ${b.manifestPath ? `来自 ${manifest.source ?? '?'}@${manifest.version ?? '?'} (${manifest.gitSha ?? '?'})` : ''})`);
|
||||
} else {
|
||||
mkdirSync(join(target, 'dist'), { recursive: true });
|
||||
copyFileSync(b.entry, targetEntry);
|
||||
if (existsSync(b.manifestPath)) copyFileSync(b.manifestPath, join(target, 'manifest.json'));
|
||||
const verify = sha256File(targetEntry);
|
||||
if (verify !== srcSha) {
|
||||
console.error(`✗ 安装后校验失败:${targetEntry} 与源不一致`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✓ 已安装运行时到 ${target}(${sizeMb}MB · sha ${srcSha.slice(0, 12)}… · 来自 ${manifest.source ?? '?'}@${manifest.version ?? '?'} (${manifest.gitSha ?? '?'}))`);
|
||||
}
|
||||
|
||||
// 可选依赖:候选预览图的 SVG 栅格化(缺了只是没有预览图,不影响其他能力)
|
||||
if (noResvg) {
|
||||
console.log('· 已按 --no-resvg 跳过 @resvg/resvg-js(候选预览图不可用)');
|
||||
} else if (resvgReady(target) && !force) {
|
||||
console.log('✓ 候选预览图依赖 @resvg/resvg-js 已就绪');
|
||||
} else {
|
||||
console.log('· 正在安装候选预览图依赖 @resvg/resvg-js(可选,需要联网访问公共 npm)…');
|
||||
const r = spawnSync('npm', ['i', '--prefix', target, '--no-save', '--no-audit', '--no-fund', '--loglevel=error', '@resvg/resvg-js@^2'], { stdio: 'inherit' });
|
||||
if (r.error || r.status !== 0) {
|
||||
console.log('! @resvg/resvg-js 未装上(自动安装失败)——其余能力不受影响,仅候选预览图不可用。');
|
||||
console.log(` 手动补装:npm i --prefix "${target}" @resvg/resvg-js@^2`);
|
||||
if (r.error) console.log(` 原因:${r.error.message}`);
|
||||
} else {
|
||||
console.log('✓ 候选预览图依赖 @resvg/resvg-js 已就绪');
|
||||
}
|
||||
}
|
||||
|
||||
// 装完立刻做一次真实 MCP 握手——「装上了」不等于「起得来」
|
||||
const launcher = join(HERE, 'deta-mcp.sh');
|
||||
const r = await smokeMcp(launcher, PROFILE);
|
||||
if (r.ok) {
|
||||
console.log(`✓ MCP 握手通过(--profile ${PROFILE}):${r.tools} 个工具${r.tokenLine ? ` · ${r.tokenLine.trim()}` : ''}`);
|
||||
console.log('\n宿主里的下一步:退出并重启宿主应用(或开新会话)即可看到工具。');
|
||||
} else {
|
||||
console.error(`✗ MCP 握手失败:${r.detail}`);
|
||||
console.error(' 排障:~/.deta/bin/deta doctor;仍不行时看启动器输出的运行时路径是否正确。');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function cmdStatus() {
|
||||
const { source, token, meta } = readToken();
|
||||
console.log(`凭据:${token ? `已存在(来源 ${source},长度 ${token.length})` : '未登录'}`);
|
||||
if (meta?.username) console.log(`账号:${meta.username}(登录于 ${meta.loginAt ?? '未知'})`);
|
||||
console.log(`工作区:${process.env.DETA_WORKSPACE || join(DETA_DIR, PROFILE, 'workspace')}`);
|
||||
const runtime = resolveRuntime();
|
||||
console.log(`运行时:${runtime || '未找到(需要本机引擎包或 DETA_SERVER_ROOT)'}`);
|
||||
console.log(`运行时:${runtime || '未找到(插件包自带运行时,正常应由启动器自动安装到 ~/.deta/runtime)'}`);
|
||||
if (runtime) {
|
||||
const prov = runtimeProvenance(runtime);
|
||||
console.log(` · 来源:${prov || '开发检出(非打包运行时,仅开发机可见)'}`);
|
||||
console.log(` · 候选预览图依赖 @resvg/resvg-js:${resvgReady(runtime) ? '已就绪' : '缺失(跑 deta install-runtime 补装;不影响其他能力)'}`);
|
||||
}
|
||||
if (!token) console.log('\n未登录时云类工具(项目列表 / 同步 / 打开 Studio)会返回 auth_required;跑 `deta login` 即可。');
|
||||
}
|
||||
|
||||
@@ -118,7 +241,12 @@ async function cmdDoctor() {
|
||||
const say = (ok, label, detail) => { console.log(`${ok ? '✓' : '✗'} ${label}${detail ? ` —— ${detail}` : ''}`); if (!ok) failed += 1; };
|
||||
|
||||
const runtime = resolveRuntime();
|
||||
say(Boolean(runtime), '领域服务运行时', runtime || '未找到:装本机引擎包到 ~/.deta/runtime,或设 DETA_SERVER_ROOT');
|
||||
say(Boolean(runtime), '领域服务运行时', runtime || '未找到:插件包自带时应由启动器自动装到 ~/.deta/runtime;否则设 DETA_SERVER_ROOT');
|
||||
if (runtime) {
|
||||
console.log(` · 来源:${runtimeProvenance(runtime) || '开发检出(非打包运行时)'}`);
|
||||
const hasResvg = resvgReady(runtime);
|
||||
console.log(` · 候选预览图依赖 @resvg/resvg-js:${hasResvg ? '已就绪' : '缺失 —— 跑 deta install-runtime 补装(只影响候选预览图)'}`);
|
||||
}
|
||||
|
||||
const { source, token } = readToken();
|
||||
say(Boolean(token), '云凭据', token ? `已存在(来源 ${source},长度 ${token.length})` : '未登录:跑 `~/.deta/bin/deta login`');
|
||||
@@ -147,18 +275,21 @@ switch (cmd) {
|
||||
case 'login': cmdLogin(rest); break;
|
||||
case 'status': cmdStatus(); break;
|
||||
case 'doctor': await cmdDoctor(); break;
|
||||
case 'install-runtime': await cmdInstallRuntime(rest); break;
|
||||
case 'help':
|
||||
case '--help':
|
||||
case '-h':
|
||||
console.log(`DETA 命令行(插件自动注册为 ~/.deta/bin/deta)
|
||||
|
||||
deta login [--identifier <账号>] [--email-code <邮箱>] 登录 deta.city(凭据写入 ~/.deta/)
|
||||
deta install-runtime [--force] [--no-resvg] 安装/更新领域服务运行时 + 可选预览图依赖
|
||||
deta status 查看凭据来源 / 运行时 / 工作区
|
||||
deta doctor 端到端自检(运行时、凭据、GSA、MCP 握手)
|
||||
|
||||
运行时通常无需手动安装:启动器会在插件升级后自动同步到 ~/.deta/runtime。
|
||||
登录后开新会话即可让宿主读到凭据(无需 source / launchctl / 重启应用)。`);
|
||||
break;
|
||||
default:
|
||||
console.error(`未知命令:${cmd}(可用:login / status / doctor)`);
|
||||
console.error(`未知命令:${cmd}(可用:login / install-runtime / status / doctor)`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,30 @@ while [ $# -gt 0 ]; do
|
||||
esac
|
||||
done
|
||||
|
||||
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
# ── 运行时自备:插件包内自带打包产物(同事机器没有开发检出,也拿不到私有 registry)──
|
||||
# 缺失或与插件包不一致时自动安装到 $HOME/.deta/runtime;只比 sha256,命中即跳过(4MB 哈希约 20ms)。
|
||||
# 为什么随插件分发:私有 registry 匿名读 401,走 npm 会让每个同事机器都要配 token。
|
||||
RUNTIME_DIR="$HOME/.deta/runtime"
|
||||
BUNDLED="$PLUGIN_ROOT/runtime/dist/deta-mcp.mjs"
|
||||
if [ -f "$BUNDLED" ]; then
|
||||
WANT_SHA="$(shasum -a 256 "$BUNDLED" 2>/dev/null | cut -d' ' -f1)"
|
||||
HAVE_SHA=""
|
||||
if [ -f "$RUNTIME_DIR/dist/deta-mcp.mjs" ]; then
|
||||
HAVE_SHA="$(shasum -a 256 "$RUNTIME_DIR/dist/deta-mcp.mjs" 2>/dev/null | cut -d' ' -f1)"
|
||||
fi
|
||||
if [ -n "$WANT_SHA" ] && [ "$WANT_SHA" != "$HAVE_SHA" ]; then
|
||||
mkdir -p "$RUNTIME_DIR/dist" 2>/dev/null || true
|
||||
if cp "$BUNDLED" "$RUNTIME_DIR/dist/deta-mcp.mjs" 2>/dev/null; then
|
||||
if [ -f "$PLUGIN_ROOT/runtime/manifest.json" ]; then
|
||||
cp "$PLUGIN_ROOT/runtime/manifest.json" "$RUNTIME_DIR/manifest.json" 2>/dev/null || true
|
||||
fi
|
||||
echo "[deta] 已从插件包安装运行时到 ${RUNTIME_DIR}" >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 运行时解析:显式 DETA_SERVER_ROOT → 本机引擎包安装位置 → 本机开发检出(打印提示)──
|
||||
ROOT="${DETA_SERVER_ROOT:-}"
|
||||
if [ -z "$ROOT" ]; then
|
||||
@@ -70,7 +94,6 @@ export DETA_TOOL_SURFACE="${DETA_SURFACE:-full}"
|
||||
|
||||
# ── 稳定入口:把命令行门面注册为 ~/.deta/bin/deta(幂等,路径随插件更新自动刷新)──
|
||||
# 插件装在宿主缓存目录,用户看不见这个路径;而用户与工具回执都需要一条可复制的命令。
|
||||
PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
BIN_DIR="$HOME/.deta/bin"
|
||||
SHIM="$BIN_DIR/deta"
|
||||
SHIM_BODY="#!/bin/sh
|
||||
@@ -123,4 +146,9 @@ fi
|
||||
|
||||
echo "[deta] 档位=${PROFILE} 运行时=${ROOT} 工作区=${WS} 工具面=${DETA_TOOL_SURFACE} 云令牌=${CLOUD_STATE} 网关桥=${BRIDGE_STATE}" >&2
|
||||
|
||||
# ── 可选依赖提示:缺 @resvg/resvg-js 只影响候选预览图,其余能力不受影响 ──
|
||||
if [ ! -f "$ROOT/node_modules/@resvg/resvg-js/package.json" ]; then
|
||||
echo "[deta] 提示:候选预览图需要 @resvg/resvg-js;运行 ~/.deta/bin/deta install-runtime 补装(其余能力不受影响)" >&2
|
||||
fi
|
||||
|
||||
exec node "$ENTRY" "$@"
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "deta-writer-plugin",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "deta-writer-plugin",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"license": "Proprietary"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "deta-writer-plugin",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"description": "DETA Writer:建筑性能报告与文档产出(Codex 插件 / dsh 组合包 双壳)",
|
||||
"type": "module",
|
||||
"license": "Proprietary",
|
||||
|
||||
+96086
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"source": "@ringoarch/ant-copilot",
|
||||
"version": "0.2.2",
|
||||
"gitSha": "984a816",
|
||||
"builtAt": "2026-09-20T08:38:38.048Z",
|
||||
"entry": "dist/deta-mcp.mjs",
|
||||
"sha256": "db05fa93049977fe69846b296e51ee6b9245552e8d96928589911c0051e8ee59",
|
||||
"bytes": 4153494,
|
||||
"note": "由 scripts/sync-runtime.mjs 生成;勿手工编辑。安装到 ~/.deta/runtime 由启动器/install-runtime 完成。",
|
||||
"plugin": "writer",
|
||||
"pluginVersion": "0.1.5"
|
||||
}
|
||||
@@ -18,10 +18,15 @@
|
||||
* —— 那是 ant-copilot 的实现细节,文档越界即漂移源。
|
||||
* W1 不得出现开发检出路径(~/Dev/ant/ant-copilot)
|
||||
* —— 本机引擎包落地后升级为错误(见章程 M2)。
|
||||
* E6 运行时包必须自洽:runtime/dist/deta-mcp.mjs 存在,其 sha256/字节数与 runtime/manifest.json 一致,
|
||||
* 且 manifest.pluginVersion 等于插件清单版本
|
||||
* —— 运行时随插件分发(私有 registry 匿名读 401,走 npm 会让每台机器都要 token),
|
||||
* 因此「升了版本忘了重跑 sync-runtime」会静默发出发错版本的运行时;这条规则把它变成红灯。
|
||||
*
|
||||
* 用法:node scripts/check-plugin-contract.mjs [--json]
|
||||
* 退出码:有 error 时 1,否则 0(warning 不失败)。
|
||||
*/
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
||||
import { join, relative, basename, resolve } from 'node:path';
|
||||
|
||||
@@ -75,6 +80,11 @@ const RULES = {
|
||||
hint: '插件清单用 logo / logoDark 指向插件根内相对路径(如 ./assets/logo.svg,深色主题给 logoDark);'
|
||||
+ '注意 iconSmall/iconLarge 是**技能**的字段,插件里写了不生效',
|
||||
},
|
||||
E6: {
|
||||
level: 'error',
|
||||
what: '运行时包不自洽(缺 runtime/、sha256 或字节数与 manifest 不符、manifest.pluginVersion 与插件版本不符)',
|
||||
hint: '运行时随插件分发:改动 ant-copilot 后跑 node scripts/sync-runtime.mjs,再升插件版本重装',
|
||||
},
|
||||
};
|
||||
|
||||
const HEADING_BAN = /工具一览|工具清单|工具列表|工具汇总|可用工具|能力一览|能力清单/;
|
||||
@@ -178,6 +188,40 @@ for (const pluginDir of ['plugins/studio', 'plugins/writer']) {
|
||||
}
|
||||
}
|
||||
|
||||
// E6:运行时包自洽(插件自带打包产物 → 装插件即可用;防「升版本忘重跑 sync-runtime」多发错版本)
|
||||
for (const pluginDir of ['plugins/studio', 'plugins/writer']) {
|
||||
const pluginRoot = join(ROOT, pluginDir);
|
||||
const entry = join(pluginRoot, 'runtime', 'dist', 'deta-mcp.mjs');
|
||||
const mPath = join(pluginRoot, 'runtime', 'manifest.json');
|
||||
if (!existsSync(entry) || !existsSync(mPath)) {
|
||||
add('E6', pluginDir, 1, '缺少运行时包(runtime/dist/deta-mcp.mjs + runtime/manifest.json)——跑 node scripts/sync-runtime.mjs');
|
||||
continue;
|
||||
}
|
||||
let m;
|
||||
try {
|
||||
m = JSON.parse(readFileSync(mPath, 'utf8'));
|
||||
} catch (e) {
|
||||
add('E6', mPath, 1, `manifest.json 解析失败:${e.message}`);
|
||||
continue;
|
||||
}
|
||||
const buf = readFileSync(entry);
|
||||
const sha = createHash('sha256').update(buf).digest('hex');
|
||||
if (sha !== m.sha256) {
|
||||
add('E6', mPath, 1, `运行时 sha256 与 manifest 不一致(文件 ${sha.slice(0, 12)}… ≠ manifest ${String(m.sha256).slice(0, 12)}…)——重跑 sync-runtime`);
|
||||
}
|
||||
if (buf.length !== m.bytes) {
|
||||
add('E6', mPath, 1, `运行时字节数与 manifest 不一致(${buf.length} ≠ ${m.bytes})`);
|
||||
}
|
||||
try {
|
||||
const pv = JSON.parse(readFileSync(join(pluginRoot, '.codex-plugin', 'plugin.json'), 'utf8')).version;
|
||||
if (m.pluginVersion !== pv) {
|
||||
add('E6', mPath, 1, `manifest.pluginVersion=${m.pluginVersion} 与插件版本 ${pv} 不一致——升版本后必须重跑 sync-runtime`);
|
||||
}
|
||||
} catch {
|
||||
// 清单本身不合法由官方校验器报
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv.includes('--json')) {
|
||||
console.log(JSON.stringify({ findings, rules: RULES }, null, 2));
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 把领域服务运行时(ant-copilot 的 esbuild 打包产物)同步进插件包。
|
||||
*
|
||||
* 为什么随插件分发而不是 npm 安装:`@ringoarch/ant-copilot` 发在私有 Gitea registry 上,
|
||||
* 匿名 GET 包元数据返回 401 —— 走 npm 路线等于要求每个同事机器上都配一个 Gitea token。
|
||||
* 打包产物本身是自包含单文件(依赖全内联),把它放进插件包即可做到「装插件 = 拿到运行时」。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/sync-runtime.mjs # studio + writer
|
||||
* node scripts/sync-runtime.mjs studio # 只同步指定插件
|
||||
* ANT_COPILOT_DIR=~/Dev/ant/ant-copilot node scripts/sync-runtime.mjs
|
||||
*
|
||||
* 前置:源仓库已构建(`npm run build:mcp` → dist/deta-mcp.mjs)。
|
||||
* 产物:plugins/<插件>/runtime/dist/deta-mcp.mjs + plugins/<插件>/runtime/manifest.json
|
||||
* manifest 记录来源版本/提交/大小/sha256 —— 供启动器与 install-runtime 自校验(防拷错、防陈旧)。
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const SRC_REPO = resolve(process.env.ANT_COPILOT_DIR ?? join(process.env.HOME ?? '', 'Dev/ant/ant-copilot'));
|
||||
const SRC_ENTRY = join(SRC_REPO, 'dist/deta-mcp.mjs');
|
||||
const plugins = process.argv.slice(2).filter((a) => !a.startsWith('-'));
|
||||
const targets = plugins.length ? plugins : ['studio', 'writer'];
|
||||
|
||||
function die(msg) {
|
||||
process.stderr.write(`[sync-runtime] ${msg}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(SRC_ENTRY)) {
|
||||
die(`源打包产物不存在:${SRC_ENTRY}\n 请先在 ${SRC_REPO} 执行 npm run build:mcp`);
|
||||
}
|
||||
|
||||
const bytes = readFileSync(SRC_ENTRY);
|
||||
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
||||
const version = JSON.parse(readFileSync(join(SRC_REPO, 'package.json'), 'utf8')).version;
|
||||
let gitSha = '';
|
||||
try {
|
||||
gitSha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: SRC_REPO, encoding: 'utf8' }).trim();
|
||||
} catch {
|
||||
gitSha = 'unknown'; // 允许在非 git 检出(如 CI 解包目录)中运行
|
||||
}
|
||||
let pluginVersion = {};
|
||||
const manifest = {
|
||||
source: '@ringoarch/ant-copilot',
|
||||
version,
|
||||
gitSha,
|
||||
builtAt: new Date().toISOString(),
|
||||
entry: 'dist/deta-mcp.mjs',
|
||||
sha256,
|
||||
bytes: bytes.length,
|
||||
note: '由 scripts/sync-runtime.mjs 生成;勿手工编辑。安装到 ~/.deta/runtime 由启动器/install-runtime 完成。',
|
||||
};
|
||||
|
||||
for (const name of targets) {
|
||||
const pluginDir = join(ROOT, 'plugins', name);
|
||||
const pj = join(pluginDir, '.codex-plugin', 'plugin.json');
|
||||
if (!existsSync(pj)) die(`插件不存在或清单缺失:${pj}`);
|
||||
const pv = JSON.parse(readFileSync(pj, 'utf8')).version ?? null;
|
||||
const outDir = join(pluginDir, 'runtime', 'dist');
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const outEntry = join(outDir, 'deta-mcp.mjs');
|
||||
copyFileSync(SRC_ENTRY, outEntry);
|
||||
const out = { ...manifest, plugin: name, pluginVersion: pv };
|
||||
writeFileSync(join(pluginDir, 'runtime', 'manifest.json'), `${JSON.stringify(out, null, 2)}\n`);
|
||||
const size = statSync(outEntry).size;
|
||||
if (size !== bytes.length) die(`写入后大小不一致(${size} != ${bytes.length}):${outEntry}`);
|
||||
process.stdout.write(
|
||||
`[sync-runtime] ${name}: ${(size / 1048576).toFixed(2)}MB sha256=${sha256.slice(0, 12)}… `
|
||||
+ `来自 ${manifest.source}@${version} (${gitSha})\n`,
|
||||
);
|
||||
}
|
||||
process.stdout.write(`[sync-runtime] 完成。提交 runtime/ 后同事装插件即自带运行时。\n`);
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# 探针:运行时供给(模拟同事机器 —— 全新 HOME、无开发检出、无凭据、无 token)
|
||||
#
|
||||
# 背景:私有 registry 匿名读 401,运行时因此**随插件包分发**(plugins/<产品>/runtime/),
|
||||
# 由启动器在首次启动时自动安装到 $HOME/.deta/runtime。本探针断言这条链路的五件事:
|
||||
# ① 首次启动自动安装(用户零操作)
|
||||
# ② install-runtime 自证(真实 MCP 握手 + 工具数)
|
||||
# ③ 幂等(重复安装不重复拷贝)
|
||||
# ④ 插件包缺运行时 → 报错清楚且指向打包缺陷(不是让用户去配环境)
|
||||
# ⑤ 运行时被改动/截断 → 下次启动自动修复
|
||||
#
|
||||
# 用法:bash scripts/verify-runtime-provision.sh [插件名,缺省 studio]
|
||||
set -u
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PROFILE="${1:-studio}"
|
||||
PLUGIN="$ROOT/plugins/$PROFILE"
|
||||
FRESH="$(mktemp -d /tmp/deta-runtime-probe.XXXXXX)"
|
||||
pass=0
|
||||
fail=0
|
||||
ok() { printf ' PASS %s\n' "$1"; pass=$((pass + 1)); }
|
||||
no() { printf ' FAIL %s\n' "$1"; fail=$((fail + 1)); }
|
||||
|
||||
[ -d "$PLUGIN" ] || { echo "插件不存在:$PLUGIN"; exit 2; }
|
||||
echo "== 沙箱 HOME=${FRESH}(插件=${PROFILE})"
|
||||
|
||||
# ① 首次启动:启动器应自行从插件包安装运行时
|
||||
OUT="$(HOME="$FRESH" bash "$PLUGIN/bin/deta-mcp.sh" --profile "$PROFILE" < /dev/null 2>&1 || true)"
|
||||
if printf '%s' "$OUT" | grep -q "已从插件包安装运行时到"; then ok "首次启动自动安装运行时"; else no "未看到自动安装提示"; fi
|
||||
if printf '%s' "$OUT" | grep -q "运行时=${FRESH}/.deta/runtime"; then ok "启动行指向沙箱运行时"; else no "启动行运行时不对:$(printf '%s' "$OUT" | grep -o '运行时=[^ ]*' | head -1)"; fi
|
||||
if [ -f "$FRESH/.deta/runtime/dist/deta-mcp.mjs" ]; then ok "运行时文件已落地"; else no "运行时文件缺失"; fi
|
||||
if [ -f "$FRESH/.deta/runtime/manifest.json" ]; then ok "manifest 随装(可溯源)"; else no "manifest 缺失"; fi
|
||||
if printf '%s' "$OUT" | grep -q "@resvg/resvg-js"; then ok "缺可选依赖时有明确提示"; else no "未提示可选依赖缺失"; fi
|
||||
|
||||
# ② install-runtime:显式安装 + 真实 MCP 握手自证
|
||||
CLI_OUT="$(HOME="$FRESH" node "$PLUGIN/bin/deta-cli.mjs" install-runtime --no-resvg 2>&1 || true)"
|
||||
if printf '%s' "$CLI_OUT" | grep -q "MCP 握手通过"; then
|
||||
ok "install-runtime 握手通过($(printf '%s' "$CLI_OUT" | grep -o '[0-9]\+ 个工具' | head -1))"
|
||||
else
|
||||
no "install-runtime 握手失败:$(printf '%s' "$CLI_OUT" | tail -2)"
|
||||
fi
|
||||
if printf '%s' "$CLI_OUT" | grep -q "已是最新"; then ok "重复安装幂等(未重复拷贝)"; else no "未识别为已是最新"; fi
|
||||
|
||||
# ③ 运行时被改动 → 下次启动自动修复
|
||||
printf 'corrupted' >> "$FRESH/.deta/runtime/dist/deta-mcp.mjs"
|
||||
OUT2="$(HOME="$FRESH" bash "$PLUGIN/bin/deta-mcp.sh" --profile "$PROFILE" < /dev/null 2>&1 || true)"
|
||||
if printf '%s' "$OUT2" | grep -q "已从插件包安装运行时到"; then ok "运行时被改动后自动修复"; else no "未自动修复被改动的运行时"; fi
|
||||
|
||||
# ④ 插件包缺运行时(模拟打包缺陷)→ 两处都要给可执行指引
|
||||
BAD="$FRESH/bad-plugin"
|
||||
mkdir -p "$BAD"
|
||||
cp -R "$PLUGIN/bin" "$BAD/bin"
|
||||
cp -R "$PLUGIN/.codex-plugin" "$BAD/.codex-plugin"
|
||||
BAD_OUT="$(HOME="$FRESH/bad-home" bash "$BAD/bin/deta-mcp.sh" --profile "$PROFILE" < /dev/null 2>&1 || true)"
|
||||
if printf '%s' "$BAD_OUT" | grep -q "未找到领域服务运行时"; then ok "缺运行时启动报错清楚"; else no "缺运行时报错不明确"; fi
|
||||
BAD_CLI="$(HOME="$FRESH/bad-home" node "$BAD/bin/deta-cli.mjs" install-runtime --no-resvg 2>&1 || true)"
|
||||
if printf '%s' "$BAD_CLI" | grep -q "插件包内没有运行时"; then ok "install-runtime 指出这是打包缺陷"; else no "install-runtime 对缺运行时的指引不对"; fi
|
||||
|
||||
echo
|
||||
echo "结果:${pass} 通过 / ${fail} 失败"
|
||||
rm -rf "$FRESH"
|
||||
[ "$fail" -eq 0 ]
|
||||
Reference in New Issue
Block a user