Files
dsh_ai1net_server/交付物/验证脚本/v-ui.sh
T

91 lines
6.8 KiB
Bash
Raw Normal View History

#!/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