Merge pull request #527 from omdsh-dev/fix/scripts-hardening

fix(scripts): e2e 脚本加固——tarball mtime 选取/DSH_CMD 统一/registry mermaid chunk/e2e-common.sh 去重
This commit is contained in:
Atlas Zeng
2026-09-03 16:30:12 +08:00
committed by GitHub
4 changed files with 236 additions and 172 deletions
+26 -79
View File
@@ -20,8 +20,8 @@
# bash scripts/e2e-aggregate-mount.sh
#
# 环境变量(均可省略):
# DSH_CMD dsh 命令;缺省 `npx -y --package @deepseek-ai/dsh dsh`
# (与 pm2 启动器同源,避免依赖可能失效的 PATH dsh)
# DSH_CMD dsh 命令;缺省 PATH 上的 `dsh`,回退 npx 拉官方包
# (同 e2e-mount.sh)
# TARBALL 插件 tarball;缺省仓库根 dsh-better-sidebar-*.tgz(须已 pack)
# PORT 固定端口(默认 0 = OS 分配,从日志解析 URL)
# DSH_HOME_BASE scratch 根目录(默认系统临时目录)。脚本始终在其下新建
@@ -30,6 +30,8 @@
# KEEP_HOME 非空时保留 scratch 目录(调试用)
#
# 退出码 = 0 通过;非 0 失败。服务器与 scratch 目录由 trap 兜底清理。
# 日志/前置校验/DSH_CMD 与 tarball 解析/scratch profile 三件套/清理 trap/
# dsh web 启动与就绪轮询的共享骨架见 scripts/e2e-common.sh。
# =============================================================================
set -euo pipefail
@@ -37,55 +39,33 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
FIXTURE_DIR="$ROOT/tests/fixtures/aggregate-better-sidebar"
DSH_CMD="${DSH_CMD:-npx -y --package @deepseek-ai/dsh dsh}"
E2E_TAG=e2e-aggregate-mount
source "$SCRIPT_DIR/e2e-common.sh"
DSH_CMD="${DSH_CMD:-dsh}"
TARBALL="${TARBALL:-}"
PORT="${PORT:-0}"
KEEP_HOME="${KEEP_HOME:-}"
say() { printf '\033[32m[e2e-aggregate-mount]\033[0m %s\n' "$*"; }
warn() { printf '\033[33m[e2e-aggregate-mount]\033[0m %s\n' "$*" >&2; }
die() { printf '\033[31m[e2e-aggregate-mount]\033[0m %s\n' "$*" >&2; exit 1; }
e2e_require_cmd node "DSH 运行需要 Node.js >= 20"
e2e_require_cmd pnpm "dsh plugin 转发给 pnpm"
e2e_require_cmd npm "打包 fixture 需要"
e2e_require_cmd curl
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)"
command -v npm >/dev/null 2>&1 || die "未找到 npm(打包 fixture 需要)"
command -v curl >/dev/null 2>&1 || die "未找到 curl"
e2e_resolve_dsh_cmd
if [ -z "$TARBALL" ]; then
TARBALL="$(ls "$ROOT"/dsh-better-sidebar-*.tgz 2>/dev/null | head -1 || true)"
fi
[ -n "$TARBALL" ] && [ -f "$TARBALL" ] || die "未找到插件 tarball:先在本仓库跑 pnpm build && npm pack,或用 TARBALL 指定"
e2e_resolve_tarball || die "未找到插件 tarball:先在本仓库跑 pnpm build && npm pack,或用 TARBALL 指定"
# ── scratch home ────────────────────────────────────────────────────────────
# 始终在本调用拥有的全新目录里运行:调用方给了 DSH_HOME_BASE(可能是真实
# ~/.dsh)时,只在其下新建子目录并只删除该子目录;缺省时直接用系统临时
# 目录。调用方提供的目录本身绝不写入或删除。
DSH_HOME_BASE="${DSH_HOME_BASE:-}"
if [ -n "$DSH_HOME_BASE" ]; then
SCRATCH="$(mktemp -d "$DSH_HOME_BASE/dsh-e2e-agg.XXXXXX")"
else
SCRATCH="$(mktemp -d /tmp/dsh-e2e-agg.XXXXXX)"
fi
# DSH_HOME 直接用 $SCRATCH(mount lane 用子路径 home/——两 lane 既有差异);
# DSH_HOME_BASE 语义与清理 trap 见 e2e-common.sh。
e2e_make_scratch dsh-e2e-agg
export DSH_HOME="$SCRATCH"
LOG_DIR="$DSH_HOME/logs"; mkdir -p "$LOG_DIR"
OUT_LOG="$LOG_DIR/dsh-web.out.log"; ERR_LOG="$LOG_DIR/dsh-web.err.log"
SERVER_PID=""
TMP_DIR=""
cleanup() {
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 [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
rm -rf "$TMP_DIR"
fi
if [ -z "$KEEP_HOME" ] && [ -d "$SCRATCH" ]; then
rm -rf "$SCRATCH"
else
warn "KEEP_HOME 已设置,保留 $SCRATCH"
fi
}
trap cleanup EXIT
trap e2e_cleanup EXIT
# ── 打包 fixture 聚合包 ─────────────────────────────────────────────────────
TMP_DIR="$(mktemp -d)"
@@ -94,38 +74,10 @@ FIXTURE_TGZ="$(ls "$TMP_DIR"/*.tgz 2>/dev/null | head -1 || true)"
[ -n "$FIXTURE_TGZ" ] || die "fixture 打包失败"
# ── 引导 scratch profile(web 模板)────────────────────────────────────────
# 先写 pnpm-workspace.yaml 的 allowBuilds / minimumReleaseAgeExclude,避免
# pnpm 11 strict-dep-builds 拦截 node-pty/protobufjs——同 e2e-mount.sh。
# 三件套 heredoc 及 pnpm-workspace 的 allowBuilds / minimumReleaseAgeExclude
# 理由见 e2e-common.sh 的 e2e_write_profile。
PROFILE_DIR="$DSH_HOME/profiles/web"
mkdir -p "$PROFILE_DIR"
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
- '@deepseek-ai/*'
EOF
e2e_write_profile "$PROFILE_DIR"
# ── 组装 profile:聚合先行,插件后到 ──────────────────────────────────────
say "安装 fixture 聚合包(先行)…"
@@ -135,20 +87,15 @@ $DSH_CMD plugin --profile web add "file:$TARBALL"
# ── 启动真实 dsh web ───────────────────────────────────────────────────────
say "启动 dsh web(--port ${PORT},日志 ${LOG_DIR})…"
$DSH_CMD web --port "$PORT" >"$OUT_LOG" 2>"$ERR_LOG" &
SERVER_PID=$!
e2e_start_dsh_web "$OUT_LOG" "$ERR_LOG"
# 等待启动 URL 或进程退出(最多 120s)。这里有意只取 origin:0.1.2-alpha.1+
# 的就绪行是 `…/?token=<43字符>` 鉴权 URL,但下方的探活全部打插件的
# `/sidebar/api/*` 路由——webserver carrier 不做鉴权(只有 /api、index 与
# /sidebar/api/* 路由——webserver carrier 不做鉴权(只有 /api、index 与
# remote.mux 升级在 browser auth 之后),origin 拼路径即正确且两版通用。
URL=""
for _ in $(seq 1 120); do
if ! kill -0 "$SERVER_PID" 2>/dev/null; then break; fi
URL="$(grep -oE 'http://127\.0\.0\.1:[0-9]+' "$OUT_LOG" | tail -1 || true)"
[ -n "$URL" ] && break
sleep 1
done
E2E_READY_RE='http://127\.0\.0\.1:[0-9]+'
E2E_READY_PICK=tail
e2e_wait_dsh_web_ready "$OUT_LOG" || true
if [ -z "$URL" ]; then
warn "out log 尾部:$(tail -5 "$OUT_LOG" 2>/dev/null || true)"
warn "err log 尾部:$(tail -5 "$ERR_LOG" 2>/dev/null || true)"
+175
View File
@@ -0,0 +1,175 @@
# =============================================================================
# scripts/e2e-common.sh — e2e-mount.sh / e2e-aggregate-mount.sh 的共享骨架:
# 日志三件套、前置校验、DSH_CMD / tarball 解析、scratch home 与清理 trap、
# scratch profile 三件套、dsh web 后台启动与就绪轮询。
#
# 纯开发侧库文件,不进 npm 包(package.json files 白名单无它);
# install.sh / install.ps1 随 npm 包独立分发,必须自包含,绝不 source 本文件。
#
# 用法:调用方先设 E2E_TAG(say/warn/die 的日志前缀)再 source:
# E2E_TAG=e2e-mount
# source "$SCRIPT_DIR/e2e-common.sh"
#
# 依赖调用方已 `set -euo pipefail` 并定义 ROOT(仓库根绝对路径)。
# 两侧脚本有意保留的差异(行为等价,靠参数 / 调用点区分):
# - DSH_HOME 子路径:mount 用 $SCRATCH/home,aggregate 直接用 $SCRATCH;
# - 就绪行 grep 正则:mount 要带 token 的完整 URL,aggregate 只取 origin
# (两侧的理由见各自脚本的就绪行注释);
# - 额外清理项:aggregate 的 fixture 打包 TMP_DIR(mount 不设即跳过)。
# =============================================================================
: "${E2E_TAG:?必须先设置 E2E_TAG(日志前缀)再 source e2e-common.sh}"
# ── 日志三件套 ───────────────────────────────────────────────────────────────
say() { printf '\033[32m[%s]\033[0m %s\n' "$E2E_TAG" "$*"; }
warn() { printf '\033[33m[%s]\033[0m %s\n' "$E2E_TAG" "$*" >&2; }
die() { printf '\033[31m[%s]\033[0m %s\n' "$E2E_TAG" "$*" >&2; exit 1; }
# ── 前置校验 ─────────────────────────────────────────────────────────────────
# e2e_require_cmd <cmd> [用途]:不在 PATH 上即 die(沿用两脚本原有报错文案)。
e2e_require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
if [ $# -ge 2 ]; then
die "未找到 $1($2)"
else
die "未找到 $1"
fi
fi
}
# ── DSH_CMD 解析 ─────────────────────────────────────────────────────────────
# PATH 上的 dsh 优先,否则 npx 拉官方包(同 scripts/install.sh)。DSH_CMD
# 缺省值(`dsh`)由调用方从环境变量取好传入。
e2e_resolve_dsh_cmd() {
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 解析 ─────────────────────────────────────────────────────────────
# TARBALL 已显式给出时原样使用;否则从仓库根 glob 里选。多个候选时取 mtime
# 最新——`ls | head -1` 的字典序会拿到旧版本号的历史 tarball,把冒烟挂到
# 过期产物上。未找到候选时返回非零,由调用方按各自文案 die。
e2e_resolve_tarball() {
if [ -z "${TARBALL:-}" ]; then
TARBALL="$(ls -t "$ROOT"/dsh-better-sidebar-*.tgz 2>/dev/null | head -1 || true)"
local count
count="$(ls "$ROOT"/dsh-better-sidebar-*.tgz 2>/dev/null | wc -l | tr -d ' ' || true)"
[ "$count" -le 1 ] || warn "发现 $count 个 tarball,按 mtime 选用最新:$(basename "$TARBALL")(建议清理其余)"
fi
[ -n "$TARBALL" ] && [ -f "$TARBALL" ]
}
# ── scratch home 与清理 ──────────────────────────────────────────────────────
# 始终在本调用拥有的全新目录里运行:调用方给了 DSH_HOME_BASE(可能是真实
# ~/.dsh)时,只在其下新建子目录并只删除该子目录;缺省时直接用系统临时
# 目录。调用方提供的目录本身绝不写入或删除。
# $1 = mktemp 模板短名(dsh-e2e-mount / dsh-e2e-agg,便于区分两 lane 的残留)。
# DSH_HOME 用 $SCRATCH 还是其子路径由调用方自行 export(两 lane 的既有差异)。
e2e_make_scratch() {
if [ -n "${DSH_HOME_BASE:-}" ]; then
SCRATCH="$(mktemp -d "$DSH_HOME_BASE/$1.XXXXXX")"
else
SCRATCH="$(mktemp -d /tmp/$1.XXXXXX)"
fi
}
# EXIT trap 兜底清理:杀 dsh web 后台进程;清 aggregate lane 的 fixture 打包
# 目录(TMP_DIR——mount lane 不设该变量即整段跳过);最后按 KEEP_HOME 决定
# 是否删除 scratch。结尾显式 exit 回传 trap 触发时的退出码。
e2e_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 [ -n "${TMP_DIR:-}" ] && [ -d "${TMP_DIR:-}" ]; then
rm -rf "$TMP_DIR"
fi
if [ -z "${KEEP_HOME:-}" ]; then
if [ -d "${SCRATCH:-}" ]; then
rm -rf "$SCRATCH"
fi
else
warn "KEEP_HOME 已设置,保留 $SCRATCH"
fi
exit "$code"
}
# ── scratch profile 三件套 ───────────────────────────────────────────────────
# 引导 scratch profile(web 模板,镜像 dsh initProfile);先写
# pnpm-workspace.yaml 的 allowBuilds / minimumReleaseAgeExclude,避免 pnpm 11
# strict-dep-builds 拦截 node-pty/protobufjs 或拒绝 <24h 新版本——同 install.sh。
# @deepseek-ai/* 通配与仓库根 pnpm-workspace.yaml 同策:钉的 DSH alpha 常在
# 发布后 24h 内跑 lane,没有豁免会被 minimumReleaseAge 直接拒装。
e2e_write_profile() {
mkdir -p "$1"
cat > "$1/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' > "$1/cordis.patch.yml"
cat > "$1/pnpm-workspace.yaml" <<'EOF'
packages:
- .
nodeLinker: hoisted
autoInstallPeers: false
allowBuilds:
node-pty: true
protobufjs: true
minimumReleaseAgeExclude:
- dsh-better-sidebar
- '@deepseek-ai/*'
EOF
}
# ── dsh web 启动 + 就绪轮询 ──────────────────────────────────────────────────
# 后台启动 dsh web(--port 0 = OS 分配,避免端口冲突;keyless 可起)。
# $1 = stdout 日志;$2 = stderr 日志(缺省合并进 $1:mount lane 单文件,
# aggregate lane 分 out/err 两个文件)。写入全局 SERVER_PID。
e2e_start_dsh_web() {
if [ $# -ge 2 ]; then
$DSH_CMD web --port "$PORT" >"$1" 2>"$2" &
else
$DSH_CMD web --port "$PORT" >"$1" 2>&1 &
fi
SERVER_PID=$!
}
# 轮询日志等待就绪行(最多 120s)。$1 = 参与轮询的日志文件;就绪行 ERE 与
# 多条命中的取舍(head/tail)由 E2E_READY_RE / E2E_READY_PICK 传入——这是
# 两条 lane 有意保留的差异(token URL vs 裸 origin,理由见各自脚本的就绪
# 行注释)。URL 提取统一为「命中串按空白分段取末段」:mount 的模式带
# `dsh web: ` 前缀(两段)故末段即完整 URL;aggregate 的模式不含空白故末段
# 即 origin——与原先两侧的 awk '{print $3}' / 直接取整行等价。
# 返回:0 = URL 已写入全局 URL;1 = 120s 超时;2 = 服务进程提前退出。
e2e_wait_dsh_web_ready() {
URL=""
local _
for _ in $(seq 1 120); do
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
return 2
fi
if URL="$(grep -oE "$E2E_READY_RE" "$1" | "${E2E_READY_PICK:-head}" -1 | awk '{print $NF}')" && [ -n "$URL" ]; then
return 0
fi
sleep 1
done
return 1
}
+33 -93
View File
@@ -22,54 +22,37 @@
# KEEP_HOME 非空时保留 scratch home(调试用)
#
# 退出码 = playwright 的退出码;服务器与 scratch 目录由 trap 兜底清理。
# 日志/前置校验/DSH_CMD 与 tarball 解析/scratch profile 三件套/清理 trap/
# dsh web 启动与就绪轮询的共享骨架见 scripts/e2e-common.sh。
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
E2E_TAG=e2e-mount
source "$SCRIPT_DIR/e2e-common.sh"
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; }
e2e_require_cmd node "DSH 运行需要 Node.js >= 20"
e2e_require_cmd pnpm "dsh plugin 转发给 pnpm"
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)"
e2e_resolve_dsh_cmd
# 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 解析(多个候选时取 mtime 最新——`ls | head -1` 的字典序会拿到
# 旧版本号的历史 tarball,把冒烟挂到过期产物上)
if [ -z "$TARBALL" ]; then
TARBALL="$(ls -t "$ROOT"/dsh-better-sidebar-*.tgz 2>/dev/null | head -1 || true)"
COUNT="$(ls "$ROOT"/dsh-better-sidebar-*.tgz 2>/dev/null | wc -l | tr -d ' ')"
[ "$COUNT" -le 1 ] || warn "发现 $COUNT 个 tarball,按 mtime 选用最新:$(basename "$TARBALL")(建议清理其余)"
fi
[ -n "$TARBALL" ] && [ -f "$TARBALL" ] || die "找不到 tarball(TARBALL 或 \$ROOT/dsh-better-sidebar-*.tgz)——先运行 pnpm build && pnpm pack"
e2e_resolve_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
# scratch home(每次全新,绝不触碰真实 ~/.dsh;DSH_HOME_BASE 语义见
# e2e-common.sh)。DSH_HOME 用子路径 home/(aggregate lane 直接用 $SCRATCH,
# 两 lane 既有差异):playwright lane 还需要独立的 workspace 目录,与
# DSH_HOME 的 profiles 布局分开。
e2e_make_scratch dsh-e2e-mount
export DSH_HOME="$SCRATCH/home"
WORKSPACE_DIR="$SCRATCH/workspace"
LOG_DIR="$SCRATCH"
@@ -78,55 +61,12 @@ 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
trap e2e_cleanup EXIT
# 步骤 1:引导 scratch profile(web 模板,镜像 dsh initProfile;先写
# pnpm-workspace.yaml 的 allowBuilds / minimumReleaseAgeExclude,避免 pnpm 11
# strict-dep-builds 拦截 node-pty/protobufjs 或拒绝 <24h 新版本——同 install.sh。
# @deepseek-ai/* 通配与仓库根 pnpm-workspace.yaml 同策:钉的 DSH alpha 常在
# 发布后 24h 内跑 lane,没有豁免会被 minimumReleaseAge 直接拒装)
# 步骤 1:引导 scratch profile(三件套 heredoc 及其理由见 e2e-common.sh
# 的 e2e_write_profile)
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
- '@deepseek-ai/*'
EOF
e2e_write_profile "$PROFILE_DIR"
# 步骤 2:官方 CLI 安装 tarball + bundle 协调(真实挂载路径)
say "执行 dsh plugin --profile web add file:$TARBALL ..."
@@ -147,8 +87,7 @@ 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=$!
e2e_start_dsh_web "$WEB_LOG"
# 就绪行解析:DSH 0.1.2-alpha.1+ 打印的是带一次性 token 的鉴权 URL
# (`dsh web: http://127.0.0.1:<port>/?token=<43字符>`,页面导航用它换取
@@ -156,19 +95,20 @@ SERVER_PID=$!
# 匹配必须延伸到空白(`[^ ]*`)——在 `/` 处截断会丢掉 token,alpha.1+
# 宿主上的整条 lane 都会挂在首屏 401。LAN 后缀 `(LAN: …)` 是下一个
# 空格分段,不会被并进来。
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; }
E2E_READY_RE='dsh web: http://127\.0\.0\.1:[0-9]+[^ ]*'
E2E_READY_PICK=head
WAIT_RC=0
e2e_wait_dsh_web_ready "$WEB_LOG" || WAIT_RC=$?
if [ "$WAIT_RC" -eq 2 ]; then
echo "=== dsh web 提前退出,日志尾部 ===" >&2
tail -30 "$WEB_LOG" >&2 || true
exit 1
fi
if [ "$WAIT_RC" -ne 0 ]; then
echo "=== 120s 内未等到 dsh web 就绪,日志尾部 ===" >&2
tail -40 "$WEB_LOG" >&2 || true
exit 1
fi
say "dsh web 就绪:${URL}(pid ${SERVER_PID})"
# 步骤 5:运行无头渲染 lane
+2
View File
@@ -35,6 +35,8 @@ const files = [
'lib/client-terminal.js.map',
'lib/client-editor.js',
'lib/client-editor.js.map',
'lib/client-mermaid.js',
'lib/client-mermaid.js.map',
'README.md',
'LICENSE',
]