- 变更规模:新增 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/ 知识文件,按口径入库)
220 lines
11 KiB
Bash
220 lines
11 KiB
Bash
#!/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
|