← back to Sanderson Onboard

scripts/harvest_morris_images.mjs

88 lines

#!/usr/bin/env node
// TK-10881 — Morris & Co image RE-HARVEST via openclaw real-Chrome (Cloudflare-cleared).
// wmorrisandco.com is a Magento 2 store (no Nucleus feed, no GraphQL). Image = PDP og:image
// on the un-challenged /media/catalog/product CDN. We already resolved SKU->product-URL from
// the sitemap (morris_sku_url_map.json). Here we in-page fetch each PDP HTML and regex og:image
// + <title>, running INSIDE the CF-cleared tab (fetch carries the clearance cookie). Serial by
// design (1 browser). Checkpoints to morris_image_harvest.jsonl (resumes, loses nothing).
//
// USAGE: node harvest_morris_images.mjs <target-id> [chunk_size]
import { execFileSync } from 'child_process';
import fs from 'fs';

const TID = process.argv[2];
const CHUNK = Number(process.argv[3] || 40);
if (!TID) { console.error('need <target-id>'); process.exit(1); }
const DIR = new URL('..', import.meta.url).pathname;
const MAP = JSON.parse(fs.readFileSync(`${DIR}pilot/morris_sku_url_map.json`, 'utf8'));
const CKPT = `${DIR}pilot/morris_image_harvest.jsonl`;

const done = new Set();
if (fs.existsSync(CKPT)) for (const l of fs.readFileSync(CKPT, 'utf8').split('\n').filter(Boolean)) {
  try { done.add(JSON.parse(l).base_code); } catch {}
}
const todo = Object.entries(MAP).filter(([sku]) => !done.has(sku));
console.log(`[morris-img] ${Object.keys(MAP).length} mapped, ${done.size} checkpointed, ${todo.length} to harvest`);

// NOTE: use appendFileSync, NOT createWriteStream — the loop blocks the event loop with
// synchronous execFileSync (openclaw) calls, so an async write stream never flushes until exit.
// appendFileSync flushes each row to disk immediately => true resumable checkpoint.
const out = { write: (s) => fs.appendFileSync(CKPT, s), end: () => {} };

function evalInPage(fnSrc) {
  // openclaw browser evaluate --target-id TID --fn '<src>' ; returns stdout (JSON-string)
  const res = execFileSync('openclaw', ['browser', 'evaluate', '--target-id', TID, '--timeout', '120000', '--fn', fnSrc],
    { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
  let s = res.trim();
  // openclaw prints the return value; if wrapped as a JSON string, decode once
  if (s.startsWith('"') && s.endsWith('"')) { try { s = JSON.parse(s); } catch {} }
  return s;
}

const chunks = [];
for (let i = 0; i < todo.length; i += CHUNK) chunks.push(todo.slice(i, i + CHUNK));

let ci = 0, ok = 0, noimg = 0, err = 0;
for (const ch of chunks) {
  ci++;
  const pairs = ch.map(([sku, url]) => [sku, url]);
  const fnSrc = `async () => {
    const pairs = ${JSON.stringify(pairs)};
    const results = [];
    // small concurrency to stay polite + avoid tab overload
    const CONC = 5;
    let idx = 0;
    async function worker() {
      while (idx < pairs.length) {
        const my = idx++; const [sku, url] = pairs[my];
        try {
          const r = await fetch(url, { headers: { 'Accept': 'text/html' } });
          const t = await r.text();
          const og = (t.match(/<meta[^>]+property=["']og:image["'][^>]+content=["']([^"']+)["']/i) || [])[1] || null;
          const title = (t.match(/<title>([^<]*)<\\/title>/i) || [])[1] || null;
          const sku_page = (t.match(/"sku"\\s*:\\s*"([^"]+)"/i) || [])[1] || null;
          results.push({ base_code: sku, url, image: og, title, sku_page, http: r.status });
        } catch (e) { results.push({ base_code: sku, url, image: null, error: String(e).slice(0, 100) }); }
      }
    }
    await Promise.all(Array.from({ length: CONC }, worker));
    return JSON.stringify(results);
  }`;
  let arr;
  try {
    const raw = evalInPage(fnSrc);
    arr = typeof raw === 'string' ? JSON.parse(raw) : raw;
  } catch (e) {
    console.error(`[morris-img] chunk ${ci}/${chunks.length} EVAL FAIL: ${String(e).slice(0,120)} — retrying once`);
    try { const raw = evalInPage(fnSrc); arr = JSON.parse(typeof raw==='string'?raw:JSON.stringify(raw)); }
    catch (e2) { console.error(`  retry failed, skipping chunk`); continue; }
  }
  for (const r of arr) {
    if (r.image) ok++; else if (r.error) err++; else noimg++;
    out.write(JSON.stringify(r) + '\n');
  }
  console.log(`[morris-img] chunk ${ci}/${chunks.length}  ok=${ok} noimg=${noimg} err=${err}`);
}
out.end();
console.log(`[morris-img] DONE ok=${ok} noimg=${noimg} err=${err}. Checkpoint: ${CKPT}`);