feat(hosts): WorkBuddy 接入(install-workbuddy:本机 stdio + 技能安装 + 握手自证)
CI / contract (push) Has been cancelled
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:
@@ -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 组合包根,共享核心、各带一层薄壳:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "studio",
|
||||
"version": "0.3.14",
|
||||
"version": "0.3.16",
|
||||
"description": "DETA Studio 插件:以 AML 为唯一事实源编辑建筑模型,编译为建筑模型并执行规则校验。",
|
||||
"author": {
|
||||
"name": "DETA",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 注入,
|
||||
# 因此登录助手写下的凭据必须由启动器自己解析——这是"登录一次即可用"的关键路径。
|
||||
|
||||
Generated
+2
-2
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ build_model({ aml?: "<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 握手)。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "writer",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.7",
|
||||
"description": "DETA Writer 插件:读取建筑模型与模拟结果,产出报告 / 文档内容。",
|
||||
"author": {
|
||||
"name": "DETA",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 注入,
|
||||
# 因此登录助手写下的凭据必须由启动器自己解析——这是"登录一次即可用"的关键路径。
|
||||
|
||||
Generated
+2
-2
@@ -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,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,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"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ]
|
||||
Reference in New Issue
Block a user