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 侧需重启并点一次信任确认。
168 lines
8.1 KiB
Bash
Executable File
168 lines
8.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# DETA 产品插件的本机 MCP stdio 启动器(唯一入口,多产品共用)
|
||
#
|
||
# 两个宿主(Codex 插件 / dsh 组合包)都通过本脚本拉起同一个领域服务;
|
||
# 产品差异只由 --profile 决定(工作区默认位置、工具面),宿主侧只声明"调哪个脚本、带哪个档位"。
|
||
#
|
||
# 用法:deta-mcp.sh [--profile <studio|writer>]
|
||
#
|
||
# 依赖:node + 已构建的领域服务运行时
|
||
#
|
||
# 环境变量:
|
||
# DETA_SERVER_ROOT 领域服务运行时根目录(未设时自动查找,见下)
|
||
# DETA_WORKSPACE AML 工作目录(引擎侧工作区真值;缺省 ~/.deta/<profile>/workspace)
|
||
# ANT_HEADLESS_WORKSPACE 同上(兼容旧名,DETA_WORKSPACE 优先)
|
||
# DETA_TOKEN deta.city 门户令牌(可选;不注入则云类工具返回 auth_required)
|
||
# ANT_WORKSPACE_BRIDGE_URL 网关基址(可选;与项目 ID 一起启用 Studio 执行器中继)
|
||
# DETA_SURFACE 工具面覆盖(缺省 full)
|
||
set -euo pipefail
|
||
|
||
# ── 产品档位:宿主在声明里传 --profile;缺省 studio ──
|
||
PROFILE="${DETA_PROFILE:-studio}"
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--profile) PROFILE="${2:?--profile 需要一个值}"; shift 2 ;;
|
||
*) break ;;
|
||
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
|
||
for cand in "$HOME/.deta/runtime" "$HOME/Dev/ant/ant-copilot"; do
|
||
if [ -f "$cand/dist/deta-mcp.mjs" ] || [ -f "$cand/dist/src/headless/mcp-server.js" ]; then ROOT="$cand"; break; fi
|
||
done
|
||
fi
|
||
|
||
if ! command -v node >/dev/null 2>&1; then
|
||
echo "[deta] 未找到 node,请先安装 Node.js" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# 入口优先取 esbuild 打包形态:tsc 产物在 Node ESM 下会因依赖包的目录 import 直接失败
|
||
ENTRY=""
|
||
if [ -n "$ROOT" ]; then
|
||
if [ -f "$ROOT/dist/deta-mcp.mjs" ]; then
|
||
ENTRY="$ROOT/dist/deta-mcp.mjs"
|
||
elif [ -f "$ROOT/dist/src/headless/mcp-server.js" ]; then
|
||
ENTRY="$ROOT/dist/src/headless/mcp-server.js"
|
||
echo "[deta] 警告:回落到未打包产物,请先在该运行时目录执行 npm run build:mcp" >&2
|
||
fi
|
||
fi
|
||
|
||
if [ -z "$ENTRY" ]; then
|
||
echo "[deta] 未找到领域服务运行时(已尝试:DETA_SERVER_ROOT、\$HOME/.deta/runtime、\$HOME/Dev/ant/ant-copilot)" >&2
|
||
echo "[deta] 请安装本机引擎包,或设置 DETA_SERVER_ROOT 指向已构建的领域服务仓库" >&2
|
||
exit 1
|
||
fi
|
||
|
||
if [ "$ROOT" = "$HOME/Dev/ant/ant-copilot" ]; then
|
||
echo "[deta] 提示:使用本机开发检出 ${ROOT}(非打包运行时)" >&2
|
||
fi
|
||
|
||
# ── 工作目录:宿主不会传项目目录(cwd 通常是插件根),按产品档位定稳定位置 ──
|
||
WS="${DETA_WORKSPACE:-${ANT_HEADLESS_WORKSPACE:-$HOME/.deta/$PROFILE/workspace}}"
|
||
mkdir -p "$WS"
|
||
export ANT_HEADLESS_WORKSPACE="$WS"
|
||
|
||
# ── 工具面:产品档位缺省全量面;DETA_SURFACE 可覆盖(如 engine)──
|
||
export DETA_TOOL_SURFACE="${DETA_SURFACE:-full}"
|
||
|
||
# ── 稳定入口:把命令行门面注册为 ~/.deta/bin/deta(幂等,路径随插件更新自动刷新)──
|
||
# 插件装在宿主缓存目录,用户看不见这个路径;而用户与工具回执都需要一条可复制的命令。
|
||
BIN_DIR="$HOME/.deta/bin"
|
||
SHIM="$BIN_DIR/deta"
|
||
SHIM_BODY="#!/bin/sh
|
||
# DETA 命令行门面(由插件启动器自动维护,勿手工编辑)
|
||
exec \"$(command -v node)\" \"$PLUGIN_ROOT/bin/deta-cli.mjs\" \"\$@\"
|
||
"
|
||
mkdir -p "$BIN_DIR" 2>/dev/null || true
|
||
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 注入,
|
||
# 因此登录助手写下的凭据必须由启动器自己解析——这是"登录一次即可用"的关键路径。
|
||
TOKEN_SOURCE=""
|
||
if [ -n "${DETA_TOKEN:-}" ]; then
|
||
TOKEN_SOURCE="环境变量"
|
||
fi
|
||
if [ -z "${DETA_TOKEN:-}" ] && [ -f "$HOME/.deta/env" ]; then
|
||
# shellcheck disable=SC1090
|
||
. "$HOME/.deta/env" >/dev/null 2>&1 || true
|
||
if [ -n "${DETA_TOKEN:-}" ]; then TOKEN_SOURCE="$HOME/.deta/env"; fi
|
||
fi
|
||
if [ -z "${DETA_TOKEN:-}" ] && [ -f "$HOME/.deta/credentials.json" ]; then
|
||
DETA_TOKEN="$(node -e "try{const c=JSON.parse(require('node:fs').readFileSync(process.env.HOME+'/.deta/credentials.json','utf8'));if(c&&typeof c.token==='string')process.stdout.write(c.token)}catch(e){}" 2>/dev/null || true)"
|
||
export DETA_TOKEN
|
||
if [ -n "${DETA_TOKEN:-}" ]; then TOKEN_SOURCE="$HOME/.deta/credentials.json"; fi
|
||
fi
|
||
|
||
# ── 网关桥(云项目工作区读写 / host-call 中继):显式 env → 本机网关 → 生产网关 ──
|
||
# 缺它时 build_model 无法把模型写进云项目(会退回只写本地工作区),用户会以为已同步。
|
||
if [ -z "${ANT_WORKSPACE_BRIDGE_URL:-}" ]; then
|
||
if (exec 3<>/dev/tcp/127.0.0.1/3180) 2>/dev/null; then
|
||
ANT_WORKSPACE_BRIDGE_URL="http://127.0.0.1:3180"
|
||
BRIDGE_STATE="本机网关 ${ANT_WORKSPACE_BRIDGE_URL}"
|
||
else
|
||
ANT_WORKSPACE_BRIDGE_URL="https://deta.city/workspace-api"
|
||
BRIDGE_STATE="生产网关 ${ANT_WORKSPACE_BRIDGE_URL}"
|
||
fi
|
||
export ANT_WORKSPACE_BRIDGE_URL
|
||
else
|
||
BRIDGE_STATE="环境变量 ${ANT_WORKSPACE_BRIDGE_URL}"
|
||
fi
|
||
|
||
if [ -n "${DETA_TOKEN:-}" ]; then
|
||
CLOUD_STATE="已注入(来源=${TOKEN_SOURCE})"
|
||
else
|
||
CLOUD_STATE="未注入——云类工具返回 auth_required 时调 login 工具(浏览器点一下即可);无该工具的形态用 ~/.deta/bin/deta login"
|
||
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" "$@"
|