import { runCustomer1688Search } from './skills/customer_1688_search.js';
import { runFetch1688Token } from './skills/fetch_1688_token.js';
import { runOzonBlockbusterFetchImages } from './skills/ozon_blockbuster_fetch_images.js';
import { runOzonBlockbusterSearch1688 } from './skills/ozon_blockbuster_search_1688.js';
import { Muxer, ArrayBufferTarget } from './vendor/mp4-muxer.mjs';
/**
 * Rich Browser Extension - Background Service Worker
 * 维护与 OpenClaw Adapter 的 WebSocket 长连接
 */

const WS_URL = "wss://bot.ecommclaw.com"; // 正式生产环境地址 (Nginx + SSL)
const OZON_OPS_API_BASE = "https://bot.ecommclaw.com/api/v1";
const IMAGE_TASK_CLAIM_MS = 5000;
const IMAGE_TASK_LEASE_MS = 300000;
const IMAGE_TASK_LIMIT = 3;
const VIDEO_TASK_CLAIM_MS = 5000;
const VIDEO_TASK_LEASE_MS = 600000;
const VIDEO_TASK_LIMIT = 2;

// 缓存 batch_fetch_images 阶段产生的完美 JPEG base64（Ozon 页面 MAIN world canvas 转出）
const imageBase64Cache = new Map();

// ─── Compact MD5 implementation (for mtop signing) ───
var md5 = (function() {
  function safeAdd(x, y) {
    var lsw = (x & 0xffff) + (y & 0xffff);
    return (((x >> 16) + (y >> 16) + (lsw >> 16)) << 16) | (lsw & 0xffff);
  }
  function bitRotateLeft(num, cnt) { return (num << cnt) | (num >>> (32 - cnt)); }
  function md5cmn(q,a,b,x,s,t) { return safeAdd(bitRotateLeft(safeAdd(safeAdd(a,q),safeAdd(x,t)),s),b); }
  function md5ff(a,b,c,d,x,s,t) { return md5cmn((b&c)|((~b)&d),a,b,x,s,t); }
  function md5gg(a,b,c,d,x,s,t) { return md5cmn((b&d)|(c&(~d)),a,b,x,s,t); }
  function md5hh(a,b,c,d,x,s,t) { return md5cmn(b^c^d,a,b,x,s,t); }
  function md5ii(a,b,c,d,x,s,t) { return md5cmn(c^(b|(~d)),a,b,x,s,t); }

  function binlMD5(x, len) {
    x[len >> 5] |= 0x80 << (len % 32);
    x[((len + 64) >>> 9 << 4) + 14] = len;
    var a = 1732584193, b = -271733879, c = -1732584194, d = 271733878;
    for (var i = 0; i < x.length; i += 16) {
      var oa=a, ob=b, oc=c, od=d;
      a=md5ff(a,b,c,d,x[i],7,-680876936);d=md5ff(d,a,b,c,x[i+1],12,-389564586);
      c=md5ff(c,d,a,b,x[i+2],17,606105819);b=md5ff(b,c,d,a,x[i+3],22,-1044525330);
      a=md5ff(a,b,c,d,x[i+4],7,-176418897);d=md5ff(d,a,b,c,x[i+5],12,1200080426);
      c=md5ff(c,d,a,b,x[i+6],17,-1473231341);b=md5ff(b,c,d,a,x[i+7],22,-45705983);
      a=md5ff(a,b,c,d,x[i+8],7,1770035416);d=md5ff(d,a,b,c,x[i+9],12,-1958414417);
      c=md5ff(c,d,a,b,x[i+10],17,-42063);b=md5ff(b,c,d,a,x[i+11],22,-1990404162);
      a=md5ff(a,b,c,d,x[i+12],7,1804603682);d=md5ff(d,a,b,c,x[i+13],12,-40341101);
      c=md5ff(c,d,a,b,x[i+14],17,-1502002290);b=md5ff(b,c,d,a,x[i+15],22,1236535329);
      a=md5gg(a,b,c,d,x[i+1],5,-165796510);d=md5gg(d,a,b,c,x[i+6],9,-1069501632);
      c=md5gg(c,d,a,b,x[i+11],14,643717713);b=md5gg(b,c,d,a,x[i],20,-373897302);
      a=md5gg(a,b,c,d,x[i+5],5,-701558691);d=md5gg(d,a,b,c,x[i+10],9,38016083);
      c=md5gg(c,d,a,b,x[i+15],14,-660478335);b=md5gg(b,c,d,a,x[i+4],20,-405537848);
      a=md5gg(a,b,c,d,x[i+9],5,568446438);d=md5gg(d,a,b,c,x[i+14],9,-1019803690);
      c=md5gg(c,d,a,b,x[i+3],14,-187363961);b=md5gg(b,c,d,a,x[i+8],20,1163531501);
      a=md5gg(a,b,c,d,x[i+13],5,-1444681467);d=md5gg(d,a,b,c,x[i+2],9,-51403784);
      c=md5gg(c,d,a,b,x[i+7],14,1735328473);b=md5gg(b,c,d,a,x[i+12],20,-1926607734);
      a=md5hh(a,b,c,d,x[i+5],4,-378558);d=md5hh(d,a,b,c,x[i+8],11,-2022574463);
      c=md5hh(c,d,a,b,x[i+11],16,1839030562);b=md5hh(b,c,d,a,x[i+14],23,-35309556);
      a=md5hh(a,b,c,d,x[i+1],4,-1530992060);d=md5hh(d,a,b,c,x[i+4],11,1272893353);
      c=md5hh(c,d,a,b,x[i+7],16,-155497632);b=md5hh(b,c,d,a,x[i+10],23,-1094730640);
      a=md5hh(a,b,c,d,x[i+13],4,681279174);d=md5hh(d,a,b,c,x[i],11,-358537222);
      c=md5hh(c,d,a,b,x[i+3],16,-722521979);b=md5hh(b,c,d,a,x[i+6],23,76029189);
      a=md5hh(a,b,c,d,x[i+9],4,-640364487);d=md5hh(d,a,b,c,x[i+12],11,-421815835);
      c=md5hh(c,d,a,b,x[i+15],16,530742520);b=md5hh(b,c,d,a,x[i+2],23,-995338651);
      a=md5ii(a,b,c,d,x[i],6,-198630844);d=md5ii(d,a,b,c,x[i+7],10,1126891415);
      c=md5ii(c,d,a,b,x[i+14],15,-1416354905);b=md5ii(b,c,d,a,x[i+5],21,-57434055);
      a=md5ii(a,b,c,d,x[i+12],6,1700485571);d=md5ii(d,a,b,c,x[i+3],10,-1894986606);
      c=md5ii(c,d,a,b,x[i+10],15,-1051523);b=md5ii(b,c,d,a,x[i+1],21,-2054922799);
      a=md5ii(a,b,c,d,x[i+8],6,1873313359);d=md5ii(d,a,b,c,x[i+15],10,-30611744);
      c=md5ii(c,d,a,b,x[i+6],15,-1560198380);b=md5ii(b,c,d,a,x[i+13],21,1309151649);
      a=md5ii(a,b,c,d,x[i+4],6,-145523070);d=md5ii(d,a,b,c,x[i+11],10,-1120210379);
      c=md5ii(c,d,a,b,x[i+2],15,718787259);b=md5ii(b,c,d,a,x[i+9],21,-343485551);
      a=safeAdd(a,oa);b=safeAdd(b,ob);c=safeAdd(c,oc);d=safeAdd(d,od);
    }
    return [a,b,c,d];
  }

  function str2binl(str) {
    var bin = [], mask = (1 << 8) - 1;
    for (var i = 0; i < str.length * 8; i += 8)
      bin[i >> 5] |= (str.charCodeAt(i / 8) & mask) << (i % 32);
    return bin;
  }

  function binl2hex(binarray) {
    var hex_tab = '0123456789abcdef', str = '';
    for (var i = 0; i < binarray.length * 32; i += 8)
      str += hex_tab.charAt((binarray[i >> 5] >>> (i % 32 + 4)) & 0xf) +
             hex_tab.charAt((binarray[i >> 5] >>> (i % 32)) & 0xf);
    return str;
  }

  return function(s) {
    return binl2hex(binlMD5(str2binl(s), s.length * 8));
  };
})();
const CLIENT_ID = "rb-" + Math.random().toString(36).slice(2, 10);
const IMAGE_TASK_WORKER_ID = "chrome-image-worker:" + CLIENT_ID;
const VIDEO_TASK_WORKER_ID = "chrome-video-worker:" + CLIENT_ID;
let TENANT_ID = ""; // 必须从 storage 加载或用户手动填写，为空时禁止连接

let ws = null;
let reconnectTimer = null;
let connected = false;
let cachedHistory = null; // 缓存历史记录
let cachedDisplayName = ""; // 缓存 agent displayName（hello_ack 收到），sidepanel 关掉再开时用 get_status 拉
let imageTaskTimer = null;
let imageTaskBusy = false;
let videoTaskTimer = null;
let videoTaskBusy = false;

// ─── 动态加载租户 ID ───

function loadTenantId(callback) {
  chrome.storage.local.get(["tenantId"], (res) => {
    if (res.tenantId) {
      TENANT_ID = res.tenantId;
      console.log("[RB] Loaded tenant ID from storage:", TENANT_ID);
    } else {
      TENANT_ID = "";
      console.log("[RB] No tenant ID configured. Will not connect until user sets one.");
      broadcastToExtension({ type: "system", text: "⚠️ 请先在侧边栏设置租户 ID (Tenant Agent ID)，然后才能连接。" });
    }
    callback && callback();
  });
}

// ─── WebSocket 连接管理 ───

function connect() {
  if (!TENANT_ID) {
    console.log("[RB] Cannot connect: no tenant ID configured.");
    broadcastToExtension({ type: "status", connected: false, error: "请先设置租户 ID" });
    return;
  }
  if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
    return;
  }

  try {
    ws = new WebSocket(WS_URL);
  } catch (e) {
    console.error("[RB] WebSocket creation failed:", e);
    scheduleReconnect();
    return;
  }

  ws.onopen = () => {
    console.log("[RB] Connected to OpenClaw");
    connected = true;
    // 握手：发送 clientId 和 tenantId
    ws.send(JSON.stringify({ type: "hello", clientId: CLIENT_ID, tenantId: TENANT_ID }));
    // 通知 side panel
    broadcastToExtension({ type: "status", connected: true, clientId: CLIENT_ID, tenantId: TENANT_ID });
  };

  ws.onmessage = (event) => {
    try {
      const data = JSON.parse(event.data);
      console.log("[RB] Received:", data.type);

      if (data.type === "hello_ack") {
        console.log("[RB] Handshake complete, clientId:", data.clientId, "tenantId:", data.tenantId, "displayName:", data.displayName || "(none)");
        cachedDisplayName = data.displayName || "";
        // 通知 sidepanel agent 的显示名（连成功后顶部 "Rich Browser" 旁显示）
        broadcastToExtension({ type: "agent_identity", displayName: cachedDisplayName });
        return;
      }

      // 新增：接收历史记录
      if (data.type === "history") {
        cachedHistory = data.messages;
        broadcastToExtension({ type: "history", messages: data.messages });
        return;
      }

      // 服务端推回的 shops_matrix.json 内容（sidepanel ⚙️ 拉店铺矩阵）
      if (data.type === "shops_matrix") {
        broadcastToExtension({ type: "shops_matrix", shops: data.shops || [], notFound: !!data.notFound, error: data.error });
        return;
      }

      // set_shops_matrix 保存结果回执（sidepanel ⚙️ 保存按钮等的就是这个）
      if (data.type === "shops_matrix_saved") {
        broadcastToExtension({ type: "shops_matrix_saved", ok: !!data.ok, count: data.count, error: data.error });
        return;
      }

      // 服务端推回的 custom models（sidepanel 模型配置面板拉服务端 config.json）
      if (data.type === "custom_models") {
        broadcastToExtension({ type: "custom_models", models: data.models || [], current_model: data.current_model || "", error: data.error });
        return;
      }

      if (data.type === "custom_model_deleted") {
        broadcastToExtension({ type: "custom_model_deleted", ok: !!data.ok, model_id: data.model_id, error: data.error });
        return;
      }

      if (data.type === "custom_model_set") {
        broadcastToExtension({ type: "custom_model_set", ok: !!data.ok, model_id: data.model_id, error: data.error });
        return;
      }

      // 用户在 sidepanel chat 上传图片落盘后，服务端立刻推回 URL，让 sidepanel 显示真图（不是 base64 字符串）
      if (data.type === "user_images_uploaded") {
        const host = WS_URL.replace(/^wss?:\/\//, "").replace(/\/$/, "");
        const fullUrls = (data.paths || []).map(p => `https://${host}${p}`);
        broadcastToExtension({ type: "user_images_uploaded", urls: fullUrls });
        return;
      }

      // upload_image 异步响应（M5 review panel / 沙盒 skill 通过 ws 直接上传图）
      if (data.type === "image_uploaded") {
        const host = WS_URL.replace(/^wss?:\/\//, "").replace(/\/$/, "");
        const fullUrl = data.path ? `https://${host}${data.path}` : null;
        broadcastToExtension({ type: "image_uploaded", reqId: data.reqId, url: fullUrl, filename: data.filename, error: data.error });
        return;
      }

      // Workflow 执行结果 → 广播给所有扩展页面（viewer、sidepanel）
      if (data.type === "workflow_result") {
        console.log("[RB] Workflow result received:", data.task_id, data.error ? "ERROR" : "OK");
        broadcastToExtension({ type: "workflow_result", task_id: data.task_id, result: data.result, error: data.error });
        return;
      }

      // Agent 回复 → 转发给 side panel，同时追加到缓存
      if (data.type === "reply") {
        if (cachedHistory) {
          cachedHistory.push({ id: Date.now().toString(36), role: "agent", text: data.text, timestamp: Date.now() });
        }
        broadcastToExtension({ type: "reply", text: data.text, mediaUrls: data.mediaUrls });
        return;
      }

      // 新增：控制指令
      if (data.type === "command") {
        const reqId = data.reqId;
        const respond = (res, err) => {
          if (reqId && ws && ws.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify({ type: "command_response", reqId, result: res, error: err }));
          }
        };

                if (data.action === "navigate") {
          // 向后兼容：data.tab_id 给了就 update 同 tab；否则 create 新 tab。
          // 多步流水线（如 customer-1688-image-search 逐张搜图）应一次拿到 tab_id 后续复用，避免每张图开新 tab。
          // Goal 2: data.active === false 时后台开/切（不抢焦点），用户可同时在别的 tab 干活
          //         未传或为 true 时维持原行为 active: true
          // Goal 2 + viewport_override：data.viewport_width / viewport_height 都给 → 创建/更新 tab 后
          //         CDP attach + Emulation.setDeviceMetricsOverride 强制按指定 viewport 渲染
          //         (不传则 0 影响原行为)。多机布局窗口尺寸不一时用这个保证选择器/坐标稳定。
          const navActive = data.active === false ? false : true;
          const applyViewport = (tabId, callback) => {
            if (!data.viewport_width || !data.viewport_height) { callback(null); return; }
            chrome.debugger.attach({ tabId }, '1.3', () => {
              const aErr = chrome.runtime.lastError;
              if (aErr && !/already attached/i.test(aErr.message)) { callback(aErr.message); return; }
              chrome.debugger.sendCommand({ tabId }, 'Emulation.setDeviceMetricsOverride', {
                width: data.viewport_width,
                height: data.viewport_height,
                deviceScaleFactor: 1,
                mobile: false,
              }, () => {
                const cErr = chrome.runtime.lastError;
                callback(cErr ? cErr.message : null);
              });
            });
          };
          const finishNav = (tabId, reused) => {
            applyViewport(tabId, (vpErr) => {
              const resp = { tab_id: tabId, reused };
              if (data.viewport_width && data.viewport_height) {
                resp.viewport_set = !vpErr;
                if (vpErr) resp.viewport_error = vpErr;
              }
              respond(resp);
            });
          };
          if (data.tab_id) {
            chrome.tabs.update(data.tab_id, { url: data.url, active: navActive }, (tab) => {
              if (chrome.runtime.lastError || !tab) {
                // tab 被关掉了，fallback 开新 tab
                chrome.tabs.create({ url: data.url, active: navActive }, (newTab) => finishNav(newTab.id, false));
              } else {
                finishNav(tab.id, true);
              }
            });
          } else {
            chrome.tabs.create({ url: data.url, active: navActive }, (tab) => {
              if (navActive) broadcastToExtension({ type: "system", text: `已在新标签页打开网页: ${data.url}` });
              finishNav(tab.id, false);
            });
          }
          return;

        } else if (data.action === "list_tabs") {
          // Diagnostic: 列当前 chrome 所有 tab，用于 debug "为什么累积多 tab"
          // 可选 url_match 过滤 (substring)；不传则全部
          const urlMatch = data.url_match || '';
          chrome.tabs.query({}, (tabs) => {
            const filtered = tabs
              .filter(t => !urlMatch || (t.url || '').includes(urlMatch))
              .map(t => ({ id: t.id, url: (t.url || '').slice(0, 100), active: t.active, windowId: t.windowId, index: t.index, title: (t.title || '').slice(0, 40) }));
            respond({ total: tabs.length, filtered_count: filtered.length, tabs: filtered });
          });
          return;
        } else if (data.action === "close_tab") {
          // Diagnostic: 关掉指定 tab_id
          chrome.tabs.remove(data.tab_id, () => respond({ closed: true, tab_id: data.tab_id }));
          return;
        } else if (data.action === "close_excess_ozon_tabs") {
          // 模块3 抓完一商品详情后调：保留 keep_tab_id (主 tab)，关掉其他所有 ozon.ru tab
          // 防 click → 新开 tab → 数据抓在新 tab → 不关 → 累积 → renderer crash
          const keepId = data.keep_tab_id;
          chrome.tabs.query({}, (allTabs) => {
            const toClose = allTabs.filter(t =>
              t.id !== keepId &&
              (t.url || '').includes('ozon.ru')
            );
            let done = 0;
            if (toClose.length === 0) { respond({ closed: 0, kept: keepId }); return; }
            toClose.forEach(t => {
              chrome.tabs.remove(t.id, () => {
                done++;
                if (done === toClose.length) {
                  respond({ closed: done, kept: keepId, closed_urls: toClose.map(t => (t.url || '').slice(0, 80)) });
                }
              });
            });
          });
          return;
        } else if (data.action === "navigate_clean") {
          // Used by viewer iframe to open 1688 links without chrome-extension referer
          chrome.tabs.create({ url: data.url });
          return;
        } else if (data.action === "ozon_blockbuster_fetch_images") {
          runOzonBlockbusterFetchImages(data, broadcastToExtension, respond, imageBase64Cache);
          return;
        } else if (data.action === "ozon_blockbuster_search_1688") {
          runOzonBlockbusterSearch1688(data, broadcastToExtension, respond, imageBase64Cache);
          return;
        } else if (data.action === "customer_1688_search") {
          runCustomer1688Search(data, broadcastToExtension, respond, imageBase64Cache);
          return;
        } else if (data.action === "fetch_1688_token") {
          runFetch1688Token(data, broadcastToExtension, respond);
          return;
        } else if (data.action === "fetch_html") {
          // service worker 里 fetch 任意 URL，credentials:"include" 自动带上用户 cookie，
          // 无 Trusted Types / 页面 CSP 限制。返回 raw HTML/text，由调用方自行解析。
          // input:  { url: string, headers?: object }
          // output: { ok, status, url, body, error? }
          (async () => {
            try {
              const headers = Object.assign({
                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
                "Accept-Language": "ru-RU,ru;q=0.9,en;q=0.8",
                "User-Agent": navigator.userAgent || "Mozilla/5.0",
              }, data.headers || {});
              const r = await fetch(data.url, {
                credentials: "include",
                redirect: "follow",
                headers,
              });
              const body = await r.text();
              respond({ ok: r.ok, status: r.status, url: r.url, body });
            } catch (e) {
              respond({ ok: false, error: String((e && e.message) || e) });
            }
          })();
          return;
        } else if (data.action === "fetch_image_urls") {
          // 通用图 URL 批量下载到 base64 jpeg：service worker 里 fetch，无 ozon CSP / Trusted Types 限制
          // 关键：用 OffscreenCanvas 把任意格式（webp/png/jpg）统一转成 jpeg，
          //       否则下游（如 1688 imgBase64 上传）拿到 webp 会返回 imageId=0
          // input:  { items: [{sku_id, image_url}, ...], task_id?, format?: "jpeg"|"png" (默认 jpeg) }
          // output: { images: [{sku_id, image_data: 'data:image/jpeg;base64,...', error?: string}, ...] }
          (async () => {
            const items = data.items || [];
            const targetFmt = data.format || 'jpeg';
            const targetMime = 'image/' + targetFmt;
            const out = [];
            for (const item of items) {
              if (!item.image_url) { out.push({ sku_id: item.sku_id, error: "no image_url" }); continue; }
              try {
                const r = await fetch(item.image_url, { credentials: "omit", referrerPolicy: "no-referrer" });
                if (!r.ok) { out.push({ sku_id: item.sku_id, error: "HTTP " + r.status }); continue; }
                const srcBlob = await r.blob();
                // 统一转 jpeg（OffscreenCanvas + createImageBitmap 在 MV3 service worker 可用）
                let outBlob = srcBlob;
                if (srcBlob.type !== targetMime) {
                  try {
                    const bitmap = await createImageBitmap(srcBlob);
                    const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
                    const ctx = canvas.getContext('2d');
                    if (targetFmt === 'jpeg') {
                      // jpeg 不支持透明，铺白底
                      ctx.fillStyle = '#fff';
                      ctx.fillRect(0, 0, bitmap.width, bitmap.height);
                    }
                    ctx.drawImage(bitmap, 0, 0);
                    outBlob = await canvas.convertToBlob({ type: targetMime, quality: 0.92 });
                  } catch (convErr) {
                    out.push({ sku_id: item.sku_id, error: 'convert failed: ' + String((convErr && convErr.message) || convErr) });
                    continue;
                  }
                }
                const dataUrl = await new Promise((res) => {
                  const fr = new FileReader();
                  fr.onloadend = () => res(fr.result);
                  fr.readAsDataURL(outBlob);
                });
                out.push({ sku_id: item.sku_id, image_data: dataUrl });
              } catch (e) {
                out.push({ sku_id: item.sku_id, error: String((e && e.message) || e) });
              }
            }
            respond({ images: out });
          })();
          return;
        } else if (data.action === "render_html") {
          // 将 HTML 内容存入 chrome.storage（首次访问 viewer.html 时从 storage 读）
          // 同时广播 render_html_update 让已打开的 viewer iframe 自动 hot-reload（chain 场景关键）
          chrome.storage.local.set({ render_html_content: data.html }, () => {
            // 广播：已打开的 viewer-bridge 会接住并把新 HTML 推给 sandbox iframe
            chrome.runtime.sendMessage({ type: 'render_html_update', html: data.html }, () => {
              // 忽略 lastError —— 没有 viewer 打开时这里会 set lastError，正常
              void chrome.runtime.lastError;
            });
            const viewerUrl = chrome.runtime.getURL('viewer.html');
            // skill 可在 render_html payload 里传 notice 字段自定义抬头；不传走通用默认。
            const notice = (typeof data.notice === 'string' && data.notice.trim()) ? data.notice : "📊 报告已就绪！点击下方按钮在浏览器中打开：";
            broadcastToExtension({
              type: "reply",
              text: notice,
              mediaUrls: [viewerUrl]
            });
            respond(true);
          });
        } else if (data.action === "chat_message") {
          // 纯文字提示：不更新 panel，不附 viewer CTA，只在侧边栏 chat 区出一条灰色系统条
          // 跟"🔗 自动进入下一步: skill-X"同款样式（sidepanel.js 处理 type:"system"）
          // 用途：M4 等多步 skill 的进度提示（每个 offer 一条），中间过程不要 panel 噪声
          // 2026-05-11：可选 mediaUrls — skill-edit-image 用它在 chat 区直接显示生成的图
          // 故意不 push cachedHistory：进度条文字是瞬时信息，不需要持久化到聊天历史
          const text = (typeof data.text === 'string' && data.text.trim()) ? data.text.trim() : '';
          const mediaUrls = Array.isArray(data.mediaUrls) ? data.mediaUrls : [];
          if (text || mediaUrls.length > 0) {
            broadcastToExtension({ type: "system", text: text, mediaUrls: mediaUrls });
          }
          respond(true);
        } else if (data.action === "query_elements") {
          chrome.tabs.query({}, (allTabs) => {
            // 调试：打印所有发现的标签页，帮助 Agent 识别
            const tabSummary = allTabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active }));
            console.log("[RB] All detected tabs:", tabSummary);

            // 智能选择逻辑：
            // 1. 优先找域名匹配的 (Ozon, 1688, etc)
            // 2. 其次找非扩展页面的活跃标签页
            // 3. 最后找任何 http/https 页面
            let targetTab = allTabs.find(t => t.active && (t.url.includes('ozon.ru') || t.url.includes('1688.com'))) || allTabs.find(t => t.url.includes('ozon.ru') || t.url.includes('1688.com'))
                         || allTabs.find(t => t.active && t.url.startsWith('http'))
                         || allTabs.find(t => t.url.startsWith('http'));
            
            if (!targetTab) {
              respond(null, "No suitable web page tab found. Seen tabs: " + allTabs.map(t => t.url).join(', '));
              return;
            }
            
            console.log("[RB] Targeting tab:", targetTab.url);
            executeScriptOnTab(targetTab.id);

            function executeScriptOnTab(tabId) {
              chrome.scripting.executeScript({
                target: { tabId: tabId },
                func: (selector, properties) => {
                  try {
                    const elements = document.querySelectorAll(selector);
                    const results = [];
                    for (const el of elements) {
                      const obj = {};
                      for (const prop of properties) {
                        if (prop === 'text' || prop === 'textContent') {
                          obj[prop] = el.textContent ? el.textContent.trim() : '';
                        } else if (prop === 'html' || prop === 'innerHTML') {
                          obj[prop] = el.innerHTML;
                        } else if (prop.startsWith('attr:')) {
                          const attrName = prop.split(':')[1];
                          obj[prop] = el.getAttribute(attrName) || '';
                        } else {
                          obj[prop] = el[prop] !== undefined ? String(el[prop]) : '';
                        }
                      }
                      results.push(obj);
                    }
                    return results;
                  } catch (e) {
                    return { __error: e.message };
                  }
                },
                args: [data.selector, data.properties || ['text']]
              }, (results) => {
                if (chrome.runtime.lastError) {
                  respond(null, chrome.runtime.lastError.message);
                } else {
                  const res = results[0]?.result;
                  if (res && res.__error) {
                    respond(null, res.__error);
                  } else {
                    respond(res);
                  }
                }
              });
            }
          });
        } else if (data.action === "click_element") {
          chrome.tabs.query({}, (allTabs) => {
            let targetTab = allTabs.find(t => t.url.includes('ozon.ru') || t.url.includes('1688.com'))
                         || allTabs.find(t => t.active && t.url.startsWith('http'))
                         || allTabs.find(t => t.url.startsWith('http'));
            if (!targetTab) { respond(null, "No web page tab found"); return; }
            
            chrome.scripting.executeScript({
              target: { tabId: targetTab.id },
              func: (selector) => {
                try {
                  const el = document.querySelector(selector);
                  if (!el) return { __error: "Element not found: " + selector };
                  el.click();
                  return true;
                } catch (e) { return { __error: e.message }; }
              },
              args: [data.selector]
            }, (results) => {
              const res = results[0]?.result;
              if (res && res.__error) respond(null, res.__error);
              else respond(true);
            });
          });
        } else if (data.action === "execute_js") {
          // Goal 2: data.tab_id 给了就**强制走该 tab**（多机场景下 URL heuristic 选错 tab 的根因）；
          //         没给就走老的 URL 兜底逻辑，行为跟改前 100% 一样
          const runOnTab = (targetTabId) => {
            chrome.scripting.executeScript({
              target: { tabId: targetTabId },
              world: "MAIN",
              func: (code, injectedArgs) => {
                try {
                  if (injectedArgs !== null && injectedArgs !== undefined) {
                    window.__ARGS__ = injectedArgs;
                  }
                  const result = (new Function(code))();
                  return result;
                } catch (e) {
                  return { __error: "Execution failed (CSP restriction or syntax error): " + e.message };
                }
              },
              args: [data.js_code, data.args !== undefined ? data.args : null]
            }, (results) => {
              if (chrome.runtime.lastError) {
                respond(null, chrome.runtime.lastError.message);
              } else {
                const res = results[0]?.result;
                if (res && typeof res === 'object' && res.__error) {
                  respond(null, res.__error);
                } else {
                  respond(res);
                }
              }
            });
          };
          if (data.tab_id) {
            // 显式指定 tab：直接用，找不到就报错
            chrome.tabs.get(data.tab_id, (tab) => {
              if (chrome.runtime.lastError || !tab) {
                respond(null, "tab_id " + data.tab_id + " not found: " + (chrome.runtime.lastError && chrome.runtime.lastError.message));
                return;
              }
              runOnTab(tab.id);
            });
          } else {
            // 老逻辑：URL 兜底
            chrome.tabs.query({}, (tabs) => {
              let targetTab = tabs.find(t => t.active && (t.url.includes('ozon.ru') || t.url.includes('1688.com'))) || tabs.find(t => t.url.includes('ozon.ru') || t.url.includes('1688.com'))
                           || tabs.find(t => t.active && t.url.startsWith('http'))
                           || tabs.find(t => t.url.startsWith('http'));
              if (!targetTab) { respond(null, "No web page tab found"); return; }
              runOnTab(targetTab.id);
            });
          }
        } else if (data.action === "cdp_human_click") {
          // 走 CDP Input.dispatchMouseEvent — 让 page 看到的 mouseEvent.isTrusted = true
          // 流程：找 ozon.ru active tab → attach (idempotent) → executeScript 算 target rect →
          //      mouseMoved 8-14 步轨迹 → mousePressed → mouseReleased → 保留 attached 给同商品后续 click
          //      caller 商品结束应调 cdp_detach 释放黄横条
          const sel = data.selector;
          if (!sel) { respond(null, "selector required"); return; }
          chrome.tabs.query({}, (allTabs) => {
            const tab = allTabs.find(t => t.active && (t.url.includes('ozon.ru') || t.url.includes('1688.com'))) ||
                        allTabs.find(t => t.url.includes('ozon.ru') || t.url.includes('1688.com')) ||
                        allTabs.find(t => t.active && t.url.startsWith('http'));
            if (!tab) { respond(null, "no target tab"); return; }
            const target = { tabId: tab.id };
            const doClick = () => {
              // 算 target rect (viewport 坐标) + 路径起点
              chrome.scripting.executeScript({
                target: { tabId: tab.id }, world: "MAIN",
                func: (selector) => {
                  // 通用诊断包 — no_target / target_hidden 都返回，便于查根因
                  const commonDiag = () => ({
                    url: location.href.slice(0, 200),
                    inner: window.innerWidth + 'x' + window.innerHeight,
                    visibilityState: document.visibilityState,
                    hasFocus: document.hasFocus(),
                    bodyCls: (document.body && document.body.className || '').slice(0, 120),
                    readyState: document.readyState,
                  });
                  const el = document.querySelector(selector);
                  if (!el) return { __error: 'no_target', diagnostic: commonDiag() };
                  const r = el.getBoundingClientRect();
                  if (r.width === 0 || r.height === 0) {
                    const cs = getComputedStyle(el);
                    return { __error: 'target_hidden', diagnostic: Object.assign(commonDiag(), {
                      elDisplay: cs.display, elVisibility: cs.visibility,
                      elOffsetParent: !!el.offsetParent,
                      elW: Math.round(r.width), elH: Math.round(r.height),
                      elX: Math.round(r.x), elY: Math.round(r.y),
                      headerSnippet: (document.querySelector('header') || { outerHTML: '' }).outerHTML.slice(0, 300),
                    })};
                  }
                  return {
                    tx: r.left + r.width / 2 + (Math.random() - 0.5) * Math.min(r.width, 20),
                    ty: r.top + r.height / 2 + (Math.random() - 0.5) * Math.min(r.height, 10),
                    sx: Math.random() * window.innerWidth,
                    sy: Math.random() * window.innerHeight,
                  };
                },
                args: [sel],
              }, (results) => {
                const coords = results && results[0] && results[0].result;
                if (!coords || coords.__error) {
                  const errName = (coords && coords.__error) || 'no_result';
                  const diagStr = coords && coords.diagnostic ? ' | diag: ' + JSON.stringify(coords.diagnostic) : '';
                  respond(null, "rect: " + errName + diagStr);
                  return;
                }
                const { tx, ty, sx, sy } = coords;
                const steps = 8 + Math.floor(Math.random() * 7);
                const stepMs = 25 + Math.floor(Math.random() * 25);
                // 视觉跟踪：page 内画红点 + CSS transition 滑到 target
                chrome.scripting.executeScript({
                  target: { tabId: tab.id }, world: "MAIN",
                  func: (sx, sy, tx, ty, ms) => {
                    let d = document.getElementById('__bot_cursor_dot');
                    if (!d) {
                      d = document.createElement('div');
                      d.id = '__bot_cursor_dot';
                      d.style.cssText = 'position:fixed;left:0;top:0;width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;background:rgba(255,0,0,0.8);border:2px solid white;box-shadow:0 0 6px rgba(0,0,0,0.5);pointer-events:none;z-index:2147483647;will-change:transform;';
                      document.body.appendChild(d);
                    }
                    d.style.transition = 'none';
                    d.style.transform = 'translate(' + sx + 'px,' + sy + 'px)';
                    void d.offsetHeight;
                    d.style.transition = 'transform ' + ms + 'ms cubic-bezier(0.4, 0, 0.2, 1)';
                    d.style.transform = 'translate(' + tx + 'px,' + ty + 'px)';
                  },
                  args: [sx, sy, tx, ty, steps * stepMs],
                }, () => { /* fire and forget */ });
                // 串行 dispatchMouseEvent 序列 (mouseMoved 轨迹 + mousePressed + mouseReleased)
                const sendOne = (i) => new Promise((resolve) => {
                  let x, y, type;
                  if (i <= steps) {
                    const t = i / steps;
                    const ease = t < 0.5 ? 2*t*t : -1 + (4-2*t)*t;
                    x = sx + (tx - sx) * ease + (Math.random() - 0.5) * 5;
                    y = sy + (ty - sy) * ease + (Math.random() - 0.5) * 5;
                    type = 'mouseMoved';
                  } else if (i === steps + 1) {
                    x = tx; y = ty; type = 'mousePressed';
                  } else {
                    x = tx; y = ty; type = 'mouseReleased';
                  }
                  const params = { type, x, y, button: type === 'mouseMoved' ? 'none' : 'left', buttons: type === 'mousePressed' ? 1 : 0, clickCount: type === 'mouseMoved' ? 0 : 1 };
                  chrome.debugger.sendCommand(target, "Input.dispatchMouseEvent", params, () => resolve());
                });
                (async () => {
                  for (let i = 0; i <= steps + 2; i++) {
                    await sendOne(i);
                    if (i <= steps) await new Promise(r => setTimeout(r, stepMs));
                  }
                  respond({ clicked: true, steps, target: { tx, ty }, tabId: tab.id });
                })();
              });
            };
            // attach（idempotent — 已 attached 就跳过）
            chrome.debugger.getTargets((targets) => {
              const already = (targets || []).find(t => t.tabId === tab.id && t.attached);
              if (already) { doClick(); return; }
              chrome.debugger.attach(target, "1.3", () => {
                if (chrome.runtime.lastError) {
                  respond(null, "attach failed: " + chrome.runtime.lastError.message);
                  return;
                }
                doClick();
              });
            });
          });
        } else if (data.action === "cdp_wheel") {
          // CDP Input.dispatchMouseEvent type=mouseWheel — isTrusted=true 真滚轮事件
          // 拆 3~6 步串行发，每步 deltaY 抖动避免均匀模式（反爬看 wheel timing 模式）
          const totalDeltaY = data.deltaY || 0;
          const totalDeltaX = data.deltaX || 0;
          const steps = data.steps || (3 + Math.floor(Math.random() * 4));  // 3~6 步
          const stepMs = data.stepMs || (40 + Math.floor(Math.random() * 40));  // 40~79ms/步
          chrome.tabs.query({}, (allTabs) => {
            const tab = allTabs.find(t => t.active && (t.url.includes('ozon.ru') || t.url.includes('1688.com'))) ||
                        allTabs.find(t => t.url.includes('ozon.ru') || t.url.includes('1688.com')) ||
                        allTabs.find(t => t.active && t.url.startsWith('http'));
            if (!tab) { respond(null, "no target tab"); return; }
            const target = { tabId: tab.id };
            const doWheel = async () => {
              const x = 400 + Math.floor(Math.random() * 200);
              const y = 300 + Math.floor(Math.random() * 200);
              const dyPerStep = totalDeltaY / steps;
              const dxPerStep = totalDeltaX / steps;
              for (let i = 0; i < steps; i++) {
                await new Promise(resolve => {
                  chrome.debugger.sendCommand(target, "Input.dispatchMouseEvent", {
                    type: 'mouseWheel',
                    x, y,
                    deltaX: dxPerStep + (Math.random() - 0.5) * 5,
                    deltaY: dyPerStep + (Math.random() - 0.5) * 10,
                    modifiers: 0,
                  }, () => resolve());
                });
                if (i < steps - 1) await new Promise(r => setTimeout(r, stepMs));
              }
              respond({ wheeled: true, steps, deltaY: totalDeltaY, deltaX: totalDeltaX });
            };
            chrome.debugger.getTargets((targets) => {
              const already = (targets || []).find(t => t.tabId === tab.id && t.attached);
              if (already) { doWheel(); return; }
              chrome.debugger.attach(target, "1.3", () => {
                if (chrome.runtime.lastError) {
                  respond(null, "attach failed: " + chrome.runtime.lastError.message);
                  return;
                }
                doWheel();
              });
            });
          });
        } else if (data.action === "cdp_detach") {
          // 释放所有当前 attached 的 tab — 商品流程结束调一次，让黄横条消失
          chrome.debugger.getTargets((targets) => {
            const attached = (targets || []).filter(t => t.attached && t.tabId);
            if (attached.length === 0) { respond({ detached: 0 }); return; }
            let remaining = attached.length;
            for (const t of attached) {
              chrome.debugger.detach({ tabId: t.tabId }, () => {
                remaining--;
                if (remaining === 0) respond({ detached: attached.length });
              });
            }
          });
        } else if (data.action === "show_progress") {
          // 通用进度显示
          // 主路径：chrome.action.setBadgeText — 跨 page navigate 不丢
          // 副路径：content_script dock — 留作 page 稳定时的额外视觉冗余（navigate 会销毁但不害事）
          // 参数 { text: '已完成 14/48', state: 'running'|'done'|'idle' }
          const text = String(data.text || '');
          const state = data.state || 'running';
          // === badge ===
          try {
            const m = text.match(/(\d+)\/(\d+)/);
            const fraction = m ? `${m[1]}/${m[2]}` : '';
            if (state === 'done') {
              chrome.action.setBadgeText({ text: '✓' });
              chrome.action.setBadgeBackgroundColor({ color: '#10b981' });
              setTimeout(() => chrome.action.setBadgeText({ text: '' }), 5000);
            } else if (state === 'idle' || !fraction) {
              chrome.action.setBadgeText({ text: '' });
            } else {
              chrome.action.setBadgeText({ text: fraction });
              chrome.action.setBadgeBackgroundColor({ color: '#2563eb' });
            }
          } catch (_) { /* badge 失败不致命 */ }
          // === dock 副路径（保留，page 稳定时可见）===
          chrome.tabs.query({}, (allTabs) => {
            const tab = allTabs.find(t => t.active && (t.url.includes('ozon.ru') || t.url.includes('1688.com'))) ||
                        allTabs.find(t => t.url.includes('ozon.ru') || t.url.includes('1688.com')) ||
                        allTabs.find(t => t.active && t.url.startsWith('http'));
            if (!tab) { respond({ delivered: true, badge: true, dock: false, reason: 'no_tab' }); return; }
            chrome.tabs.sendMessage(tab.id, { __rb: 'progress_update', text, state }, () => {
              const dockOk = !chrome.runtime.lastError;
              respond({ delivered: true, badge: true, dock: dockOk, tab_id: tab.id });
            });
          });
        }
        return;
      }

      // 错误消息
      if (data.type === "error") {
        broadcastToExtension({ type: "error", message: data.message });
        return;
      }
    } catch (e) {
      console.error("[RB] Failed to parse message:", e);
    }
  };

  ws.onclose = () => {
    console.log("[RB] Disconnected");
    connected = false;
    cachedDisplayName = ""; // 断连后清掉，避免重连成不同 agent 时 sidepanel 用旧名误导
    broadcastToExtension({ type: "status", connected: false });
    scheduleReconnect();
  };

  ws.onerror = (err) => {
    console.error("[RB] WebSocket error");
    connected = false;
  };
}

function scheduleReconnect() {
  clearTimeout(reconnectTimer);
  reconnectTimer = setTimeout(connect, 3000);
}

function sendToOpenClaw(payload) {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(payload));
    return true;
  }
  return false;
}

async function postJson(url, body, headers = {}) {
  const res = await fetch(url, {
    method: "POST",
    headers: { "content-type": "application/json", ...headers },
    body: JSON.stringify(body || {}),
  });
  const text = await res.text();
  let data = {};
  try { data = text ? JSON.parse(text) : {}; } catch (_) { data = { raw: text }; }
  if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${text.slice(0, 300)}`);
  return data;
}

async function blobToBase64(blob) {
  const buf = await blob.arrayBuffer();
  const bytes = new Uint8Array(buf);
  let binary = "";
  const chunkSize = 0x8000;
  for (let i = 0; i < bytes.length; i += chunkSize) {
    binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize));
  }
  return btoa(binary);
}

let imageOffscreenCreating = null;

async function ensureImageOffscreenDocument() {
  if (!chrome.offscreen || typeof chrome.offscreen.createDocument !== "function") {
    throw new Error("chrome.offscreen is not available");
  }
  if (typeof chrome.offscreen.hasDocument === "function" && await chrome.offscreen.hasDocument()) return;
  if (imageOffscreenCreating) return imageOffscreenCreating;
  imageOffscreenCreating = chrome.offscreen.createDocument({
    url: chrome.runtime.getURL("image-offscreen.html"),
    reasons: ["BLOBS"],
    justification: "Process marketplace product images with OpenCV.js before upload",
  }).finally(() => {
    imageOffscreenCreating = null;
  });
  return imageOffscreenCreating;
}

async function processImageTaskWithOpenCv(task) {
  await ensureImageOffscreenDocument();
  const response = await chrome.runtime.sendMessage({
    type: "rb_process_image_offscreen",
    task,
  });
  if (!response || response.ok !== true) {
    throw new Error((response && response.error) || "OpenCV offscreen image processing failed");
  }
  return response.result;
}

async function processVideoTaskWithOffscreen(task) {
  await ensureImageOffscreenDocument();
  const response = await chrome.runtime.sendMessage({
    type: "rb_process_video_offscreen",
    task,
  });
  if (!response || response.ok !== true) {
    throw new Error((response && response.error) || "offscreen video processing failed");
  }
  return response.result;
}

function canvasFromBitmap(bitmap) {
  const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
  const ctx = canvas.getContext("2d", { willReadFrequently: true });
  ctx.drawImage(bitmap, 0, 0);
  return { canvas, ctx };
}

function getAverageColor(ctx, x, y, w, h) {
  const sx = Math.min(ctx.canvas.width - 1, Math.max(0, Math.floor(x)));
  const sy = Math.min(ctx.canvas.height - 1, Math.max(0, Math.floor(y)));
  const sw = Math.max(1, Math.min(Math.floor(w), ctx.canvas.width - sx));
  const sh = Math.max(1, Math.min(Math.floor(h), ctx.canvas.height - sy));
  const data = ctx.getImageData(sx, sy, sw, sh).data;
  let r = 0, g = 0, b = 0, n = 0;
  for (let i = 0; i < data.length; i += 16) {
    r += data[i]; g += data[i + 1]; b += data[i + 2]; n++;
  }
  if (!n) return [245, 245, 245];
  return [Math.round(r / n), Math.round(g / n), Math.round(b / n)];
}

function inpaintBottomRightLite(canvas, ctx) {
  const w = canvas.width;
  const h = canvas.height;
  const x = Math.floor(w * 0.85);
  const y = Math.floor(h * 0.92);
  const sample = getAverageColor(ctx, Math.max(0, x - Math.floor(w * 0.12)), Math.max(0, y - Math.floor(h * 0.08)), Math.floor(w * 0.1), Math.floor(h * 0.06));
  ctx.save();
  ctx.fillStyle = `rgb(${sample[0]}, ${sample[1]}, ${sample[2]})`;
  ctx.fillRect(x, y, w - x, h - y);
  ctx.restore();
}

function mergeBoxes(boxes, gap) {
  const out = [];
  for (const box of boxes) {
    let merged = false;
    for (const cur of out) {
      const separate =
        box.x2 + gap < cur.x1 || cur.x2 + gap < box.x1 ||
        box.y2 + gap < cur.y1 || cur.y2 + gap < box.y1;
      if (!separate) {
        cur.x1 = Math.min(cur.x1, box.x1);
        cur.y1 = Math.min(cur.y1, box.y1);
        cur.x2 = Math.max(cur.x2, box.x2);
        cur.y2 = Math.max(cur.y2, box.y2);
        merged = true;
        break;
      }
    }
    if (!merged) out.push({ ...box });
  }
  return out;
}

function detectTextLikeBoxes(canvas, ctx) {
  const maxSide = 900;
  const scale = Math.min(1, maxSide / Math.max(canvas.width, canvas.height));
  const sw = Math.max(1, Math.round(canvas.width * scale));
  const sh = Math.max(1, Math.round(canvas.height * scale));
  const sample = scale < 1 ? new OffscreenCanvas(sw, sh) : canvas;
  const sctx = scale < 1 ? sample.getContext("2d", { willReadFrequently: true }) : ctx;
  if (scale < 1) sctx.drawImage(canvas, 0, 0, sw, sh);

  const img = sctx.getImageData(0, 0, sw, sh);
  const data = img.data;
  const lum = new Uint8Array(sw * sh);
  let sum = 0;
  for (let i = 0, p = 0; i < data.length; i += 4, p++) {
    const v = Math.round(data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114);
    lum[p] = v;
    sum += v;
  }
  const mean = sum / Math.max(1, lum.length);
  let variance = 0;
  for (let i = 0; i < lum.length; i++) {
    const d = lum[i] - mean;
    variance += d * d;
  }
  const std = Math.sqrt(variance / Math.max(1, lum.length));
  const edgeThreshold = Math.max(24, Math.min(58, std * 0.7));

  const edge = new Uint8Array(sw * sh);
  for (let y = 1; y < sh - 1; y++) {
    for (let x = 1; x < sw - 1; x++) {
      const p = y * sw + x;
      const gx = Math.abs(lum[p + 1] - lum[p - 1]);
      const gy = Math.abs(lum[p + sw] - lum[p - sw]);
      if (gx + gy > edgeThreshold) edge[p] = 1;
    }
  }

  const mask = new Uint8Array(sw * sh);
  const rx = Math.max(2, Math.round(sw * 0.006));
  const ry = Math.max(1, Math.round(sh * 0.003));
  for (let y = 1; y < sh - 1; y++) {
    for (let x = 1; x < sw - 1; x++) {
      if (!edge[y * sw + x]) continue;
      const x1 = Math.max(0, x - rx);
      const x2 = Math.min(sw - 1, x + rx);
      const y1 = Math.max(0, y - ry);
      const y2 = Math.min(sh - 1, y + ry);
      for (let yy = y1; yy <= y2; yy++) {
        const row = yy * sw;
        for (let xx = x1; xx <= x2; xx++) mask[row + xx] = 1;
      }
    }
  }

  const visited = new Uint8Array(sw * sh);
  const boxes = [];
  const qx = [];
  const qy = [];
  for (let y = 0; y < sh; y++) {
    for (let x = 0; x < sw; x++) {
      const start = y * sw + x;
      if (!mask[start] || visited[start]) continue;
      let head = 0;
      qx.length = 0; qy.length = 0;
      qx.push(x); qy.push(y); visited[start] = 1;
      let x1 = x, x2 = x, y1 = y, y2 = y, count = 0;
      while (head < qx.length) {
        const cx = qx[head];
        const cy = qy[head++];
        count++;
        if (cx < x1) x1 = cx;
        if (cx > x2) x2 = cx;
        if (cy < y1) y1 = cy;
        if (cy > y2) y2 = cy;
        const neighbors = [[cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1]];
        for (const [nx, ny] of neighbors) {
          if (nx < 0 || ny < 0 || nx >= sw || ny >= sh) continue;
          const np = ny * sw + nx;
          if (mask[np] && !visited[np]) {
            visited[np] = 1;
            qx.push(nx); qy.push(ny);
          }
        }
      }
      const bw = x2 - x1 + 1;
      const bh = y2 - y1 + 1;
      const area = bw * bh;
      const fill = count / Math.max(1, area);
      const imageArea = sw * sh;
      const wideEnough = bw >= Math.max(18, sw * 0.035);
      const tallEnough = bh >= Math.max(5, sh * 0.006);
      const notHuge = area < imageArea * 0.22 && bh < sh * 0.35 && bw < sw * 0.96;
      const textShape = fill > 0.05 && fill < 0.92 && bw / Math.max(1, bh) > 0.8;
      if (wideEnough && tallEnough && notHuge && textShape) {
        boxes.push({ x1, y1, x2, y2 });
      }
    }
  }

  let merged = boxes;
  for (let i = 0; i < 3; i++) merged = mergeBoxes(merged, Math.max(4, Math.round(sw * 0.012)));
  const padX = Math.max(4, Math.round(sw * 0.008));
  const padY = Math.max(3, Math.round(sh * 0.006));
  return merged.map((b) => ({
    x: Math.max(0, Math.round((b.x1 - padX) / scale)),
    y: Math.max(0, Math.round((b.y1 - padY) / scale)),
    w: Math.min(canvas.width, Math.round((b.x2 - b.x1 + 1 + padX * 2) / scale)),
    h: Math.min(canvas.height, Math.round((b.y2 - b.y1 + 1 + padY * 2) / scale)),
  })).filter((b) => b.w > 0 && b.h > 0);
}

function isEdgeRect(rect, canvas, marginRatio = 0.24) {
  const cx = rect.x + rect.w / 2;
  const cy = rect.y + rect.h / 2;
  return (
    cx <= canvas.width * marginRatio ||
    cx >= canvas.width * (1 - marginRatio) ||
    cy <= canvas.height * marginRatio ||
    cy >= canvas.height * (1 - marginRatio)
  );
}

function maskTextLikeRegions(canvas, ctx, options = {}) {
  const boxes = detectTextLikeBoxes(canvas, ctx);
  let kept = 0;
  for (const box of boxes) {
    if (options.edgeOnly && !isEdgeRect(box, canvas)) continue;
    const pad = Math.max(8, Math.round(Math.min(canvas.width, canvas.height) * 0.01));
    const sx = Math.max(0, box.x - pad);
    const sy = Math.max(0, box.y - pad);
    const sw = Math.min(canvas.width - sx, box.w + pad * 2);
    const sh = Math.min(canvas.height - sy, box.h + pad * 2);
    const top = getAverageColor(ctx, sx, Math.max(0, sy - sh), sw, Math.max(2, Math.min(sh, sy)));
    const bottom = getAverageColor(ctx, sx, Math.min(canvas.height - 1, sy + sh), sw, Math.max(2, Math.min(sh, canvas.height - sy - sh)));
    const left = getAverageColor(ctx, Math.max(0, sx - sw), sy, Math.max(2, Math.min(sw, sx)), sh);
    const right = getAverageColor(ctx, Math.min(canvas.width - 1, sx + sw), sy, Math.max(2, Math.min(sw, canvas.width - sx - sw)), sh);
    const color = [
      Math.round((top[0] + bottom[0] + left[0] + right[0]) / 4),
      Math.round((top[1] + bottom[1] + left[1] + right[1]) / 4),
      Math.round((top[2] + bottom[2] + left[2] + right[2]) / 4),
    ];
    ctx.save();
    ctx.fillStyle = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
    ctx.fillRect(sx, sy, sw, sh);
    ctx.restore();
    kept++;
  }
  return kept;
}

function borderCanvas(canvas, ctx, op) {
  const parts = String(op || "").split(":");
  const px = Math.max(0, parseInt(parts[1] || "50", 10) || 50);
  if (!px) return { canvas, ctx };
  let color = parts[2] || "auto";
  if (color === "auto") {
    const avg = getAverageColor(ctx, 0, 0, canvas.width, canvas.height).map(v => Math.min(255, v + 60));
    color = `rgb(${avg[0]}, ${avg[1]}, ${avg[2]})`;
  }
  const next = new OffscreenCanvas(canvas.width + px * 2, canvas.height + px * 2);
  const nctx = next.getContext("2d", { willReadFrequently: true });
  nctx.fillStyle = color;
  nctx.fillRect(0, 0, next.width, next.height);
  nctx.drawImage(canvas, px, px);
  return { canvas: next, ctx: nctx };
}

function watermarkCanvas(canvas, ctx, op) {
  const text = String(op || "").slice("watermark:".length) || "ecommclaw";
  const fontSize = Math.max(8, Math.min(18, Math.floor(canvas.width / 90)));
  ctx.save();
  ctx.font = `${fontSize}px sans-serif`;
  ctx.fillStyle = "rgba(255,255,255,0.32)";
  ctx.textBaseline = "bottom";
  const metrics = ctx.measureText(text);
  ctx.fillText(text, canvas.width - metrics.width - 12, canvas.height - 12);
  ctx.textBaseline = "middle";
  ctx.fillText(text, Math.max(12, canvas.width / 2 - metrics.width / 2), canvas.height / 2);
  ctx.restore();
}

async function processImageTaskInBrowser(task) {
  const ops = Array.isArray(task.ops) ? task.ops : [];
  if (ops.includes("ocr_inpaint_text") || ops.includes("ocr_inpaint_text_edges")) {
    try {
      return await processImageTaskWithOpenCv(task);
    } catch (e) {
      console.warn("[RB image-task] OpenCV offscreen failed, falling back to lite mode:", e && e.message);
    }
  }
  const res = await fetch(task.source_url, { credentials: "omit", cache: "no-store" });
  if (!res.ok) throw new Error(`image fetch failed ${res.status}: ${task.source_url}`);
  const sourceBlob = await res.blob();
  const bitmap = await createImageBitmap(sourceBlob);
  let { canvas, ctx } = canvasFromBitmap(bitmap);
  for (const op of ops) {
    if (op === "download" || !op) continue;
    if (op === "inpaint_bottom_right") {
      inpaintBottomRightLite(canvas, ctx);
    } else if (op === "ocr_inpaint_text") {
      maskTextLikeRegions(canvas, ctx);
    } else if (op === "ocr_inpaint_text_edges") {
      maskTextLikeRegions(canvas, ctx, { edgeOnly: true });
    } else if (String(op).startsWith("border:")) {
      const next = borderCanvas(canvas, ctx, op);
      canvas = next.canvas;
      ctx = next.ctx;
    } else if (String(op).startsWith("watermark:")) {
      watermarkCanvas(canvas, ctx, op);
    } else if (String(op).startsWith("save:")) {
      continue;
    } else {
      console.warn("[RB image-task] unsupported op skipped:", op);
    }
  }
  const outBlob = await canvas.convertToBlob({ type: "image/jpeg", quality: 0.88 });
  return {
    content_base64: await blobToBase64(outBlob),
    width: canvas.width,
    height: canvas.height,
  };
}

async function claimAndProcessImageTasks() {
  if (!TENANT_ID || imageTaskBusy) return;
  imageTaskBusy = true;
  try {
    const claimed = await postJson(`${OZON_OPS_API_BASE}/image-tasks/claim`, {
      worker_id: IMAGE_TASK_WORKER_ID,
      limit: IMAGE_TASK_LIMIT,
      lease_ms: IMAGE_TASK_LEASE_MS,
    }, { "x-agent-id": TENANT_ID });
    const tasks = Array.isArray(claimed.tasks) ? claimed.tasks : [];
    const handleOneImageTask = async (task) => {
      try {
        const processed = await processImageTaskInBrowser(task);
        await postJson(`${OZON_OPS_API_BASE}/image-tasks/${task.id}/upload`, {
          worker_id: IMAGE_TASK_WORKER_ID,
          content_base64: processed.content_base64,
          width: processed.width,
          height: processed.height,
        }, { "x-agent-id": TENANT_ID });
        console.log("[RB image-task] done", task.id);
      } catch (e) {
        const retry = Number(task.attempts || 0) < 3;
        console.warn("[RB image-task] failed", task.id, e && e.message);
        await postJson(`${OZON_OPS_API_BASE}/image-tasks/${task.id}/fail`, {
          worker_id: IMAGE_TASK_WORKER_ID,
          error: e && e.message ? e.message : String(e),
          retry,
        }, { "x-agent-id": TENANT_ID }).catch(err => console.warn("[RB image-task] fail report failed", err.message));
      }
    };
    await Promise.all(tasks.map(task => handleOneImageTask(task)));
  } catch (e) {
    console.warn("[RB image-task] claim loop error:", e && e.message);
  } finally {
    imageTaskBusy = false;
  }
}

function bytesToBase64(bytes) {
  let binary = "";
  const chunkSize = 0x8000;
  for (let i = 0; i < bytes.length; i += chunkSize) {
    binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize));
  }
  return btoa(binary);
}

function even(n) {
  const v = Math.max(2, Math.round(n));
  return v % 2 === 0 ? v : v - 1;
}

function pickVideoSize(bitmap, aspect) {
  if (aspect === "3:4") return { width: 720, height: 960 };
  const maxSide = 960;
  const ratio = bitmap.width / Math.max(1, bitmap.height);
  if (ratio >= 1) {
    return { width: even(maxSide), height: even(maxSide / ratio) };
  }
  return { width: even(maxSide * ratio), height: even(maxSide) };
}

function drawVideoFrame(ctx, canvas, bitmap, aspect, frameIndex, frameCount) {
  ctx.fillStyle = "#f6f7f9";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  const progress = frameCount > 1 ? frameIndex / (frameCount - 1) : 0;
  const zoom = 1.0 + progress * 0.018;
  if (aspect === "3:4") {
    const targetRatio = canvas.width / canvas.height;
    const sourceRatio = bitmap.width / Math.max(1, bitmap.height);
    let sx = 0, sy = 0, sw = bitmap.width, sh = bitmap.height;
    if (sourceRatio > targetRatio) {
      sw = Math.round(bitmap.height * targetRatio);
      sx = Math.round((bitmap.width - sw) / 2);
    } else {
      sh = Math.round(bitmap.width / targetRatio);
      sy = Math.round((bitmap.height - sh) / 2);
    }
    const dw = canvas.width * zoom;
    const dh = canvas.height * zoom;
    ctx.drawImage(bitmap, sx, sy, sw, sh, (canvas.width - dw) / 2, (canvas.height - dh) / 2, dw, dh);
    return;
  }
  const dw = canvas.width * zoom;
  const dh = canvas.height * zoom;
  ctx.drawImage(bitmap, (canvas.width - dw) / 2, (canvas.height - dh) / 2, dw, dh);
}

async function generateVideoTaskInBrowser(task) {
  if (!globalThis.VideoEncoder || !globalThis.VideoFrame || !globalThis.OffscreenCanvas) {
    return await processVideoTaskWithOffscreen(task);
  }
  const res = await fetch(task.source_url, { credentials: "omit", cache: "no-store" });
  if (!res.ok) throw new Error(`video source image fetch failed ${res.status}: ${task.source_url}`);
  const sourceBlob = await res.blob();
  const bitmap = await createImageBitmap(sourceBlob);
  const aspect = task.aspect || "source";
  const size = pickVideoSize(bitmap, aspect);
  const fps = Math.max(1, Math.min(60, Number(task.fps || 24)));
  const durationSec = Math.max(1, Math.min(60, Number(task.duration_sec || 10)));
  const frameCount = Math.max(1, Math.round(fps * durationSec));
  const config = {
    codec: "avc1.42E01F",
    width: size.width,
    height: size.height,
    bitrate: 1200000,
    framerate: fps,
    avc: { format: "avc" },
  };
  const support = await VideoEncoder.isConfigSupported(config);
  if (!support.supported) throw new Error("H.264 WebCodecs config is not supported");

  const target = new ArrayBufferTarget();
  const muxer = new Muxer({
    target,
    video: { codec: "avc", width: size.width, height: size.height, frameRate: fps },
    fastStart: "in-memory",
    firstTimestampBehavior: "strict",
  });
  const canvas = new OffscreenCanvas(size.width, size.height);
  const ctx = canvas.getContext("2d");
  let encoderError = null;
  const encoder = new VideoEncoder({
    output: (chunk, meta) => muxer.addVideoChunk(chunk, meta),
    error: (e) => { encoderError = e; },
  });
  encoder.configure(config);
  for (let i = 0; i < frameCount; i++) {
    drawVideoFrame(ctx, canvas, bitmap, aspect, i, frameCount);
    const frame = new VideoFrame(canvas, {
      timestamp: Math.round(i * 1_000_000 / fps),
      duration: Math.round(1_000_000 / fps),
    });
    encoder.encode(frame, { keyFrame: i % Math.max(1, fps * 2) === 0 });
    frame.close();
    if (encoderError) throw encoderError;
  }
  await encoder.flush();
  encoder.close();
  if (encoderError) throw encoderError;
  muxer.finalize();
  const bytes = new Uint8Array(target.buffer);
  return {
    content_base64: bytesToBase64(bytes),
    width: size.width,
    height: size.height,
    size_bytes: bytes.byteLength,
  };
}

async function claimAndProcessVideoTasks() {
  if (!TENANT_ID || videoTaskBusy) return;
  videoTaskBusy = true;
  try {
    const claimed = await postJson(`${OZON_OPS_API_BASE}/video-tasks/claim`, {
      worker_id: VIDEO_TASK_WORKER_ID,
      limit: VIDEO_TASK_LIMIT,
      lease_ms: VIDEO_TASK_LEASE_MS,
    }, { "x-agent-id": TENANT_ID });
    const tasks = Array.isArray(claimed.tasks) ? claimed.tasks : [];
    const handleOneVideoTask = async (task) => {
      try {
        const processed = await generateVideoTaskInBrowser(task);
        await postJson(`${OZON_OPS_API_BASE}/video-tasks/${task.id}/upload`, {
          worker_id: VIDEO_TASK_WORKER_ID,
          content_base64: processed.content_base64,
          width: processed.width,
          height: processed.height,
          size_bytes: processed.size_bytes,
        }, { "x-agent-id": TENANT_ID });
        console.log("[RB video-task] done", task.id, processed.width + "x" + processed.height, processed.size_bytes);
      } catch (e) {
        const retry = Number(task.attempts || 0) < 3;
        console.warn("[RB video-task] failed", task.id, e && e.message);
        await postJson(`${OZON_OPS_API_BASE}/video-tasks/${task.id}/fail`, {
          worker_id: VIDEO_TASK_WORKER_ID,
          error: e && e.message ? e.message : String(e),
          retry,
        }, { "x-agent-id": TENANT_ID }).catch(err => console.warn("[RB video-task] fail report failed", err.message));
      }
    };
    await Promise.all(tasks.map(task => handleOneVideoTask(task)));
  } catch (e) {
    console.warn("[RB video-task] claim loop error:", e && e.message);
  } finally {
    videoTaskBusy = false;
  }
}

function startBrowserImageWorker() {
  if (imageTaskTimer) return;
  imageTaskTimer = setInterval(claimAndProcessImageTasks, IMAGE_TASK_CLAIM_MS);
  claimAndProcessImageTasks();
}

function startBrowserVideoWorker() {
  if (videoTaskTimer) return;
  videoTaskTimer = setInterval(claimAndProcessVideoTasks, VIDEO_TASK_CLAIM_MS);
  claimAndProcessVideoTasks();
}

// ─── 心跳保活（每 30 秒） ───

setInterval(() => {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "ping" }));
  }
}, 30000);

// ─── 与 Side Panel / Content Script 通信 ───

function broadcastToExtension(msg) {
  chrome.runtime.sendMessage(msg).catch(() => {
    // side panel 可能未打开，忽略
  });
}

// 接收来自 side panel 的消息
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.target !== "background") return;

  
  // ── Workflow 回调：viewer-bridge 转发过来，直接走 WebSocket 发服务端，不过 LLM ──
  if (msg.action === "workflow_callback") {
    console.log("[RB] Sending workflow_callback to server:", msg.payload);
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({
        type: "workflow_callback",
        task_id: msg.payload.task_id,
        skill_id: msg.payload.skill_id,
        payload: msg.payload,
      }));
    } else {
      broadcastToExtension({ type: "workflow_result", task_id: msg.payload.task_id, error: "未连接到服务端，请检查网络" });
    }
    return true;
  }

  // ── 二次交互 chain：viewer-bridge 转发过来，启动【新】 skill 的 Phase 1，不过 LLM ──
  // 触发链路：技能 HTML window.__ocStartSkill(...) → sandbox postMessage __oc_start_skill__
  //          → viewer-bridge → 这里 → ws → monitor.ts handleStartSkill → run_tenant_skill (Phase 1)
  if (msg.action === "start_skill") {
    console.log("[RB] Starting new skill (chain) on server:", msg.skill_id, msg.payload);
    if (ws && ws.readyState === WebSocket.OPEN) {
      // 历史里写一条 system 提示让 LLM 知道任务推进了（仅日志，LLM 不参与决策）
      if (cachedHistory) {
        cachedHistory.push({
          id: Date.now().toString(36),
          role: "system",
          text: `🔗 自动进入下一步: ${msg.skill_id}`,
          timestamp: Date.now(),
        });
        broadcastToExtension({ type: "history", messages: cachedHistory });
      }
      ws.send(JSON.stringify({
        type: "start_skill",
        skill_id: msg.skill_id,
        payload: msg.payload || {},
      }));
    } else {
      broadcastToExtension({ type: "workflow_result", task_id: "", error: "未连接到服务端，请检查网络" });
    }
    return true;
  }

  if (msg.action === "dispatch_atomic_skill") {
    console.log("[RB] Dispatching atomic skill to OpenClaw:", msg.skill_id, msg.payload);
    // Send a message to the OpenClaw backend as if the user typed a slash command
    // We format it nicely so the AI understands it's an atomic action request.
    const textCommand = `/[ATOMIC_CALL] skill_id=${msg.skill_id} payload=${JSON.stringify(msg.payload)}`;
    
    if (ws && ws.readyState === WebSocket.OPEN) {
      // Append to local history for user visibility
      if (cachedHistory) {
        cachedHistory.push({ 
          id: Date.now().toString(36), 
          role: "user", 
          text: `⚡ 执行操作: ${msg.skill_id} \n\`\`\`json\n${JSON.stringify(msg.payload, null, 2)}\n\`\`\``, 
          timestamp: Date.now() 
        });
        broadcastToExtension({ type: "history", messages: cachedHistory });
      }
      ws.send(JSON.stringify({
        type: "client_message",
        text: textCommand,
        tenantId: TENANT_ID,
        clientId: CLIENT_ID,
      }));
    }
    if (sendResponse) sendResponse({ status: "dispatched" });
    return true;
  }

if (msg.action === "switch_ozon_shop") {
    // 零延迟切换 Ozon 店铺：找到 Ozon 标签页并注入 cookie，然后跳转
    chrome.tabs.query({ url: "*://seller.ozon.ru/*" }, (tabs) => {
      let targetTab = tabs[0];
      if (targetTab) {
        chrome.scripting.executeScript({
          target: { tabId: targetTab.id },
          func: (shopId, url) => {
            document.cookie = "sc_company_id=" + shopId + "; path=/; domain=.ozon.ru";
            location.href = url;
          },
          args: [msg.shopId, msg.url]
        }).then(() => {
          chrome.tabs.update(targetTab.id, { active: true });
          chrome.windows.update(targetTab.windowId, { focused: true });
        }).catch(err => console.error("Error switching shop:", err));
      } else {
        // 如果没有打开过 Ozon 标签页，需要先打开一个空白页
        chrome.tabs.create({ url: "https://seller.ozon.ru/app/dashboard/main", active: true }, (newTab) => {
           // Wait a bit for navigation to start, then inject cookie
           setTimeout(() => {
             chrome.scripting.executeScript({
               target: { tabId: newTab.id },
               func: (shopId, url) => {
                 document.cookie = "sc_company_id=" + shopId + "; path=/; domain=.ozon.ru";
                 location.href = url;
               },
               args: [msg.shopId, msg.url]
             }).catch(err => console.error("Error switching shop:", err));
           }, 1500);
        });
      }
    });
    if (sendResponse) sendResponse({ status: "switching" });
    return true;
  }


  // 更新租户 ID 并强制重连
  if (msg.action === "update_tenant_id") {
    TENANT_ID = msg.tenantId;
    console.log("[RB] Updating Tenant ID and reconnecting:", TENANT_ID);
    startBrowserImageWorker();
    startBrowserVideoWorker();
    if (ws) {
      ws.close(); // 触发 onclose，继而触发重连
    } else {
      connect();
    }
    return;
  }

  if (msg.action === "send_message") {
    // 用户发送消息时，同步追加到缓存（去掉 [System Context] 后缀保持干净）
    const cleanText = msg.text.split("\n[System Context")[0];
    if (cachedHistory) {
      cachedHistory.push({ id: Date.now().toString(36), role: "user", text: cleanText, timestamp: Date.now() });
    }
    const ok = sendToOpenClaw({
      type: "message",
      text: msg.text,
      context: msg.context || undefined,
      images: msg.images || undefined,
    });
    sendResponse({ ok, connected });
    return;
  }

  if (msg.action === "get_status") {
    sendResponse({ connected, clientId: CLIENT_ID, wsUrl: WS_URL, history: cachedHistory, displayName: cachedDisplayName });
    return;
  }

  if (msg.action === "get_shops_matrix") {
    const ok = sendToOpenClaw({ type: "get_shops_matrix" });
    sendResponse({ ok, connected });
    return;
  }

  if (msg.action === "set_shops_matrix") {
    // 入参校验：必须有 shops 数组（A 式扁平 schema）
    if (!Array.isArray(msg.shops)) {
      sendResponse({ ok: false, error: "msg.shops 必须是数组" });
      return;
    }
    const ok = sendToOpenClaw({ type: "set_shops_matrix", shops: msg.shops });
    sendResponse({ ok, connected });
    return;
  }

  if (msg.action === "get_custom_models") {
    const ok = sendToOpenClaw({ type: "get_custom_models" });
    sendResponse({ ok, connected });
    return;
  }

  if (msg.action === "delete_custom_model") {
    const ok = sendToOpenClaw({ type: "delete_custom_model", model_id: msg.model_id });
    sendResponse({ ok, connected });
    return;
  }

  if (msg.action === "set_custom_model") {
    const ok = sendToOpenClaw({
      type: "set_custom_model",
      model_id: msg.model_id,
      provider_base_url: msg.provider_base_url,
      provider_api_key: msg.provider_api_key,
      provider_api: msg.provider_api,
    });
    sendResponse({ ok, connected });
    return;
  }

  // 直接 ws 上传图片到当前 agent 的 uploads/，回复 image_uploaded（含完整 URL）
  // 调用方传 reqId 以异步等待结果（image_uploaded 推送会带回相同 reqId）
  if (msg.action === "upload_image") {
    const ok = sendToOpenClaw({ type: "upload_image", reqId: msg.reqId, base64: msg.base64 });
    sendResponse({ ok, connected });
    return;
  }

  if (msg.action === "get_page_context") {
    // 向当前活跃 Tab 的 content script 请求页面上下文
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      if (tabs[0]) {
        chrome.tabs.sendMessage(tabs[0].id, { action: "extract_context" }, (response) => {
          sendResponse(response || { text: "", url: "" });
        });
      } else {
        sendResponse({ text: "", url: "" });
      }
    });
    return true; // 异步响应
  }
});

// ─── 点击扩展图标打开 Side Panel ───
// 官方推荐写法：用 setPanelBehavior 让 Chrome 自己处理点图标 → 开 panel
// 旧写法 chrome.action.onClicked + chrome.sidePanel.open() 在 MV3 service worker
// 冷启动时会丢首次点击（user gesture 上下文已 expired），且 chrome 对 open() 的
// 手势检测会偶发拒绝。setPanelBehavior 绑定是声明式的，Chrome 直接路由，无 gesture 限制。
chrome.sidePanel
  .setPanelBehavior({ openPanelOnActionClick: true })
  .catch((e) => console.error('[RB] setPanelBehavior failed:', e));

// content_script 内"小耳朵"被点击 → 请求开 sidepanel。
// user gesture 透过 chrome.runtime.sendMessage 透传，sidePanel.open 在 Chrome 116+ 允许此场景。
chrome.runtime.onMessage.addListener((msg, sender) => {
  if (msg && msg.__rb === 'open_sidepanel' && sender.tab) {
    chrome.sidePanel.open({ tabId: sender.tab.id })
      .catch((e) => console.error('[RB] sidePanel.open from ear failed:', e));
  }
});

// ─── 启动连接 ───

loadTenantId(() => {
  startBrowserImageWorker();
  startBrowserVideoWorker();
  connect();
});
console.log("[RB] Rich Browser background started, clientId:", CLIENT_ID);
