120 lines
6.5 KiB
JavaScript
120 lines
6.5 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
// perception-kit v1 — 铸渊自有双轨感知工具(神笔马良,宿主无关)
|
|||
|
|
// 协议正本:TOOL-SANDBOX/perception-kit/TCS-PROTOCOL-PERCEPTION-TRANSLATION-V1-0001.tcs
|
|||
|
|
// 用法:
|
|||
|
|
// node perceive.mjs --app "HoloLake" # 对运行中的App窗口做双轨感知
|
|||
|
|
// node perceive.mjs --url http://127.0.0.1:5210 # 对dev server做DOM轨感知
|
|||
|
|
import { execFileSync } from "node:child_process";
|
|||
|
|
import { createHash } from "node:crypto";
|
|||
|
|
import fs from "node:fs";
|
|||
|
|
import path from "node:path";
|
|||
|
|
|
|||
|
|
const KIT_DIR = decodeURIComponent(new URL(".", import.meta.url).pathname);
|
|||
|
|
const OUT_DIR = path.join(KIT_DIR, "perceptions");
|
|||
|
|
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|||
|
|
|
|||
|
|
const shasum = (v) => createHash("sha256").update(v).digest("hex");
|
|||
|
|
const run = (cmd, args, opts = {}) => {
|
|||
|
|
try { return execFileSync(cmd, args, { encoding: "utf8", timeout: 15000, ...opts }); }
|
|||
|
|
catch (e) { return { __error: String(e.message).slice(0, 200) }; }
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const arg = (name) => {
|
|||
|
|
const i = process.argv.indexOf(name);
|
|||
|
|
return i >= 0 ? process.argv[i + 1] : null;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const now = () => new Date().toISOString();
|
|||
|
|
const pid = now().replace(/[-:TZ.]/g, "").slice(0, 14) + "-" + Math.random().toString(36).slice(2, 6);
|
|||
|
|
const receipt = {
|
|||
|
|
schema: "guanghu.perception-receipt/v1",
|
|||
|
|
perception_id: `ZY-PERC-${pid}`,
|
|||
|
|
occurred_at: now(),
|
|||
|
|
operator: "ICE-P-ZY001(铸渊)perception-kit v1",
|
|||
|
|
target: arg("--app") ? `app:${arg("--app")}` : (arg("--url") ? `url:${arg("--url")}` : "unknown"),
|
|||
|
|
semantic_track: {}, translation_track: {}, detector_findings: [], conclusion: "NOT_SEEN_NO_TARGET",
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ── 语义轨:进程状态 ─────────────────────────────
|
|||
|
|
const procList = run("/bin/ps", ["-axo", "pid,comm,etime"]);
|
|||
|
|
if (typeof procList === "string") {
|
|||
|
|
const name = (arg("--app") || "HoloLake").toLowerCase();
|
|||
|
|
const procs = procList.split("\n").filter((l) => l.toLowerCase().includes(name));
|
|||
|
|
if (process.env.PERCEIVE_DEBUG) { console.error(`[debug] ps bytes=${procList.length} name=${name} matches=${procs.length}`); console.error(`[debug] head=${JSON.stringify(procList.slice(0,80))} tail=${JSON.stringify(procList.slice(-80))}`); }
|
|||
|
|
receipt.semantic_track.process_state = {
|
|||
|
|
found: procs.length,
|
|||
|
|
sample: procs.slice(0, 5),
|
|||
|
|
};
|
|||
|
|
} else receipt.semantic_track.process_state = procList;
|
|||
|
|
|
|||
|
|
// ── 翻译轨:窗口截图 ─────────────────────────────
|
|||
|
|
if (arg("--app")) {
|
|||
|
|
// 用 CGWindowList 经 python 找窗口 ID(不弹权限窗:仅查询)
|
|||
|
|
const winInfo = run("/usr/bin/python3", ["-c", `
|
|||
|
|
import Quartz, json
|
|||
|
|
wins = []
|
|||
|
|
for w in Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListOptionOnScreenOnly | Quartz.kCGWindowListExcludeDesktopElements, Quartz.kCGNullWindowID):
|
|||
|
|
name = w.get("kCGWindowName", "") or w.get("kCGWindowOwnerName", "")
|
|||
|
|
if "${arg("--app")}".lower() in str(name).lower() or "${arg("--app")}".lower() in str(w.get("kCGWindowOwnerName", "")).lower():
|
|||
|
|
wins.append({"id": w.get("kCGWindowNumber"), "name": name, "owner": w.get("kCGWindowOwnerName"), "bounds": list(w.get("kCGWindowBounds", {}).values())})
|
|||
|
|
print(json.dumps(wins[:5]))
|
|||
|
|
`]);
|
|||
|
|
let win;
|
|||
|
|
try { win = JSON.parse(winInfo); } catch { win = []; }
|
|||
|
|
if (Array.isArray(win) && win.length) {
|
|||
|
|
const shot = path.join(OUT_DIR, `${receipt.perception_id}.png`);
|
|||
|
|
const cap = run("/usr/sbin/screencapture", ["-x", "-l", String(win[0].id), shot]);
|
|||
|
|
if (!cap || cap.__error === undefined || fs.existsSync(shot)) {
|
|||
|
|
const buf = fs.readFileSync(shot);
|
|||
|
|
receipt.translation_track = {
|
|||
|
|
capture_method: `screencapture -l ${win[0].id}`,
|
|||
|
|
window: win[0],
|
|||
|
|
screenshot_path: shot,
|
|||
|
|
screenshot_sha256: shasum(buf),
|
|||
|
|
screenshot_bytes: buf.length,
|
|||
|
|
note: buf.length < 20000 && buf.length > 0 ? "截图异常小,可能无屏幕录制权限(黑屏帧)" : "ok",
|
|||
|
|
};
|
|||
|
|
} else receipt.translation_track = { error: cap.__error };
|
|||
|
|
} else receipt.translation_track = { error: "window_not_found", queried: winInfo };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 语义轨:DOM(url模式)+ 重影检测器 ─────────────
|
|||
|
|
if (arg("--url")) {
|
|||
|
|
const html = run("/usr/bin/curl", ["-s", "--max-time", "10", arg("--url")]);
|
|||
|
|
if (typeof html === "string" && html.length) {
|
|||
|
|
receipt.semantic_track.dom_snapshot_digest = {
|
|||
|
|
url: arg("--url"),
|
|||
|
|
bytes: html.length,
|
|||
|
|
sha256: shasum(html),
|
|||
|
|
title: (html.match(/<title[^>]*>([^<]*)<\/title>/i) || [, ""])[1],
|
|||
|
|
element_counts: {
|
|||
|
|
div: (html.match(/<div/g) || []).length,
|
|||
|
|
button: (html.match(/<button/g) || []).length,
|
|||
|
|
section: (html.match(/<section/g) || []).length,
|
|||
|
|
canvas: (html.match(/<canvas/g) || []).length,
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
// 重影检测器:两个UI表面容器同时可见
|
|||
|
|
const surfaceMarkers = ["official-world", "domain-home", "channel-world", "world-pool", "starlake", "traditional-surface", "qoder-home"];
|
|||
|
|
const present = surfaceMarkers.filter((m) => html.includes(m));
|
|||
|
|
if (present.length >= 2) {
|
|||
|
|
receipt.detector_findings.push({
|
|||
|
|
detector: "overlap_detector",
|
|||
|
|
verdict: "SURFACES_COEXIST",
|
|||
|
|
detail: `DOM中同时出现多个UI表面标记:${present.join(", ")}——疑似新旧UI叠加(重影),需与截图互证`,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
fs.writeFileSync(path.join(OUT_DIR, `${receipt.perception_id}.dom.html`), html);
|
|||
|
|
} else receipt.semantic_track.dom_snapshot_digest = { error: String(html.__error || "empty") };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 结论综合 ─────────────────────────────────
|
|||
|
|
const semanticOk = receipt.semantic_track.process_state?.found > 0 || receipt.semantic_track.dom_snapshot_digest?.sha256;
|
|||
|
|
const shotOk = receipt.translation_track.screenshot_sha256 && receipt.translation_track.note === "ok";
|
|||
|
|
const overlap = receipt.detector_findings.some((d) => d.verdict === "SURFACES_COEXIST");
|
|||
|
|
receipt.conclusion = semanticOk && shotOk ? (overlap ? "SEEN_AND_UNEXPECTED" : "SEEN_AND_EXPECTED")
|
|||
|
|
: (semanticOk || shotOk) ? "PARTIAL_SEEN_ONE_TRACK" : (receipt.translation_track.error ? "NOT_SEEN_CAPTURE_FAILED" : "NOT_SEEN_NO_TARGET");
|
|||
|
|
|
|||
|
|
const outFile = path.join(OUT_DIR, `${receipt.perception_id}.json`);
|
|||
|
|
fs.writeFileSync(outFile, JSON.stringify(receipt, null, 2));
|
|||
|
|
console.log(JSON.stringify({ perception_id: receipt.perception_id, conclusion: receipt.conclusion, semantic_ok: !!semanticOk, translation_ok: !!shotOk, findings: receipt.detector_findings.length, out: outFile }, null, 2));
|