Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

344 lines
13 KiB
JavaScript

import * as fs from 'node:fs';
import * as path from 'node:path';
import * as crypto from 'node:crypto';
import { cli, Strategy } from '@jackwener/opencli/registry';
import { ArgumentError, CommandExecutionError, TimeoutError } from '@jackwener/opencli/errors';
const GROK_URL = 'https://grok.com/';
const SESSION_HINT = 'Likely login/auth/challenge/session issue in the existing grok.com browser session.';
function normalizeBooleanFlag(value) {
if (typeof value === 'boolean') return value;
const normalized = String(value ?? '').trim().toLowerCase();
return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
}
/**
* Validate a positive-integer arg without silently flooring/clamping.
* Throws ArgumentError on `0`, negatives, non-integers, or non-numeric input.
*/
function normalizePositiveInteger(value, defaultValue, label) {
const raw = value ?? defaultValue;
const n = Number(raw);
if (!Number.isInteger(n) || n <= 0) {
throw new ArgumentError(`${label} must be a positive integer`);
}
return n;
}
function dedupeBySrc(images) {
const seen = new Set();
const out = [];
for (const img of images) {
if (!img.src || seen.has(img.src)) continue;
seen.add(img.src);
out.push(img);
}
return out;
}
function imagesSignature(images) {
return images.map(i => i.src).sort().join('|');
}
function extFromContentType(ct) {
if (!ct) return 'jpg';
if (ct.includes('png')) return 'png';
if (ct.includes('webp')) return 'webp';
if (ct.includes('gif')) return 'gif';
return 'jpg';
}
function buildFilename(src, ct) {
const ext = extFromContentType(ct);
const hash = crypto.createHash('sha1').update(src).digest('hex').slice(0, 12);
return `grok-${Date.now()}-${hash}.${ext}`;
}
/** Check whether the tab is already on grok.com (any path). */
async function isOnGrok(page) {
const url = await page.evaluate('window.location.href').catch(() => '');
if (typeof url !== 'string' || !url) return false;
try {
const hostname = new URL(url).hostname;
return hostname === 'grok.com' || hostname.endsWith('.grok.com');
} catch {
return false;
}
}
async function tryStartFreshChat(page) {
await page.evaluate(`(() => {
const isVisible = (node) => {
if (!(node instanceof HTMLElement)) return false;
const rect = node.getBoundingClientRect();
const style = window.getComputedStyle(node);
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
};
const candidates = Array.from(document.querySelectorAll('a, button')).filter(node => {
if (!isVisible(node)) return false;
const text = (node.textContent || '').trim().toLowerCase();
const aria = (node.getAttribute('aria-label') || '').trim().toLowerCase();
const href = node.getAttribute('href') || '';
return text.includes('new chat')
|| text.includes('new conversation')
|| aria.includes('new chat')
|| aria.includes('new conversation')
|| href === '/';
});
const target = candidates[0];
if (target instanceof HTMLElement) target.click();
})()`);
}
async function sendPrompt(page, prompt) {
const promptJson = JSON.stringify(prompt);
return page.evaluate(`(async () => {
try {
const waitFor = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const composerSelector = '.ProseMirror[contenteditable="true"]';
const isVisibleEnabledSubmit = (node) => {
if (!(node instanceof HTMLButtonElement)) return false;
const rect = node.getBoundingClientRect();
const style = window.getComputedStyle(node);
return !node.disabled
&& rect.width > 0
&& rect.height > 0
&& style.visibility !== 'hidden'
&& style.display !== 'none';
};
let pm = null;
let box = null;
for (let attempt = 0; attempt < 12; attempt += 1) {
const composer = document.querySelector(composerSelector);
if (composer instanceof HTMLElement) {
pm = composer;
break;
}
const textarea = document.querySelector('textarea');
if (textarea instanceof HTMLTextAreaElement) {
box = textarea;
break;
}
await waitFor(1000);
}
// Prefer the ProseMirror composer when present (current grok.com UI).
if (pm && pm.editor && pm.editor.commands) {
try {
if (pm.editor.commands.clearContent) pm.editor.commands.clearContent();
pm.editor.commands.focus();
pm.editor.commands.insertContent(${promptJson});
for (let attempt = 0; attempt < 6; attempt += 1) {
const sbtn = Array.from(document.querySelectorAll('button[aria-label="Submit"], button[aria-label="\\u63d0\\u4ea4"]'))
.find(isVisibleEnabledSubmit);
if (sbtn) {
sbtn.click();
return { ok: true, msg: 'pm-submit' };
}
await waitFor(500);
}
} catch (e) { /* fall through to textarea */ }
}
// Fallback: legacy textarea composer.
if (!box) return { ok: false, msg: 'no composer (neither ProseMirror nor textarea)' };
box.focus(); box.value = '';
document.execCommand('selectAll');
document.execCommand('insertText', false, ${promptJson});
for (let attempt = 0; attempt < 6; attempt += 1) {
const btn = Array.from(document.querySelectorAll('button[aria-label="\\u63d0\\u4ea4"], button[aria-label="Submit"]'))
.find(isVisibleEnabledSubmit);
if (btn) {
btn.click();
return { ok: true, msg: 'clicked' };
}
const sub = Array.from(document.querySelectorAll('button[type="submit"]'))
.find(isVisibleEnabledSubmit);
if (sub) {
sub.click();
return { ok: true, msg: 'clicked-submit' };
}
await waitFor(500);
}
box.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
return { ok: true, msg: 'enter' };
} catch (e) { return { ok: false, msg: e && e.toString ? e.toString() : String(e) }; }
})()`);
}
/** Read <img> elements from all message bubbles so callers can filter by baseline. */
async function getBubbleImageSets(page) {
const result = await page.evaluate(`(() => {
const bubbles = document.querySelectorAll('div.message-bubble, [data-testid="message-bubble"]');
return Array.from(bubbles).map(bubble => Array.from(bubble.querySelectorAll('img'))
.map(img => ({
src: img.currentSrc || img.src || '',
w: img.naturalWidth || img.width || 0,
h: img.naturalHeight || img.height || 0,
}))
.filter(i => i.src && /^https?:/.test(i.src))
// Ignore tiny UI/avatar images that may live in the bubble chrome.
.filter(i => (i.w === 0 || i.w >= 128) && (i.h === 0 || i.h >= 128)));
})()`);
const raw = Array.isArray(result) ? result : [];
return raw.map(dedupeBySrc);
}
function pickLatestImageCandidate(bubbleImageSets, baselineCount) {
const freshSets = bubbleImageSets.slice(Math.max(0, baselineCount));
for (let i = freshSets.length - 1; i >= 0; i -= 1) {
if (freshSets[i].length) return freshSets[i];
}
return [];
}
// Download through the browser's fetch so grok.com cookies and referer are
// attached automatically — assets.grok.com is gated by Cloudflare and will
// refuse direct curl/node downloads.
async function fetchImageAsBase64(page, url) {
const urlJson = JSON.stringify(url);
return page.evaluate(`(async () => {
try {
const res = await fetch(${urlJson}, { credentials: 'include', referrer: 'https://grok.com/' });
if (!res.ok) return { ok: false, error: 'HTTP ' + res.status };
const blob = await res.blob();
const buf = await blob.arrayBuffer();
const bytes = new Uint8Array(buf);
let binary = '';
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
return { ok: true, base64: btoa(binary), contentType: blob.type || 'image/jpeg' };
} catch (e) { return { ok: false, error: e && e.message || String(e) }; }
})()`);
}
async function saveImages(page, images, outDir) {
fs.mkdirSync(outDir, { recursive: true });
const results = [];
for (const img of images) {
const fetched = await fetchImageAsBase64(page, img.src);
if (!fetched || !fetched.ok) {
// Fail loudly on per-image download failure rather than emit a sentinel
// row with path = '[DOWNLOAD FAILED] ...' that downstream tools cannot
// distinguish from a real path. assets.grok.com is Cloudflare-gated, so
// a single 401/403 typically means the whole batch is unrecoverable.
const reason = fetched?.error ? `: ${fetched.error}` : '';
throw new CommandExecutionError(
`Failed to download grok image ${img.src}${reason}`,
'assets.grok.com download requires the live grok.com browser session — verify the tab is logged in and try again.',
);
}
const filepath = path.join(outDir, buildFilename(img.src, fetched.contentType));
fs.writeFileSync(filepath, Buffer.from(fetched.base64 || '', 'base64'));
results.push({ ...img, path: filepath });
}
return results;
}
function toRow(img, savedPath = '') {
return { url: img.src, width: img.w, height: img.h, path: savedPath };
}
export const imageCommand = cli({
site: 'grok',
name: 'image',
description: 'Generate images on grok.com and return image URLs',
access: 'write',
domain: 'grok.com',
strategy: Strategy.COOKIE,
browser: true,
siteSession: 'persistent',
args: [
{ name: 'prompt', positional: true, type: 'string', required: true, help: 'Image generation prompt' },
{ name: 'timeout', type: 'int', default: 240, help: 'Max seconds to wait for the image (default: 240)' },
{ name: 'new', type: 'boolean', default: false, help: 'Start a new chat before sending (default: false)' },
{ name: 'count', type: 'int', default: 1, help: 'Minimum images to wait for before returning (default: 1)' },
{ name: 'out', type: 'string', default: '', help: 'Directory to save downloaded images (uses browser session to bypass auth)' },
],
columns: ['url', 'width', 'height', 'path'],
func: async (page, kwargs) => {
const prompt = kwargs.prompt;
const timeoutMs = (kwargs.timeout || 240) * 1000;
const newChat = normalizeBooleanFlag(kwargs.new);
const minCount = normalizePositiveInteger(kwargs.count, 1, 'count');
const outDir = (kwargs.out || '').toString().trim();
if (newChat) {
await page.goto(GROK_URL);
await page.wait(2);
await tryStartFreshChat(page);
await page.wait(2);
} else if (!(await isOnGrok(page))) {
await page.goto(GROK_URL);
await page.wait(3);
}
const baselineBubbleCount = (await getBubbleImageSets(page)).length;
const sendResult = await sendPrompt(page, prompt);
if (!sendResult || !sendResult.ok) {
throw new CommandExecutionError(
`Grok composer rejected the prompt: ${JSON.stringify(sendResult)}`,
SESSION_HINT,
);
}
const startTime = Date.now();
let lastSignature = '';
let stableCount = 0;
let lastImages = [];
while (Date.now() - startTime < timeoutMs) {
await page.wait(3);
const bubbleImageSets = await getBubbleImageSets(page);
const images = pickLatestImageCandidate(bubbleImageSets, baselineBubbleCount);
if (images.length >= minCount) {
const signature = imagesSignature(images);
if (signature === lastSignature) {
stableCount += 1;
// Require two consecutive stable reads (~6s) before declaring done.
if (stableCount >= 2) {
if (outDir) {
const saved = await saveImages(page, images, outDir);
return saved.map(s => toRow(s, s.path));
}
return images.map(i => toRow(i));
}
} else {
stableCount = 0;
lastSignature = signature;
lastImages = images;
}
}
}
// Timeout — keep best-effort partial results if any image bubble showed
// up, otherwise surface the timeout instead of a sentinel row.
if (lastImages.length) {
if (outDir) {
const saved = await saveImages(page, lastImages, outDir);
return saved.map(s => toRow(s, s.path));
}
return lastImages.map(i => toRow(i));
}
throw new TimeoutError('grok image generation', Math.round(timeoutMs / 1000));
},
});
export const __test__ = {
normalizeBooleanFlag,
normalizePositiveInteger,
isOnGrok,
dedupeBySrc,
imagesSignature,
extFromContentType,
buildFilename,
pickLatestImageCandidate,
};