* fix(bundle): 聚合双挂载自动退让 —— 已有同包名启用挂载时不再挂自己
@linxin666/dsh-web-ui-all 等聚合包以独立条目 id(web-ui-better-sidebar)
挂载 dsh-better-sidebar 时,插件自身 bundle patch 的 better-sidebar 行会
与之双挂载:两个实例都注册 /sidebar/api,第二个抛 duplicate prefix route,
整个插件树启动失败。
better-sidebar 行的 disabled 改为 !!js 表达式:loader 按 entry-list 顺序
处理本行时,若已存在其他启用中的同包名挂载(排除自身 id),本行自动
disabled,由聚合行接管 sidebar。顺序假设(聚合 bundle 先于本包)写入注释。
- cordis.patch.yml:守卫表达式 + 说明注释
- README:常见问题「两个侧边栏」更新为自动退让说明
- scripts/e2e-aggregate-mount.sh:聚合双挂载真实挂载回归(scratch profile
聚合先行 + 插件后到 → boot 无 duplicate、/sidebar/api 存活)
- tests/fixtures/aggregate-better-sidebar:最小聚合 fixture
* ci(plugin-mount): 增加聚合双挂载回归 step
复用已装好的钉住版 DSH CLI 与已 pack 的 tarball,跑
scripts/e2e-aggregate-mount.sh(fixture 聚合先行 → 插件 bundle 行必须
自动退让,boot 无 duplicate prefix route)。
* fix(review): 双轴审查反馈——双语 README、注释补全、脚本健壮性
- README_EN.md:FAQ「两个侧边栏」行同步自动退让说明(双语惯例)
- cordis.patch.yml:注释补上计划要求的「反向顺序为已知限制,
不引入运行时单例守卫」一句
- scripts/e2e-aggregate-mount.sh:TMP_DIR 纳入 trap 清理;FIXTURE_TGZ
glob 加 2>/dev/null || true 让 die 路径生效; 花括号显式化
(LANG=UTF-8 下 bash 会把紧跟的全角字符吞进变量名,实测复现)
- tests/fixtures/aggregate-better-sidebar/package.json:补 "type": "module"
与 lib/index.js 的 ESM 一致
* fix(e2e): 保护调用方提供的 DSH_HOME_BASE——只删除本调用拥有的子目录
chatgpt-codex-connector P2:当 DSH_HOME_BASE 指向已存在的目录(如开发者
真实 ~/.dsh)时,脚本会把 scratch profile 写进去并在退出时 rm -rf 整个
根目录。现在脚本始终在根目录下新建本调用拥有的独立子目录
(DSH_HOME_BASE/dsh-e2e-*.XXXXXX),只写入/删除该子目录;调用方提供的
目录本身绝不触碰。缺省路径仍用系统临时目录。
- scripts/e2e-aggregate-mount.sh:SCRATCH 子目录化 + 清理逻辑
- scripts/e2e-mount.sh:同款存量问题一并修复(bot 只审了 diff 内脚本)
* test(e2e): 用真实 API 方法探活——terminal.deps 需返回 ok:true
chatgpt-codex-connector P2:generic missing-route 404 无法证明 sidebar
路由真的注册了。新增对插件自身 handler /sidebar/api/terminal.deps 的
POST 探活,要求 HTTP 200 + {"ok":true};未知方法 404 保留为次要断言。
168 lines
6.3 KiB
Bash
Executable File
168 lines
6.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# =============================================================================
|
||
# dsh-better-sidebar 挂载冒烟编排(CI + 本地):
|
||
#
|
||
# 1. 用官方 CLI 把 npm 打包产物(tarball)真实挂载进一个全新 scratch
|
||
# profile(`dsh plugin --profile web add file:<tarball>`,触发
|
||
# dsh.profile.bundles 协调,与用户安装路径一致);
|
||
# 2. 启动真实 `dsh web`(keyless,--port 0 取 OS 分配端口);
|
||
# 3. 运行 tests/e2e 无头渲染 lane(Playwright Chromium):断言外壳与
|
||
# 插件挂载、无崩溃标记,并驱动内置 tab 深扫。
|
||
#
|
||
# 用法:
|
||
# bash scripts/e2e-mount.sh [--grep <playwright-filter>]
|
||
#
|
||
# 环境变量(均可省略):
|
||
# DSH_CMD dsh 命令;缺省 PATH 上的 `dsh`,回退 npx 拉官方包
|
||
# TARBALL 插件 tarball;缺省仓库根 dsh-better-sidebar-*.tgz(须已 pack)
|
||
# PORT 固定端口(默认 0 = OS 分配,从日志解析 URL)
|
||
# DSH_HOME_BASE 覆盖 scratch 根目录(默认系统临时目录)。脚本始终在其下
|
||
# 新建本调用拥有的独立子目录,只写入/删除该子目录;调用方
|
||
# 提供的目录本身(可能是真实 ~/.dsh)绝不写入或删除。
|
||
# KEEP_HOME 非空时保留 scratch home(调试用)
|
||
#
|
||
# 退出码 = playwright 的退出码;服务器与 scratch 目录由 trap 兜底清理。
|
||
# =============================================================================
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
|
||
DSH_CMD="${DSH_CMD:-dsh}"
|
||
PORT="${PORT:-0}"
|
||
TARBALL="${TARBALL:-}"
|
||
GREP_FILTER=""
|
||
if [ "${1:-}" = "--grep" ]; then GREP_FILTER="${2:?--grep 需要参数}"; fi
|
||
|
||
say() { printf '\033[32m[e2e-mount]\033[0m %s\n' "$*"; }
|
||
warn() { printf '\033[33m[e2e-mount]\033[0m %s\n' "$*" >&2; }
|
||
die() { printf '\033[31m[e2e-mount]\033[0m %s\n' "$*" >&2; exit 1; }
|
||
|
||
command -v node >/dev/null 2>&1 || die "未找到 node(DSH 运行需要 Node.js >= 20)"
|
||
command -v pnpm >/dev/null 2>&1 || die "未找到 pnpm(dsh plugin 转发给 pnpm)"
|
||
|
||
# dsh CLI 解析:PATH 上的 dsh 优先,否则 npx 拉官方包(同 scripts/install.sh)
|
||
if ! command -v "$DSH_CMD" >/dev/null 2>&1; then
|
||
if command -v npx >/dev/null 2>&1; then
|
||
say "PATH 上无 $DSH_CMD,回退 npx -y --package @deepseek-ai/dsh"
|
||
DSH_CMD="npx -y --package @deepseek-ai/dsh dsh"
|
||
else
|
||
die "未找到 $DSH_CMD 或 npx;请先安装 DSH CLI(npm i -g @deepseek-ai/dsh)或用 DSH_CMD 指定"
|
||
fi
|
||
fi
|
||
|
||
# tarball 解析
|
||
if [ -z "$TARBALL" ]; then
|
||
TARBALL="$(ls "$ROOT"/dsh-better-sidebar-*.tgz 2>/dev/null | head -1 || true)"
|
||
fi
|
||
[ -n "$TARBALL" ] && [ -f "$TARBALL" ] || die "找不到 tarball(TARBALL 或 \$ROOT/dsh-better-sidebar-*.tgz)——先运行 pnpm build && pnpm pack"
|
||
TARBALL="$(cd "$(dirname "$TARBALL")" && pwd)/$(basename "$TARBALL")"
|
||
say "tarball: $TARBALL"
|
||
|
||
# scratch home(每次全新,绝不触碰真实 ~/.dsh):调用方给了 DSH_HOME_BASE
|
||
# 时,只在其下新建本调用拥有的子目录并只删除该子目录;缺省时直接用系统
|
||
# 临时目录。
|
||
if [ -n "${DSH_HOME_BASE:-}" ]; then
|
||
SCRATCH="$(mktemp -d "$DSH_HOME_BASE/dsh-e2e-mount.XXXXXX")"
|
||
else
|
||
SCRATCH="$(mktemp -d /tmp/dsh-e2e-mount.XXXXXX)"
|
||
fi
|
||
export DSH_HOME="$SCRATCH/home"
|
||
WORKSPACE_DIR="$SCRATCH/workspace"
|
||
LOG_DIR="$SCRATCH"
|
||
WEB_LOG="$LOG_DIR/web.log"
|
||
mkdir -p "$DSH_HOME/profiles/web" "$WORKSPACE_DIR"
|
||
say "scratch home: ${DSH_HOME}(DSH_HOME=${DSH_HOME})"
|
||
|
||
SERVER_PID=""
|
||
cleanup() {
|
||
local code=$?
|
||
if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
||
kill "$SERVER_PID" 2>/dev/null || true
|
||
wait "$SERVER_PID" 2>/dev/null || true
|
||
fi
|
||
if [ -z "${KEEP_HOME:-}" ]; then
|
||
rm -rf "$SCRATCH"
|
||
else
|
||
warn "KEEP_HOME 已设置,保留 $SCRATCH"
|
||
fi
|
||
exit "$code"
|
||
}
|
||
trap cleanup EXIT
|
||
|
||
# 步骤 1:引导 scratch profile(web 模板,镜像 dsh initProfile;先写
|
||
# pnpm-workspace.yaml 的 allowBuilds / minimumReleaseAgeExclude,避免 pnpm 11
|
||
# strict-dep-builds 拦截 node-pty/protobufjs 或拒绝 <24h 新版本——同 install.sh)
|
||
PROFILE_DIR="$DSH_HOME/profiles/web"
|
||
cat > "$PROFILE_DIR/package.json" <<EOF
|
||
{
|
||
"name": "dsh-profile-web",
|
||
"private": true,
|
||
"dependencies": {},
|
||
"dsh": {
|
||
"profile": {
|
||
"bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"]
|
||
}
|
||
}
|
||
}
|
||
EOF
|
||
printf '[]\n' > "$PROFILE_DIR/cordis.patch.yml"
|
||
cat > "$PROFILE_DIR/pnpm-workspace.yaml" <<'EOF'
|
||
packages:
|
||
- .
|
||
|
||
nodeLinker: hoisted
|
||
autoInstallPeers: false
|
||
|
||
allowBuilds:
|
||
node-pty: true
|
||
protobufjs: true
|
||
|
||
minimumReleaseAgeExclude:
|
||
- dsh-better-sidebar
|
||
EOF
|
||
|
||
# 步骤 2:官方 CLI 安装 tarball + bundle 协调(真实挂载路径)
|
||
say "执行 dsh plugin --profile web add file:$TARBALL ..."
|
||
$DSH_CMD plugin --profile web add "file:$TARBALL"
|
||
|
||
# 步骤 3:校验挂载生效(dsh.profile.bundles 含 dsh-better-sidebar)
|
||
if ! node -e '
|
||
const fs = require("fs");
|
||
const p = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||
const bundles = p.dsh?.profile?.bundles ?? [];
|
||
process.exit(bundles.includes("dsh-better-sidebar") ? 0 : 1);
|
||
' "$PROFILE_DIR/package.json"; then
|
||
warn "dsh-better-sidebar 未出现在 dsh.profile.bundles 中——挂载未注册"
|
||
cat "$PROFILE_DIR/package.json"
|
||
exit 1
|
||
fi
|
||
say "挂载已注册:dsh.profile.bundles 包含 dsh-better-sidebar"
|
||
|
||
# 步骤 4:启动 dsh web(--port 0 = OS 分配,避免端口冲突;keyless 可起)
|
||
say "启动 dsh web(port=${PORT})..."
|
||
$DSH_CMD web --port "$PORT" > "$WEB_LOG" 2>&1 &
|
||
SERVER_PID=$!
|
||
|
||
URL=""
|
||
for _ in $(seq 1 120); do
|
||
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||
echo "=== dsh web 提前退出,日志尾部 ===" >&2
|
||
tail -30 "$WEB_LOG" >&2 || true
|
||
exit 1
|
||
fi
|
||
if URL="$(grep -oE 'dsh web: http://127\.0\.0\.1:[0-9]+' "$WEB_LOG" | head -1 | awk '{print $3}')" && [ -n "$URL" ]; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
[ -n "$URL" ] || { echo "=== 120s 内未等到 dsh web 就绪,日志尾部 ===" >&2; tail -40 "$WEB_LOG" >&2 || true; exit 1; }
|
||
say "dsh web 就绪:${URL}(pid ${SERVER_PID})"
|
||
|
||
# 步骤 5:运行无头渲染 lane
|
||
say "运行 Playwright 无头渲染 lane..."
|
||
DSH_E2E_URL="$URL" DSH_E2E_WORKSPACE="$WORKSPACE_DIR" \
|
||
pnpm exec playwright test ${GREP_FILTER:+--grep "$GREP_FILTER"}
|
||
|
||
say "通过:插件挂载到真实 DSH 后无头渲染未崩溃"
|