Files
dsh_ai1net_server/交付物/验证脚本/v-ui.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

92 lines
6.8 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-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