diff --git a/Dockerfile b/Dockerfile index 78326b9..5b2bece 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,7 +33,6 @@ RUN ./node_modules/.bin/playwright install --with-deps --only-shell chromium \ && rm -rf /var/lib/apt/lists/* /tmp/* COPY --from=build /app/dist ./dist COPY --from=build /app/dist-client ./dist-client -COPY scripts ./scripts EXPOSE 8080 CMD ["node", "dist/server.js"] diff --git a/scripts/cdp-eval.py b/scripts/cdp-eval.py deleted file mode 100755 index a09daec..0000000 --- a/scripts/cdp-eval.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python3 -import base64 -import hashlib -import json -import os -import socket -import struct -import sys -import urllib.parse -import urllib.request - - -CDP_ENDPOINT = os.environ.get("CDP_ENDPOINT", "http://127.0.0.1:9222") - - -def get_page_ws_url(): - with urllib.request.urlopen(f"{CDP_ENDPOINT}/json/list", timeout=30) as response: - targets = json.loads(response.read().decode("utf-8")) - pages = [target for target in targets if target.get("type") == "page"] - if not pages: - raise SystemExit("No CDP page target found.") - selected = next((page for page in pages if "chatgpt.com/share/" in page.get("url", "")), pages[0]) - return selected["webSocketDebuggerUrl"], selected - - -def websocket_connect(ws_url): - parsed = urllib.parse.urlparse(ws_url) - if parsed.scheme != "ws": - raise SystemExit(f"Only ws:// endpoints are supported, got {ws_url}") - host = parsed.hostname or "127.0.0.1" - port = parsed.port or 80 - path = parsed.path - if parsed.query: - path += "?" + parsed.query - - sock = socket.create_connection((host, port), timeout=30) - key = base64.b64encode(os.urandom(16)).decode("ascii") - request = ( - f"GET {path} HTTP/1.1\r\n" - f"Host: {host}:{port}\r\n" - "Upgrade: websocket\r\n" - "Connection: Upgrade\r\n" - f"Sec-WebSocket-Key: {key}\r\n" - "Sec-WebSocket-Version: 13\r\n" - "\r\n" - ).encode("ascii") - sock.sendall(request) - response = b"" - while b"\r\n\r\n" not in response: - response += sock.recv(4096) - header = response.split(b"\r\n\r\n", 1)[0].decode("iso-8859-1") - if " 101 " not in header.split("\r\n", 1)[0]: - raise SystemExit(f"WebSocket upgrade failed:\n{header}") - accept = base64.b64encode(hashlib.sha1((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode("ascii")).digest()).decode("ascii") - if f"Sec-WebSocket-Accept: {accept}".lower() not in header.lower(): - raise SystemExit("WebSocket accept header did not match.") - return sock - - -def send_frame(sock, payload): - data = payload.encode("utf-8") - header = bytearray([0x81]) - if len(data) < 126: - header.append(0x80 | len(data)) - elif len(data) < 65536: - header.append(0x80 | 126) - header.extend(struct.pack("!H", len(data))) - else: - header.append(0x80 | 127) - header.extend(struct.pack("!Q", len(data))) - mask = os.urandom(4) - header.extend(mask) - masked = bytes(byte ^ mask[index % 4] for index, byte in enumerate(data)) - sock.sendall(bytes(header) + masked) - - -def recv_exact(sock, size): - chunks = [] - remaining = size - while remaining: - chunk = sock.recv(remaining) - if not chunk: - raise EOFError("WebSocket closed") - chunks.append(chunk) - remaining -= len(chunk) - return b"".join(chunks) - - -def recv_frame(sock): - first, second = recv_exact(sock, 2) - opcode = first & 0x0F - masked = second & 0x80 - length = second & 0x7F - if length == 126: - length = struct.unpack("!H", recv_exact(sock, 2))[0] - elif length == 127: - length = struct.unpack("!Q", recv_exact(sock, 8))[0] - mask = recv_exact(sock, 4) if masked else b"" - payload = recv_exact(sock, length) if length else b"" - if masked: - payload = bytes(byte ^ mask[index % 4] for index, byte in enumerate(payload)) - if opcode == 0x8: - raise EOFError("WebSocket closed") - if opcode == 0x9: - return None - if opcode != 0x1: - return None - return payload.decode("utf-8") - - -def call(sock, method, params=None, message_id=1): - send_frame(sock, json.dumps({"id": message_id, "method": method, "params": params or {}})) - while True: - message = recv_frame(sock) - if not message: - continue - parsed = json.loads(message) - if parsed.get("id") == message_id: - return parsed - - -def main(): - expression = sys.stdin.read().strip() - if not expression: - expression = "({ title: document.title, url: location.href, text: document.body.innerText.slice(0, 1000) })" - - ws_url, target = get_page_ws_url() - sock = websocket_connect(ws_url) - try: - result = call( - sock, - "Runtime.evaluate", - { - "expression": expression, - "returnByValue": True, - "awaitPromise": True, - "timeout": 30000, - }, - ) - finally: - sock.close() - - print(json.dumps({"target": target, "result": result}, ensure_ascii=False, indent=2)) - - -if __name__ == "__main__": - main() diff --git a/scripts/debug-chatgpt-share.mjs b/scripts/debug-chatgpt-share.mjs deleted file mode 100644 index 680c71b..0000000 --- a/scripts/debug-chatgpt-share.mjs +++ /dev/null @@ -1,170 +0,0 @@ -import { chromium } from "playwright"; -import { spawn } from "node:child_process"; -import { existsSync, readdirSync } from "node:fs"; -import { mkdtemp, rm } from "node:fs/promises"; -import net from "node:net"; -import os from "node:os"; -import path from "node:path"; - -const sourceUrl = process.argv[2]; -const holdMs = Number(process.env.DEBUG_HOLD_MS || "900000"); -const port = Number(process.env.DEBUG_PORT || "9222"); -const proxyPort = process.env.DEBUG_PROXY_PORT ? Number(process.env.DEBUG_PROXY_PORT) : null; -const cdpOnly = process.env.DEBUG_CDP_ONLY === "1"; - -if (!sourceUrl) { - console.error("usage: node scripts/debug-chatgpt-share.mjs "); - process.exit(2); -} - -let browser; -let context; -let browserProcess; -let proxyServer; -let userDataDir; - -function now() { - return new Date().toISOString(); -} - -function findChromiumExecutable() { - const preferred = process.env.DEBUG_CHROMIUM_EXECUTABLE || chromium.executablePath(); - if (existsSync(preferred)) return preferred; - - const browsersRoot = process.env.PLAYWRIGHT_BROWSERS_PATH || path.dirname(path.dirname(preferred)); - const headlessShell = readdirSync(browsersRoot) - .filter((entry) => entry.startsWith("chromium_headless_shell-")) - .sort() - .reverse() - .map((entry) => path.join(browsersRoot, entry, "chrome-headless-shell-linux64", "chrome-headless-shell")) - .find((candidate) => existsSync(candidate)); - - if (headlessShell) return headlessShell; - throw new Error(`No Chromium executable found. Tried ${preferred} and ${browsersRoot}/chromium_headless_shell-*.`); -} - -async function close() { - if (context) await context.close().catch(() => undefined); - if (browser) await browser.close().catch(() => undefined); - if (proxyServer) await new Promise((resolve) => proxyServer.close(resolve)).catch(() => undefined); - if (browserProcess && !browserProcess.killed) browserProcess.kill("SIGTERM"); - if (userDataDir) await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined); -} - -process.on("SIGINT", () => close().finally(() => process.exit(130))); -process.on("SIGTERM", () => close().finally(() => process.exit(143))); - -userDataDir = await mkdtemp(path.join(os.tmpdir(), "aishare-debug-")); -const executablePath = findChromiumExecutable(); -console.log(`[${now()}] Chromium executable: ${executablePath}`); -browserProcess = spawn(executablePath, [ - "--headless", - "--no-sandbox", - "--disable-dev-shm-usage", - "--disable-gpu", - "--hide-scrollbars", - "--mute-audio", - "--window-size=1360,900", - "--remote-debugging-address=0.0.0.0", - `--remote-debugging-port=${port}`, - `--user-data-dir=${userDataDir}`, - cdpOnly ? sourceUrl : "about:blank" -], { - stdio: ["ignore", "pipe", "pipe"] -}); - -browserProcess.stdout.on("data", (data) => process.stdout.write(data)); -browserProcess.stderr.on("data", (data) => process.stderr.write(data)); -browserProcess.on("exit", (code, signal) => { - console.log(`[${now()}] Chromium exited code=${code} signal=${signal}`); -}); - -let devtoolsReady = false; -for (let attempt = 0; attempt < 100; attempt += 1) { - const response = await fetch(`http://127.0.0.1:${port}/json/version`).catch(() => null); - if (response?.ok) { - devtoolsReady = true; - break; - } - await new Promise((resolve) => setTimeout(resolve, 250)); -} -if (!devtoolsReady) throw new Error(`Chromium DevTools endpoint did not start on 127.0.0.1:${port}.`); - -if (proxyPort) { - proxyServer = net.createServer((client) => { - const upstream = net.connect(port, "127.0.0.1"); - client.pipe(upstream); - upstream.pipe(client); - client.on("error", () => upstream.destroy()); - upstream.on("error", () => client.destroy()); - }); - await new Promise((resolve, reject) => { - proxyServer.once("error", reject); - proxyServer.listen(proxyPort, "0.0.0.0", resolve); - }); - console.log(`[${now()}] DevTools proxy: 0.0.0.0:${proxyPort} -> 127.0.0.1:${port}`); -} - -if (cdpOnly) { - console.log(`[${now()}] CDP-only mode; holding browser for ${Math.round(holdMs / 1000)}s.`); - await new Promise((resolve) => setTimeout(resolve, holdMs)); - await close(); - process.exit(0); -} - -browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`); - -context = await browser.newContext({ - viewport: { width: 1360, height: 900 }, - deviceScaleFactor: 1, - locale: "en-US" -}); - -const page = await context.newPage(); -page.setDefaultTimeout(30_000); - -console.log(`[${now()}] Chromium DevTools port: ${port}`); -console.log(`[${now()}] Opening ${sourceUrl}`); - -await page.goto(sourceUrl, { waitUntil: "commit", timeout: 45_000 }).catch((error) => { - console.log(`[${now()}] goto did not complete: ${error.message}`); -}); -await page.waitForLoadState("domcontentloaded", { timeout: 20_000 }).catch(() => undefined); -await page.waitForLoadState("networkidle", { timeout: 10_000 }).catch(() => undefined); -await page.waitForTimeout(3000); - -const summary = await page.evaluate(() => { - const selectors = [ - "[data-message-author-role]", - "[data-message-id]", - "[data-testid]", - "main article", - "main [role=listitem]", - "main .markdown", - "main [class*=markdown]", - "script[type='application/json']", - "script#__NEXT_DATA__" - ]; - - const counts = Object.fromEntries(selectors.map((selector) => [selector, document.querySelectorAll(selector).length])); - const title = document.querySelector("h1")?.textContent?.trim() || document.title; - const bodySample = (document.body.textContent || "").replace(/\s+/g, " ").trim().slice(0, 600); - const testIds = Array.from(document.querySelectorAll("[data-testid]")) - .map((element) => element.getAttribute("data-testid")) - .filter(Boolean) - .slice(0, 80); - - return { - href: location.href, - title, - counts, - testIds, - bodySample - }; -}); - -console.log(JSON.stringify(summary, null, 2)); -console.log(`[${now()}] Holding browser for ${Math.round(holdMs / 1000)}s.`); - -await new Promise((resolve) => setTimeout(resolve, holdMs)); -await close(); diff --git a/src/capture.ts b/src/capture.ts index d908848..3f5d6c2 100644 --- a/src/capture.ts +++ b/src/capture.ts @@ -106,7 +106,10 @@ function findChromiumExecutable(): string { .filter((entry) => entry.startsWith("chromium_headless_shell-")) .sort() .reverse() - .map((entry) => path.join(browsersRoot, entry, "chrome-headless-shell-linux64", "chrome-headless-shell")) + .flatMap((entry) => [ + path.join(browsersRoot, entry, "chrome-linux", "headless_shell"), + path.join(browsersRoot, entry, "chrome-headless-shell-linux64", "chrome-headless-shell") + ]) .find((candidate) => nodeFs.existsSync(candidate)); if (headlessShell) return headlessShell;