mirror of
https://github.com/towishy/Owen-Graphite.git
synced 2026-07-22 04:40:30 +00:00
138 lines
8.2 KiB
JavaScript
138 lines
8.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import { writeFile } from "node:fs/promises";
|
|
|
|
const args = new Map();
|
|
for (let index = 2; index < process.argv.length; index += 1) {
|
|
const key = process.argv[index];
|
|
if (!key.startsWith("--")) continue;
|
|
const next = process.argv[index + 1] || "";
|
|
if (next && !next.startsWith("--")) {
|
|
args.set(key.slice(2), next);
|
|
index += 1;
|
|
} else {
|
|
args.set(key.slice(2), "true");
|
|
}
|
|
}
|
|
|
|
const endpoint = args.get("endpoint") || "http://127.0.0.1:9222/json";
|
|
const selector = args.get("selector") || "body";
|
|
const scenario = args.get("scenario") || "resting";
|
|
const pseudo = (args.get("pseudo") || "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
const outPath = args.get("out") || "";
|
|
const expressionArg = args.get("expr") || "";
|
|
const mouseAction = args.get("mouse") || "";
|
|
const actionSelector = args.get("action-selector") || selector;
|
|
const statusOnly = args.get("status") === "true";
|
|
|
|
const targets = await fetch(endpoint).then((response) => response.json());
|
|
const target = targets.find((item) => item.type === "page" && item.url.startsWith("app://obsidian.md/"));
|
|
if (!target) throw new Error("No Obsidian CDP page target found. Start Obsidian with --remote-debugging-port=9222.");
|
|
|
|
const socket = new WebSocket(target.webSocketDebuggerUrl);
|
|
let nextId = 1;
|
|
const pending = new Map();
|
|
|
|
socket.addEventListener("message", (event) => {
|
|
const message = JSON.parse(event.data);
|
|
const waiter = pending.get(message.id);
|
|
if (!waiter) return;
|
|
pending.delete(message.id);
|
|
if (message.error) waiter.reject(new Error(JSON.stringify(message.error)));
|
|
else waiter.resolve(message.result);
|
|
});
|
|
|
|
await new Promise((resolve, reject) => {
|
|
socket.addEventListener("open", resolve, { once: true });
|
|
socket.addEventListener("error", reject, { once: true });
|
|
});
|
|
|
|
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
|
const id = nextId;
|
|
nextId += 1;
|
|
pending.set(id, { resolve, reject });
|
|
socket.send(JSON.stringify({ id, method, params }));
|
|
});
|
|
|
|
const evaluate = async (expression) => {
|
|
const result = await send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true });
|
|
if (result.exceptionDetails) throw new Error(result.exceptionDetails.text || "Runtime.evaluate failed");
|
|
return result.result.value;
|
|
};
|
|
|
|
await send("Runtime.enable");
|
|
await send("DOM.enable");
|
|
await send("CSS.enable");
|
|
|
|
if (statusOnly) {
|
|
const output = await evaluate(`(() => {
|
|
const appRef = window.app || globalThis.app;
|
|
const titleVersion = (document.title.match(/Obsidian\\s+([0-9.]+)/) || [])[1] || "";
|
|
return {
|
|
schema: "owen-graphite/cdp-status/1",
|
|
ok: true,
|
|
capturedAt: new Date().toISOString(),
|
|
url: location.href,
|
|
title: document.title,
|
|
obsidianVersion: appRef && appRef.getVersion ? appRef.getVersion() : titleVersion,
|
|
vaultName: appRef && appRef.vault ? appRef.vault.getName() : "",
|
|
bodyClasses: String(document.body.className || "")
|
|
};
|
|
})()`);
|
|
if (outPath) await writeFile(outPath, JSON.stringify(output, null, 2) + "\n", "utf8");
|
|
else console.log(JSON.stringify(output, null, 2));
|
|
socket.close();
|
|
process.exit(0);
|
|
}
|
|
|
|
if (expressionArg) {
|
|
const output = await evaluate(expressionArg);
|
|
if (outPath) await writeFile(outPath, JSON.stringify(output, null, 2) + "\n", "utf8");
|
|
else console.log(JSON.stringify(output, null, 2));
|
|
socket.close();
|
|
process.exit(0);
|
|
}
|
|
|
|
if (pseudo.length) {
|
|
const documentResult = await send("DOM.getDocument", { depth: -1, pierce: true });
|
|
const queryResult = await send("DOM.querySelector", { nodeId: documentResult.root.nodeId, selector });
|
|
if (queryResult.nodeId) await send("CSS.forcePseudoState", { nodeId: queryResult.nodeId, forcedPseudoClasses: pseudo });
|
|
}
|
|
|
|
if (mouseAction) {
|
|
const actionRect = await evaluate(`(() => { const target = document.querySelector(${JSON.stringify(actionSelector)}); if (!target) return null; const rect = target.getBoundingClientRect(); return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }; })()`);
|
|
if (!actionRect) throw new Error(`No action target found for ${actionSelector}`);
|
|
const button = mouseAction === "context" ? "right" : "left";
|
|
if (mouseAction === "hover") {
|
|
await send("Input.dispatchMouseEvent", { type: "mouseMoved", x: actionRect.x, y: actionRect.y });
|
|
} else if (["click", "context"].includes(mouseAction)) {
|
|
await send("Input.dispatchMouseEvent", { type: "mousePressed", button, x: actionRect.x, y: actionRect.y, clickCount: 1 });
|
|
await send("Input.dispatchMouseEvent", { type: "mouseReleased", button, x: actionRect.x, y: actionRect.y, clickCount: 1 });
|
|
} else {
|
|
throw new Error(`Unsupported mouse action: ${mouseAction}`);
|
|
}
|
|
}
|
|
|
|
const captureExpression = `(() => {
|
|
const selector = ${JSON.stringify(selector)};
|
|
const scenario = ${JSON.stringify(scenario)};
|
|
const target = document.querySelector(selector);
|
|
if (!target) return { scenario, selector, error: "NO_TARGET_FOUND", bodyClasses: String(document.body.className || "") };
|
|
const computedProps = ["display", "position", "width", "height", "minHeight", "maxHeight", "margin", "padding", "lineHeight", "overflow", "transform", "background", "backgroundColor", "border", "borderRadius", "boxShadow", "outline", "outlineOffset", "opacity", "zIndex", "color", "fontSize"];
|
|
const labelFor = (element) => { const id = element.id ? '#' + element.id : ''; const className = String(element.className || '').trim().replace(/\\s+/g, '.'); return element.tagName.toLowerCase() + id + (className ? '.' + className : ''); };
|
|
const previewFor = (element) => String(element.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 80);
|
|
const readComputed = (element) => { const computed = getComputedStyle(element); return Object.fromEntries(computedProps.map((prop) => [prop, computed[prop]])); };
|
|
const readRect = (element) => { const rect = element.getBoundingClientRect(); return { x: Number(rect.x.toFixed(2)), y: Number(rect.y.toFixed(2)), width: Number(rect.width.toFixed(2)), height: Number(rect.height.toFixed(2)) }; };
|
|
const shouldKeepRule = (rule) => /hover|focus|active|selected|is-active|background|box-shadow|outline|border|transform|height|padding|display|position|z-index|opacity|color|font-size/i.test(rule.cssText);
|
|
const walkRules = (rules, element, out) => { for (const rule of Array.from(rules)) { if (rule.cssRules) { walkRules(rule.cssRules, element, out); continue; } if (!rule.selectorText || !shouldKeepRule(rule)) continue; try { if (element.matches(rule.selectorText)) out.push({ selector: rule.selectorText, css: rule.style.cssText, stylesheet: rule.parentStyleSheet && rule.parentStyleSheet.href }); } catch {} } };
|
|
const matchedRulesFor = (element) => { const matched = []; for (const sheet of Array.from(document.styleSheets)) { try { walkRules(sheet.cssRules, element, matched); } catch {} } return matched.slice(-120); };
|
|
const chain = [];
|
|
for (let element = target; element && chain.length < 10; element = element.parentElement) chain.push(element);
|
|
const appRef = window.app || globalThis.app;
|
|
const titleVersion = (document.title.match(/Obsidian\\s+([0-9.]+)/) || [])[1] || "";
|
|
return { schema: "owen-graphite/runtime-capture-fragment/1", scenario, selector, capturedAt: new Date().toISOString(), url: location.href, title: document.title, obsidianVersion: appRef && appRef.getVersion ? appRef.getVersion() : titleVersion, vaultName: appRef && appRef.vault ? appRef.vault.getName() : "", bodyClasses: String(document.body.className || ""), target: labelFor(target), activeElement: document.activeElement ? labelFor(document.activeElement) : "", domChain: chain.map((element) => ({ node: labelFor(element), inlineStyle: element.getAttribute("style") || "", textPreview: previewFor(element) })), rectChain: chain.map((element) => ({ node: labelFor(element), rect: readRect(element) })), computedGeometry: Object.fromEntries(chain.map((element) => [labelFor(element), readComputed(element)])), matchedRules: chain.map((element) => ({ node: labelFor(element), rules: matchedRulesFor(element) })), inlineStyle: target.getAttribute("style") || "" };
|
|
})()`;
|
|
|
|
const output = await evaluate(captureExpression);
|
|
if (outPath) await writeFile(outPath, JSON.stringify(output, null, 2) + "\n", "utf8");
|
|
else console.log(JSON.stringify(output, null, 2));
|
|
socket.close();
|