feat(hosts): WorkBuddy 接入(install-workbuddy:本机 stdio + 技能安装 + 握手自证)
CI / contract (push) Has been cancelled

WorkBuddy 自定义 MCP 支持 command/args/env(取证:其二进制 calculateConfigHash 先判 entry.command),因此走与 Codex 同款的本机 stdio full 工具面,不退远程引擎面。

落地:① 启动器多维护一个 MCP 稳定入口 ~/.deta/bin/deta-mcp-<档位>(指向当前插件副本,升级自动刷新;WB 信任哈希按命令行算,路径稳定=升级不重弹信任);② deta-cli 新增 install-workbuddy [--http]——备份并合并 ~/.workbuddy/mcp.json(同名旧条目与旧远程条目替换掉,防双工具面)、把插件内全部技能装进 ~/.workbuddy/skills/(单一事实源,带 icon.svg)、用宿主将执行的同一条命令做真实握手自证;③ aml-design 技能两处 Codex 特有措辞改宿主中立;④ 契约校验加 E7(装配源齐备 + README 指引);⑤ 探针 scripts/verify-workbuddy.sh(配置形态/稳定入口/技能/握手/WB 日志,5 项断言)。

实测:studio 0.3.16 已装副本重跑幂等通过(握手 39 工具);writer 0.1.7 在沙箱 HOME 验证(deta_writer + report-authoring 技能 + 握手 39 工具)。WB 侧需重启并点一次信任确认。
This commit is contained in:
ringo
2026-09-20 17:13:15 +08:00
parent 7ca4923ac3
commit 1d1000ddf5
16 changed files with 411 additions and 26 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "writer",
"version": "0.1.5",
"version": "0.1.7",
"description": "DETA Writer 插件:读取建筑模型与模拟结果,产出报告 / 文档内容。",
"author": {
"name": "DETA",
+121 -6
View File
@@ -15,7 +15,7 @@
* 在用户自己的终端里交互读取(不回显)。
*/
import { spawn, spawnSync } from 'node:child_process';
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { homedir } from 'node:os';
import { dirname, join, resolve, basename } from 'node:path';
@@ -171,7 +171,7 @@ async function cmdInstallRuntime(argv) {
// 装完立刻做一次真实 MCP 握手——「装上了」不等于「起得来」
const launcher = join(HERE, 'deta-mcp.sh');
const r = await smokeMcp(launcher, PROFILE);
const r = await smokeMcp('bash', [launcher, '--profile', PROFILE]);
if (r.ok) {
console.log(`✓ MCP 握手通过(--profile ${PROFILE}):${r.tools} 个工具${r.tokenLine ? ` · ${r.tokenLine.trim()}` : ''}`);
console.log('\n宿主里的下一步:退出并重启宿主应用(或开新会话)即可看到工具。');
@@ -210,9 +210,9 @@ function resolveRuntime() {
return '';
}
function smokeMcp(launcher, profile) {
function smokeMcp(cmd, args) {
return new Promise((resolveP) => {
const child = spawn('bash', [launcher, '--profile', profile], { stdio: ['pipe', 'pipe', 'pipe'] });
const child = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] });
let out = '';
let err = '';
let done = false;
@@ -262,7 +262,7 @@ async function cmdDoctor() {
if (runtime) {
const launcher = join(HERE, 'deta-mcp.sh');
const profile = PROFILE;
const r = await smokeMcp(launcher, profile);
const r = await smokeMcp('bash', [launcher, '--profile', profile]);
say(r.ok, `MCP 握手(--profile ${profile})`, r.ok ? `${r.tools} 个工具 · ${r.server?.name ?? '?'}${r.tokenLine ? ` · ${r.tokenLine.trim()}` : ''}` : r.detail);
}
@@ -270,12 +270,126 @@ async function cmdDoctor() {
process.exit(failed ? 1 : 0);
}
/**
* 接入 WorkBuddy:把本档位的 MCP 以 stdio 写进 ~/.workbuddy/mcp.json,并把领域技能装进
* ~/.workbuddy/skills/。WorkBuddy 的自定义 MCP 支持 command/args/env 形态
* (取证:其配置哈希 calculateConfigHash 先判 entry.command),本机 stdio = 与 Codex 同款
* full 工具面。--http 回落为远程引擎面(适合没装本机运行时的机器)。
*/
async function cmdInstallWorkbuddy(argv) {
const useHttp = argv.includes('--http');
const wbDir = join(HOME, '.workbuddy');
if (!existsSync(wbDir)) {
console.error('✗ 未检测到 WorkBuddy(没有 ~/.workbuddy 目录)。');
console.error(' 请先安装并启动一次 WorkBuddy,再重跑本命令。');
process.exit(1);
}
// 1) 稳定入口(与启动器维护的那份内容一致;启动器未跑过时这里先写出来)
const binDir = join(DETA_DIR, 'bin');
const shim = join(binDir, `deta-mcp-${PROFILE}`);
const launcher = join(HERE, 'deta-mcp.sh');
const shimBody = '#!/bin/bash\n# DETA MCP 稳定入口(由插件自动维护,勿手工编辑)\n'
+ 'export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"\n'
+ `exec bash "${launcher}" --profile ${PROFILE} "$@"\n`;
mkdirSync(binDir, { recursive: true });
writeFileSync(shim, shimBody);
chmodSync(shim, 0o755);
// 2) 合并 ~/.workbuddy/mcp.json(备份 → 合并 → 写完再校验)
const mcpPath = join(wbDir, 'mcp.json');
const serverName = `deta_${PROFILE}`;
const entry = useHttp
? { type: 'streamable-http', url: 'https://deta.city/mcp-api/mcp', disabled: false }
: { type: 'stdio', command: shim, args: [], disabled: false };
let cfg = { mcpServers: {} };
if (existsSync(mcpPath)) {
const raw = readFileSync(mcpPath, 'utf8');
try {
cfg = JSON.parse(raw);
} catch (e) {
console.error(`✗ ${mcpPath} 不是合法 JSON:${e.message}`);
console.error(' 未做任何改动;请先手工修复或备份后删除该文件。');
process.exit(1);
}
copyFileSync(mcpPath, `${mcpPath}.bak-${Date.now()}`);
}
if (!cfg.mcpServers || typeof cfg.mcpServers !== 'object') cfg.mcpServers = {};
// 旧的远程条目与新的本机条目指向同一套工具 → 并存会出双工具面,替换之
const replaced = [];
if (!useHttp && cfg.mcpServers.deta_mcp) {
delete cfg.mcpServers.deta_mcp;
replaced.push('deta_mcp(远程引擎面)');
}
if (cfg.mcpServers[serverName]) replaced.push(`${serverName}(旧配置)`);
cfg.mcpServers[serverName] = entry;
writeFileSync(mcpPath, JSON.stringify(cfg, null, 2) + '\n');
console.log(`✓ MCP 已写入 ${mcpPath}:${serverName}(${useHttp ? '远程 streamable-http' : `本机 stdio → ${shim}`})`);
if (replaced.length) console.log(` · 已替换旧条目:${replaced.join('、')}(防同服务双工具面)`);
// 3) 技能:把插件内的全部技能原样装进 WB 技能目录(单一事实源,不另写一份;
// 各档位的技能不同——studio 是 aml-design,writer 是 report-authoring)
const skillsSrc = join(HERE, '..', 'skills');
const skillsDst = join(wbDir, 'skills');
const logo = join(HERE, '..', 'assets', 'logo.svg');
let installedSkills = 0;
if (existsSync(skillsSrc)) {
for (const name of readdirSync(skillsSrc)) {
const src = join(skillsSrc, name);
if (!existsSync(join(src, 'SKILL.md'))) continue;
const dst = join(skillsDst, name);
cpSync(src, dst, { recursive: true });
if (existsSync(logo)) copyFileSync(logo, join(dst, 'icon.svg'));
installedSkills += 1;
console.log(`✓ 技能已安装到 ${dst}`);
}
}
if (!installedSkills) {
console.log(`! 插件包内未找到技能(${skillsSrc} 为空,打包缺陷);已跳过技能安装`);
}
// 4) 自证:用 WorkBuddy 将要执行的同一条命令做一次真实握手
if (!useHttp) {
const r = await smokeMcp('bash', [shim]);
if (r.ok) {
console.log(`✓ 握手自证通过:${r.tools} 个工具(${r.server?.name ?? '?'})`);
} else {
console.error(`✗ 握手自证失败:${r.detail}`);
console.error(' 排障:~/.deta/bin/deta doctor');
process.exit(1);
}
} else {
try {
const res = await fetch('https://deta.city/mcp-api/mcp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}',
});
const challenge = res.headers.get('www-authenticate') ?? '';
if (res.status === 401 && challenge.includes('resource_metadata')) {
console.log('✓ 远程端点在线且 OAuth 通道在位(401 + resource_metadata)');
} else {
console.log(`! 远程端点响应异常(HTTP ${res.status})——请检查网络后重试`);
}
} catch (e) {
console.log(`! 远程端点不可达:${e.message}`);
}
}
console.log('\nWorkBuddy 里的下一步:');
console.log(' 1) 退出并重启 WorkBuddy(或在设置里刷新 MCP 列表)');
console.log(` 2) 首次会弹「是否信任 MCP 服务器 ${serverName}」→ 点允许(路径稳定,插件升级后不再询问)`);
console.log(' 3) 开新对话,说「列出 deta 的能力」核对工具面');
console.log(' 4) 首次使用云能力时调 login 工具走浏览器授权(与 Codex 同一套,不需要终端)');
}
const [cmd = 'help', ...rest] = process.argv.slice(2);
switch (cmd) {
case 'login': cmdLogin(rest); break;
case 'status': cmdStatus(); break;
case 'doctor': await cmdDoctor(); break;
case 'install-runtime': await cmdInstallRuntime(rest); break;
case 'install-workbuddy': await cmdInstallWorkbuddy(rest); break;
case 'help':
case '--help':
case '-h':
@@ -283,6 +397,7 @@ switch (cmd) {
deta login [--identifier <账号>] [--email-code <邮箱>] 登录 deta.city(凭据写入 ~/.deta/)
deta install-runtime [--force] [--no-resvg] 安装/更新领域服务运行时 + 可选预览图依赖
deta install-workbuddy [--http] 接入 WorkBuddy(写 mcp.json + 装技能 + 握手自证)
deta status 查看凭据来源 / 运行时 / 工作区
deta doctor 端到端自检(运行时、凭据、GSA、MCP 握手)
@@ -290,6 +405,6 @@ switch (cmd) {
登录后开新会话即可让宿主读到凭据(无需 source / launchctl / 重启应用)。`);
break;
default:
console.error(`未知命令:${cmd}(可用:login / install-runtime / status / doctor)`);
console.error(`未知命令:${cmd}(可用:login / install-runtime / install-workbuddy / status / doctor)`);
process.exit(2);
}
+13
View File
@@ -105,6 +105,19 @@ if [ ! -f "$SHIM" ] || [ "$(cat "$SHIM" 2>/dev/null)" != "$SHIM_BODY" ]; then
printf '%s' "$SHIM_BODY" > "$SHIM" 2>/dev/null && chmod 0755 "$SHIM" 2>/dev/null || true
fi
# ── MCP 稳定入口:~/.deta/bin/deta-mcp-<档位>(供不支持插件市场的宿主以 stdio 接入,如 WorkBuddy)──
# 指向当前插件副本;升级后下一次启动自动刷新。宿主对 MCP 的信任按命令行算哈希,
# 路径稳定 ⇒ 升级插件不再重弹信任确认。PATH 前缀兜住 GUI 拉起时找不到 homebrew node 的情况。
MCP_SHIM="$BIN_DIR/deta-mcp-${PROFILE}"
MCP_SHIM_BODY="#!/bin/bash
# DETA MCP 稳定入口(由插件启动器自动维护,勿手工编辑)
export PATH=\"/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:\${PATH:-}\"
exec bash \"$PLUGIN_ROOT/bin/deta-mcp.sh\" --profile ${PROFILE} \"\$@\"
"
if [ ! -f "$MCP_SHIM" ] || [ "$(cat "$MCP_SHIM" 2>/dev/null)" != "$MCP_SHIM_BODY" ]; then
printf '%s' "$MCP_SHIM_BODY" > "$MCP_SHIM" 2>/dev/null && chmod 0755 "$MCP_SHIM" 2>/dev/null || true
fi
# ── 云令牌:显式环境变量 → ~/.deta/env → ~/.deta/credentials.json ──
# 桌面应用(Finder/Dock 启动)不会继承终端的 export,也不是每个人都有 launchctl 注入,
# 因此登录助手写下的凭据必须由启动器自己解析——这是"登录一次即可用"的关键路径。
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "deta-writer-plugin",
"version": "0.1.5",
"version": "0.1.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "deta-writer-plugin",
"version": "0.1.5",
"version": "0.1.7",
"license": "Proprietary"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "deta-writer-plugin",
"version": "0.1.5",
"version": "0.1.7",
"description": "DETA Writer:建筑性能报告与文档产出(Codex 插件 / dsh 组合包 双壳)",
"type": "module",
"license": "Proprietary",
+2 -2
View File
@@ -2,11 +2,11 @@
"source": "@ringoarch/ant-copilot",
"version": "0.2.2",
"gitSha": "984a816",
"builtAt": "2026-09-20T08:38:38.048Z",
"builtAt": "2026-09-20T09:11:33.439Z",
"entry": "dist/deta-mcp.mjs",
"sha256": "db05fa93049977fe69846b296e51ee6b9245552e8d96928589911c0051e8ee59",
"bytes": 4153494,
"note": "由 scripts/sync-runtime.mjs 生成;勿手工编辑。安装到 ~/.deta/runtime 由启动器/install-runtime 完成。",
"plugin": "writer",
"pluginVersion": "0.1.5"
"pluginVersion": "0.1.7"
}