chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次) - .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…), 目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪 - .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/) - .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*) - .gitignore 补:备份件(*.bak-*) - 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
This commit is contained in:
1 parent
30b46dbd0c
commit
c1b5e4d966
735 files changed
+153192
-2415
No files matched your search
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# v-baseline.sh —— S0 尺子①:取两端基线(平台侧共享层 / 客户端 profile)
|
||||
# 规格来源:交付物/功能打包到基础插件-分步实施方案-20260926.md §3.10.1 / §3.10.2 S0-1·S0-2
|
||||
# -----------------------------------------------------------------------------
|
||||
# 约定(§3.10.1 三条)
|
||||
# 1. 退出码即结论:rc=0 绿(两端都取到)/rc≠0 红(有面取不到,逐面标注)
|
||||
# 2. 只许测活体:本尺子取的是**远端真实共享层**与**本机真实 profile**,⛔ 不用快照/config 推断
|
||||
# 3. 落点:脚本在 交付物/验证脚本/;读数落 tmp/p39/(⛔ 不入库,数值会漂)
|
||||
# 用法
|
||||
# bash v-baseline.sh # 取两端基线,写读数文件
|
||||
# bash v-baseline.sh --stable # 只打印**稳定块**(不含取数时间)⇒ 连跑两次逐字一致的比较对象
|
||||
# -----------------------------------------------------------------------------
|
||||
# 复核命令(照抄即可复现本条基线):
|
||||
# 平台侧:ssh [email protected] 'for p in /var/lib/dshs/bundled-plugins/*/; do ...; done'
|
||||
# —— 本脚本内 remote_block() 就是它;指纹算法 = 逐文件 sha256sum → 按字节序排序 → 再 sha256sum 取前 8
|
||||
# 客户端:读 $DSH_DESKTOP_HOME/profiles/desktop/package.json 的 dsh.profile.bundles
|
||||
# -----------------------------------------------------------------------------
|
||||
# ⛔ 只读:不写任何生产文件、不改任何工作区文件;唯一写动作 = 往 $OUT(=tmp/p39) 落读数
|
||||
# =============================================================================
|
||||
set -u
|
||||
|
||||
SELF_DIR="$(cd "$(dirname "$0")" && pwd -W)"
|
||||
[ -d "$SELF_DIR" ] || SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
WS="$(cd "$SELF_DIR/../.." && pwd -W)"
|
||||
[ -d "$WS" ] || WS="$(cd "$SELF_DIR/../.." && pwd)"
|
||||
OUT="${P39_OUT:-$WS/tmp/p39}"
|
||||
DATE="$(date +%Y%m%d)"
|
||||
STABLE_ONLY=0
|
||||
[ "${1:-}" = "--stable" ] && STABLE_ONLY=1
|
||||
|
||||
SSH_HOST="${P39_47:-root@47.77.182.89}"
|
||||
SHARED_DIR="${P39_SHARED:-/var/lib/dshs/bundled-plugins}"
|
||||
DSH_DESKTOP_HOME="${DSH_DESKTOP_HOME:-E:/github/dsh-desktop-0.1.7rc2/apps/desktop/.desktop-build/development/home-adapt}"
|
||||
PY="${PY:-E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe}"
|
||||
|
||||
mkdir -p "$OUT"
|
||||
|
||||
# ------------------------------- 平台侧 --------------------------------------
|
||||
platform_block() {
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=8 "$SSH_HOST" "DSH_SHARED_DIR='$SHARED_DIR' bash -s" <<'REMOTE' 2>/dev/null
|
||||
set -u
|
||||
D="${DSH_SHARED_DIR:-/var/lib/dshs/bundled-plugins}"
|
||||
[ -d "$D" ] || { echo "ERR no-such-dir $D"; exit 9; }
|
||||
for p in $(ls "$D" | sort); do
|
||||
d="$D/$p"
|
||||
[ -d "$d" ] || continue
|
||||
ver=$(grep -m1 '"version"' "$d/package.json" 2>/dev/null | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
|
||||
n=$(find "$d" -type f 2>/dev/null | wc -l)
|
||||
b=$(find "$d" -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1} END{printf "%d", s+0}')
|
||||
fp=$(find "$d" -type f -print0 2>/dev/null | sort -z | xargs -0 sha256sum 2>/dev/null | sha256sum | cut -c1-8)
|
||||
printf 'PKG\t%s\t%s\t%s\t%s\t%s\n' "$p" "${ver:-?}" "$n" "$b" "$fp"
|
||||
done
|
||||
REMOTE
|
||||
}
|
||||
|
||||
# ------------------------------- 客户端侧 ------------------------------------
|
||||
client_block() {
|
||||
local pj="$DSH_DESKTOP_HOME/profiles/desktop/package.json"
|
||||
[ -f "$pj" ] || { echo "ERR no-such-profile $pj"; return 9; }
|
||||
"$PY" - "$pj" <<'PYEOF' 2>/dev/null
|
||||
import json, sys
|
||||
d = json.load(open(sys.argv[1], encoding='utf-8'))
|
||||
b = ((d.get('dsh') or {}).get('profile') or {}).get('bundles') or []
|
||||
print("BUNDLE_COUNT\t%d" % len(b))
|
||||
for x in b:
|
||||
print("BUNDLE\t%s" % x)
|
||||
print("LOCAL_SCOPE_COUNT\t%d" % sum(1 for x in b if str(x).startswith('@dsh-local/')))
|
||||
PYEOF
|
||||
}
|
||||
|
||||
PLAT_RAW="$(platform_block)"; PLAT_RC=$?
|
||||
CLI_RAW="$(client_block)"; CLI_RC=$?
|
||||
|
||||
pl_err=0; cl_err=0
|
||||
[ "$PLAT_RC" != 0 ] && pl_err=1
|
||||
printf '%s\n' "$PLAT_RAW" | grep -q '^ERR' && pl_err=1
|
||||
printf '%s\n' "$CLI_RAW" | grep -q '^ERR' && cl_err=1
|
||||
|
||||
PLAT_STABLE="$(printf '%s\n' "$PLAT_RAW" | sort)"
|
||||
CLI_STABLE="$(printf '%s\n' "$CLI_RAW")"
|
||||
|
||||
if [ "$STABLE_ONLY" = "1" ]; then
|
||||
echo "### STABLE · 平台侧($SSH_HOST:$SHARED_DIR 包数+每包指纹)"
|
||||
printf '%s\n' "$PLAT_STABLE"
|
||||
echo "### STABLE · 客户端侧($DSH_DESKTOP_HOME/profiles/desktop bundle 清单)"
|
||||
printf '%s\n' "$CLI_STABLE"
|
||||
[ $((pl_err + cl_err)) -ne 0 ] && exit 1
|
||||
exit 0
|
||||
fi
|
||||
|
||||
{
|
||||
echo "════ 基线 · 平台侧(共享层)════"
|
||||
echo "取数时间:$(date -Iseconds)"
|
||||
echo "复核命令:ssh $SSH_HOST 'ls $SHARED_DIR' 后逐包 find|sha256sum(见本脚本 platform_block())"
|
||||
echo "指纹算法:逐文件 sha256sum → 按路径字节序排序 → 再 sha256sum 取前 8 位"
|
||||
echo "⚠️ 本指纹 ⛔ 与平台 treeSha256 不同源:平台权威 treeSha256 见节点同步记录(.node-sync-reports.json)"
|
||||
echo "⚠️ 字节口径:本行 = 目录内全部文件 sizes 之和(含 node_modules);⛔ 与平台 pack/tgz 字节(如 mcn-suite 659 文件 / 2,018,751 B)**不是同一口径**,⛔ 不可互比"
|
||||
echo "---- 数据(PKG / 版本 / 文件数 / 字节 / 指纹8)----"
|
||||
printf '%s\n' "$PLAT_RAW"
|
||||
} > "$OUT/baseline-平台-$DATE.txt"
|
||||
|
||||
{
|
||||
echo "════ 基线 · 客户端侧(本机 profile bundle 清单)════"
|
||||
echo "取数时间:$(date -Iseconds)"
|
||||
echo "复核命令:cat $DSH_DESKTOP_HOME/profiles/desktop/package.json → dsh.profile.bundles"
|
||||
echo "---- 数据 ----"
|
||||
printf '%s\n' "$CLI_RAW"
|
||||
} > "$OUT/baseline-客户端-$DATE.txt"
|
||||
|
||||
printf '%s\n' "$PLAT_STABLE" > "$OUT/.stable-平台-$DATE.txt"
|
||||
printf '%s\n' "$CLI_STABLE" > "$OUT/.stable-客户端-$DATE.txt"
|
||||
|
||||
echo "── v-baseline 结果 ──"
|
||||
if [ "$pl_err" = "0" ]; then
|
||||
echo "平台侧:✅ 取到 $(printf '%s\n' "$PLAT_RAW" | grep -c '^PKG') 个包"
|
||||
printf '%s\n' "$PLAT_RAW" | sed 's/^/ /'
|
||||
else
|
||||
echo "平台侧:❌ 未取到(rc=$PLAT_RC) ⇒ 原始输出:$(printf '%s' "$PLAT_RAW" | head -c 200)"
|
||||
fi
|
||||
if [ "$cl_err" = "0" ]; then
|
||||
echo "客户端侧:✅ 取到 $(printf '%s\n' "$CLI_RAW" | grep -c $'^BUNDLE\t') 个 bundle(@dsh-local/* 计 $(printf '%s\n' "$CLI_RAW" | sed -n 's/^LOCAL_SCOPE_COUNT\t//p') 个)"
|
||||
printf '%s\n' "$CLI_RAW" | sed 's/^/ /'
|
||||
else
|
||||
echo "客户端侧:❌ 未取到(rc=$CLI_RC) ⇒ 原始输出:$(printf '%s' "$CLI_RAW" | head -c 200)"
|
||||
fi
|
||||
echo "读数落盘:$OUT/baseline-平台-$DATE.txt · $OUT/baseline-客户端-$DATE.txt"
|
||||
echo "⛔ 基线数字只作开工对照,⛔ 不写进判据(判据一律取「前后一致」)"
|
||||
|
||||
[ $((pl_err + cl_err)) -ne 0 ] && exit 1
|
||||
exit 0
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# v-neg.sh —— S0 尺子④:负控驱动(**S0 的唯一完成判据**)
|
||||
# 规格来源:交付物/功能打包到基础插件-分步实施方案-20260926.md §3.10.1(负控驱动行)· §3.10.2 S0-5
|
||||
# -----------------------------------------------------------------------------
|
||||
# 断言(两个都成立才 rc=0 绿)
|
||||
# ① 改坏靶子一处 ⇒ v-plugin.sh 对应面 **必须红(rc≠0)**
|
||||
# ② 改回(用未改动的靶子)⇒ 同一条判据 **必须绿(rc=0)**
|
||||
# -----------------------------------------------------------------------------
|
||||
# 🔴 铁律:改坏**只动 tmp/ 里的副本** ——
|
||||
# ⛔ 不碰任何其他工作区的文件(用户 2026-09-24 明令);⛔ 不改真 profile、⛔ 不改真日志
|
||||
# 连跑两次一致的比较对象:见 v-baseline.sh --stable(本脚本只负责红/绿双态)
|
||||
# 用法
|
||||
# bash v-neg.sh 装配|取数|回调|运行|all
|
||||
# =============================================================================
|
||||
set -u
|
||||
|
||||
SELF_DIR="$(cd "$(dirname "$0")" && pwd -W)"
|
||||
[ -d "$SELF_DIR" ] || SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
WS="$(cd "$SELF_DIR/../.." && pwd -W)"
|
||||
[ -d "$WS" ] || WS="$(cd "$SELF_DIR/../.." && pwd)"
|
||||
OUT="${P39_OUT:-$WS/tmp/p39}"
|
||||
NEG="$OUT/neg"
|
||||
DATE="$(date +%Y%m%d)"
|
||||
REC="$OUT/neg-记录-$DATE.txt"
|
||||
VPLUGIN="$SELF_DIR/v-plugin.sh"
|
||||
|
||||
DSH_DESKTOP_HOME="${DSH_DESKTOP_HOME:-E:/github/dsh-desktop-0.1.7rc2/apps/desktop/.desktop-build/development/home-adapt}"
|
||||
TARGET="@dsh-client/portal-first-screen"
|
||||
PROFILE_JSON="$DSH_DESKTOP_HOME/profiles/desktop/package.json"
|
||||
|
||||
mkdir -p "$NEG"
|
||||
: > "$REC"
|
||||
|
||||
CASE="${1:-}"
|
||||
[ -z "$CASE" ] && { echo "用法: v-neg.sh <装配|取数|回调|运行|all>"; exit 2; }
|
||||
|
||||
log() { printf '%s\n' "$*" >> "$REC"; }
|
||||
|
||||
# 跑一条判据,返回 rc;输出同时落记录
|
||||
run_face() { # $1=face $2..=额外参数
|
||||
local face="$1"; shift
|
||||
local tmpf="$OUT/.neg-last.out"
|
||||
bash "$VPLUGIN" "$face" "$@" > "$tmpf" 2>&1
|
||||
local rc=$?
|
||||
cat "$tmpf" >> "$REC"
|
||||
printf '%s' "$rc"
|
||||
}
|
||||
|
||||
# 找靶子入口(真件)
|
||||
resolve_entry() {
|
||||
"$NODE_BIN" -e 'const{createRequire}=require("module");const r=createRequire(process.argv[1]);process.stdout.write(r.resolve(process.argv[2]))' "$PROFILE_JSON" "$TARGET" 2>/dev/null
|
||||
}
|
||||
NODE_BIN="${NODE:-E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3/node.exe}"
|
||||
REAL_ENTRY="$(resolve_entry)"
|
||||
REAL_PKG_JSON="$("$NODE_BIN" -e 'const{createRequire}=require("module");const r=createRequire(process.argv[1]);process.stdout.write(r.resolve(process.argv[2]+"/package.json"))' "$PROFILE_JSON" "$TARGET" 2>/dev/null)"
|
||||
REAL_PKG_DIR="$(dirname "$REAL_PKG_JSON")"
|
||||
|
||||
overall=0
|
||||
declare -a SUMMARY
|
||||
|
||||
one_case() { # $1 = case name
|
||||
local c="$1"
|
||||
local dir="$NEG/$c"; rm -rf "$dir"; mkdir -p "$dir"
|
||||
log ""
|
||||
log "════ 负控 case【$c】 $(date -Iseconds) ════"
|
||||
|
||||
local HEALTHY_RC BROKEN_RC=1
|
||||
|
||||
case "$c" in
|
||||
装配)
|
||||
# 破坏点(单变量):从 profile 的 dsh.profile.bundles 里删掉靶子那一行
|
||||
cp "$PROFILE_JSON" "$dir/package.json"
|
||||
"$PY_BIN" - "$dir/package.json" "$TARGET" <<'PYEOF'
|
||||
import json, sys
|
||||
p, t = sys.argv[1], sys.argv[2]
|
||||
d = json.load(open(p, encoding='utf-8'))
|
||||
b = d['dsh']['profile']['bundles']
|
||||
d['dsh']['profile']['bundles'] = [x for x in b if x != t]
|
||||
json.dump(d, open(p, 'w', encoding='utf-8'), indent=2, ensure_ascii=False)
|
||||
PYEOF
|
||||
# 给副本补上**靶子自身**的最小 node_modules 投影(只复制靶子,不投影整棵树)
|
||||
# ⇒ 破坏点收窄为单变量:只有 profile 的 bundles 声明缺了靶子
|
||||
STUB="$dir/node_modules/@dsh-client/portal-first-screen"
|
||||
mkdir -p "$STUB"
|
||||
cp "$REAL_PKG_JSON" "$STUB/package.json"
|
||||
cp -r "$REAL_PKG_DIR/lib" "$STUB/lib"
|
||||
[ -f "$REAL_PKG_DIR/cordis.patch.yml" ] && cp "$REAL_PKG_DIR/cordis.patch.yml" "$STUB/cordis.patch.yml"
|
||||
JUNC="stub"
|
||||
log "改坏方式:删掉副本 profile 的 bundles 行;node_modules 投影=已建(stub 只含靶子自身 ⇒ 单变量:只有 bundles 声明这一处被改坏)"
|
||||
BROKEN_RC="$(run_face 装配 --profile-json "$dir/package.json")"
|
||||
log ""
|
||||
log "▶ 改坏后:rc=$BROKEN_RC(期望 ≠0)"
|
||||
;;
|
||||
取数)
|
||||
cp "$REAL_ENTRY" "$dir/index.js"
|
||||
sed -i 's/^export const name = /export const nameQ39broken = /' "$dir/index.js"
|
||||
log "改坏方式:副本产物里把导出 name 改名(单变量)"
|
||||
BROKEN_RC="$(run_face 取数 --entry "$dir/index.js")"
|
||||
log ""
|
||||
log "▶ 改坏后:rc=$BROKEN_RC(期望 ≠0)"
|
||||
;;
|
||||
回调)
|
||||
cp "$REAL_ENTRY" "$dir/index.js"
|
||||
sed -i 's/^export function apply(/export function applyQ39broken(/' "$dir/index.js"
|
||||
log "改坏方式:副本产物里把 apply 改名(单变量)"
|
||||
BROKEN_RC="$(run_face 回调 --entry "$dir/index.js")"
|
||||
log ""
|
||||
log "▶ 改坏后:rc=$BROKEN_RC(期望 ≠0)"
|
||||
;;
|
||||
运行)
|
||||
REAL_LOG="$(ls -1t "$DSH_DESKTOP_HOME/logs/"startup-*.log 2>/dev/null | head -1)"
|
||||
if [ -z "$REAL_LOG" ]; then
|
||||
log "⚠️ 无真启动日志 ⇒ 本 case 未取到(rc=3)"
|
||||
SUMMARY+=("$c: 未取到(无真启动日志)")
|
||||
overall=1
|
||||
return
|
||||
fi
|
||||
cp "$REAL_LOG" "$dir/fake-failed.log"
|
||||
printf "\n Failed plugins (1):\n %s\n Error: [S0 负控构造] 靶子启动失败(⛔ 构造副本,真日志未被改动)\n" "$TARGET" >> "$dir/fake-failed.log"
|
||||
log "改坏方式:把真日志**副本**改出一行靶子失败条目(单变量;⛔ 真日志未动)"
|
||||
BROKEN_RC="$(run_face 运行 --log "$dir/fake-failed.log")"
|
||||
log ""
|
||||
log "▶ 改坏后:rc=$BROKEN_RC(期望 ≠0)"
|
||||
;;
|
||||
esac
|
||||
|
||||
HEALTHY_RC="$(run_face "$c")"
|
||||
log ""
|
||||
log "▶ 改回(真靶子):rc=$HEALTHY_RC(期望 0)"
|
||||
|
||||
local ok=1
|
||||
[ "$BROKEN_RC" = "0" ] && ok=0
|
||||
[ "$HEALTHY_RC" != "0" ] && ok=0
|
||||
if [ "$ok" = "1" ]; then
|
||||
log "✅ case【$c】负控通过:改坏 rc=$BROKEN_RC(红)/改回 rc=$HEALTHY_RC(绿)"
|
||||
SUMMARY+=("$c: ✅ 通过(改坏 rc=$BROKEN_RC 红 · 改回 rc=$HEALTHY_RC 绿)")
|
||||
else
|
||||
log "❌ case【$c】负控未通过:改坏 rc=$BROKEN_RC / 改回 rc=$HEALTHY_RC(要求 改坏≠0 且 改回=0)"
|
||||
SUMMARY+=("$c: ❌ 未通过(改坏 rc=$BROKEN_RC · 改回 rc=$HEALTHY_RC)")
|
||||
overall=1
|
||||
fi
|
||||
}
|
||||
|
||||
PY_BIN="${PY:-E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe}"
|
||||
[ -z "$REAL_ENTRY" ] || [ -f "$REAL_ENTRY" ] || { echo "⛔ 解析不到真靶子入口($TARGET)⇒ 负控前置不成立"; exit 1; }
|
||||
|
||||
if [ "$CASE" = "all" ]; then
|
||||
for c in 装配 运行 取数 回调; do one_case "$c"; done
|
||||
else
|
||||
case "$CASE" in 装配|运行|取数|回调) one_case "$CASE" ;; *) echo "⛔ 未知 case:$CASE"; exit 2 ;; esac
|
||||
fi
|
||||
|
||||
echo "════ v-neg 结果 ════"
|
||||
for s in "${SUMMARY[@]}"; do echo " $s"; done
|
||||
echo "记录(含全部原始输出):$REC"
|
||||
[ "$overall" != "0" ] && { echo "⇒ 红:负控未全过(⛔ 不通过负控的判据不算判据)"; exit 1; }
|
||||
echo "⇒ 绿:红/绿双态均成立"
|
||||
exit 0
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# v-plugin.sh —— S0 尺子②:插件四判据(装配 / 运行 / 取数 / 回调)
|
||||
# 规格来源:交付物/功能打包到基础插件-分步实施方案-20260926.md §3.10.1(尺子表)· §3.10.2 S0-3
|
||||
# -----------------------------------------------------------------------------
|
||||
# 约定
|
||||
# 1. **退出码即结论**:rc=0 绿 / rc=1 红 / rc=3 **未取到**(⛔ 3 不是绿 —— 取不到就如实报,⛔ 不折算成"已知")
|
||||
# 2. **只许测活体**:装配/运行 取真实 profile 与真实 boot 日志;取数/回调 **真实 import 并真实执行 apply()**
|
||||
# —— ⛔ 不用 dump-config 类"不 boot 就出结果"的路子;⛔ 不用静态 grep 冒充运行结论
|
||||
# 3. 落点:脚本在 交付物/验证脚本/;临时 harness 落 tmp/p39/
|
||||
# 用法
|
||||
# bash v-plugin.sh 装配|运行|取数|回调 [--target N] [--home H] [--profile-json F] [--log F] [--entry F]
|
||||
# 英文别名:assembly|run|fetch|callback
|
||||
# 靶子:默认 @dsh-client/portal-first-screen(**已有插件**,已装在开发态桌面 profile)
|
||||
# -----------------------------------------------------------------------------
|
||||
# 复核命令
|
||||
# 装配: cat $HOME/profiles/desktop/package.json ;node createRequire().resolve(target)
|
||||
# 运行: ls $HOME/logs/startup-*.log ;grep -n <target> <最新启动日志>
|
||||
# 取数: node tmp/p39/_probe.mjs fetch <entry>
|
||||
# 回调: node tmp/p39/_probe.mjs callback <entry>
|
||||
# =============================================================================
|
||||
set -u
|
||||
|
||||
SELF_DIR="$(cd "$(dirname "$0")" && pwd -W)"
|
||||
[ -d "$SELF_DIR" ] || SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
WS="$(cd "$SELF_DIR/../.." && pwd -W)"
|
||||
[ -d "$WS" ] || WS="$(cd "$SELF_DIR/../.." && pwd)"
|
||||
OUT="${P39_OUT:-$WS/tmp/p39}"
|
||||
PTRACE="$OUT/_probe.mjs"
|
||||
|
||||
NODE="${NODE:-E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3/node.exe}"
|
||||
PY="${PY:-E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe}"
|
||||
DSH_DESKTOP_HOME="${DSH_DESKTOP_HOME:-E:/github/dsh-desktop-0.1.7rc2/apps/desktop/.desktop-build/development/home-adapt}"
|
||||
|
||||
TARGET="@dsh-client/portal-first-screen"
|
||||
PROFILE_JSON=""
|
||||
LOG_FILE=""
|
||||
ENTRY=""
|
||||
|
||||
FACE_RAW="${1:-}"
|
||||
[ -z "$FACE_RAW" ] && { echo "用法: v-plugin.sh <装配|运行|取数|回调> [--target N] [--home H]..."; exit 2; }
|
||||
shift || true
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--target) TARGET="$2"; shift 2 ;;
|
||||
--home) DSH_DESKTOP_HOME="$2"; shift 2 ;;
|
||||
--profile-json) PROFILE_JSON="$2"; shift 2 ;;
|
||||
--log) LOG_FILE="$2"; shift 2 ;;
|
||||
--entry) ENTRY="$2"; shift 2 ;;
|
||||
*) echo "⛔ 未知参数:$1"; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$FACE_RAW" in
|
||||
装配|assembly|a) FACE=装配 ;;
|
||||
运行|run|r) FACE=运行 ;;
|
||||
取数|fetch|f) FACE=取数 ;;
|
||||
回调|callback|c) FACE=回调 ;;
|
||||
*) echo "⛔ 未知判据面:$FACE_RAW(只许 装配|运行|取数|回调)"; exit 2 ;;
|
||||
esac
|
||||
|
||||
[ -z "$PROFILE_JSON" ] && PROFILE_JSON="$DSH_DESKTOP_HOME/profiles/desktop/package.json"
|
||||
PROFILE_DIR="$(dirname "$PROFILE_JSON")"
|
||||
|
||||
mkdir -p "$OUT"
|
||||
green() { echo "✅ 绿(rc=0)|判据【$FACE】:$*"; exit 0; }
|
||||
red() { echo "❌ 红(rc=1)|判据【$FACE】:$*"; exit 1; }
|
||||
na() { echo "⚠️ 未取到(rc=3)|判据【$FACE】:$*"; exit 3; }
|
||||
|
||||
resolve_entry() {
|
||||
"$NODE" -e 'const{createRequire}=require("module");const r=createRequire(process.argv[1]);process.stdout.write(r.resolve(process.argv[2]))' "$PROFILE_JSON" "$TARGET" 2>/dev/null
|
||||
}
|
||||
|
||||
case "$FACE" in
|
||||
# ---------------------------------------------------------------------------
|
||||
装配)
|
||||
[ -f "$PROFILE_JSON" ] || na "profile 不存在:$PROFILE_JSON"
|
||||
echo "── 【装配】靶子=$TARGET profile=$PROFILE_JSON"
|
||||
PY_OUT="$("$PY" - "$PROFILE_JSON" "$TARGET" <<'PYEOF' 2>/dev/null
|
||||
import json, sys
|
||||
pj, target = sys.argv[1], sys.argv[2]
|
||||
d = json.load(open(pj, encoding='utf-8'))
|
||||
b = ((d.get('dsh') or {}).get('profile') or {}).get('bundles') or []
|
||||
print("IN_BUNDLES=%s" % ("yes" if target in b else "no"))
|
||||
print("BUNDLE_COUNT=%d" % len(b))
|
||||
PYEOF
|
||||
)"
|
||||
echo " ① 声明在 profile bundles:$(printf '%s\n' "$PY_OUT" | sed -n 's/^IN_BUNDLES=//p')(该 profile 共 $(printf '%s\n' "$PY_OUT" | sed -n 's/^BUNDLE_COUNT=//p') 个 bundle)"
|
||||
printf '%s\n' "$PY_OUT" | grep -q '^IN_BUNDLES=yes' || red "① 靶子不在 dsh.profile.bundles 里 ⇒ 装配前提不成立"
|
||||
|
||||
RESOLVED="$(resolve_entry)"
|
||||
if [ -z "$RESOLVED" ] || [ ! -f "$RESOLVED" ]; then
|
||||
red "② 从 profile 目录解析不到靶子入口(createRequire.resolve 失败或文件不存在):'$RESOLVED'"
|
||||
fi
|
||||
echo " ② 自 profile 目录解析成功:$RESOLVED"
|
||||
|
||||
PKGJSON="$("$NODE" -e 'const{createRequire}=require("module");const r=createRequire(process.argv[1]);process.stdout.write(r.resolve(process.argv[2]+"/package.json"))' "$PROFILE_JSON" "$TARGET" 2>/dev/null)"
|
||||
[ -f "$PKGJSON" ] || red "③ 解析不到靶子 package.json"
|
||||
PKGDIR="$(dirname "$PKGJSON")"
|
||||
PATCH="$(sed -n 's/.*"patch"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$PKGJSON" | head -1)"
|
||||
if [ -n "$PATCH" ]; then
|
||||
[ -f "$PKGDIR/${PATCH#./}" ] || red "③ 声明了装载补丁 $PATCH 但文件不存在:$PKGDIR/${PATCH#./}"
|
||||
echo " ③ 装载补丁在位:${PATCH#./}"
|
||||
else
|
||||
echo " ③ ⚠️ 该包未声明 dsh.bundle.patch(非 profile 装载形态)"
|
||||
fi
|
||||
echo " 入口文件:$RESOLVED ($(find "$RESOLVED" -type f -printf '%s\n' 2>/dev/null || wc -c < "$RESOLVED") B)"
|
||||
green "三条装配前提(bundles 声明 / 可解析 / 产物在位)全部成立"
|
||||
;;
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
运行)
|
||||
HOME_DIR="$DSH_DESKTOP_HOME"
|
||||
if [ -z "$LOG_FILE" ]; then
|
||||
LOG_FILE="$(ls -1t "$HOME_DIR/logs/"startup-*.log 2>/dev/null | head -1)"
|
||||
fi
|
||||
[ -n "$LOG_FILE" ] && [ -f "$LOG_FILE" ] || na "无启动日志可取($HOME_DIR/logs/startup-*.log 不存在)⇒ 运行态证据只能取活体 boot,本机当前无新 boot 记录"
|
||||
echo "── 【运行】靶子=$TARGET 日志=$LOG_FILE(mtime $(date -r "$LOG_FILE" -Iseconds 2>/dev/null || echo '?'))"
|
||||
HITS="$(grep -n "$TARGET" "$LOG_FILE" 2>/dev/null | head -5)"
|
||||
FAILBLOCK="$(grep -c 'Failed plugins' "$LOG_FILE" 2>/dev/null || echo 0)"
|
||||
if [ -n "$HITS" ]; then
|
||||
echo " 日志中命中靶子的失败条目:"
|
||||
printf '%s\n' "$HITS" | sed 's/^/ /' | cut -c1-160
|
||||
red "靶子出现在启动失败的 Failed plugins 列表里 ⇒ 运行面红"
|
||||
fi
|
||||
echo " 日志中无靶子的失败条目(该日志 'Failed plugins' 出现 $FAILBLOCK 次)"
|
||||
echo ' ⚠️ 证据强度:**无失败条目**(弱证据,⛔ 不是「已挂载」的证明)'
|
||||
echo " ⚠️ 已知坑:**启动日志只记失败条目**,插件自己的 logger.info 不在其中 ⇒「成功挂载」只能靠 RPC/浏览器级"
|
||||
green "靶子未出现于启动失败面(本面为**失败面判据**:出现即红;不出现 ≠ 已挂载)"
|
||||
;;
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
取数|回调)
|
||||
if [ -z "$ENTRY" ]; then
|
||||
ENTRY="$(resolve_entry)"
|
||||
[ -n "$ENTRY" ] && [ -f "$ENTRY" ] || na "解析不到靶子入口 ⇒ 无法真实 import"
|
||||
fi
|
||||
[ -f "$ENTRY" ] || na "入口文件不存在:$ENTRY"
|
||||
|
||||
cat > "$PTRACE" <<'JSEOF'
|
||||
import { pathToFileURL } from 'node:url';
|
||||
const face = process.argv[2];
|
||||
const entry = process.argv[3];
|
||||
const calls = [];
|
||||
function summarize(a) {
|
||||
if (a === null) return 'null';
|
||||
const t = typeof a;
|
||||
if (t === 'string') return a.length > 70 ? JSON.stringify(a.slice(0, 70) + '…') : JSON.stringify(a);
|
||||
if (t === 'function') return '<fn>';
|
||||
if (t === 'number' || t === 'boolean' || t === 'undefined') return String(a);
|
||||
if (t === 'object') {
|
||||
if (typeof a.path === 'string') return JSON.stringify({ path: a.path, methods: a.methods === undefined ? null : a.methods });
|
||||
try { const s = JSON.stringify(a); return s && s.length > 70 ? s.slice(0, 70) + '…' : String(s); } catch (e) { return '<obj>'; }
|
||||
}
|
||||
return String(a);
|
||||
}
|
||||
function mkSpy(path, depth) {
|
||||
const fn = function () {};
|
||||
return new Proxy(fn, {
|
||||
get(t, k) {
|
||||
if (typeof k === 'symbol') return undefined;
|
||||
if (k === 'then') return undefined;
|
||||
if (depth > 5) return undefined;
|
||||
return mkSpy(path + '.' + String(k), depth + 1);
|
||||
},
|
||||
apply(t, th, args) { calls.push({ call: path, args: args.map(summarize) }); return mkSpy(path + '()', depth + 1); },
|
||||
});
|
||||
}
|
||||
const out = (o) => process.stdout.write(JSON.stringify(o, null, 1) + '\n');
|
||||
let mod;
|
||||
try { mod = await import(pathToFileURL(entry).href); }
|
||||
catch (e) { out({ face, entry, verdict: 'UNAVAILABLE', reason: 'import failed: ' + (e && e.message) }); process.exit(3); }
|
||||
|
||||
if (face === 'fetch') {
|
||||
const checks = {
|
||||
'导出 apply 是函数': typeof mod.apply === 'function',
|
||||
'导出 inject 是数组': Array.isArray(mod.inject),
|
||||
'inject 含 connection(E1 路由能挂的前提)': Array.isArray(mod.inject) && mod.inject.includes('connection'),
|
||||
'导出 name 是非空字符串': typeof mod.name === 'string' && mod.name.length > 0,
|
||||
};
|
||||
const bad = Object.entries(checks).filter(([, v]) => !v).map(([k]) => k);
|
||||
out({ face, entry, exportsOnModule: Object.keys(mod), checks, verdict: bad.length ? 'RED' : 'GREEN' });
|
||||
process.exit(bad.length ? 1 : 0);
|
||||
}
|
||||
|
||||
const ctx = mkSpy('ctx', 0);
|
||||
let applyErr = null;
|
||||
try {
|
||||
const p = Promise.resolve().then(() => mod.apply(ctx));
|
||||
const to = new Promise((_, rej) => setTimeout(() => rej(new Error('timeout 8000ms')), 8000));
|
||||
await Promise.race([p, to]);
|
||||
} catch (e) { applyErr = (e && e.message) || String(e); }
|
||||
const onCalls = calls.filter((c) => c.call === 'ctx.on');
|
||||
const routeCalls = calls.filter((c) => c.call === 'ctx.connection.fetch.register');
|
||||
const checks = {
|
||||
'apply 可调用且未抛错': applyErr === null,
|
||||
'注册了回调(ctx.on 至少一次)': onCalls.length >= 1,
|
||||
};
|
||||
const bad = Object.entries(checks).filter(([, v]) => !v).map(([k]) => k);
|
||||
out({ face, entry, applyError: applyErr, callCount: calls.length, onCalls, routeCalls, checks, verdict: bad.length ? 'RED' : 'GREEN' });
|
||||
process.exit(bad.length ? 1 : 0);
|
||||
JSEOF
|
||||
|
||||
echo "── 【$FACE】靶子=$TARGET entry=$ENTRY"
|
||||
if [ "$FACE" = "取数" ]; then
|
||||
echo ' ⚠️ 证据强度:**真实 import + 真实模块求值**(产物被真的执行了)⇒ 对「包产物能不能被宿主加载器吃下」这一面成立;'
|
||||
echo ' ⛔ 本面**不含端到端 RPC 取数**(本机当前无活体实例/RPC 靶子)⇒ 端到端取数须等 S2 后在实例内补测'
|
||||
else
|
||||
echo ' ⚠️ 证据强度:**真实调用 apply(ctx)**(spy ctx 记录真实副作用;apply 抛错即红)'
|
||||
fi
|
||||
ROUT="$("$NODE" "$PTRACE" "$([ "$FACE" = "取数" ] && echo fetch || echo callback)" "$ENTRY" 2>&1)"; RC=$?
|
||||
printf '%s\n' "$ROUT" | sed 's/^/ /' | cut -c1-200
|
||||
case "$RC" in
|
||||
0) green "靶子真实产物满足本面判据(原始输出见上,退出码 rc=0)" ;;
|
||||
3) na "真实 import 未成功 ⇒ 本面未取到(⛔ 不是绿):$(printf '%s' "$ROUT" | tr '\n' ' ' | head -c 200)" ;;
|
||||
*) red "本面判据不成立(rc=$RC,原始输出见上)" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# v-ui.sh —— S0 尺子③:浏览器级判据(**客户端 UI 必须过这一关**)
|
||||
# 规格来源:交付物/功能打包到基础插件-分步实施方案-20260926.md §3.10.1(浏览器级行)· §3.10.2 S0-4 · §5 第 5 层
|
||||
# -----------------------------------------------------------------------------
|
||||
# 为什么非要有这把尺子(方案 §4-A1 / §5 三条铁律第 1 条):
|
||||
# `node --check` +「client bundle 可组合」+「boot 本包零报错」**三条全绿也证明不了客户端能加载**
|
||||
# ⇒ UI 类交付的唯一有效判据 = **真壳里出现卡片 + JS 错误 0 + 无 `Failed to load plugins`**
|
||||
# -----------------------------------------------------------------------------
|
||||
# 退出码:rc=0 绿(三条判据全过)/rc=1 红/rc=3 **未取到**(本机没有运行中的真壳;⛔ 3 不是绿)
|
||||
# 用法:bash v-ui.sh [--cdp http://127.0.0.1:9330]
|
||||
# -----------------------------------------------------------------------------
|
||||
# ⛔ 本脚本**不自动启动真壳**:真壳的开发态目录属**客户端线**(另一条 lane,归属不明 ⇒ 只报告不动手,
|
||||
# R7-边界),且启动会写它的运行态、污染本棒刚取的基线。⇒ 未取到时**只给出启动指引**。
|
||||
# =============================================================================
|
||||
set -u
|
||||
|
||||
SELF_DIR="$(cd "$(dirname "$0")" && pwd -W)"
|
||||
[ -d "$SELF_DIR" ] || SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
WS="$(cd "$SELF_DIR/../.." && pwd -W)"
|
||||
[ -d "$WS" ] || WS="$(cd "$SELF_DIR/../.." && pwd)"
|
||||
OUT="${P39_OUT:-$WS/tmp/p39}"
|
||||
NODE="${NODE:-E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3/node.exe}"
|
||||
CDP="${P39_CDP:-http://127.0.0.1:9330}"
|
||||
MARKER_FAIL="Failed to load plugins"
|
||||
|
||||
[ "${1:-}" = "--cdp" ] && CDP="$2"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
# ---- ① 先探有没有真壳在跑(不跑就不猜)--------------------------------------
|
||||
if ! curl -s --max-time 4 "$CDP/json/list" > "$OUT/.cdp-list.json" 2>/dev/null; then
|
||||
echo "⚠️ 未取到(rc=3)|判据【浏览器级】:CDP 端 $CDP 不可达 ⇒ 本机当前**没有运行中的真壳**"
|
||||
echo " ⛔ 本脚本 **不自动启动真壳**(开发态目录属客户端线,另属一条 lane;且会写其运行态、污染刚取的基线)"
|
||||
echo " 启动指引(在**客户端线**窗口内执行,⛔ 本棒不执行):"
|
||||
echo " 启动器:E:/ProgramData/AIProject/dsh-ai1net-desktop/.workbuddy/_devkit/launch-desktop-dev-017.mts"
|
||||
echo " 环境: DSH_HOME=E:/github/dsh-desktop-0.1.7rc2/apps/desktop/.desktop-build/development/home-adapt"
|
||||
echo " DSH_DESKTOP_DSH_DIR=<同上>/project-adapt"
|
||||
echo " DSH_DESKTOP_PRIMARY_RUNTIME_DIR=E:/github/dsh-desktop-0.1.7rc2/apps/desktop/.desktop-build/targets/win-x64/runtime/primary-runtime"
|
||||
echo " DSH_DESKTOP_HOST_INSPECT_PORT=9330"
|
||||
echo ' ⇒ 壳起来后重跑本脚本即可取到读数(⛔ 不许拿「三条静态全绿」顶替本面)'
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# ---- ② 真壳在 ⇒ 走 CDP:刷新一次页面,收 JS 错误,再读 DOM -------------------
|
||||
cat > "$OUT/_ui_probe.mjs" <<'JSEOF'
|
||||
const cdp = process.argv[2];
|
||||
const markerFail = process.argv[3];
|
||||
const list = await (await fetch(cdp + '/json/list')).json();
|
||||
const pages = list.filter((t) => t.type === 'page' && !String(t.url).startsWith('devtools://'));
|
||||
if (pages.length === 0) { console.log(JSON.stringify({ verdict: 'UNAVAILABLE', reason: 'no page target' })); process.exit(3); }
|
||||
const page = pages.find((t) => String(t.url).startsWith('dsh-app://')) || pages[0];
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
let id = 0;
|
||||
const pending = new Map();
|
||||
const errors = [];
|
||||
const send = (method, params = {}) => new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method, params })); });
|
||||
ws.addEventListener('message', (ev) => {
|
||||
const m = JSON.parse(ev.data);
|
||||
if (m.id && pending.has(m.id)) { const p = pending.get(m.id); pending.delete(m.id); m.error ? p.rej(new Error(m.error.message)) : p.res(m.result); return; }
|
||||
if (m.method === 'Log.entryAdded' && m.params?.entry?.level === 'error') errors.push('log: ' + String(m.params.entry.text).slice(0, 160));
|
||||
if (m.method === 'Runtime.exceptionThrown') errors.push('exception: ' + String(m.params?.exceptionDetails?.text || '').slice(0, 160));
|
||||
if (m.method === 'Runtime.consoleAPICalled' && m.params?.type === 'error') errors.push('console.error: ' + (m.params.args || []).map((a) => String(a.value ?? a.description ?? '')).join(' ').slice(0, 160));
|
||||
});
|
||||
await new Promise((res, rej) => { ws.addEventListener('open', res); ws.addEventListener('error', () => rej(new Error('ws error'))); setTimeout(() => rej(new Error('ws timeout')), 8000); });
|
||||
await send('Runtime.enable'); await send('Log.enable'); await send('Page.enable');
|
||||
await send('Page.reload', { ignoreCache: false });
|
||||
await new Promise((r) => setTimeout(r, 7000));
|
||||
const expr = `(() => { const b = document.body ? (document.body.innerText || '') : ''; const root = document.querySelector('#root'); return JSON.stringify({ url: location.href, title: document.title, rootLen: root ? root.innerHTML.length : -1, navCount: document.querySelectorAll('nav').length, cardNodes: document.querySelectorAll('[class*=card],[data-card]').length, hasFailedText: b.includes(${JSON.stringify(markerFail)}), bodyHead: b.slice(0, 300) }); })()`;
|
||||
const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true });
|
||||
let dom = null;
|
||||
try { dom = JSON.parse(r.result.value); } catch (e) { dom = { parseError: String(e) }; }
|
||||
const checks = {
|
||||
'页面是真壳渲染的(dsh-app://)': String(dom.url || '').startsWith('dsh-app://'),
|
||||
'页面画出内容(#root 非空)': (dom.rootLen || 0) > 0,
|
||||
'JS 错误 0': errors.length === 0,
|
||||
'无 "Failed to load plugins"': dom.hasFailedText === false,
|
||||
};
|
||||
const bad = Object.entries(checks).filter(([, v]) => !v).map(([k]) => k);
|
||||
console.log(JSON.stringify({ target: { url: page.url, title: page.title }, dom, jsErrors: errors, checks, verdict: bad.length ? 'RED' : 'GREEN' }, null, 1));
|
||||
process.exit(bad.length ? 1 : 0);
|
||||
JSEOF
|
||||
|
||||
TARGET_URL="$(tr ',' '\n' < "$OUT/.cdp-list.json" | sed -n 's/.*"url":"\(dsh-app:[^"]*\)".*/\1/p' | head -1)"
|
||||
echo "── 【浏览器级】CDP=$CDP 靶页=${TARGET_URL:-(未识别到 dsh-app:// 页)}"
|
||||
"$NODE" "$OUT/_ui_probe.mjs" "$CDP" "$MARKER_FAIL" 2>&1 | sed 's/^/ /' | cut -c1-200
|
||||
RC=${PIPESTATUS[0]}
|
||||
case "$RC" in
|
||||
0) echo "✅ 绿(rc=0)|判据【浏览器级】:卡片/内容出现 + JS 错误 0 + 无 Failed to load plugins"; exit 0 ;;
|
||||
3) echo "⚠️ 未取到(rc=3)|判据【浏览器级】:CDP 有端点但无可用 page 靶"; exit 3 ;;
|
||||
*) echo "❌ 红(rc=$RC)|判据【浏览器级】:三条里至少一条不成立(原始输出见上)"; exit 1 ;;
|
||||
esac
|
||||
Reference in new issue
Block a user