站点按 bloub 三栏做自定义、动画时间线和设置,支持序列导出。 形体只留常见几种并配中文名;动画页右栏拉满时间线旁空位,滚动条隐藏。 机芯补 GROK.create / <grok-bot>,以及暂停冻结。
443 lines
14 KiB
JavaScript
443 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
// Headless snapshot of replica/index.html against $_t mn() invariants.
|
|
import { spawn } from "node:child_process";
|
|
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join, dirname } from "node:path";
|
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
|
|
const CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
const PAGE = pathToFileURL(join(dirname(fileURLToPath(import.meta.url)), "index.html")).href;
|
|
const OUT = process.env.SNAP_DIR || "/tmp/grok-replica-snap";
|
|
|
|
class Cdp {
|
|
constructor(ws) {
|
|
this.ws = ws;
|
|
this.id = 0;
|
|
this.pending = new Map();
|
|
this.ws.addEventListener("message", (ev) => {
|
|
const msg = JSON.parse(ev.data);
|
|
const p = this.pending.get(msg.id);
|
|
if (!p) return;
|
|
this.pending.delete(msg.id);
|
|
msg.error ? p.reject(new Error(JSON.stringify(msg.error))) : p.resolve(msg.result);
|
|
});
|
|
}
|
|
send(method, params = {}) {
|
|
const id = ++this.id;
|
|
this.ws.send(JSON.stringify({ id, method, params }));
|
|
return new Promise((resolve, reject) => {
|
|
this.pending.set(id, { resolve, reject });
|
|
});
|
|
}
|
|
async eval(expr) {
|
|
const r = await this.send("Runtime.evaluate", {
|
|
expression: expr,
|
|
returnByValue: true,
|
|
awaitPromise: true,
|
|
});
|
|
if (r.exceptionDetails) {
|
|
throw new Error(r.exceptionDetails.text || "evaluate failed");
|
|
}
|
|
return r.result.value;
|
|
}
|
|
}
|
|
|
|
function waitPort(child) {
|
|
return new Promise((resolve, reject) => {
|
|
let buf = "";
|
|
const on = (chunk) => {
|
|
buf += chunk.toString();
|
|
const m = buf.match(/DevTools listening on (ws:\/\/[^\s]+)/);
|
|
if (m) resolve(m[1]);
|
|
};
|
|
child.stderr.on("data", on);
|
|
child.stdout.on("data", on);
|
|
child.on("exit", (code) => reject(new Error(`chrome exited ${code}: ${buf.slice(-400)}`)));
|
|
setTimeout(() => reject(new Error(`chrome debug timeout: ${buf.slice(-400)}`)), 15000);
|
|
});
|
|
}
|
|
|
|
async function sleep(ms) {
|
|
await new Promise((r) => setTimeout(r, ms));
|
|
}
|
|
|
|
async function shot(cdp, name) {
|
|
const r = await cdp.send("Page.captureScreenshot", { format: "png" });
|
|
await writeFile(join(OUT, `${name}.png`), Buffer.from(r.data, "base64"));
|
|
}
|
|
|
|
const SNAP_JS = `(() => {
|
|
const bot = window.__bots && window.__bots[0];
|
|
const svg = document.getElementById("bot");
|
|
const body = bot?.body || svg?.querySelector("g > path");
|
|
const eyes = bot?.eyeEls || svg?.querySelectorAll("g g path");
|
|
const d = body?.getAttribute("d") || "";
|
|
const eye = (el) => el ? ({
|
|
d: (el.getAttribute("d") || "").slice(0, 48),
|
|
t: el.getAttribute("transform") || "",
|
|
display: el.style.display || "",
|
|
}) : null;
|
|
return {
|
|
state: bot?.state,
|
|
mode: bot?.mode,
|
|
shape: bot?.shapeName,
|
|
eyeFrom: bot?.eyeFrom,
|
|
eyeTo: bot?.eyeTo,
|
|
morph: bot?.eyeMorph?.x,
|
|
blink: bot?.blink?.x,
|
|
overlayX: bot?.overlay?.x,
|
|
ovKind: bot?.ovKind,
|
|
spin: bot?.spin?.x,
|
|
bodyN: d.length,
|
|
bodyHead: d.slice(0, 72),
|
|
eyes: [eye(eyes?.[0]), eye(eyes?.[1])],
|
|
};
|
|
})()`;
|
|
|
|
async function snap(cdp, label) {
|
|
const data = await cdp.eval(SNAP_JS);
|
|
data.label = label;
|
|
return data;
|
|
}
|
|
|
|
async function click(cdp, sel) {
|
|
await cdp.eval(`document.querySelector(${JSON.stringify(sel)}).click()`);
|
|
}
|
|
|
|
async function main() {
|
|
await mkdir(OUT, { recursive: true });
|
|
const profile = await mkdtemp(join(tmpdir(), "grok-snap-"));
|
|
const chrome = spawn(CHROME, [
|
|
"--headless=new",
|
|
"--disable-gpu",
|
|
"--no-first-run",
|
|
"--hide-scrollbars",
|
|
"--window-size=1100,900",
|
|
`--user-data-dir=${profile}`,
|
|
"--remote-debugging-port=0",
|
|
"--allow-file-access-from-files",
|
|
PAGE,
|
|
], { stdio: ["ignore", "pipe", "pipe"] });
|
|
const browserWs = await waitPort(chrome);
|
|
const port = new URL(browserWs).port;
|
|
let pageWs = null;
|
|
for (let i = 0; i < 40; i++) {
|
|
const tabs = await fetch(`http://127.0.0.1:${port}/json/list`).then((r) => r.json());
|
|
const page = tabs.find((t) => t.type === "page" && t.webSocketDebuggerUrl);
|
|
if (page) {
|
|
pageWs = page.webSocketDebuggerUrl;
|
|
break;
|
|
}
|
|
await sleep(100);
|
|
}
|
|
if (!pageWs) throw new Error("no page target");
|
|
const ws = new WebSocket(pageWs);
|
|
await new Promise((res, rej) => {
|
|
ws.addEventListener("open", res);
|
|
ws.addEventListener("error", rej);
|
|
});
|
|
const cdp = new Cdp(ws);
|
|
await cdp.send("Page.enable");
|
|
await cdp.send("Runtime.enable");
|
|
await cdp.send("Page.bringToFront");
|
|
for (let i = 0; i < 50; i++) {
|
|
const ready = await cdp.eval("!!(window.__bots && window.__bots[0] && window.__bots[0].body)");
|
|
if (ready) break;
|
|
await sleep(100);
|
|
}
|
|
const report = [];
|
|
report.push(await cdp.eval(`(() => {
|
|
const [stage, login] = window.__bots || [];
|
|
return {
|
|
label: "boot-modes",
|
|
stage: { state: stage?.state, mode: stage?.mode, moodN: stage?.moodN },
|
|
login: { state: login?.state, mode: login?.mode, moodN: login?.moodN },
|
|
};
|
|
})()`));
|
|
await click(cdp, "#mode-hold");
|
|
await click(cdp, "#opt-follow"); // default on → off for stable transforms
|
|
await sleep(80);
|
|
const states = [
|
|
"playful", "laughing", "sleeping", "waking", "idle", "celebrate",
|
|
"thinking", "writing", "orbit", "humming", "loading", "notifying",
|
|
];
|
|
for (const name of states) {
|
|
await click(cdp, `button[data-state="${name}"]`);
|
|
await sleep(name === "sleeping" ? 400 : 700);
|
|
const s = await snap(cdp, name);
|
|
report.push(s);
|
|
await shot(cdp, name);
|
|
}
|
|
|
|
// sleeping timeline: start from curious (eye 3) then sleep
|
|
await click(cdp, 'button[data-state="curious"]');
|
|
await sleep(200);
|
|
await click(cdp, 'button[data-state="sleeping"]');
|
|
for (const ms of [300, 1300, 2200]) {
|
|
await sleep(ms === 300 ? 300 : ms - (ms === 1300 ? 300 : 1300));
|
|
report.push(await snap(cdp, `sleeping@${ms}ms`));
|
|
}
|
|
await shot(cdp, "sleeping-late");
|
|
|
|
// turnAt: bean / tablet / cloud + one spin, sample eyes
|
|
for (const shape of ["bean", "tablet", "cloud"]) {
|
|
await click(cdp, 'button[data-state="idle"]');
|
|
await click(cdp, `button[data-shape="${shape}"]`);
|
|
await sleep(80);
|
|
await click(cdp, "#act-spin");
|
|
const samples = [];
|
|
for (let i = 0; i < 12; i++) {
|
|
await sleep(90);
|
|
samples.push(await snap(cdp, `${shape}-spin-${i}`));
|
|
}
|
|
report.push({
|
|
label: `${shape}-spin`,
|
|
shape,
|
|
hidden: samples.filter((s) => s.eyes?.some((e) => e.display === "none")).length,
|
|
shown: samples.filter((s) => s.eyes?.every((e) => e.display !== "none")).length,
|
|
transforms: [...new Set(samples.map((s) => s.eyes?.[0]?.t).filter(Boolean))].length,
|
|
ovKind: samples[0]?.ovKind,
|
|
samples: samples.map((s) => ({
|
|
t: s.eyes?.[0]?.t?.slice(0, 40),
|
|
d0: s.eyes?.[1]?.display,
|
|
d1: s.eyes?.[0]?.display,
|
|
bodyHead: s.bodyHead,
|
|
})),
|
|
});
|
|
await shot(cdp, `${shape}-spin`);
|
|
}
|
|
|
|
// Gr() Lt/Mt: locals default to 1 each frame (not prev)
|
|
await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
b.setMode("hold");
|
|
b.setState("laughing", { resetEyes: false });
|
|
b.poseOut.lid = 0.7;
|
|
b.poseOut.eyeBoost = 1.12;
|
|
return true;
|
|
})()`);
|
|
await sleep(80);
|
|
await click(cdp, 'button[data-state="loading"]');
|
|
await sleep(80);
|
|
report.push(await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
return {
|
|
label: "loading-after-laughing",
|
|
lid: b.poseOut.lid,
|
|
eyeBoost: b.poseOut.eyeBoost,
|
|
trick: b.trick?.kind || null,
|
|
spinTurn: !!b.spinTurn,
|
|
};
|
|
})()`));
|
|
|
|
await click(cdp, 'button[data-state="excited"]');
|
|
await sleep(80);
|
|
await click(cdp, 'button[data-state="waking"]');
|
|
await sleep(1600);
|
|
report.push(await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
return {
|
|
label: "[email protected]",
|
|
dt: (performance.now() - b.stateAt) / 1000,
|
|
lid: b.poseOut.lid,
|
|
eyeBoost: b.poseOut.eyeBoost,
|
|
ty: b.poseOut.ty,
|
|
squash: b.poseOut.squash,
|
|
};
|
|
})()`));
|
|
|
|
await click(cdp, 'button[data-state="dragging"]');
|
|
await sleep(50);
|
|
const drag = await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
const now = performance.now();
|
|
const dt = (now - b.stateAt) / 1000;
|
|
const a = window.GROK_POSE.applyPose("dragging", 0, 0.05, now, b.ctx, { prev: { spin: 0, tx: 0, ty: 0, squash: 1, lid: 1, eyeBoost: 1.06 } });
|
|
const mid = window.GROK_POSE.applyPose("dragging", 0, 1.2, now, b.ctx, { prev: { spin: 0, tx: 0, ty: 0, squash: 1, lid: 1, eyeBoost: 1 } });
|
|
const late = window.GROK_POSE.applyPose("dragging", 0, 2.5, now, b.ctx, { prev: { spin: 0, tx: 0, ty: 0, squash: 1, lid: 1, eyeBoost: 1.06 } });
|
|
return { label: "dragging-Lt", dt, a: a.eyeBoost, mid: mid.eyeBoost, late: late.eyeBoost };
|
|
})()`);
|
|
report.push(drag);
|
|
|
|
// hold 切态 must not tear rn/ys
|
|
await click(cdp, 'button[data-state="idle"]');
|
|
await sleep(60);
|
|
await click(cdp, "#act-spin");
|
|
await sleep(40);
|
|
const beforeHold = await cdp.eval(`({ spinTurn: !!window.__bots[0].spinTurn, trick: window.__bots[0].trick?.kind || null })`);
|
|
await click(cdp, 'button[data-state="thinking"]');
|
|
await sleep(40);
|
|
report.push(await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
return {
|
|
label: "hold-keeps-pn",
|
|
before: ${JSON.stringify(beforeHold)},
|
|
state: b.state,
|
|
spinTurn: !!b.spinTurn,
|
|
trick: b.trick?.kind || null,
|
|
};
|
|
})()`));
|
|
|
|
// notifying badge = rest ring 7/8
|
|
await click(cdp, 'button[data-state="notifying"]');
|
|
await sleep(400);
|
|
report.push(await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
const R = b.geo.Re;
|
|
const ring = window.GROK_FX.shapeRing(b.geo.shapes[b.shapeName].path, R);
|
|
const Yl = ring[Math.round(ring.length * 7 / 8) % ring.length];
|
|
const badge = b.badge;
|
|
const vis = badge && badge.style.display !== "none";
|
|
const cx = vis ? +badge.getAttribute("cx") : null;
|
|
const cy = vis ? +badge.getAttribute("cy") : null;
|
|
return {
|
|
label: "notifying-Yl",
|
|
vis,
|
|
cx, cy,
|
|
Yl,
|
|
dx: vis ? Math.abs(cx - Yl[0]) : null,
|
|
dy: vis ? Math.abs(cy - Yl[1]) : null,
|
|
notifyX: b.notify.x,
|
|
};
|
|
})()`));
|
|
|
|
// excited pn must survive 1200ms onboard cut to idle
|
|
await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
b.setMode("hold");
|
|
b.moodN = 7;
|
|
b.setState("excited", { resetEyes: false });
|
|
b._pn(1);
|
|
b.mode = "onboarding";
|
|
b.stateAt = performance.now() - 1190;
|
|
return true;
|
|
})()`);
|
|
await sleep(80);
|
|
report.push(await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
return {
|
|
label: "onboard-excited-to-idle",
|
|
state: b.state,
|
|
moodN: b.moodN,
|
|
spinTurn: !!b.spinTurn,
|
|
trick: b.trick?.kind || null,
|
|
};
|
|
})()`));
|
|
|
|
await click(cdp, 'button[data-state="idle"]');
|
|
await sleep(40);
|
|
await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
b.trick = null;
|
|
b.spinOnce(1);
|
|
return true;
|
|
})()`);
|
|
await sleep(50);
|
|
report.push(await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
const rn = b.spinTurn;
|
|
const turn = b.extras.turn;
|
|
return {
|
|
label: "pn-post-spring",
|
|
hasRn: !!rn,
|
|
turn,
|
|
rnX: rn ? rn.x : null,
|
|
match: rn ? Math.abs(turn - rn.x) < 1e-9 : turn == null,
|
|
};
|
|
})()`));
|
|
|
|
await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
b.setFollowPointer(true);
|
|
b.pointerRaw = { x: 0, y: 0 };
|
|
b.pointer.tx = 12;
|
|
b.pointer.ty = 8;
|
|
b.pointer.x = 12;
|
|
b.pointer.y = 8;
|
|
return true;
|
|
})()`);
|
|
await sleep(30);
|
|
await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
b.pointerRaw = null;
|
|
return true;
|
|
})()`);
|
|
await sleep(80);
|
|
report.push(await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
return {
|
|
label: "pointer-leave-decay",
|
|
tracking: !!(b.gazeTarget || (b.followPointer && b.pointerRaw)),
|
|
x: b.pointer.x,
|
|
tx: b.pointer.tx,
|
|
decaying: b.pointer.tx === 0 && b.pointer.x > 0.05 && b.pointer.x < 12,
|
|
};
|
|
})()`));
|
|
|
|
await click(cdp, 'button[data-state="writing"]');
|
|
await sleep(400);
|
|
await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
b.fx.ink = [[10, 10], [20, 20]];
|
|
b.fx.recvTick = 3;
|
|
b.fx.recvDir = 1.2;
|
|
b.setState("writing", { resetEyes: false });
|
|
return true;
|
|
})()`);
|
|
report.push(await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
return {
|
|
label: "setState-resets-ink-recv",
|
|
ink: b.fx.ink.length,
|
|
recvTick: b.fx.recvTick,
|
|
recvDir: b.fx.recvDir,
|
|
};
|
|
})()`));
|
|
|
|
await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
b.setMode("hold");
|
|
b.trick = null;
|
|
b.spinTurn = null;
|
|
b.hopAt = -1;
|
|
b.particles.clear();
|
|
b.overlay.x = 0; b.overlay.v = 0; b.overlay.t = 0;
|
|
b.overlayTurn.x = 0; b.overlayTurn.v = 0; b.overlayTurn.t = 0;
|
|
b.overlayMix.x = 1; b.overlayMix.v = 0; b.overlayMix.t = 1;
|
|
b.shapeSpring.x = 1; b.shapeSpring.v = 0; b.shapeSpring.t = 1;
|
|
b.eyeMorph.x = 1; b.eyeMorph.v = 0; b.eyeMorph.t = 1;
|
|
b.ovOn = false;
|
|
b.ovKind = null;
|
|
b.ovTarget = null;
|
|
b.ovPrev = null;
|
|
b.setState("sleeping", { resetEyes: false });
|
|
b.setPaused(true);
|
|
return true;
|
|
})()`);
|
|
await sleep(200);
|
|
report.push(await cdp.eval(`(() => {
|
|
const b = window.__bots[0];
|
|
return {
|
|
label: "pause-settle-snap",
|
|
raf: b._raf,
|
|
eyeFrom: b.eyeFrom,
|
|
eyeTo: b.eyeTo,
|
|
poseOut: b.poseOut,
|
|
fromPolys: b._fromPolys,
|
|
};
|
|
})()`));
|
|
await cdp.eval(`window.__bots[0].setPaused(false)`);
|
|
|
|
await writeFile(join(OUT, "report.json"), JSON.stringify(report, null, 2));
|
|
console.log(JSON.stringify(report, null, 2));
|
|
|
|
chrome.kill("SIGTERM");
|
|
ws.close();
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|