zhuyuan-ice-gl-zy001/TOOL-SANDBOX/perception-kit/perceive.mjs
冰朔 d38861fb9f perception-kit v1 + 2026-08-25 各轮TCS正本/回执/状态(夜间工作)
- TOOL-SANDBOX/perception-kit: 双轨感知工具(协议TCS+GIR+perceive.mjs+验收回执)
- ZCODE-DEV-20260825: round1-10 TCS事件正本/编译GIR/人类投影/脑周期状态/待读路线图
- 范围严格限定本session新增路径;既有脏变未触碰;未配置remote,未push
2026-08-25 23:44:01 +08:00

120 lines
6.5 KiB
JavaScript
Executable file
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 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));