feat: add grok.com site support (#60)
Add support for grok.com site with two commands: - ask: Send a message to Grok and get response - debug: Debug grok page structure Implementation uses Playwright CDP protocol with fallback DOM selectors (div.message-bubble, [data-testid="message-bubble"]) for reliability. Co-authored-by: xdord <xdord@xdorddeMac-mini.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2 files changed
+139
No files matched your search
@@ -0,0 +1,90 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
export const askCommand = cli({
|
||||
site: 'grok',
|
||||
name: 'ask',
|
||||
description: 'Send a message to Grok and get response',
|
||||
domain: 'grok.com',
|
||||
strategy: Strategy.COOKIE,
|
||||
browser: true,
|
||||
args: [
|
||||
{ name: 'prompt', type: 'string', required: true },
|
||||
{ name: 'timeout', type: 'int', default: 120 },
|
||||
{ name: 'new', type: 'boolean', default: false },
|
||||
],
|
||||
columns: ['response'],
|
||||
func: async (page: IPage, kwargs: Record<string, any>) => {
|
||||
const prompt = kwargs.prompt as string;
|
||||
const timeoutMs = ((kwargs.timeout as number) || 120) * 1000;
|
||||
const newChat = kwargs.new as boolean;
|
||||
|
||||
if (newChat) {
|
||||
await page.goto('https://grok.com');
|
||||
await page.wait(2);
|
||||
await page.evaluate(`(() => {
|
||||
const btn = [...document.querySelectorAll('a, button')].find(b => {
|
||||
const t = (b.textContent || '').trim().toLowerCase();
|
||||
return t.includes('new') || b.getAttribute('href') === '/';
|
||||
});
|
||||
if (btn) btn.click();
|
||||
})()`);
|
||||
await page.wait(2);
|
||||
}
|
||||
|
||||
await page.goto('https://grok.com');
|
||||
await page.wait(3);
|
||||
|
||||
const promptJson = JSON.stringify(prompt);
|
||||
|
||||
const sendResult = await page.evaluate(`(async () => {
|
||||
try {
|
||||
const box = document.querySelector('textarea');
|
||||
if (!box) return { ok: false, msg: 'no textarea' };
|
||||
box.focus(); box.value = '';
|
||||
document.execCommand('selectAll');
|
||||
document.execCommand('insertText', false, ${promptJson});
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
const btn = document.querySelector('button[aria-label="\\u63d0\\u4ea4"]');
|
||||
if (btn && !btn.disabled) { btn.click(); return { ok: true, msg: 'clicked' }; }
|
||||
const sub = [...document.querySelectorAll('button[type="submit"]')].find(b => !b.disabled);
|
||||
if (sub) { sub.click(); return { ok: true, msg: 'clicked-submit' }; }
|
||||
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.toString() }; }
|
||||
})()`);
|
||||
|
||||
if (!sendResult || !sendResult.ok) {
|
||||
return [{ response: '[SEND FAILED] ' + JSON.stringify(sendResult) }];
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
let lastText = '';
|
||||
let stableCount = 0;
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
await page.wait(3);
|
||||
const response = await page.evaluate(`(() => {
|
||||
const bubbles = document.querySelectorAll('div.message-bubble, [data-testid="message-bubble"]');
|
||||
if (bubbles.length < 2) return '';
|
||||
const last = bubbles[bubbles.length - 1];
|
||||
const text = (last.innerText || '').trim();
|
||||
if (!text || text.length < 2) return '';
|
||||
return text;
|
||||
})()`);
|
||||
|
||||
if (response && response.length > 2) {
|
||||
if (response === lastText) {
|
||||
stableCount++;
|
||||
if (stableCount >= 2) return [{ response }];
|
||||
} else {
|
||||
stableCount = 0;
|
||||
}
|
||||
}
|
||||
lastText = response || '';
|
||||
}
|
||||
|
||||
if (lastText) return [{ response: lastText }];
|
||||
return [{ response: '[NO RESPONSE]' }];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import type { IPage } from '../../types.js';
|
||||
|
||||
export const debugCommand = cli({
|
||||
site: 'grok',
|
||||
name: 'debug',
|
||||
description: 'Debug grok page structure',
|
||||
domain: 'grok.com',
|
||||
strategy: Strategy.COOKIE,
|
||||
browser: true,
|
||||
columns: ['data'],
|
||||
func: async (page: IPage, _kwargs: Record<string, any>) => {
|
||||
await page.goto('https://grok.com');
|
||||
await page.wait(3);
|
||||
|
||||
// Get all button-like elements near textarea
|
||||
const debug = await page.evaluate(`(() => {
|
||||
const ta = document.querySelector('textarea');
|
||||
if (!ta) return { error: 'no textarea' };
|
||||
|
||||
// Get parent containers
|
||||
let parent = ta.parentElement;
|
||||
const parents = [];
|
||||
for (let i = 0; i < 5 && parent; i++) {
|
||||
parents.push({
|
||||
tag: parent.tagName,
|
||||
class: parent.className?.substring(0, 80),
|
||||
childCount: parent.children.length,
|
||||
});
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
// Find buttons in the form/container near textarea
|
||||
const form = ta.closest('form') || ta.closest('[class*="composer"]') || ta.closest('[class*="input"]') || ta.parentElement?.parentElement;
|
||||
const buttons = form ? [...form.querySelectorAll('button')].map(b => ({
|
||||
testid: b.getAttribute('data-testid'),
|
||||
type: b.type,
|
||||
disabled: b.disabled,
|
||||
text: (b.textContent || '').substring(0, 30),
|
||||
html: b.outerHTML.substring(0, 200),
|
||||
rect: b.getBoundingClientRect().toJSON(),
|
||||
})) : [];
|
||||
|
||||
return { parents, buttons, formTag: form?.tagName, formClass: form?.className?.substring(0, 80) };
|
||||
})()`);
|
||||
|
||||
return [{ data: JSON.stringify(debug, null, 2) }];
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user