First increment of the STREAM_BROWSER real-time-info modes (true = browser, false = Gemini): - browse-search.mjs: drives the on-screen Chrome via CDP so the action shows on the broadcast. `search` returns the top Google results (title/url/snippet); `youtube` plays the first result. Verified live: real-time Seoul weather results, and IU 'Good Day' MV playback. - .env.example: GEMINI_API_KEY / GEMINI_MODEL for the false-mode Gemini account. - docs/stream_browser_modes.md: architecture + integration map (brain config, the two mode-gated tools, registry, design decisions) for the remaining wiring. The Python brain wiring (config.py mode/gemini fields, browseAndSearch + geminiSearch tools, registry, specs, llm_contexts) lands next - it needs a running brain and a Gemini key to verify, rather than committing untested edits into the 39k-line engine. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
63 lines
2.8 KiB
JavaScript
63 lines
2.8 KiB
JavaScript
// True-mode browser action core. Drives the on-screen Chrome (CDP at CDP_PORT,
|
|
// default 9222) so the action is visible on the Go-Live broadcast, and prints a
|
|
// JSON result on stdout for the Python `browseAndSearch` tool to wrap.
|
|
//
|
|
// node browse-search.mjs "<query>" [search|youtube]
|
|
//
|
|
// - search : Google-search the query, return the top organic results.
|
|
// - youtube : search YouTube and play the first result.
|
|
import { chromium } from 'playwright';
|
|
|
|
const CDP = process.env.CDP_PORT || '9222';
|
|
const query = process.argv[2] || '';
|
|
const mode = (process.argv[3] || 'search').toLowerCase();
|
|
const out = (o) => { process.stdout.write(JSON.stringify(o)); };
|
|
|
|
if (!query) { out({ ok: false, error: 'no query' }); process.exit(1); }
|
|
|
|
let b;
|
|
try {
|
|
b = await chromium.connectOverCDP(`http://localhost:${CDP}`);
|
|
const ctx = b.contexts()[0];
|
|
const page = ctx.pages()[0] || (await ctx.newPage());
|
|
page.setDefaultTimeout(20000);
|
|
await page.bringToFront().catch(() => {});
|
|
|
|
if (mode === 'youtube') {
|
|
await page.goto(`https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForSelector('ytd-video-renderer a#video-title, a#video-title', { timeout: 20000 });
|
|
const first = page.locator('ytd-video-renderer a#video-title, a#video-title').first();
|
|
const title = (await first.getAttribute('title').catch(() => '')) || (await first.innerText().catch(() => ''));
|
|
await first.click();
|
|
await page.waitForSelector('#movie_player', { timeout: 20000 });
|
|
await page.evaluate(() => { const v = document.querySelector('video'); if (v && v.paused) v.play(); });
|
|
out({ ok: true, mode, title: (title || '').trim(), url: page.url() });
|
|
} else {
|
|
await page.goto(`https://www.google.com/search?q=${encodeURIComponent(query)}&hl=ko`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(1500);
|
|
const results = await page.evaluate(() => {
|
|
const seen = new Set();
|
|
const items = [];
|
|
for (const h of Array.from(document.querySelectorAll('a h3'))) {
|
|
const a = h.closest('a');
|
|
const url = a?.href || '';
|
|
if (!url || seen.has(url) || url.includes('google.com')) continue;
|
|
const block = h.closest('div[data-hveid], div.g') || a.parentElement;
|
|
let snippet = '';
|
|
const sn = block?.querySelector('div[data-sncf], div[style*="webkit-line-clamp"], .VwiC3b');
|
|
snippet = (sn?.innerText || '').trim();
|
|
seen.add(url);
|
|
items.push({ title: h.innerText.trim(), url, snippet });
|
|
if (items.length >= 6) break;
|
|
}
|
|
return items;
|
|
});
|
|
out({ ok: true, mode, query, count: results.length, results });
|
|
}
|
|
await b.close();
|
|
} catch (e) {
|
|
try { await b?.close(); } catch { /* ignore */ }
|
|
out({ ok: false, error: String(e?.message || e) });
|
|
process.exit(1);
|
|
}
|