- TOOL-SANDBOX/perception-kit: 双轨感知工具(协议TCS+GIR+perceive.mjs+验收回执) - ZCODE-DEV-20260825: round1-10 TCS事件正本/编译GIR/人类投影/脑周期状态/待读路线图 - 范围严格限定本session新增路径;既有脏变未触碰;未配置remote,未push
120 lines
6.5 KiB
JavaScript
Executable file
120 lines
6.5 KiB
JavaScript
Executable file
#!/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));
|