From 1d1000ddf52ed70876409456ec988d3457509b5f Mon Sep 17 00:00:00 2001 From: ringo Date: Sun, 20 Sep 2026 17:13:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(hosts):=20WorkBuddy=20=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=EF=BC=88install-workbuddy=EF=BC=9A=E6=9C=AC=E6=9C=BA=20stdio?= =?UTF-8?q?=20+=20=E6=8A=80=E8=83=BD=E5=AE=89=E8=A3=85=20+=20=E6=8F=A1?= =?UTF-8?q?=E6=89=8B=E8=87=AA=E8=AF=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 侧需重启并点一次信任确认。 --- README.md | 16 +++ plugins/studio/.codex-plugin/plugin.json | 2 +- plugins/studio/bin/deta-cli.mjs | 127 +++++++++++++++++++++- plugins/studio/bin/deta-mcp.sh | 13 +++ plugins/studio/package-lock.json | 4 +- plugins/studio/package.json | 2 +- plugins/studio/runtime/manifest.json | 4 +- plugins/studio/skills/aml-design/SKILL.md | 4 +- plugins/writer/.codex-plugin/plugin.json | 2 +- plugins/writer/bin/deta-cli.mjs | 127 +++++++++++++++++++++- plugins/writer/bin/deta-mcp.sh | 13 +++ plugins/writer/package-lock.json | 4 +- plugins/writer/package.json | 2 +- plugins/writer/runtime/manifest.json | 4 +- scripts/check-plugin-contract.mjs | 26 +++++ scripts/verify-workbuddy.sh | 87 +++++++++++++++ 16 files changed, 411 insertions(+), 26 deletions(-) create mode 100644 scripts/verify-workbuddy.sh diff --git a/README.md b/README.md index e34ca05..e11365e 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,22 @@ codex plugin marketplace list codex mcp list | grep -i studio # 应显示 stdio 形态(Command 列)与 studio_mcp ``` +## 接入 WorkBuddy + +WorkBuddy 的自定义 MCP 支持 command/args 形态(经其二进制的配置哈希实现取证确认), +因此可以拿到与 Codex 同款的本机 full 工具面: + +```bash +~/.deta/bin/deta install-workbuddy # 写 ~/.workbuddy/mcp.json(本机 stdio)+ 装技能 + 握手自证 +``` + +- 稳定入口是 `~/.deta/bin/deta-mcp-<档位>`(随插件升级自动刷新;WorkBuddy 对 MCP 的信任按命令行 + 算哈希,入口路径稳定 ⇒ 插件升级后不再重弹信任确认); +- 领域技能随安装装进 `~/.workbuddy/skills/`(与 Codex 插件内的是同一份,单一事实源); +- 装完**退出并重启 WorkBuddy**,首次会弹「是否信任 MCP 服务器」→ 点允许,开新对话即可用; +- 无本机运行时的机器用 `~/.deta/bin/deta install-workbuddy --http` 回落为远程引擎面(deta.city,OAuth 授权); +- 验收:`bash scripts/verify-workbuddy.sh`(配置形态 / 稳定入口 / 技能 / 真实握手 / WB 日志)。 + ## 双壳架构(Codex + dsh 同一产物) `plugins/studio/` 这一个目录同时是 Codex 插件根与 dsh 组合包根,共享核心、各带一层薄壳: diff --git a/plugins/studio/.codex-plugin/plugin.json b/plugins/studio/.codex-plugin/plugin.json index a4343a6..5a1601d 100644 --- a/plugins/studio/.codex-plugin/plugin.json +++ b/plugins/studio/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "studio", - "version": "0.3.14", + "version": "0.3.16", "description": "DETA Studio 插件:以 AML 为唯一事实源编辑建筑模型,编译为建筑模型并执行规则校验。", "author": { "name": "DETA", diff --git a/plugins/studio/bin/deta-cli.mjs b/plugins/studio/bin/deta-cli.mjs index dfb8e6e..9e3bdd2 100644 --- a/plugins/studio/bin/deta-cli.mjs +++ b/plugins/studio/bin/deta-cli.mjs @@ -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); } diff --git a/plugins/studio/bin/deta-mcp.sh b/plugins/studio/bin/deta-mcp.sh index 45e10a1..f27a654 100755 --- a/plugins/studio/bin/deta-mcp.sh +++ b/plugins/studio/bin/deta-mcp.sh @@ -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 注入, # 因此登录助手写下的凭据必须由启动器自己解析——这是"登录一次即可用"的关键路径。 diff --git a/plugins/studio/package-lock.json b/plugins/studio/package-lock.json index 518214c..dacdc3e 100644 --- a/plugins/studio/package-lock.json +++ b/plugins/studio/package-lock.json @@ -1,12 +1,12 @@ { "name": "deta-studio-plugin", - "version": "0.3.14", + "version": "0.3.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "deta-studio-plugin", - "version": "0.3.14", + "version": "0.3.16", "license": "Proprietary" } } diff --git a/plugins/studio/package.json b/plugins/studio/package.json index a985595..4454beb 100644 --- a/plugins/studio/package.json +++ b/plugins/studio/package.json @@ -1,6 +1,6 @@ { "name": "deta-studio-plugin", - "version": "0.3.14", + "version": "0.3.16", "description": "DETA Studio:AML 建筑模型读写与编译校验(Codex 插件 / dsh 组合包 双壳)", "type": "module", "license": "Proprietary", diff --git a/plugins/studio/runtime/manifest.json b/plugins/studio/runtime/manifest.json index 999f7e4..8457a57 100644 --- a/plugins/studio/runtime/manifest.json +++ b/plugins/studio/runtime/manifest.json @@ -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": "studio", - "pluginVersion": "0.3.14" + "pluginVersion": "0.3.16" } diff --git a/plugins/studio/skills/aml-design/SKILL.md b/plugins/studio/skills/aml-design/SKILL.md index fba35c0..037b501 100644 --- a/plugins/studio/skills/aml-design/SKILL.md +++ b/plugins/studio/skills/aml-design/SKILL.md @@ -187,7 +187,7 @@ build_model({ aml?: "", files?: { "aml.xml": "…" }, projectId?: "2 ## 在右侧面板打开 Studio,并在其中跑模拟 -Studio 就是deta.city 上的建筑性能应用。在 Codex 右侧内置浏览器面板打开它之后, +Studio 就是deta.city 上的建筑性能应用。在宿主内置浏览器面板打开它之后, 看模型、跑模拟、看结果图层都在面板里完成;本插件的 MCP 只负责把本地改动推上去,不执行模拟。 打开步骤(用宿主内置浏览器 = browser 插件的 `iab`): @@ -200,7 +200,7 @@ Studio 就是deta.city 上的建筑性能应用。在 Codex 右侧内置浏览 确认 `executor_online: true` → 再调 `simulate` / `studio_command` 等执行器侧工具。 - 若返回 `auth_required` → 未登录:**直接调 `login` 工具**(建议 `waitSeconds: 90`),把返回的 `url` 交给用户在内置浏览器打开——首次输一次账号密码或邮箱验证码,之后只需点「登录并授权」按钮。 - **不需要终端,也不要去找 `codex mcp login`**;用户点完后本工具会自动换取令牌并写入当前会话 + **不需要终端,也不要去找宿主自己的 MCP 登录命令**;用户点完后本工具会自动换取令牌并写入当前会话 (凭据过期时 `login` 会先用 refresh token 静默续期,用户完全无感)。 仅当当前形态没有 `login` 工具(CLI 等)时,才让用户在终端运行 `~/.deta/bin/deta login`; 排查用 `~/.deta/bin/deta doctor`(自检运行时 / 凭据 / GSA / MCP 握手)。 diff --git a/plugins/writer/.codex-plugin/plugin.json b/plugins/writer/.codex-plugin/plugin.json index e8531a9..fd5891b 100644 --- a/plugins/writer/.codex-plugin/plugin.json +++ b/plugins/writer/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "writer", - "version": "0.1.5", + "version": "0.1.7", "description": "DETA Writer 插件:读取建筑模型与模拟结果,产出报告 / 文档内容。", "author": { "name": "DETA", diff --git a/plugins/writer/bin/deta-cli.mjs b/plugins/writer/bin/deta-cli.mjs index dfb8e6e..9e3bdd2 100644 --- a/plugins/writer/bin/deta-cli.mjs +++ b/plugins/writer/bin/deta-cli.mjs @@ -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); } diff --git a/plugins/writer/bin/deta-mcp.sh b/plugins/writer/bin/deta-mcp.sh index 6a34e31..2435989 100755 --- a/plugins/writer/bin/deta-mcp.sh +++ b/plugins/writer/bin/deta-mcp.sh @@ -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 注入, # 因此登录助手写下的凭据必须由启动器自己解析——这是"登录一次即可用"的关键路径。 diff --git a/plugins/writer/package-lock.json b/plugins/writer/package-lock.json index b769021..6c553dc 100644 --- a/plugins/writer/package-lock.json +++ b/plugins/writer/package-lock.json @@ -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" } } diff --git a/plugins/writer/package.json b/plugins/writer/package.json index 812d6cd..7604078 100644 --- a/plugins/writer/package.json +++ b/plugins/writer/package.json @@ -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", diff --git a/plugins/writer/runtime/manifest.json b/plugins/writer/runtime/manifest.json index b2debae..001d62f 100644 --- a/plugins/writer/runtime/manifest.json +++ b/plugins/writer/runtime/manifest.json @@ -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" } diff --git a/scripts/check-plugin-contract.mjs b/scripts/check-plugin-contract.mjs index 84eb440..b945835 100644 --- a/scripts/check-plugin-contract.mjs +++ b/scripts/check-plugin-contract.mjs @@ -85,6 +85,11 @@ const RULES = { what: '运行时包不自洽(缺 runtime/、sha256 或字节数与 manifest 不符、manifest.pluginVersion 与插件版本不符)', hint: '运行时随插件分发:改动 ant-copilot 后跑 node scripts/sync-runtime.mjs,再升插件版本重装', }, + E7: { + level: 'error', + what: 'WorkBuddy 接入的装配源不齐:插件 skills/ 下没有技能目录,或 README 未写 install-workbuddy', + hint: 'install-workbuddy 把插件内技能装进 ~/.workbuddy/skills/(单一事实源),且 README 必须给用户这条路径', + }, }; const HEADING_BAN = /工具一览|工具清单|工具列表|工具汇总|可用工具|能力一览|能力清单/; @@ -222,6 +227,27 @@ for (const pluginDir of ['plugins/studio', 'plugins/writer']) { } } +// E7:WorkBuddy 接入的装配源(插件技能目录)与 README 指引 +for (const pluginDir of ['plugins/studio', 'plugins/writer']) { + const skillsDir = join(ROOT, pluginDir, 'skills'); + let count = 0; + if (existsSync(skillsDir)) { + for (const name of readdirSync(skillsDir)) { + if (existsSync(join(skillsDir, name, 'SKILL.md'))) count += 1; + } + } + if (count === 0) { + add('E7', skillsDir, 1, 'skills/ 下没有任何含 SKILL.md 的技能目录(install-workbuddy 的装配源缺失)'); + } +} +{ + const readmePath = join(ROOT, 'README.md'); + const readme = existsSync(readmePath) ? readFileSync(readmePath, 'utf8') : ''; + if (!readme.includes('install-workbuddy')) { + add('E7', readmePath, 1, 'README 未写 install-workbuddy(WorkBuddy 用户没有安装路径)'); + } +} + if (process.argv.includes('--json')) { console.log(JSON.stringify({ findings, rules: RULES }, null, 2)); } else { diff --git a/scripts/verify-workbuddy.sh b/scripts/verify-workbuddy.sh new file mode 100644 index 0000000..5a85c04 --- /dev/null +++ b/scripts/verify-workbuddy.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# 探针:WorkBuddy 接入(deta install-workbuddy 的验收) +# +# 断言: +# ① ~/.workbuddy/mcp.json 合法且含 deta_<档位> 的 stdio 条目(command 指向稳定入口,无残留远程条目) +# ② 稳定入口 ~/.deta/bin/deta-mcp-<档位> 存在且可执行 +# ③ 技能 ~/.workbuddy/skills/aml-design/ 已安装(SKILL.md + icon.svg) +# ④ 用宿主将要执行的同一条命令做真实 MCP 握手(报工具数) +# ⑤ WorkBuddy 日志里该 server 已握手(非致命:应用未重启时跳过) +# +# 用法:bash scripts/verify-workbuddy.sh [档位,缺省 studio] +set -u +PROFILE="${1:-studio}" +WB="$HOME/.workbuddy" +NAME="deta_${PROFILE}" +SHIM="$HOME/.deta/bin/deta-mcp-${PROFILE}" +pass=0 +fail=0 +ok() { printf ' PASS %s\n' "$1"; pass=$((pass + 1)); } +no() { printf ' FAIL %s\n' "$1"; fail=$((fail + 1)); } +warn() { printf ' WARN %s\n' "$1"; } + +echo "== WorkBuddy 接入检查(档位=${PROFILE},服务名=${NAME})" + +# ① 配置 +if [ -f "$WB/mcp.json" ]; then + RESULT="$(python3 - "$WB/mcp.json" "$NAME" "$SHIM" <<'PY' +import json, sys +try: + d = json.load(open(sys.argv[1])) +except Exception as e: + print(f'ERR 配置不是合法 JSON: {e}'); sys.exit(0) +servers = (d.get('mcpServers') or {}) +name, shim = sys.argv[2], sys.argv[3] +e = servers.get(name) +if not e: + print(f'ERR 缺少 {name} 条目'); sys.exit(0) +if e.get('type') != 'stdio': + print(f"ERR 形态不对: {e.get('type')}"); sys.exit(0) +if e.get('command') != shim: + print(f"ERR command 应为稳定入口: {e.get('command')}"); sys.exit(0) +legacy = [k for k in servers if k == 'deta_mcp'] +if legacy: + print(f'ERR 仍残留远程条目: {legacy}'); sys.exit(0) +print('OK') +PY +)" + if [ "$RESULT" = "OK" ]; then ok "mcp.json 条目形态正确(stdio + 稳定入口,无残留)"; else no "$RESULT"; fi +else + no "没有 $WB/mcp.json" +fi + +# ② 稳定入口 +if [ -x "$SHIM" ]; then ok "稳定入口存在且可执行"; else no "稳定入口缺失或不可执行:$SHIM"; fi + +# ③ 技能 +if [ -f "$WB/skills/aml-design/SKILL.md" ]; then + if grep -q "list_capabilities" "$WB/skills/aml-design/SKILL.md"; then ok "技能已安装(SKILL.md 含能力目录指引)"; else no "技能内容不完整(缺 list_capabilities 指引)"; fi + if [ -f "$WB/skills/aml-design/icon.svg" ]; then ok "技能图标已安装"; else warn "技能图标缺失(icon.svg)"; fi +else + no "技能未安装:$WB/skills/aml-design/SKILL.md" +fi + +# ④ 真实握手(与宿主将执行的是同一条命令) +if [ -x "$SHIM" ]; then + OUT="$(timeout 30 bash "$SHIM" < /dev/null 2>&1 || bash "$SHIM" < /dev/null 2>&1 || true)" + TOOLS="$(printf '%s' "$OUT" | grep -o '工具面: full([0-9]* 个)' | head -1)" + if [ -n "$TOOLS" ]; then ok "握手自证(${TOOLS})"; else no "握手失败:$(printf '%s' "$OUT" | head -2)"; fi +else + no "无法握手:稳定入口不存在" +fi + +# ⑤ WorkBuddy 是否已加载(应用未重启时 WARN 而非 FAIL) +LDIR="$WB/logs/$(date +%Y-%m-%d)" +if [ -d "$LDIR" ]; then + if grep -hq "MCP-Connect.*ok configId=custom-mcp:${NAME}" "$LDIR"/*.log 2>/dev/null; then + ok "WorkBuddy 已握手(日志见 MCP-Connect ok)" + else + warn "今天的 WorkBuddy 日志里还没有 ${NAME} 的握手 —— 重启 WorkBuddy 后再看" + fi +else + warn "没有今天的 WorkBuddy 日志目录(应用今天没启动过?)" +fi + +echo +echo "结果:${pass} 通过 / ${fail} 失败" +[ "$fail" -eq 0 ]