Files
dsh_ai1net_server/交付物/验证脚本/v-plugin.sh
T
admin c1b5e4d966 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/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

220 lines
11 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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