(function pddTableExporter() {
  const EXTENSION_VERSION = "1.2.1";
  const BUTTON_BASE_ID = "pdd-table-exporter-button";
  const TOAST_ID = "pdd-table-exporter-toast";
  const NETWORK_CAPTURE_STORAGE_KEY = "pddTableExporterNetworkCaptureEnabled";
  const DEFAULT_P1_METRIC_HEADERS = [
    "成交花费(元)",
    "实际投产比",
    "成交笔数",
    "曝光量",
    "点击量",
    "点击率",
    "直接成交笔数",
    "每笔成交花费(元)"
  ];
  const runtime = {
    syncTimer: null,
    domObserver: null,
    syncQueued: false,
    decodeCache: new Map(),
    networkHookReady: false,
    networkCaptureEnabled: false,
    networkRecords: [],
    networkSeenTotal: 0,
    networkInstalledAt: "",
    pageRequestResolvers: new Map()
  };

  const networkCaptureAction = {
    key: "network-capture",
    label: "开始抓接口",
    run: toggleNetworkCapture
  };
  const networkExportAction = {
    key: "network-export",
    label: "导出接口JSON",
    run: exportNetworkCaptureJson
  };
  const quantCsvExportAction = {
    key: "quant-csv-export",
    label: "导出量化CSV",
    run: exportQuantCsvFromNetworkRecords
  };
  const adHourlyCsvExportAction = {
    key: "ad-hourly-csv-export",
    label: "导出昨日分时CSV",
    run: () => exportAdHourlyCsvFromNetworkRecords({ scope: "current" })
  };
  const adHourlyCachedCsvExportAction = {
    key: "ad-hourly-cached-csv-export",
    label: "导出已抓昨日分时CSV",
    run: () => exportAdHourlyCsvFromNetworkRecords({ scope: "cached" })
  };
  const adBatchYesterdayHourlyCsvAction = {
    key: "ad-batch-yesterday-hourly-csv",
    label: "批量导出昨日分时CSV",
    run: batchExportYesterdayHourlyCsvForCurrentList
  };
  const adCurrentFetchAction = {
    key: "ad-current-fetch",
    label: "补采车当前选择",
    run: fetchAdCurrentSelection
  };
  const adThirtyDayFetchAction = {
    key: "ad-thirty-day-fetch",
    label: "自动采车30天",
    run: autoFetchAdThirtyDays
  };
  const adBatchFetchAction = {
    key: "ad-batch-fetch",
    label: "批量采车ID",
    run: batchFetchPromotionIds
  };
  const shopHourlyFetchAction = {
    key: "shop-hourly-fetch",
    label: "补采店铺分时",
    run: fetchShopHourlyForKnownGoods
  };
  const shopDailyFetchAction = {
    key: "shop-daily-fetch",
    label: "补采店铺日级",
    run: fetchShopDailyForKnownGoods
  };
  const endpointScoutAction = {
    key: "endpoint-scout",
    label: "侦察历史接口",
    run: scoutLoadedEndpointScripts
  };

  const pageConfigs = [
    {
      key: "promotion",
      match: () =>
        window.location.hostname === "yingxiao.pinduoduo.com" &&
        /^\/goods\/promotion\/list\/?$/.test(window.location.pathname),
      findAnchor: findPromotionAnchor,
      actions: [
        {
          key: "current",
          label: "导出 CSV",
          exportRows: exportPromotionPage
        }
      ]
    },
    {
      key: "goods-effect",
      match: () =>
        window.location.hostname === "mms.pinduoduo.com" &&
        /^\/sycm\/goods_effect\/?$/.test(window.location.pathname),
      findAnchor: findGoodsEffectAnchor,
      actions: [
        {
          key: "current",
          label: "导出当前页明细",
          exportRows: exportGoodsEffectPage
        }
      ]
    }
  ];

  boot();

  function boot() {
    window.__pddTableExporterDebug = () => JSON.parse(JSON.stringify(runtime.lastPromotionDebug || {}));
    ensureObserver();
    startRouteLoop();
    syncButtons();
  }

  function shouldAutoCaptureNetwork() {
    return /^(yingxiao|mms)\.pinduoduo\.com$/.test(window.location.hostname);
  }

  function ensureObserver() {
    if (runtime.domObserver) {
      return;
    }

    runtime.domObserver = new MutationObserver(() => {
      queueSync();
    });

    runtime.domObserver.observe(document.documentElement, {
      childList: true,
      subtree: true
    });
  }

  function startRouteLoop() {
    if (!runtime.syncTimer) {
      runtime.syncTimer = window.setInterval(syncButtons, 1200);
    }

    window.addEventListener("hashchange", syncButtons);
    window.addEventListener("popstate", syncButtons);
  }

  function queueSync() {
    if (runtime.syncQueued) {
      return;
    }

    runtime.syncQueued = true;
    window.requestAnimationFrame(() => {
      runtime.syncQueued = false;
      syncButtons();
    });
  }

  function syncButtons() {
    pageConfigs.forEach((config) => {
      if (!config.match()) {
        removeButton(config.key);
        return;
      }

      const anchor = config.findAnchor();
      if (!anchor) {
        removeButton(config.key);
        return;
      }

      ensureButton(config, anchor);
    });
  }

  function ensureButton(config, anchor) {
    const buttonId = getButtonId(config.key);
    let wrapper = document.getElementById(buttonId);

    if (!wrapper) {
      wrapper = document.createElement("div");
      wrapper.id = buttonId;
      wrapper.style.display = "inline-flex";
      wrapper.style.gap = "8px";
      wrapper.style.alignItems = "center";
      wrapper.style.marginLeft = "12px";
      wrapper.style.verticalAlign = "middle";
      ["pointerdown", "pointerup", "mousedown", "mouseup"].forEach((eventName) => {
        wrapper.addEventListener(eventName, (event) => stopPageCloseEvent(event, { preventDefault: false }), true);
      });
      config.actions.forEach((action) => {
        const button = createExportButton(action);
        button.addEventListener("click", async (event) => {
          stopPageCloseEvent(event);
          await runExport(action, button);
        });
        wrapper.appendChild(button);
      });
    }

    if (config.placement === "fixed") {
      applyFixedToolbarStyle(wrapper, config);
      if (wrapper.parentElement !== document.body) {
        document.body.appendChild(wrapper);
      }
      return;
    }

    if (config.key === "promotion-detail") {
      applyPromotionDetailInlineToolbarStyle(wrapper, anchor);
      if (wrapper.previousElementSibling !== anchor || wrapper.parentElement !== anchor.parentElement) {
        anchor.insertAdjacentElement("afterend", wrapper);
      }
      return;
    }

    applyNormalInlineToolbarStyle(wrapper);
    if (wrapper.previousElementSibling !== anchor || wrapper.parentElement !== anchor.parentElement) {
      anchor.insertAdjacentElement("afterend", wrapper);
    }
  }

  function applyNormalInlineToolbarStyle(wrapper) {
    Object.assign(wrapper.style, {
      position: "static",
      left: "auto",
      top: "auto",
      right: "auto",
      maxWidth: "none",
      maxHeight: "none",
      overflow: "visible",
      zIndex: "auto",
      display: "inline-flex",
      flexWrap: "wrap",
      gap: "8px",
      alignItems: "center",
      marginLeft: "12px",
      padding: "0",
      borderRadius: "0",
      background: "transparent",
      boxShadow: "none",
      backdropFilter: "none",
      pointerEvents: "auto"
    });
  }

  function applyPromotionDetailInlineToolbarStyle(wrapper, anchor) {
    applyNormalInlineToolbarStyle(wrapper);
    wrapper.dataset.pddToolbarPlacement = "promotion-budget-title";
    const parent = anchor.parentElement;
    if (parent) {
      parent.style.display = "flex";
      parent.style.alignItems = "center";
      parent.style.flexWrap = "wrap";
      parent.style.gap = "10px";
    }
    Object.assign(anchor.style, {
      display: "inline-flex",
      alignItems: "center",
      marginRight: "0",
      whiteSpace: "nowrap"
    });
    Object.assign(wrapper.style, {
      marginLeft: "6px",
      rowGap: "8px",
      flex: "1 1 520px",
      minWidth: "0",
      maxWidth: "100%"
    });
    Array.from(wrapper.querySelectorAll('button[data-role="export-button"]')).forEach((button) => {
      button.style.height = "34px";
      button.style.padding = "0 12px";
      button.style.fontSize = "13px";
      button.style.whiteSpace = "nowrap";
    });
  }

  function applyFixedToolbarStyle(wrapper, config) {
    const style = config.getFixedStyle?.() || {};
    Object.assign(wrapper.style, {
      position: "fixed",
      left: style.left || "360px",
      top: style.top || "88px",
      right: style.right || "auto",
      maxWidth: style.maxWidth || "calc(100vw - 420px)",
      maxHeight: style.maxHeight || "calc(100vh - 96px)",
      overflow: "visible",
      zIndex: "2147483647",
      display: "inline-flex",
      flexWrap: "wrap",
      gap: "8px",
      alignItems: "center",
      marginLeft: "0",
      padding: "6px",
      borderRadius: "8px",
      background: "rgba(255, 255, 255, 0.92)",
      boxShadow: "0 8px 24px rgba(15, 23, 42, 0.14)",
      backdropFilter: "blur(8px)",
      pointerEvents: "auto"
    });

    const fixedButtons = Array.from(wrapper.querySelectorAll('button[data-role="export-button"]'));
    fixedButtons.forEach((button) => {
      button.style.height = style.compact ? "30px" : "34px";
      button.style.padding = style.compact ? "0 10px" : "0 12px";
      button.style.fontSize = style.compact ? "12px" : "13px";
      button.style.whiteSpace = "nowrap";
    });
  }

  function stopPageCloseEvent(event, options = {}) {
    if (!event) {
      return;
    }
    if (options.preventDefault !== false) {
      event.preventDefault?.();
    }
    event.stopPropagation?.();
  }

  function removeButton(key) {
    const existing = document.getElementById(getButtonId(key));
    if (existing) {
      existing.remove();
    }
  }

  function createExportButton(action) {
    const button = document.createElement("button");
    button.type = "button";
    button.textContent = action.label;
    button.dataset.role = "export-button";
    button.dataset.actionKey = action.key || "";
    button.style.cssText = [
      "height: 36px",
      "padding: 0 16px",
      "border: 1px solid #4f63ff",
      "border-radius: 8px",
      "background: #4f63ff",
      "color: #fff",
      "font-size: 14px",
      "font-weight: 500",
      "letter-spacing: 0",
      "cursor: pointer",
      "line-height: 1",
      "white-space: nowrap",
      "touch-action: manipulation",
      "transition: background 160ms ease, border-color 160ms ease, box-shadow 160ms ease",
      "box-shadow: 0 2px 7px rgba(79, 99, 255, 0.2)"
    ].join("; ");
    button.addEventListener("mouseenter", () => {
      button.style.background = "#4054ee";
      button.style.borderColor = "#4054ee";
      button.style.boxShadow = "0 3px 9px rgba(79, 99, 255, 0.26)";
    });
    button.addEventListener("mouseleave", () => {
      if (!button.disabled) {
        button.style.background = "#4f63ff";
        button.style.borderColor = "#4f63ff";
        button.style.boxShadow = "0 2px 7px rgba(79, 99, 255, 0.2)";
      }
    });
    return button;
  }

  async function runExport(action, button) {
    if (button.disabled) {
      return;
    }
    if (runtime.activeActionKey && runtime.activeActionKey !== action.key) {
      showToast(`正在执行「${runtime.activeActionLabel || "任务"}」，请等它完成后再导出。`, 6000);
      return;
    }

    const defaultText = action.label;
    runtime.activeActionKey = action.key || "export";
    runtime.activeActionLabel = action.label || defaultText;
    button.disabled = true;
    button.textContent = "导出中...";
    button.style.cursor = "wait";
    button.style.background = "#aeb9ff";
    button.style.borderColor = "#aeb9ff";

    try {
      const result = action.run ? await action.run(action, button) : await action.exportRows();
      if (result?.headers && result?.rows) {
        downloadCsv(result.filename, result.headers, result.rows);
        showToast(result.message || `已导出 ${result.rows.length} 条数据`, result.toastDurationMs || 5000);
      } else if (Array.isArray(result?.files)) {
        result.files.forEach((file, index) => {
          window.setTimeout(() => {
            if (file.headers && file.rows) {
              downloadCsv(file.filename, file.headers, file.rows);
            } else {
              downloadText(file.filename, file.text || "", file.extension || "txt", file.mime || "text/plain;charset=utf-8");
            }
          }, index * 260);
        });
        showToast(result.message || `已导出 ${result.files.length} 个文件`, result.toastDurationMs || 6000);
      } else if (result?.text) {
        downloadText(result.filename, result.text, result.extension || "txt", result.mime || "text/plain;charset=utf-8");
        showToast(result.message || "已导出文件", result.toastDurationMs || 6000);
      } else if (result?.message) {
        showToast(result.message, result.toastDurationMs || 6000);
      }
    } catch (error) {
      console.error("[pdd-table-exporter]", error);
      showToast(error.message || "导出失败", 9000);
    } finally {
      runtime.activeActionKey = "";
      runtime.activeActionLabel = "";
      button.disabled = false;
      button.textContent = action.label || defaultText;
      button.style.cursor = "pointer";
      button.style.background = "#4f63ff";
      button.style.borderColor = "#4f63ff";
      button.style.boxShadow = "0 2px 7px rgba(79, 99, 255, 0.2)";
      updateNetworkButtonLabels();
    }
  }

  function installNetworkHook() {
    if (document.getElementById("pdd-table-exporter-page-hook")) {
      return;
    }

    const script = document.createElement("script");
    script.id = "pdd-table-exporter-page-hook";
    script.src = chrome.runtime.getURL("page-hook.js");
    script.onload = () => script.remove();
    script.onerror = () => showToast("接口监听脚本注入失败，请刷新页面后重试");
    (document.head || document.documentElement).appendChild(script);
  }

  function handleNetworkHookMessage(event) {
    if (event.source !== window) {
      return;
    }
    const message = event.data;
    if (!message || message.channel !== "__PDD_TABLE_EXPORTER_NETWORK__") {
      return;
    }

    if (message.type === "installed") {
      runtime.networkHookReady = true;
      runtime.networkInstalledAt = new Date().toISOString();
      runtime.networkSeenTotal = Number(message.payload?.seenTotal || 0);
      updateNetworkButtonLabels();
      return;
    }

    if (message.type === "state") {
      runtime.networkHookReady = true;
      runtime.networkCaptureEnabled = Boolean(message.payload?.enabled);
      runtime.networkSeenTotal = Number(message.payload?.seenTotal || runtime.networkSeenTotal || 0);
      if (!runtime.networkCaptureEnabled && Array.isArray(message.payload?.records)) {
        runtime.networkRecords = mergeNetworkRecords(runtime.networkRecords, message.payload.records);
      }
      updateNetworkButtonLabels();
      return;
    }

    if (message.type === "record" && message.payload) {
      runtime.networkRecords.push(message.payload);
      runtime.networkSeenTotal += 1;
      if (runtime.networkRecords.length > 800) {
        runtime.networkRecords.splice(0, runtime.networkRecords.length - 800);
      }
      updateNetworkButtonLabels();
      return;
    }

    if (message.type === "request-result" && message.payload?.requestId) {
      const pending = runtime.pageRequestResolvers.get(message.payload.requestId);
      if (pending) {
        runtime.pageRequestResolvers.delete(message.payload.requestId);
        pending.resolve(message.payload);
      }
      return;
    }

    if (message.type === "dump" && Array.isArray(message.payload?.records)) {
      runtime.networkCaptureEnabled = Boolean(message.payload.enabled);
      runtime.networkSeenTotal = Number(message.payload.seenTotal || message.payload.records.length || 0);
      runtime.networkRecords = mergeNetworkRecords(runtime.networkRecords, message.payload.records);
      updateNetworkButtonLabels();
    }
  }

  async function toggleNetworkCapture(action) {
    installNetworkHook();
    const nextEnabled = !runtime.networkCaptureEnabled;
    if (nextEnabled) {
      runtime.networkRecords = [];
      runtime.networkSeenTotal = 0;
    }
    persistNetworkCapture(nextEnabled);
    postNetworkCommand({
      type: "enable",
      enabled: nextEnabled,
      clear: nextEnabled
    });
    runtime.networkCaptureEnabled = nextEnabled;
    action.label = nextEnabled ? "停止抓接口" : "开始抓接口";
    updateNetworkButtonLabels();
    return {
      message: nextEnabled
        ? "已开始抓接口：刷新页面后也会自动继续抓。现在可以切日期、打开分时图或商品详情。"
        : `已停止抓接口，当前记录 ${runtime.networkRecords.length} 条。`
    };
  }

  async function exportNetworkCaptureJson() {
    postNetworkCommand({ type: "dump" });
    await sleep(180);
    if (!runtime.networkRecords.length) {
      throw new Error(
        runtime.networkHookReady
          ? "接口监听已注入，但还没有记录到 fetch/XHR。请点“开始抓接口”后刷新当前页面，或打开商品详情/切换日期后再导出。如果仍为空，页面可能在 iframe、Worker 或非 fetch/XHR 通道里请求。"
          : "接口监听脚本还没有注入成功。请刷新页面，确认插件已重新加载，然后再点“开始抓接口”。"
      );
    }
    const payload = {
      exportedAt: new Date().toISOString(),
      page: location.href,
      hookReady: runtime.networkHookReady,
      captureEnabled: runtime.networkCaptureEnabled,
      seenTotal: runtime.networkSeenTotal,
      recordCount: runtime.networkRecords.length,
      relevantCount: runtime.networkRecords.filter((item) => item.relevant).length,
      records: runtime.networkRecords
    };
    return {
      filename: buildFilename("拼多多接口抓取记录"),
      extension: "json",
      mime: "application/json;charset=utf-8",
      text: JSON.stringify(payload, null, 2),
      message: `已导出 ${runtime.networkRecords.length} 条接口记录`
    };
  }

  async function fetchAdCurrentSelection() {
    const context = await getCurrentAdReportContext();
    pushPromotionContextMappingRecord(context);
    const startedAt = performance.now();
    const result = await pageContextRequest({
      method: "POST",
      url: context.url,
      headers: {
        "content-type": "application/json;charset=UTF-8"
      },
      body: JSON.stringify(context.templateBody),
      timeoutMs: 18000
    });
    pushExtensionNetworkRecord({
      source: "extension-ad-current-fetch",
      method: "POST",
      url: context.url,
      requestBody: JSON.stringify(context.templateBody),
      status: result.status || "",
      contentType: result.contentType || "",
      durationMs: Math.round(performance.now() - startedAt),
      relevant: true,
      responseText: result.responseText || ""
    });

    const parsed = parseJsonSafe(result.responseText);
    const resultData = parsed?.result || {};
    const dailyCount = Array.isArray(resultData.dailyReportList) ? resultData.dailyReportList.length : 0;
    const hourlyCount = Array.isArray(resultData.hourlyReportList) ? resultData.hourlyReportList.length : 0;
    if (!result.ok || (!dailyCount && !hourlyCount && !resultData.dailyReport)) {
      throw new Error("当前直通车选择没有返回可导出的日级/分时数据。请先在详情里切换一次日期或数据趋势。");
    }

    const startDate = normalizeDate(context.templateBody.startDate);
    const endDate = normalizeDate(context.templateBody.endDate);
    const rangeText = startDate && endDate && startDate !== endDate ? `${startDate} 至 ${endDate}` : startDate || endDate || "当前选择";
    const hourlyNote = hourlyCount ? `，分时 ${hourlyCount} 小时` : "，当前选择不是单日或接口未返回分时";
    return {
      message: `已补采直通车当前选择：广告ID ${context.adId}，${rangeText}，日级 ${dailyCount || (resultData.dailyReport ? 1 : 0)} 条${hourlyNote}。现在点“导出量化CSV”。`
    };
  }

  async function autoFetchAdThirtyDays() {
    if (window.location.hostname !== "yingxiao.pinduoduo.com") {
      throw new Error("自动采车30天只能在拼多多推广平台使用。");
    }

    let context = await getCurrentPromotionContext({ requireReportTemplate: false });
    await ensurePageHookReady();
    ensureNetworkCaptureEnabled({ clear: true });
    if (!context.adId) {
      context = await waitForPromotionContextWithAdId(context, 4200);
    }
    pushPromotionContextMappingRecord(context);

    const dates = recentDates(30, { includeToday: true });
    let success = 0;
    let operationCount = 0;
    const failures = [];
    let resolvedAdId = context.adId || "";

    for (let index = 0; index < dates.length; index += 1) {
      const date = dates[index];
      try {
        showToast(`自动采车30天：正在采集 ${date}（${index + 1}/${dates.length}）`, 3000);
        await switchPromotionDrawerMenu("推广详情", "0");
        let record = null;
        let lastError = null;
        for (let attempt = 0; attempt < 2; attempt += 1) {
          const beforeCount = runtime.networkRecords.length;
          const changed = await selectPromotionDetailDate(date, { attempt });
          if (!changed) {
            lastError = new Error("没有把日期稳定选成同一天");
            await sleep(180 + attempt * 120);
            continue;
          }
          try {
            record = await waitForPromotionAdReportRecord(date, beforeCount, resolvedAdId, 18000);
            break;
          } catch (error) {
            lastError = error;
            if (attempt >= 1) {
              showToast(`自动采车30天：${date} 接口还没稳定，继续校验`, 3000);
            }
            await sleep(260 + attempt * 180);
          }
        }
        if (!record) {
          throw lastError || new Error(`没有等到 ${date} 的直通车数据趋势接口`);
        }
        const body = parseJsonSafe(record.requestBody);
        resolvedAdId = resolvedAdId || normalizeId(body?.entityId);
        success += 1;
        await sleep(220);
      } catch (error) {
        failures.push(`${date}${error?.message ? `(${error.message})` : ""}`);
        showToast(`自动采车30天：${date} 失败，继续下一天`, 8000);
      }

      if ((index + 1) % 3 === 0 || index + 1 === dates.length) {
        showToast(`自动采车30天：已处理 ${index + 1}/${dates.length}`, 3000);
      }
      await sleep(100);
    }

    operationCount = await capturePromotionOperationLogsThirtyDays({
      ...context,
      adId: resolvedAdId || context.adId
    }).catch(() => 0);

    if (resolvedAdId && !context.adId) {
      pushPromotionContextMappingRecord({ ...context, adId: resolvedAdId });
    }

    const failNote = failures.length ? `，失败 ${failures.length} 天：${failures.slice(0, 8).join("、")}` : "";
    const operationNote = operationCount ? `，操作记录 ${operationCount} 条` : "";
    return {
      message: `直通车30天采集完成：成功 ${success}/${dates.length} 天${operationNote}${failNote}。现在点“导出量化CSV”生成日级、分时和操作记录表。`,
      toastDurationMs: failures.length ? 16000 : 10000,
      successCount: success,
      failureCount: failures.length,
      adId: resolvedAdId || context.adId || "",
      goodsId: context.goodsId || ""
    };
  }

  async function waitForPromotionContextWithAdId(initialContext, timeoutMs) {
    const startedAt = Date.now();
    let context = initialContext || {};
    while (!context.adId && Date.now() - startedAt < timeoutMs) {
      await sleep(300);
      context = await getCurrentPromotionContext({ requireReportTemplate: false }).catch(() => context);
    }
    return context;
  }

  async function batchFetchPromotionIds(action, button) {
    if (window.location.hostname !== "yingxiao.pinduoduo.com") {
      throw new Error("批量采车ID只能在拼多多推广平台使用。");
    }
    if (findPromotionDetailRoot()) {
      await closePromotionDetailDrawer();
    }

    const ids = await promptBatchPromotionIds();
    if (!ids.length) {
      return { message: "没有输入商品ID，已取消批量采集。" };
    }

    const results = [];
    for (let index = 0; index < ids.length; index += 1) {
      const goodsId = ids[index];
      if (button) {
        button.textContent = `${index + 1}/${ids.length} ${goodsId}`;
      }
      showToast(`批量采车ID：正在处理 ${goodsId}（${index + 1}/${ids.length}）`, 8000);

      try {
        await closePromotionDetailDrawer();
        await searchPromotionGoodsId(goodsId);
        await openPromotionSearchResult(goodsId);
        await waitForPromotionDetailGoodsId(goodsId, 16000);

        const fetchResult = await autoFetchAdThirtyDays();
        if (!fetchResult?.successCount) {
          throw new Error(fetchResult?.message || "30天采集没有成功数据");
        }

        const exportResult = await exportQuantCsvFromNetworkRecords();
        const filename = buildFilename(`拼多多量化导出包_${goodsId}`);
        downloadText(filename, exportResult.text || "", exportResult.extension || "html", exportResult.mime || "text/html;charset=utf-8");
        results.push({ goodsId, ok: true, message: `成功 ${fetchResult.successCount}/30 天` });
        showToast(`批量采车ID：${goodsId} 已导出`, 5000);
      } catch (error) {
        console.error("[pdd-table-exporter] batch item failed", goodsId, error);
        results.push({ goodsId, ok: false, message: error?.message || "失败" });
        showToast(`批量采车ID：${goodsId} 失败：${error?.message || "未知错误"}`, 12000);
      } finally {
        await closePromotionDetailDrawer();
        await sleep(900);
      }
    }

    const failed = results.filter((item) => !item.ok);
    const failedText = failed.length ? `，失败：${failed.map((item) => `${item.goodsId}(${item.message})`).slice(0, 5).join("、")}` : "";
    return {
      message: `批量采车ID完成：成功 ${results.length - failed.length}/${results.length}${failedText}`,
      toastDurationMs: failed.length ? 16000 : 9000
    };
  }

  function promptBatchPromotionIds() {
    return new Promise((resolve) => {
      const existing = document.getElementById("pdd-batch-id-modal");
      if (existing) {
        existing.remove();
      }

      const overlay = document.createElement("div");
      overlay.id = "pdd-batch-id-modal";
      overlay.style.cssText = [
        "position: fixed",
        "inset: 0",
        "z-index: 2147483646",
        "background: rgba(15, 23, 42, 0.22)",
        "display: flex",
        "align-items: center",
        "justify-content: center",
        "backdrop-filter: blur(6px)"
      ].join("; ");

      const panel = document.createElement("div");
      panel.style.cssText = [
        "width: min(560px, calc(100vw - 48px))",
        "background: rgba(255,255,255,0.96)",
        "border: 1px solid rgba(148,163,184,0.32)",
        "border-radius: 14px",
        "box-shadow: 0 24px 80px rgba(15,23,42,0.24)",
        "padding: 22px",
        "font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
        "color: #0f172a"
      ].join("; ");

      panel.innerHTML = `
        <div style="font-size:18px;font-weight:700;margin-bottom:8px;">批量采车商品ID</div>
        <div style="font-size:13px;color:#64748b;line-height:1.6;margin-bottom:14px;">每行一个商品ID，也支持空格、逗号分隔。插件会逐个搜索、打开详情、采集30天并自动导出量化包。</div>
        <textarea style="width:100%;height:240px;box-sizing:border-box;border:1px solid #cbd5e1;border-radius:10px;padding:12px;font-size:14px;line-height:1.5;outline:none;resize:vertical;" placeholder="940350309264&#10;940346770924"></textarea>
        <div style="display:flex;justify-content:flex-end;gap:10px;margin-top:14px;">
          <button type="button" data-role="cancel" style="height:36px;padding:0 16px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;color:#0f172a;font-weight:600;cursor:pointer;">取消</button>
          <button type="button" data-role="confirm" style="height:36px;padding:0 18px;border:1px solid #1677ff;border-radius:8px;background:#1677ff;color:#fff;font-weight:700;cursor:pointer;">开始采集</button>
        </div>
      `;
      overlay.appendChild(panel);
      document.body.appendChild(overlay);

      const textarea = panel.querySelector("textarea");
      const cleanup = (ids) => {
        overlay.remove();
        resolve(ids || []);
      };
      panel.querySelector('[data-role="cancel"]').addEventListener("click", () => cleanup([]));
      panel.querySelector('[data-role="confirm"]').addEventListener("click", () => cleanup(parseBatchGoodsIds(textarea.value)));
      overlay.addEventListener("click", (event) => {
        if (event.target === overlay) {
          cleanup([]);
        }
      });
      textarea.addEventListener("keydown", (event) => {
        if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
          cleanup(parseBatchGoodsIds(textarea.value));
        }
        if (event.key === "Escape") {
          cleanup([]);
        }
      });
      window.setTimeout(() => textarea.focus(), 50);
    });
  }

  function parseBatchGoodsIds(text) {
    return Array.from(new Set(String(text || "").match(/[0-9]{8,20}/g) || []));
  }

  async function searchPromotionGoodsId(goodsId) {
    const input = findPromotionSearchInput();
    if (!input) {
      throw new Error("没有找到推广列表搜索框");
    }
    triggerPreciseElementClick(input, { xRatio: 0.25, yRatio: 0.5 });
    input.focus();
    setNativeInputValue(input, "");
    input.dispatchEvent(new Event("input", { bubbles: true }));
    input.dispatchEvent(new Event("change", { bubbles: true }));
    await sleep(120);
    setNativeInputValue(input, goodsId);
    input.dispatchEvent(new Event("input", { bubbles: true }));
    input.dispatchEvent(new Event("change", { bubbles: true }));
    input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true }));
    input.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", bubbles: true }));
    const searchIcon = input.closest(".anq-input-search, .anq-input-affix-wrapper")?.querySelector(".anq-input-suffix-item-search-icon, [class*='search-icon'], [role='button']");
    if (searchIcon) {
      triggerClick(searchIcon);
    }
    await sleep(2200);
  }

  function findPromotionSearchInput() {
    return Array.from(document.querySelectorAll('input[data-testid="trTableSearch"], input[placeholder*="推广名称"], input[placeholder*="商品 ID"], input[placeholder*="商品ID"]'))
      .filter((node) => isVisibleElement(node))
      .sort((left, right) => {
        const leftRect = left.getBoundingClientRect();
        const rightRect = right.getBoundingClientRect();
        return leftRect.top - rightRect.top || leftRect.left - rightRect.left;
      })[0] || null;
  }

  async function openPromotionSearchResult(goodsId) {
    await waitForCondition(
      () => Boolean(findPromotionResultNameNode(goodsId, { allowFallback: false })),
      14000,
      250,
      `没有等到商品ID ${goodsId} 的搜索结果`
    );
    const title = findPromotionResultNameNode(goodsId, { allowFallback: true });
    if (!title) {
      throw new Error(`没有找到商品ID ${goodsId} 的结果标题`);
    }
    triggerPreciseElementClick(title, { xRatio: 0.35, yRatio: 0.5 });
    await sleep(1200);
  }

  function findPromotionResultNameNode(goodsId, options = {}) {
    const detailRoot = findPromotionDetailRoot();
    const titles = Array.from(document.querySelectorAll('div[data-testid="Name"], [class*="NameRow_title"]'))
      .filter((node) => isVisibleElement(node))
      .filter((node) => !detailRoot || !detailRoot.contains(node));
    const exact = titles.find((node) => promotionResultNodeContainsGoodsId(node, goodsId));
    return exact || (options.allowFallback ? titles[0] || null : null);
  }

  function promotionResultNodeContainsGoodsId(node, goodsId) {
    let current = node;
    for (let depth = 0; current && depth < 8; depth += 1) {
      const text = cleanText(current.innerText || current.textContent || "");
      if (text.includes(goodsId)) {
        return true;
      }
      current = current.parentElement;
    }
    return false;
  }

  async function waitForPromotionDetailGoodsId(goodsId, timeoutMs) {
    await waitForCondition(() => {
      const root = findPromotionDetailRoot();
      const info = findPromotionDetailInfoFromDom();
      const text = cleanText((root || findPromotionDetailFallbackRoot() || document.body).innerText || "");
      const hasDetailSignal = Boolean(root || findPromotionBudgetTitleNode(document.body) || findPromotionDrawerMenu("推广详情"));
      return hasDetailSignal && (info.goodsId === goodsId || text.includes(goodsId));
    }, timeoutMs, 300, `没有打开商品ID ${goodsId} 的推广详情`);
  }

  async function closePromotionDetailDrawer() {
    for (let attempt = 0; attempt < 4; attempt += 1) {
      const root = findPromotionDetailRoot();
      if (!root) {
        return true;
      }
      const closeButton = findPromotionDetailCloseButton(root);
      if (closeButton) {
        triggerPreciseElementClick(closeButton, { xRatio: 0.5, yRatio: 0.5 });
      } else {
        const input = findPromotionSearchInput();
        if (input) {
          triggerPreciseElementClick(input, { xRatio: 0.18, yRatio: 0.5 });
        }
        document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", code: "Escape", bubbles: true }));
      }
      await sleep(650);
    }
    return !findPromotionDetailRoot();
  }

  function findPromotionDetailCloseButton(root) {
    const scoped = root || findPromotionDetailRoot() || document.body;
    const candidates = Array.from(scoped.querySelectorAll("button, span, div"))
      .filter((node) => isVisibleElement(node))
      .filter((node) => {
        const text = cleanText(node.innerText || node.textContent || "");
        const label = String(node.getAttribute("aria-label") || node.getAttribute("title") || node.className || "");
        return text === "×" || text === "X" || /close|关闭|Drawer.*close|icon.*close/i.test(label);
      })
      .sort((left, right) => {
        const leftRect = left.getBoundingClientRect();
        const rightRect = right.getBoundingClientRect();
        return rightRect.right - leftRect.right || leftRect.top - rightRect.top;
      });
    return candidates[0] || null;
  }

  function ensureNetworkCaptureEnabled(options = {}) {
    installNetworkHook();
    runtime.networkHookReady = true;
    runtime.networkCaptureEnabled = true;
    persistNetworkCapture(true);
    if (options.clear) {
      runtime.networkRecords = [];
      runtime.networkSeenTotal = 0;
    }
    postNetworkCommand({
      type: "enable",
      enabled: true,
      clear: Boolean(options.clear)
    });
    updateNetworkButtonLabels();
  }

  async function selectPromotionDetailDate(dateText, options = {}) {
    const date = normalizeDate(dateText);
    const maxAttempts = options.maxAttempts ?? 1;

    for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
      const root = findPromotionDetailRoot() || document.body;
      const picker = findPromotionDateRangePicker(root);
      if (!picker) {
        return false;
      }
      if (await runPromotionDatePickerSelection(date, picker, attempt)) {
        return true;
      }

      const latestRoot = findPromotionDetailRoot() || document.body;
      const latestPicker = findPromotionDateRangePicker(latestRoot) || picker;
      const state = getPromotionDateRangeState(latestPicker);
      if (state.start || state.end) {
        showToast(`日期未稳定：目标 ${date}，当前 ${state.start || "?"} ~ ${state.end || "?"}，重新点日期`, 2800);
      }
      document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", code: "Escape", bubbles: true }));
      await sleep(180 + attempt * 120);
    }

    return false;
  }

  async function runPromotionDatePickerSelection(date, picker, attempt = 0) {
    const currentRoot = findPromotionDetailRoot() || document.body;
    const currentPicker = findPromotionDateRangePicker(currentRoot) || picker;
    openPromotionDateRangePicker(currentPicker);
    await waitForDatePickerPanelOpen(1200);
    const result = await clickDatePickerSingleDayRange(date);
    if (!result.ok) {
      return false;
    }
    const stable = await waitForPromotionDateRangeExact(date, currentPicker, 1800 + attempt * 400);
    if (stable) {
      await sleep(280);
      return true;
    }
    return false;
  }

  async function waitForPromotionDateRangeExact(dateText, picker, timeoutMs) {
    const targetDate = normalizeDate(dateText);
    const startedAt = Date.now();
    while (Date.now() - startedAt < timeoutMs) {
      const root = findPromotionDetailRoot() || document.body;
      const currentPicker = findPromotionDateRangePicker(root) || picker || root;
      const state = getPromotionDateRangeState(currentPicker);
      if (state.start === targetDate && state.end === targetDate) {
        return true;
      }
      await sleep(140);
    }
    const root = findPromotionDetailRoot() || document.body;
    const state = getPromotionDateRangeState(findPromotionDateRangePicker(root) || picker || root);
    return state.start === targetDate && state.end === targetDate;
  }

  function getPromotionDateRangeState(root = document.body) {
    const scoped = root || document.body;
    const inputValues = Array.from(scoped.querySelectorAll("input"))
      .filter((node) => isVisibleElement(node))
      .map((node) => String(node.value || "").trim())
      .filter(Boolean);
    const sources = inputValues.length ? inputValues : [cleanText(scoped.innerText || scoped.textContent || "")];
    const explicitRange = sources.find((source) => /[~至到]\s*20\d{2}[/-]\d{1,2}[/-]\d{1,2}/.test(source));
    if (explicitRange) {
      return parseDateRangeText(explicitRange);
    }
    if (inputValues.length >= 2) {
      const joinedInputs = inputValues.join(" ~ ");
      const range = parseDateRangeText(joinedInputs);
      if (range.start || range.end) {
        return range;
      }
    }
    for (const source of sources) {
      const range = parseDateRangeText(source);
      if (range.start || range.end) {
        return range;
      }
    }
    const joined = sources.join(" ");
    return parseDateRangeText(joined);
  }

  function parseDateRangeText(text) {
    const value = String(text || "");
    const rangeMatch = value.match(
      /(20\d{2})[/-](\d{1,2})[/-](\d{1,2})\s*[~至到-]\s*(20\d{2})[/-](\d{1,2})[/-](\d{1,2})/
    );
    if (rangeMatch) {
      return {
        start: normalizeDate(`${rangeMatch[1]}-${rangeMatch[2]}-${rangeMatch[3]}`),
        end: normalizeDate(`${rangeMatch[4]}-${rangeMatch[5]}-${rangeMatch[6]}`),
        raw: value
      };
    }
    const dates = Array.from(value.matchAll(/(20\d{2})[/-](\d{1,2})[/-](\d{1,2})/g))
      .map((match) => normalizeDate(`${match[1]}-${match[2]}-${match[3]}`));
    if (dates.length >= 2) {
      return { start: dates[0], end: dates[dates.length - 1], raw: value };
    }
    if (dates.length === 1) {
      return { start: dates[0], end: dates[0], raw: value };
    }
    return { start: "", end: "", raw: value };
  }

  function findPromotionDateRangePicker(root) {
    const scoped = root || document.body;
    const exact = Array.from(scoped.querySelectorAll(".anq-date-range-picker-merge, .anq-date-range-picker.anq-picker-range"))
      .filter((node) => isVisibleElement(node))
      .filter((node) => node.querySelector('input[placeholder*="开始"], input[placeholder*="结束"], .anq-picker-suffix'));
    if (exact.length) {
      return exact.sort((left, right) => {
        const leftRect = left.getBoundingClientRect();
        const rightRect = right.getBoundingClientRect();
        return leftRect.top - rightRect.top || rightRect.right - leftRect.right;
      })[0];
    }

    return Array.from(scoped.querySelectorAll(".anq-picker-range"))
      .filter((node) => isVisibleElement(node))
      .find((node) => /开始日期|结束日期|~|20\d{2}[-/]\d{1,2}[-/]\d{1,2}/.test(cleanText(node.innerText || node.textContent || ""))) || null;
  }

  function openPromotionDateRangePicker(picker) {
    if (activatePromotionDateRangeStartInput(picker)) {
      return;
    }
    const fallback = picker.querySelector(".anq-picker-suffix") || picker;
    triggerClick(fallback);
  }

  function activatePromotionDateRangeStartInput(picker) {
    const startInput =
      picker.querySelector('input[placeholder*="开始"]') ||
      picker.querySelector(".anq-picker-input:first-child input") ||
      picker.querySelector("input");
    const startTarget = startInput?.closest(".anq-picker-input") || startInput;
    if (startTarget) {
      triggerPreciseElementClick(startTarget, { xRatio: 0.5, yRatio: 0.52 });
      if (startInput && startInput !== startTarget) {
        triggerPreciseElementClick(startInput, { xRatio: 0.5, yRatio: 0.52 });
      }
      startInput?.focus?.();
      try {
        startInput?.setSelectionRange?.(0, 0);
      } catch (error) {
        // Some readonly range inputs do not allow selection changes.
      }
      startInput?.dispatchEvent?.(new FocusEvent("focus", { bubbles: true }));
      return true;
    }
    return false;
  }

  async function waitForPromotionAdReportRecord(dateText, beforeCount, adId, timeoutMs, options = {}) {
    const targetDate = normalizeDate(dateText);
    const intervalMs = Math.max(160, Number(options.intervalMs || 160));
    const cached = findPromotionAdReportRecord(targetDate, adId);
    if (cached) {
      return cached;
    }

    const startedAt = Date.now();
    while (Date.now() - startedAt < timeoutMs) {
      postNetworkCommand({ type: "dump" });
      await sleep(intervalMs);
      const matched = findPromotionAdReportRecord(targetDate, adId, beforeCount);
      if (matched) {
        return matched;
      }
    }
    const latestCached = findPromotionAdReportRecord(targetDate, adId);
    if (latestCached) {
      return latestCached;
    }
    throw new Error(`没有等到 ${targetDate} 的直通车数据趋势接口`);
  }

  function findPromotionAdReportRecord(targetDate, adId, beforeCount = 0) {
    return runtime.networkRecords
      .slice(Math.max(0, beforeCount))
      .slice()
      .reverse()
      .find((record) => isPromotionAdReportRecordForDate(record, targetDate, adId));
  }

  function isPromotionAdReportRecordForDate(record, targetDate, adId) {
    if (!record || !(record.url || "").includes("/poseidon/api/report/queryHourlyRangeReport")) {
      return false;
    }
    const body = parseJsonSafe(record.requestBody) || {};
    if (Number(body.queryDimensionType) !== 2) {
      return false;
    }
    if (adId && normalizeId(body.entityId) && normalizeId(body.entityId) !== normalizeId(adId)) {
      return false;
    }
    const startDate = normalizeDate(body.startDate);
    const endDate = normalizeDate(body.endDate || body.startDate);
    if (targetDate && (startDate !== targetDate || endDate !== targetDate)) {
      return false;
    }
    const result = parseJsonSafe(record.responseText)?.result || {};
    const dailyRows = Array.isArray(result.dailyReportList) ? result.dailyReportList : [];
    const hourlyRows = Array.isArray(result.hourlyReportList) ? result.hourlyReportList : [];
    const hasTargetDaily = dailyRows.some((item) => normalizeDate(item.statDate || item.date) === targetDate);
    const hasTargetHourly = hourlyRows.some((item) => normalizeDate(item.statDate || item.date || body.startDate) === targetDate);
    const singleDaily = result.dailyReport && normalizeDate(result.dailyReport.statDate || result.dailyReport.date || body.startDate) === targetDate;
    return hasTargetDaily || hasTargetHourly || singleDaily;
  }

  function getPromotionDetailStatsDate(root = document.body) {
    const text = cleanText(root?.innerText || "");
    const match = text.match(/统计时间[:：]?\s*(20\d{2}[-/]\d{1,2}[-/]\d{1,2})/);
    return match ? normalizeDate(match[1]) : "";
  }

  async function waitForPromotionStatsDateChange(dateText, beforeStatsDate, timeoutMs) {
    const targetDate = normalizeDate(dateText);
    const startedAt = Date.now();
    while (Date.now() - startedAt < timeoutMs) {
      const current = getPromotionDetailStatsDate(findPromotionDetailRoot() || document.body);
      if (current === targetDate || (beforeStatsDate && current && current !== beforeStatsDate)) {
        return true;
      }
      await sleep(160);
    }
    return false;
  }

  function setNativeInputValue(input, value) {
    const descriptor = Object.getOwnPropertyDescriptor(input.constructor.prototype, "value");
    if (descriptor?.set) {
      descriptor.set.call(input, value);
    } else {
      input.value = value;
    }
  }

  async function clickDatePickerSingleDayRange(dateText) {
    const targetDate = normalizeDate(dateText);
    if (!(await waitForDatePickerPanelOpen(1200))) {
      return { ok: false, wrongAnchor: false, reason: "日期面板没有展开" };
    }
    if (!(await ensureDatePickerMonthVisible(targetDate))) {
      return { ok: false, wrongAnchor: false, reason: "没有翻到目标月份" };
    }

    const firstCell = findExactDatePickerCell(targetDate);
    if (!firstCell) {
      return { ok: false, wrongAnchor: false };
    }
    triggerDatePickerCellClick(getDatePickerClickTarget(firstCell));
    await sleep(100);
    const secondCell = findExactDatePickerCell(targetDate) || firstCell;
    if (!secondCell) {
      return { ok: false, wrongAnchor: false };
    }
    triggerDatePickerCellClick(getDatePickerClickTarget(secondCell));
    await sleep(260);

    clickDatePickerConfirm();
    await sleep(160);
    const root = findPromotionDetailRoot() || document.body;
    const state = getPromotionDateRangeState(findPromotionDateRangePicker(root) || root);
    return {
      ok: state.start === targetDate && state.end === targetDate,
      wrongAnchor: state.end === targetDate && state.start && state.start !== targetDate,
      state
    };
  }

  function triggerDatePickerCellClick(node) {
    if (!node) {
      return;
    }
    const target = node instanceof HTMLElement ? node : getDatePickerClickTarget(node);
    const rect = target.getBoundingClientRect?.() || { left: 0, top: 0, width: 0, height: 0 };
    const eventInit = {
      bubbles: true,
      cancelable: true,
      composed: true,
      view: window,
      clientX: rect.left + rect.width / 2,
      clientY: rect.top + rect.height / 2
    };
    ["pointerdown", "mousedown", "pointerup", "mouseup", "click"].forEach((eventName) => {
      const EventClass = eventName.startsWith("pointer") && typeof PointerEvent === "function" ? PointerEvent : MouseEvent;
      target.dispatchEvent(new EventClass(eventName, eventInit));
    });
  }

  function triggerPreciseElementClick(node, options = {}) {
    if (!(node instanceof HTMLElement)) {
      return;
    }
    const rect = node.getBoundingClientRect();
    const xRatio = Number.isFinite(options.xRatio) ? options.xRatio : 0.5;
    const yRatio = Number.isFinite(options.yRatio) ? options.yRatio : 0.5;
    const clientX = Math.min(rect.right - 2, Math.max(rect.left + 2, rect.left + rect.width * xRatio));
    const clientY = Math.min(rect.bottom - 2, Math.max(rect.top + 2, rect.top + rect.height * yRatio));
    const hit = document.elementFromPoint(clientX, clientY);
    const target = hit && (node.contains(hit) || hit.contains(node)) ? hit : node;
    const eventInit = {
      bubbles: true,
      cancelable: true,
      composed: true,
      view: window,
      clientX,
      clientY
    };
    ["pointerdown", "mousedown", "pointerup", "mouseup", "click"].forEach((eventName) => {
      const EventClass = eventName.startsWith("pointer") && typeof PointerEvent === "function" ? PointerEvent : MouseEvent;
      target.dispatchEvent(new EventClass(eventName, eventInit));
    });
  }

  async function ensureDatePickerMonthVisible(dateText) {
    const targetDate = normalizeDate(dateText);
    const targetMonth = parseDateMonthIndex(targetDate);
    if (targetMonth === null) {
      return false;
    }

    for (let attempt = 0; attempt < 6; attempt += 1) {
      if (findExactDatePickerCell(targetDate)) {
        return true;
      }

      const months = collectVisibleDatePickerMonths();
      if (!months.length) {
        await sleep(120);
        continue;
      }
      const minMonth = Math.min(...months);
      const maxMonth = Math.max(...months);
      const direction = targetMonth < minMonth ? "prev" : targetMonth > maxMonth ? "next" : "";
      if (!direction) {
        return Boolean(findExactDatePickerCell(targetDate));
      }

      const button = findDatePickerMonthButton(direction);
      if (!button) {
        return false;
      }
      triggerClick(button);
      await sleep(180);
    }
    return Boolean(findExactDatePickerCell(targetDate));
  }

  function collectVisibleDatePickerMonths() {
    const months = new Set();
    findVisibleDatePickerRoots().forEach((root) => {
      const text = cleanText(root.innerText || "");
      const regex = /(20\d{2})年\s*(\d{1,2})月/g;
      let match = regex.exec(text);
      while (match) {
        months.add(Number(match[1]) * 12 + Number(match[2]) - 1);
        match = regex.exec(text);
      }
    });
    return Array.from(months);
  }

  function parseDateMonthIndex(dateText) {
    const match = normalizeDate(dateText).match(/(20\d{2})-(\d{1,2})-\d{1,2}/);
    if (!match) {
      return null;
    }
    return Number(match[1]) * 12 + Number(match[2]) - 1;
  }

  function findDatePickerMonthButton(direction) {
    const anqRoots = findVisibleDatePickerRoots().filter((root) => root.matches?.(".anq-picker-panel-container"));
    const anqSelector = direction === "prev" ? ".anq-picker-header-prev-btn" : ".anq-picker-header-next-btn";
    const anqButtons = anqRoots
      .flatMap((root) => Array.from(root.querySelectorAll(anqSelector)))
      .filter((node) => isVisibleElement(node));
    if (anqButtons.length) {
      return direction === "prev" ? anqButtons[0] : anqButtons[anqButtons.length - 1];
    }
    return null;
  }

  async function waitForDatePickerPanelOpen(timeoutMs = 1200) {
    const startedAt = Date.now();
    while (Date.now() - startedAt < timeoutMs) {
      if (findVisibleDatePickerRoots().some((root) => root.matches?.(".anq-picker-panel-container"))) {
        return true;
      }
      await sleep(80);
    }
    return findVisibleDatePickerRoots().some((root) => root.matches?.(".anq-picker-panel-container"));
  }

  function findExactDatePickerCell(dateText) {
    const date = normalizeDate(dateText);
    const roots = findVisibleDatePickerRoots().filter((root) => root.matches?.(".anq-picker-panel-container"));
    const scopedRoots = roots.length ? roots : findVisibleDatePickerRoots();
    const exactCells = scopedRoots
      .flatMap((pickerRoot) => Array.from(pickerRoot.querySelectorAll(`td.anq-picker-cell[title="${date}"]`)))
      .filter((node) => isVisibleElement(node))
      .filter((node) => !isDisabledDateCell(node))
      .sort((left, right) => dateCellExactScore(left) - dateCellExactScore(right));
    return exactCells[0] || null;
  }

  function findDatePickerCell(dateText) {
    const variants = buildDateTextVariants(dateText);
    const roots = findVisibleDatePickerRoots();
    const scopedRoots = roots.length ? roots : [document.body];
    const exactAnqCells = scopedRoots
      .flatMap((pickerRoot) => Array.from(pickerRoot.querySelectorAll(`td.anq-picker-cell[title="${variants.date}"]`)))
      .filter((node) => isVisibleElement(node))
      .filter((node) => !isDisabledDateCell(node))
      .sort((left, right) => dateCellExactScore(left) - dateCellExactScore(right));
    if (exactAnqCells.length) {
      return exactAnqCells[0];
    }

    const candidates = scopedRoots
      .flatMap((pickerRoot) => [pickerRoot, ...Array.from(pickerRoot.querySelectorAll("td, div, span, button"))])
      .filter((node) => isVisibleElement(node))
      .filter((node) => isDatePickerCellCandidate(node))
      .filter((node) => !isDisabledDateCell(node))
      .map((node) => {
        const text = cleanText(node.innerText || "");
        const attrs = cleanText(`${node.getAttribute("title") || ""} ${node.getAttribute("aria-label") || ""}`);
        const exactAttr = variants.full.some((variant) => attrs.includes(variant));
        const exactText = variants.full.some((variant) => text === variant);
        const dayText = text === variants.day;
        if (!exactAttr && !exactText && !dayText) {
          return null;
        }
        return {
          node,
          score: dateCellScore(node, variants, { exactAttr, exactText, dayText })
        };
      })
      .filter(Boolean)
      .sort((left, right) => {
        return left.score - right.score || getElementArea(left.node) - getElementArea(right.node);
      });

    const cell = candidates.find((item) => {
      const rect = item.node.getBoundingClientRect();
      return rect.width <= 110 && rect.height <= 78;
    }) || candidates[0];
    return cell?.node || null;
  }

  function findVisibleDatePickerRoots() {
    const anqContainers = Array.from(document.querySelectorAll(".anq-picker-panel-container"))
      .filter((node) => isVisibleElement(node))
      .filter((node) => {
        const text = cleanText(node.innerText || "");
        const className = String(node.className || "");
        return /anq-picker-panel-container/.test(className) && /20\d{2}年|一二三四五六日|\d{1,2}/.test(text);
      })
      .sort((left, right) => getElementArea(left) - getElementArea(right));
    if (anqContainers.length) {
      return anqContainers;
    }

    return Array.from(document.querySelectorAll('[class*="picker"], [class*="calendar"], [role="dialog"], [role="grid"]'))
      .filter((node) => isVisibleElement(node))
      .filter((node) => {
        const text = cleanText(node.innerText || "");
        const className = String(node.className || "");
        return /picker|calendar|日期|自定义|确定|取消|上一月|下一月|Su|Mo|Tu|We|Th|Fr|Sa|一二三四五六日/.test(`${className} ${text}`);
      })
      .sort((left, right) => getElementArea(left) - getElementArea(right));
  }

  function isDatePickerCellCandidate(node) {
    const className = String(node.className || "");
    const role = node.getAttribute("role") || "";
    const text = cleanText(node.innerText || "");
    const attrs = `${node.getAttribute("title") || ""} ${node.getAttribute("aria-label") || ""}`;
    return /picker.*cell|cell.*inner|calendar|date/i.test(className) || role === "gridcell" || /20\d{2}[-/]\d{1,2}[-/]\d{1,2}/.test(attrs) || /^\d{1,2}$/.test(text);
  }

  function isDisabledDateCell(node) {
    const disabledNode = node.closest?.('[class*="disabled"], [aria-disabled="true"]');
    return Boolean(disabledNode);
  }

  function dateCellScore(node, variants, matchFlags) {
    let score = 50;
    if (matchFlags.exactAttr) {
      score -= 40;
    }
    if (matchFlags.exactText) {
      score -= 25;
    }
    const panelText = cleanText((node.closest?.('[class*="picker-panel"], [class*="calendar-panel"], [class*="panel"]') || node.closest?.("table")?.parentElement || node.parentElement)?.innerText || "");
    if (panelText.includes(variants.monthTitle)) {
      score -= 18;
    }
    const className = String(node.className || "");
    if (/range-start|range-end|selected|in-range/i.test(className)) {
      score -= 4;
    }
    return score;
  }

  function dateCellExactScore(node) {
    let score = 0;
    const className = String(node.className || "");
    if (!/anq-picker-cell-in-view/.test(className)) {
      score += 20;
    }
    if (/anq-picker-cell-disabled/.test(className)) {
      score += 100;
    }
    score += getElementArea(node) / 10000;
    return score;
  }

  function getDatePickerClickTarget(node) {
    return (
      node.querySelector?.('[class*="picker-cell-inner"], [class*="cell-inner"]') ||
      node.closest?.("td")?.querySelector?.('[class*="picker-cell-inner"], [class*="cell-inner"]') ||
      node
    );
  }

  function clickDatePickerConfirm() {
    const confirm = findVisibleDatePickerRoots()
      .flatMap((root) => Array.from(root.querySelectorAll("button, span, div")))
      .find((node) => isVisibleElement(node) && cleanText(node.textContent) === "确定");
    if (confirm) {
      triggerClick(confirm);
    }
  }

  function buildDateTextVariants(dateText) {
    const date = normalizeDate(dateText);
    const match = date.match(/(20\d{2})-(\d{2})-(\d{2})/);
    if (!match) {
      return { date, full: [dateText], day: String(dateText), monthTitle: "" };
    }
    const year = match[1];
    const paddedMonth = match[2];
    const month = String(Number(match[2]));
    const day = String(Number(match[3]));
    return {
      date,
      full: [
        date,
        date.replace(/-/g, "/"),
        `${year}年${month}月${day}日`,
        `${year}年 ${month}月`,
        `${year}-${paddedMonth}-${match[3]}`,
        `${Number(match[2])}-${day}`,
        `${Number(match[2])}/${day}`
      ],
      day,
      monthTitle: `${year}年${month}月`
    };
  }

  async function capturePromotionOperationLogs(context = {}) {
    const beforeCount = runtime.networkRecords.length;
    if (!await switchPromotionDrawerMenu("操作记录", "3", { waitForDetail: false })) {
      return 0;
    }
    let rows = [];
    try {
      await waitForCondition(
        () => hasPromotionOperationNetworkRecordSince(beforeCount),
        3200,
        160,
        "操作记录加载超时"
      ).catch(() => {});
      await sleep(520);

      rows = parsePromotionOperationRows(context);
      if (rows.length) {
        pushExtensionNetworkRecord({
          source: "extension-promotion-operation-logs",
          method: "GET",
          url: "/extension/promotion/operation-logs",
          requestBody: JSON.stringify({
            adId: context.adId || "",
            goodsId: context.goodsId || "",
            capturedFrom: location.href
          }),
          status: 200,
          contentType: "application/json",
          durationMs: 0,
          relevant: true,
          responseText: JSON.stringify({
            success: true,
            result: {
              operationLogs: rows
            }
          })
        });
      }
    } finally {
      await switchPromotionDrawerMenu("推广详情", "0");
    }
    return rows.length + countPromotionOperationNetworkRecordsSince(beforeCount);
  }

  async function capturePromotionOperationLogsThirtyDays(context = {}) {
    await switchPromotionDrawerMenu("推广详情", "0").catch(() => {});
    await clickPromotionDetailQuickThirtyDays().catch(() => {});
    await sleep(680);

    if (!await switchPromotionDrawerMenu("操作记录", "3", { waitForDetail: false })) {
      return 0;
    }

    let rows = [];
    try {
      await waitForPromotionOperationPanelReady().catch(() => {});
      await clickPromotionDetailQuickThirtyDays().catch(() => {});
      await sleep(700);
      await setPromotionOperationPageSize(300).catch(() => {});
      const expectedCount = getPromotionOperationExpectedCount();
      await waitForCondition(
        () => {
          const currentRows = parsePromotionOperationRows(context);
          if (!expectedCount) {
            return currentRows.length > 0;
          }
          return currentRows.length >= Math.min(expectedCount, 300);
        },
        5200,
        200,
        "操作记录表格加载超时"
      ).catch(() => {});

      rows = parsePromotionOperationRows(context);
      if (rows.length) {
        pushExtensionNetworkRecord({
          source: "extension-promotion-operation-logs",
          method: "GET",
          url: "/extension/promotion/operation-logs",
          requestBody: JSON.stringify({
            adId: context.adId || "",
            goodsId: context.goodsId || "",
            range: "近30日",
            expectedCount,
            capturedRows: rows.length,
            capturedFrom: location.href
          }),
          status: 200,
          contentType: "application/json",
          durationMs: 0,
          relevant: true,
          responseText: JSON.stringify({
            success: true,
            result: {
              expectedCount,
              operationLogs: rows
            }
          })
        });
      }
    } finally {
      await switchPromotionDrawerMenu("推广详情", "0").catch(() => {});
    }

    return rows.length;
  }

  async function clickPromotionDetailQuickThirtyDays() {
    const option = findPromotionDetailQuickThirtyDaysOption();
    if (!option) {
      return false;
    }
    const beforeActive = cleanText(option.closest?.(".DateAreaV2_wrapper__vNmhd")?.querySelector('[data-testid^="DateAreaQuickOption_"][class*="isActive"]')?.textContent || "");
    triggerClick(option);
    await waitForCondition(
      () => {
        const active = cleanText(option.closest?.(".DateAreaV2_wrapper__vNmhd")?.querySelector('[data-testid^="DateAreaQuickOption_"][class*="isActive"]')?.textContent || "");
        return active.replace(/\s+/g, "") === "近30日" || beforeActive.replace(/\s+/g, "") === "近30日";
      },
      2600,
      120,
      "切换操作记录近30日失败"
    ).catch(() => {});
    return true;
  }

  function findPromotionDetailQuickThirtyDaysOption() {
    const roots = Array.from(document.querySelectorAll(".DateAreaV2_wrapper__vNmhd, [class*='DateAreaV2_wrapper']"))
      .filter((node) => isVisibleElement(node))
      .sort((left, right) => {
        const leftRect = left.getBoundingClientRect();
        const rightRect = right.getBoundingClientRect();
        return rightRect.right - leftRect.right || leftRect.top - rightRect.top;
      });
    const options = roots.flatMap((root) => Array.from(root.querySelectorAll('[data-testid^="DateAreaQuickOption_"]')));
    return options.find((node) => {
      const text = cleanText(node.textContent).replace(/\s+/g, "");
      return isVisibleElement(node) && isInteractableOnScreen(node) && (node.getAttribute("data-testid") === "DateAreaQuickOption_3" || text === "近30日");
    }) || null;
  }

  async function waitForPromotionOperationPanelReady() {
    await waitForCondition(
      () => Boolean(findPromotionOperationTableRoot() || /操作明细/.test(cleanText(document.body.innerText || ""))),
      6000,
      180,
      "操作记录面板加载超时"
    );
  }

  async function setPromotionOperationPageSize(size = 300) {
    const root = findPromotionOperationPaginationRoot();
    if (!root) {
      return false;
    }
    const current = cleanText(root.querySelector(".anq-select-selection-item")?.textContent || "");
    if (current === String(size)) {
      return true;
    }
    const selector = root.querySelector(".anq-select-selector, .anq-select");
    if (!selector) {
      return false;
    }
    triggerClick(selector);
    await sleep(260);
    const option = findAnqSelectOption(String(size));
    if (!option) {
      return false;
    }
    triggerClick(option);
    await waitForCondition(
      () => cleanText(root.querySelector(".anq-select-selection-item")?.textContent || "") === String(size),
      3200,
      140,
      `切换每页 ${size} 条失败`
    ).catch(() => {});
    await sleep(600);
    return true;
  }

  function findPromotionOperationPaginationRoot() {
    const roots = Array.from(document.querySelectorAll(".anq-pagination-wrapper"))
      .filter((node) => isVisibleElement(node))
      .filter((node) => /每页|共\s*\d+\s*条/.test(cleanText(node.innerText || node.textContent || "")));
    return roots[0] || null;
  }

  function findAnqSelectOption(text) {
    return Array.from(document.querySelectorAll(".anq-select-item-option, [role='option']"))
      .filter((node) => isVisibleElement(node))
      .find((node) => cleanText(node.textContent) === text || cleanText(node.getAttribute("title") || "") === text) || null;
  }

  function getPromotionOperationExpectedCount() {
    const scope = document.querySelector("#drawer-detail-content, [data-testid='logContent']") || document.body;
    const text = cleanText(scope.innerText || scope.textContent || "");
    const detailMatch = text.match(/共查询到\s*([0-9,]+)\s*条数据/);
    if (detailMatch) {
      return Number(detailMatch[1].replace(/,/g, ""));
    }
    const paginationMatch = text.match(/共\s*([0-9,]+)\s*条/);
    return paginationMatch ? Number(paginationMatch[1].replace(/,/g, "")) : 0;
  }

  function extractOperationLogBodyAdIds(body) {
    const ids = [];
    const collect = (value) => {
      if (Array.isArray(value)) {
        value.forEach(collect);
      } else {
        const normalized = normalizeId(value);
        if (normalized) {
          ids.push(normalized);
        }
      }
    };
    collect(body?.fieldToValue?.adIds);
    collect(body?.adIds);
    collect(body?.adId);
    collect(body?.entityId);
    collect(body?.promotionId);
    return Array.from(new Set(ids));
  }

  function findPromotionDrawerMenu(title) {
    return Array.from(document.querySelectorAll('[class*="DrawerMenu_menuWrap"], [class*="DrawerMenu_menu"]'))
      .filter((node) => isVisibleElement(node))
      .map((node) => node.closest?.('[class*="DrawerMenu_menuWrap"]') || node)
      .find((node) => cleanText(node.innerText || node.textContent || "").includes(title)) || null;
  }

  async function switchPromotionDrawerMenu(title, testId, options = {}) {
    const menu = findPromotionDrawerMenuByTestId(testId) || findPromotionDrawerMenu(title);
    if (!menu) {
      return false;
    }
    menu.scrollIntoView?.({ block: "center", inline: "center" });
    await sleep(80);
    triggerClick(menu.querySelector('[class*="DrawerMenu_menu"]') || menu);
    if (options.waitForDetail !== false && title === "推广详情") {
      await waitForCondition(
        () => Boolean(findPromotionDateRangePicker() || findPromotionBudgetTitleNode(document.body)),
        4200,
        160,
        "推广详情恢复超时"
      ).catch(() => sleep(420));
    } else {
      await sleep(520);
    }
    return true;
  }

  function findPromotionDrawerMenuByTestId(testId) {
    return Array.from(document.querySelectorAll(`[class*="DrawerMenu_menuWrap"][data-testid="${testId}"]`))
      .filter((node) => isVisibleElement(node))[0] || null;
  }

  function hasPromotionOperationNetworkRecordSince(beforeCount) {
    return countPromotionOperationNetworkRecordsSince(beforeCount) > 0;
  }

  function countPromotionOperationNetworkRecordsSince(beforeCount) {
    return runtime.networkRecords.slice(beforeCount).filter((record) => /queryAdOperationLogList/i.test(record.url || "")).length;
  }

  function findPromotionOperationTableRoot() {
    return Array.from(document.querySelectorAll(".anq-table, table"))
      .filter((node) => isVisibleElement(node))
      .find((node) => {
        const text = cleanText(node.innerText || node.textContent || "");
        return /操作时间/.test(text) && /操作详情/.test(text);
      }) || null;
  }

  function parsePromotionOperationRows(context = {}) {
    const tableRoot = findPromotionOperationTableRoot();
    if (!tableRoot) {
      return [];
    }
    return Array.from(tableRoot.querySelectorAll("tr.anq-table-row, tr[data-row-key]"))
      .filter((row) => isVisibleElement(row))
      .map((row) => {
        const cells = Array.from(row.querySelectorAll("td"));
        if (cells.length < 5) {
          return null;
        }
        const operationTime = normalizeOperationTime(cleanText(cells[0].innerText || cells[0].textContent || ""));
        const objectText = cleanText(cells[2].innerText || cells[2].textContent || "");
        const operation = cleanText(cells[3].innerText || cells[3].textContent || "");
        const operationDetail = cleanText(cells[4].innerText || cells[4].textContent || "");
        if (!operationTime || !operation || !operationDetail) {
          return null;
        }
        return {
          goodsId: extractFirstMatch(objectText, /商品ID[:：]\s*([0-9]{8,20})/) || context.goodsId || "",
          adId: context.adId || "",
          operationTime,
          operation,
          operationDetail
        };
      })
      .filter(Boolean);
  }

  function normalizeOperationTime(value) {
    const text = String(value || "").trim();
    if (/^\d{10,13}$/.test(text)) {
      const numeric = Number(text);
      const date = new Date(text.length === 10 ? numeric * 1000 : numeric);
      if (!Number.isNaN(date.getTime())) {
        return `${formatLocalDate(date)} ${formatClock(date)}`;
      }
    }
    const match = text.match(/(20\d{2})[-/](\d{1,2})[-/](\d{1,2})\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?/);
    if (!match) {
      return text;
    }
    return `${match[1]}-${match[2].padStart(2, "0")}-${match[3].padStart(2, "0")} ${match[4].padStart(2, "0")}:${match[5].padStart(2, "0")}:${(match[6] || "00").padStart(2, "0")}`;
  }

  async function fetchAdDailyForCurrentPromotion() {
    const context = await getCurrentAdReportContext();
    const range = recentDateRange(30, { includeToday: false });
    const requestBody = buildAdReportRequestBody(context.templateBody, {
      adId: context.adId,
      startDate: range.start,
      endDate: range.end,
      endDayHour: 23
    });
    const startedAt = performance.now();
    const result = await pageContextRequest({
      method: "POST",
      url: context.url,
      headers: {
        "content-type": "application/json;charset=UTF-8"
      },
      body: requestBody,
      timeoutMs: 18000
    });
    pushExtensionNetworkRecord({
      source: "extension-ad-daily-fetch",
      method: "POST",
      url: context.url,
      requestBody,
      status: result.status || "",
      contentType: result.contentType || "",
      durationMs: Math.round(performance.now() - startedAt),
      relevant: true,
      responseText: result.responseText || ""
    });

    const parsed = parseJsonSafe(result.responseText);
    const rows = parsed?.result?.dailyReportList || [];
    if (!result.ok || !Array.isArray(rows) || !rows.length) {
      throw new Error("直通车日级补采没有拿到 dailyReportList。请先点“开始抓接口”，打开该计划详情并切一次数据趋势日期后再试。");
    }

    return {
      message: `已补采当前直通车计划近30天日级：广告ID ${context.adId}，${range.start} 至 ${range.end}，${rows.length} 天。现在点“导出量化CSV”。`
    };
  }

  async function fetchAdHourlyForCurrentPromotion() {
    const context = await getCurrentAdReportContext();
    const dates = recentDates(30, { includeToday: true });
    let success = 0;
    const failures = [];

    for (let index = 0; index < dates.length; index += 1) {
      const date = dates[index];
      const requestBody = buildAdReportRequestBody(context.templateBody, {
        adId: context.adId,
        startDate: date,
        endDate: date,
        endDayHour: date === formatLocalDate(new Date()) ? new Date().getHours() : 23
      });
      const startedAt = performance.now();
      try {
        const result = await pageContextRequest({
          method: "POST",
          url: context.url,
          headers: {
            "content-type": "application/json;charset=UTF-8"
          },
          body: requestBody,
          timeoutMs: 18000
        });
        pushExtensionNetworkRecord({
          source: "extension-ad-hourly-fetch",
          method: "POST",
          url: context.url,
          requestBody,
          status: result.status || "",
          contentType: result.contentType || "",
          durationMs: Math.round(performance.now() - startedAt),
          relevant: true,
          responseText: result.responseText || ""
        });

        const parsed = parseJsonSafe(result.responseText);
        if (result.ok && Array.isArray(parsed?.result?.hourlyReportList) && parsed.result.hourlyReportList.length) {
          success += 1;
        } else {
          failures.push(date);
        }
      } catch (error) {
        failures.push(date);
      }

      if ((index + 1) % 5 === 0 || index + 1 === dates.length) {
        showToast(`直通车分时补采 ${index + 1}/${dates.length}`);
      }
      await sleep(140);
    }

    const failNote = failures.length ? `，失败 ${failures.length} 天：${failures.slice(0, 5).join("、")}` : "";
    return {
      message: `已补采当前直通车计划近30天分时：广告ID ${context.adId}，成功 ${success}/${dates.length} 天${failNote}。现在点“导出量化CSV”。`
    };
  }

  async function fetchShopHourlyForKnownGoods() {
    if (window.location.hostname !== "mms.pinduoduo.com") {
      throw new Error("店铺分时补采只能在店铺后台商品效果页使用。");
    }

    await ensurePageHookReady();
    postNetworkCommand({ type: "dump" });
    await sleep(220);

    const { goodsIds, source } = collectShopGoodsIdsForCurrentView();
    if (!goodsIds.length) {
      throw new Error("没有识别到商品ID。请先在商品效果页加载商品列表，或打开过商品详情后再试。");
    }

    const limitedGoodsIds = goodsIds.slice(0, 200);
    const targetDate = getGoodsEffectStatsDate();
    let useCustomDate = false;
    let customDateUnsupported = false;

    if (targetDate && !isTodayOrYesterday(targetDate)) {
      showToast(`正在测试自定义日期分时：${targetDate}`);
      try {
        await fetchShopDayTrendCandidate(targetDate);
        const testBody = buildShopHourlyRequestBody(limitedGoodsIds[0], targetDate);
        const testResult = await pageContextRequest({
          method: "POST",
          url: "/sydney/api/goodsDataShow/queryGoodsStatDtr",
          headers: {
            "content-type": "application/json;charset=UTF-8"
          },
          body: testBody
        });
        pushExtensionNetworkRecord({
          source: "extension-shop-hourly-custom-date-test",
          method: "POST",
          url: "/sydney/api/goodsDataShow/queryGoodsStatDtr",
          requestBody: testBody,
          status: testResult.status || "",
          contentType: testResult.contentType || "",
          durationMs: testResult.durationMs || "",
          relevant: true,
          responseText: testResult.responseText || ""
        });
        useCustomDate = shopDtrResponseContainsDate(testResult.responseText, targetDate);
        customDateUnsupported = !useCustomDate;
      } catch (error) {
        customDateUnsupported = true;
      }
    }

    let success = 0;
    const failures = [];

    for (let index = 0; index < limitedGoodsIds.length; index += 1) {
      const goodsId = limitedGoodsIds[index];
      const requestBody = buildShopHourlyRequestBody(goodsId, useCustomDate ? targetDate : "");
      const startedAt = performance.now();
      try {
        const result = await pageContextRequest({
          method: "POST",
          url: "/sydney/api/goodsDataShow/queryGoodsStatDtr",
          headers: {
            "content-type": "application/json;charset=UTF-8"
          },
          body: requestBody
        });
        pushExtensionNetworkRecord({
          source: "extension-shop-hourly-fetch",
          method: "POST",
          url: "/sydney/api/goodsDataShow/queryGoodsStatDtr",
          requestBody,
          status: result.status || "",
          contentType: result.contentType || "",
          durationMs: Math.round(performance.now() - startedAt),
          relevant: true,
          responseText: result.responseText || ""
        });
        const parsed = parseJsonSafe(result.responseText);
        if (result.ok && parsed?.result) {
          success += 1;
        } else {
          failures.push(goodsId);
        }
      } catch (error) {
        failures.push(goodsId);
      }

      if ((index + 1) % 10 === 0 || index + 1 === limitedGoodsIds.length) {
        showToast(`店铺分时补采 ${index + 1}/${limitedGoodsIds.length}`);
      }
      await sleep(80);
    }

    const sourceNote = source === "current-table" ? "，按当前表格商品补采" : "，未读到当前表格，已按页面/接口记录兜底补采";
    const limitedNote = goodsIds.length > limitedGoodsIds.length ? `，已限制前 ${limitedGoodsIds.length} 个` : "";
    const failNote = failures.length ? `，失败 ${failures.length} 个：${failures.slice(0, 5).join("、")}` : "";
    const dateNote = useCustomDate
      ? `，已按自定义日期 ${targetDate} 请求`
      : customDateUnsupported
        ? `，当前分时接口未返回自定义日期 ${targetDate}，已按接口实际返回日期导出`
        : "";
    return {
      message: `已补采店铺分时 ${success}/${limitedGoodsIds.length} 个商品${sourceNote}${dateNote}${limitedNote}${failNote}。现在点“导出量化CSV”即可生成店铺后台分时表。`
    };
  }

  async function fetchShopDailyForKnownGoods() {
    if (window.location.hostname !== "mms.pinduoduo.com") {
      throw new Error("店铺日级补采只能在店铺后台商品效果页使用。");
    }

    await ensurePageHookReady();
    postNetworkCommand({ type: "dump" });
    await sleep(220);

    const { goodsIds, source } = collectShopGoodsIdsForCurrentView();
    if (!goodsIds.length) {
      throw new Error("没有识别到商品ID。请先在商品效果页加载商品列表，或打开过商品详情后再试。");
    }

    const limitedGoodsIds = goodsIds.slice(0, 200);
    let success = 0;
    const failures = [];

    for (let index = 0; index < limitedGoodsIds.length; index += 1) {
      const goodsId = limitedGoodsIds[index];
      const requestBody = buildShopDailyRequestBody(goodsId);
      const startedAt = performance.now();
      try {
        const result = await pageContextRequest({
          method: "POST",
          url: "/sydney/api/goodsDataShow/queryGoodsStat",
          headers: {
            "content-type": "application/json;charset=UTF-8"
          },
          body: requestBody
        });
        pushExtensionNetworkRecord({
          source: "extension-shop-daily-fetch",
          method: "POST",
          url: "/sydney/api/goodsDataShow/queryGoodsStat",
          requestBody,
          status: result.status || "",
          contentType: result.contentType || "",
          durationMs: Math.round(performance.now() - startedAt),
          relevant: true,
          responseText: result.responseText || ""
        });
        const parsed = parseJsonSafe(result.responseText);
        if (result.ok && Array.isArray(parsed?.result?.dayGoodsDetailList)) {
          success += 1;
        } else {
          failures.push(goodsId);
        }
      } catch (error) {
        failures.push(goodsId);
      }

      if ((index + 1) % 10 === 0 || index + 1 === limitedGoodsIds.length) {
        showToast(`店铺日级补采 ${index + 1}/${limitedGoodsIds.length}`);
      }
      await sleep(80);
    }

    const sourceNote = source === "current-table" ? "，按当前表格商品补采" : "，未读到当前表格，已按页面/接口记录兜底补采";
    const limitedNote = goodsIds.length > limitedGoodsIds.length ? `，已限制前 ${limitedGoodsIds.length} 个` : "";
    const failNote = failures.length ? `，失败 ${failures.length} 个：${failures.slice(0, 5).join("、")}` : "";
    return {
      message: `已补采店铺日级 ${success}/${limitedGoodsIds.length} 个商品${sourceNote}${limitedNote}${failNote}。现在点“导出量化CSV”即可生成店铺后台日级历史表。`
    };
  }

  async function fetchShopDayTrendCandidate(targetDate) {
    if (!targetDate) {
      return null;
    }
    const requestBody = JSON.stringify({ statDate: targetDate });
    const startedAt = performance.now();
    const result = await pageContextRequest({
      method: "POST",
      url: "/sydney/api/goodsDataShow/queryGoodsDataDayTrendForMms",
      headers: {
        "content-type": "application/json;charset=UTF-8"
      },
      body: requestBody,
      timeoutMs: 12000
    });
    pushExtensionNetworkRecord({
      source: "extension-shop-day-trend-candidate",
      method: "POST",
      url: "/sydney/api/goodsDataShow/queryGoodsDataDayTrendForMms",
      requestBody,
      status: result.status || "",
      contentType: result.contentType || "",
      durationMs: result.durationMs || Math.round(performance.now() - startedAt),
      relevant: true,
      responseText: result.responseText || ""
    });
    return result;
  }

  async function scoutLoadedEndpointScripts() {
    const urls = collectLoadedScriptUrls();
    if (!urls.length) {
      throw new Error("没有找到可侦察的页面脚本资源，请刷新页面后重试。");
    }

    const results = [];
    let checked = 0;
    const limitedUrls = urls.slice(0, 120);
    for (const url of limitedUrls) {
      checked += 1;
      if (checked % 10 === 0 || checked === limitedUrls.length) {
        showToast(`接口侦察 ${checked}/${limitedUrls.length}`);
      }
      try {
        const text = await fetchTextResource(url);
        results.push(analyzeEndpointScript(url, text));
      } catch (error) {
        results.push({
          url,
          error: error?.message || "读取失败",
          endpoints: [],
          functions: [],
          snippets: []
        });
      }
      await sleep(25);
    }

    const html = buildEndpointScoutHtml(results, {
      checked: limitedUrls.length,
      total: urls.length,
      page: location.href,
      exportedAt: new Date().toLocaleString()
    });
    return {
      filename: buildFilename("侦察_历史分时接口候选"),
      extension: "html",
      mime: "text/html;charset=utf-8",
      text: html,
      message: `已生成接口侦察报告：检查 ${limitedUrls.length}/${urls.length} 个资源`
    };
  }

  function collectLoadedScriptUrls() {
    const urls = new Set();
    const addUrl = (value) => {
      if (!value) {
        return;
      }
      try {
        const url = new URL(value, location.href);
        if (!/^https?:$/.test(url.protocol)) {
          return;
        }
        const text = url.href;
        if (
          /\.(?:js|mjs)(?:[?#].*)?$/i.test(text) ||
          /\/static\/|\/assets\/|\/chunk|\/bundle|\/umi|\/mms|\/sycm|\/goods/i.test(text)
        ) {
          urls.add(text);
        }
      } catch (error) {
        // Ignore malformed resource URLs.
      }
    };

    document.querySelectorAll("script[src]").forEach((node) => addUrl(node.src));
    performance.getEntriesByType("resource").forEach((entry) => addUrl(entry.name));
    return Array.from(urls).sort((left, right) => left.localeCompare(right));
  }

  async function fetchTextResource(url) {
    try {
      const response = await fetch(url, {
        credentials: "include",
        cache: "force-cache"
      });
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      const text = await response.text();
      return text.slice(0, 2200000);
    } catch (error) {
      const proxyResult = await fetchTextResourceViaExtension(url);
      if (!proxyResult?.ok) {
        throw new Error(proxyResult?.error || error?.message || "Failed to fetch");
      }
      return String(proxyResult.text || "").slice(0, 2200000);
    }
  }

  function fetchTextResourceViaExtension(url) {
    return new Promise((resolve) => {
      if (typeof chrome === "undefined" || !chrome.runtime?.sendMessage) {
        resolve({ ok: false, error: "扩展后台通道不可用" });
        return;
      }

      chrome.runtime.sendMessage(
        {
          type: "FETCH_TEXT_RESOURCE",
          url
        },
        (response) => {
          const runtimeError = chrome.runtime.lastError;
          if (runtimeError) {
            resolve({ ok: false, error: runtimeError.message || "后台读取失败" });
            return;
          }
          resolve(response || { ok: false, error: "后台没有返回数据" });
        }
      );
    });
  }

  function analyzeEndpointScript(url, text) {
    const endpoints = uniqueMatches(text, /["'`]((?:https?:\/\/[^"'`]+)?\/(?:sydney|mms-gateway|uranus|earth|newjersy)\/api\/[^"'`\\\s)]+)/g)
      .slice(0, 240);
    const functions = uniqueMatches(text, /\b(query[A-Za-z0-9_$]{3,80}|get[A-Za-z0-9_$]{3,80}|fetch[A-Za-z0-9_$]{3,80})\b/g)
      .filter((item) => endpointScoutScore(item) > 0)
      .slice(0, 240);
    const snippets = collectEndpointScoutSnippets(text).slice(0, 80);
    const score =
      endpoints.reduce((sum, item) => sum + endpointScoutScore(item), 0) +
      functions.reduce((sum, item) => sum + endpointScoutScore(item), 0) +
      snippets.reduce((sum, item) => sum + item.score, 0);
    return {
      url,
      size: text.length,
      score,
      endpoints,
      functions,
      snippets
    };
  }

  function uniqueMatches(text, regex) {
    const seen = new Set();
    const values = [];
    let match;
    while ((match = regex.exec(text))) {
      const value = String(match[1] || match[0] || "").trim();
      if (value && !seen.has(value)) {
        seen.add(value);
        values.push(value);
      }
    }
    return values;
  }

  function collectEndpointScoutSnippets(text) {
    const keywords = [
      "queryGoodsStatDtr",
      "queryGoodsStat",
      "queryGoodsDataDayTrendForMms",
      "goodsDataDayTrend",
      "statHr",
      "statDate",
      "goodsUv",
      "payOrdrCnt",
      "todayGoodsDetailList",
      "yesterdayGoodsDetailList",
      "startDate",
      "endDate",
      "queryDate",
      "trend",
      "chart",
      "hour",
      "Dtr"
    ];
    const snippets = [];
    const seen = new Set();
    keywords.forEach((keyword) => {
      let index = text.indexOf(keyword);
      let count = 0;
      while (index >= 0 && count < 10) {
        const start = Math.max(0, index - 260);
        const end = Math.min(text.length, index + 420);
        const snippet = text.slice(start, end).replace(/\s+/g, " ");
        const key = snippet.slice(0, 220);
        if (!seen.has(key)) {
          seen.add(key);
          snippets.push({
            keyword,
            score: endpointScoutScore(snippet),
            snippet
          });
        }
        count += 1;
        index = text.indexOf(keyword, index + keyword.length);
      }
    });
    return snippets.sort((left, right) => right.score - left.score);
  }

  function endpointScoutScore(text) {
    const source = String(text || "").toLowerCase();
    const weights = [
      ["stathr", 12],
      ["hourly", 10],
      ["hour", 6],
      ["dtr", 8],
      ["trend", 6],
      ["chart", 5],
      ["goodsuv", 8],
      ["payordrcnt", 8],
      ["goodsid", 5],
      ["startdate", 5],
      ["enddate", 5],
      ["querydate", 5],
      ["goodsstat", 4],
      ["goodsdetail", 2]
    ];
    return weights.reduce((score, [keyword, weight]) => score + (source.includes(keyword) ? weight : 0), 0);
  }

  function buildEndpointScoutHtml(results, meta) {
    const sorted = results.slice().sort((left, right) => (right.score || 0) - (left.score || 0));
    const endpointCounts = new Map();
    sorted.forEach((result) => {
      (result.endpoints || []).forEach((endpoint) => {
        endpointCounts.set(endpoint, (endpointCounts.get(endpoint) || 0) + 1);
      });
    });
    const topEndpoints = Array.from(endpointCounts.entries())
      .sort((left, right) => right[1] - left[1] || endpointScoutScore(right[0]) - endpointScoutScore(left[0]))
      .slice(0, 240);

    const rows = sorted
      .map((result) => `
        <section class="card">
          <h2>${escapeHtml(result.url)}</h2>
          <p>score ${escapeHtml(result.score || 0)} · size ${escapeHtml(result.size || 0)}${result.error ? ` · ${escapeHtml(result.error)}` : ""}</p>
          ${renderScoutList("接口", result.endpoints)}
          ${renderScoutList("函数/符号", result.functions)}
          ${(result.snippets || []).slice(0, 12).map((item) => `
            <div class="snippet">
              <b>${escapeHtml(item.keyword)} · score ${escapeHtml(item.score)}</b>
              <pre>${escapeHtml(item.snippet)}</pre>
            </div>
          `).join("")}
        </section>
      `)
      .join("");

    return `<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <title>历史分时接口候选侦察</title>
  <style>
    body { margin: 0; padding: 28px; background: #f5f7fa; color: #111827; font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Microsoft YaHei", sans-serif; }
    h1 { margin: 0; font-size: 28px; }
    h2 { margin: 0; font-size: 15px; word-break: break-all; }
    p { color: #64748b; margin: 6px 0 0; }
    .hero, .card { background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; box-shadow: 0 14px 34px rgba(15,23,42,.06); }
    .hero { padding: 20px; margin-bottom: 16px; }
    .card { padding: 16px; margin-top: 14px; overflow: hidden; }
    .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin-top: 14px; }
    .stat { background: #f8fafc; border: 1px solid #edf2f7; border-radius: 10px; padding: 10px; }
    .stat b { display: block; font-size: 22px; margin-top: 4px; }
    .chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
    .chip { border: 1px solid #d7dce5; border-radius: 999px; padding: 5px 9px; background: #fbfdff; font-size: 12px; }
    .snippet { margin-top: 10px; border: 1px solid #edf2f7; border-radius: 10px; background: #fbfdff; overflow: hidden; }
    .snippet b { display: block; padding: 8px 10px; background: #f8fafc; }
    pre { margin: 0; padding: 10px; white-space: pre-wrap; word-break: break-all; font-size: 12px; line-height: 1.5; }
  </style>
</head>
<body>
  <div class="hero">
    <h1>历史分时接口候选侦察</h1>
    <p>页面：${escapeHtml(meta.page)} · 导出：${escapeHtml(meta.exportedAt)}</p>
    <div class="grid">
      <div class="stat">已检查资源<b>${escapeHtml(meta.checked)}</b></div>
      <div class="stat">发现接口<b>${escapeHtml(topEndpoints.length)}</b></div>
      <div class="stat">资源总数<b>${escapeHtml(meta.total)}</b></div>
    </div>
  </div>
  <section class="card">
    <h2>接口候选汇总</h2>
    <div class="chips">
      ${topEndpoints.map(([endpoint, count]) => `<span class="chip">${escapeHtml(endpoint)} × ${escapeHtml(count)}</span>`).join("") || '<span class="chip">未发现接口字符串</span>'}
    </div>
  </section>
  ${rows}
</body>
</html>`;
  }

  function renderScoutList(title, values) {
    if (!values?.length) {
      return "";
    }
    return `
      <div class="chips">
        <b>${escapeHtml(title)}</b>
        ${values.slice(0, 80).map((value) => `<span class="chip">${escapeHtml(value)}</span>`).join("")}
      </div>
    `;
  }

  async function exportQuantCsvFromNetworkRecords() {
    postNetworkCommand({ type: "dump" });
    await sleep(180);
    if (!runtime.networkRecords.length) {
      throw new Error("还没有接口记录。请先抓接口，再点“导出量化CSV”。");
    }

    const parsedRecords = runtime.networkRecords
      .map((record) => ({
        ...record,
        bodyJson: parseJsonSafe(record.requestBody),
        responseJson: parseJsonSafe(record.responseText)
      }))
      .filter((record) => record.responseJson);

    const promotionMappings = collectPromotionMappings(parsedRecords);
    const adDailyRows = [];
    const adHourlyRows = [];
    const shopDailyRows = [];
    const shopHourlyRows = [];
    const afterSalesRows = [];

    parsedRecords.forEach((record) => {
      const url = record.url || "";
      if (url.includes("/poseidon/api/report/queryHourlyRangeReport")) {
        collectAdReportRows(record, promotionMappings, adDailyRows, adHourlyRows);
      } else if (url.includes("/sydney/api/goodsDataShow/queryGoodsStatDtr")) {
        collectShopDtrRows(record, shopHourlyRows);
      } else if (url.includes("/sydney/api/goodsDataShow/queryGoodsStat")) {
        collectShopStatRows(record, shopDailyRows);
      } else if (url.includes("/sydney/api/mallCoreData/queryGoodsAfterSales")) {
        collectAfterSalesRows(record, afterSalesRows);
      } else if (url.includes("/sydney/api/goodsDataShow/queryGoodsDetailVOListForMMS")) {
        collectGoodsListMappings(record, promotionMappings);
      }
    });

    const promotionOperationLogs = extractPromotionOperationLogsFromRecords(parsedRecords, promotionMappings);
    const scopedAdDailyRows = limitRowsToLatestDates(adDailyRows, 30);
    const denseAdHourlyRows = expandAdHourlyRowsToFullHours(
      limitRowsToLatestDates(adHourlyRows, 30),
      scopedAdDailyRows
    );
    const scopedAdHourlyRows = applyOperationLogsToHourlyRows(
      denseAdHourlyRows,
      promotionOperationLogs
    );
    const mappingRows = Array.from(promotionMappings.values()).sort(compareByGoodsAndAd);
    const files = [];

    if (mappingRows.length) {
      files.push({
        category: "通用",
        filename: buildQuantCsvFilename("通用_商品ID映射表", mappingRows),
        headers: [
          "商品ID",
          "广告ID",
          "计划ID",
          "店铺ID",
          "商品名称",
          "当前目标投产比",
          "当前日限额",
          "今日花费",
          "来源"
        ],
        rows: mappingRows.map((item) => ({
          "商品ID": item.goodsId || "",
          "广告ID": item.adId || "",
          "计划ID": item.planId || "",
          "店铺ID": item.mallId || "",
          "商品名称": item.goodsName || "",
          "当前目标投产比": item.targetRoi || "",
          "当前日限额": item.dailyLimit || "",
          "今日花费": item.dailyCost || "",
          "来源": item.source || ""
        }))
      });
    }

    addCsvFile(files, "直通车", "直通车_商品日级历史", scopedAdDailyRows, [
      "商品ID",
      "日期",
      "当前目标投产比",
      "当前日限额",
      "花费",
      "实际投产比",
      "曝光",
      "点击",
      "成交笔数",
      "直接成交笔数",
      "点击率",
      "广告ID",
      "口径"
    ]);
    addCsvFile(files, "直通车", "直通车_操作记录", promotionOperationLogs.map((item) => ({
      "商品ID": item.goodsId || "",
      "广告ID": item.adId || "",
      "操作时间": item.operationTime || "",
      "操作": item.operation || "",
      "操作详情": item.operationDetail || ""
    })), [
      "商品ID",
      "广告ID",
      "操作时间",
      "操作",
      "操作详情"
    ], { force: true });
    addCsvFile(files, "直通车", "直通车_商品分时历史", scopedAdHourlyRows, [
      "商品ID",
      "日期",
      "小时",
      "小时区间",
      "当前目标投产比",
      "当前日限额",
      "花费",
      "实际投产比",
      "曝光",
      "点击",
      "成交笔数",
      "直接成交笔数",
      "点击率",
      "操作时间",
      "操作",
      "操作详情",
      "广告ID",
      "口径"
    ]);
    addCsvFile(files, "店铺后台", "店铺后台_商品日级历史", shopDailyRows, [
      "商品ID",
      "日期",
      "访客",
      "成交",
      "收藏",
      "浏览量",
      "成交金额",
      "商品名称",
      "口径"
    ]);
    addCsvFile(files, "店铺后台", "店铺后台_商品分时历史", shopHourlyRows, [
      "商品ID",
      "日期",
      "小时",
      "小时区间",
      "访客",
      "成交",
      "收藏",
      "浏览量",
      "成交金额",
      "累计访客",
      "累计成交",
      "商品名称",
      "口径"
    ]);
    addCsvFile(files, "店铺后台", "店铺后台_售后退款", afterSalesRows, [
      "商品ID",
      "日期",
      "商品名称",
      "店铺ID",
      "店铺名称",
      "近1月退款成功订单数",
      "近1月退款金额(分)",
      "近1月退款金额(元)",
      "平台介入订单数1m",
      "平台介入率1m",
      "品质退款率1m",
      "商品品质帮助率1m"
    ]);

    const diagnostics = buildQuantDiagnostics({
      exporterVersion: EXTENSION_VERSION,
      recordCount: runtime.networkRecords.length,
      parsedRecordCount: parsedRecords.length,
      operationLogRows: promotionOperationLogs.length,
      urls: countBy(runtime.networkRecords.map((record) => record.url || ""))
    }, files);

    if (!files.length) {
      files.push({
        category: "诊断",
        filename: buildFilename("诊断_量化导出诊断"),
        extension: "html",
        mime: "text/html;charset=utf-8",
        text: buildQuantExportHtml([], diagnostics)
      });
    }

    return {
      filename: buildFilename("拼多多量化导出包"),
      extension: "html",
      mime: "text/html;charset=utf-8",
      text: buildQuantExportHtml(files, diagnostics),
      message: `已生成量化导出包：${files.length} 张表`
    };
  }

  async function exportAdHourlyCsvFromNetworkRecords(options = {}) {
    postNetworkCommand({ type: "dump" });
    await sleep(180);
    if (!runtime.networkRecords.length) {
      throw new Error("还没有接口记录。请先抓接口或先执行“自动采车30天”。");
    }

    const parsedRecords = runtime.networkRecords
      .map((record) => ({
        ...record,
        bodyJson: parseJsonSafe(record.requestBody),
        responseJson: parseJsonSafe(record.responseText)
      }))
      .filter((record) => record.responseJson);

    const promotionMappings = collectPromotionMappings(parsedRecords);
    const adDailyRows = [];
    const adHourlyRows = [];
    parsedRecords.forEach((record) => {
      const url = record.url || "";
      if (url.includes("/poseidon/api/report/queryHourlyRangeReport")) {
        collectAdReportRows(record, promotionMappings, adDailyRows, adHourlyRows);
      } else if (url.includes("/sydney/api/goodsDataShow/queryGoodsDetailVOListForMMS")) {
        collectGoodsListMappings(record, promotionMappings);
      }
    });

    const yesterday = formatLocalDate(addLocalDays(new Date(), -1));
    const exportScope = options.scope === "current"
      ? getCurrentPromotionExportScope(runtime.networkRecords, promotionMappings)
      : options.scope === "targets"
        ? buildPromotionExportScope(options.targets)
        : null;
    if (options.scope === "current" && !exportScope) {
      throw new Error("没有识别到当前推广详情商品。请确认右侧推广详情已打开后再导出昨日分时。");
    }
    if (options.scope === "targets" && !exportScope) {
      throw new Error("没有识别到要导出的推广计划。请先确认当前列表已加载。");
    }

    const yesterdayHourlyRows = adHourlyRows.filter((row) =>
      normalizeDate(row["日期"]) === yesterday && rowMatchesPromotionExportScope(row, exportScope)
    );
    const yesterdayDailyRows = adDailyRows.filter((row) =>
      normalizeDate(row["日期"]) === yesterday && rowMatchesPromotionExportScope(row, exportScope)
    );
    if (!yesterdayHourlyRows.length && !yesterdayDailyRows.length) {
      const scopeText = exportScope ? `当前商品 ${Array.from(exportScope.goodsIds).join("、") || Array.from(exportScope.adIds).join("、")}` : "已抓记录";
      throw new Error(`没有找到 ${scopeText} 在 ${yesterday} 的直通车分时数据。请先采集昨天日期，或执行“自动采车30天”后再导出昨日 00:00-24:00 分时。`);
    }

    const promotionOperationLogs = extractPromotionOperationLogsFromRecords(parsedRecords, promotionMappings)
      .filter((log) => operationLogSlot(log.operationTime).date === yesterday && logMatchesPromotionExportScope(log, exportScope));
    const rows = applyOperationLogsToHourlyRows(
      expandAdHourlyRowsToFullHours(
        yesterdayHourlyRows,
        yesterdayDailyRows
      ),
      promotionOperationLogs
    ).filter((row) => normalizeDate(row["日期"]) === yesterday);
    const headers = [
      "商品ID",
      "日期",
      "小时",
      "小时区间",
      "当前目标投产比",
      "当前日限额",
      "花费",
      "实际投产比",
      "曝光",
      "点击",
      "成交笔数",
      "直接成交笔数",
      "点击率",
      "操作时间",
      "操作",
      "操作详情",
      "广告ID",
      "口径"
    ];
    const deduped = sortQuantRows(dedupeRows(rows, headers), headers);
    if (!deduped.length) {
      throw new Error("没有可导出的直通车分时数据。请先打开推广详情并采集日期。");
    }
    return {
      filename: buildQuantCsvFilename(options.filenamePrefix || (exportScope ? "直通车_昨日分时" : "直通车_已抓昨日分时"), deduped),
      headers,
      rows: deduped
    };
  }

  async function batchExportYesterdayHourlyCsvForCurrentList(action, button) {
    if (window.location.hostname !== "yingxiao.pinduoduo.com") {
      throw new Error("批量导出昨日分时只能在拼多多推广平台使用。");
    }

    await waitForForegroundTab("批量导出昨日分时需要保持当前标签页在前台");
    await ensurePageHookReady();
    ensureNetworkCaptureEnabled({ clear: false });
    postNetworkCommand({ type: "dump" });
    await sleep(240);

    const parsedRecords = runtime.networkRecords
      .map((record) => ({
        ...record,
        bodyJson: parseJsonSafe(record.requestBody),
        responseJson: parseJsonSafe(record.responseText)
      }))
      .filter((record) => record.responseJson);
    const promotionMappings = collectPromotionMappings(parsedRecords);
    const targets = await collectPromotionTargetsForYesterdayHourlyFetch(parsedRecords, promotionMappings);
    if (!targets.length) {
      throw new Error("没有识别到当前列表里的推广计划。请先等列表加载完成，或先点一次“导出CSV”确认列表可读取。");
    }

    const limitedTargets = targets.slice(0, 300);
    const limitedNote = targets.length > limitedTargets.length ? `，已限制前 ${limitedTargets.length} 个` : "";
    const yesterday = formatLocalDate(addLocalDays(new Date(), -1));
    const reportTemplates = findAdReportTemplates(runtime.networkRecords);
    const template = reportTemplates[0]?.body || {};
    const url = reportTemplates[0]?.record?.url || "/mms-gateway/poseidon/api/report/queryHourlyRangeReport";
    let success = 0;
    let failures = [];
    let directFailureStreak = 0;

    for (let index = 0; index < limitedTargets.length; index += 1) {
      const target = limitedTargets[index];
      if (!shouldTryDirectYesterdayHourlyFetch(target) || directFailureStreak >= 2) {
        failures.push({
          target,
          reason: "需要打开详情读取请求参数"
        });
        continue;
      }
      if (button) {
        button.textContent = `${index + 1}/${limitedTargets.length}`;
      }
      showToast(`批量采昨日分时：${index + 1}/${limitedTargets.length}`, 3000);
      pushPromotionContextMappingRecord(target);

      const requestBody = buildAdReportRequestBody(template, {
        adId: target.adId,
        startDate: yesterday,
        endDate: yesterday,
        endDayHour: 23
      });
      const startedAt = performance.now();

      try {
        const result = await pageContextRequest({
          method: "POST",
          url,
          headers: {
            "content-type": "application/json;charset=UTF-8"
          },
          body: requestBody,
          timeoutMs: 18000
        });
        pushExtensionNetworkRecord({
          source: "extension-ad-yesterday-hourly-batch-fetch",
          method: "POST",
          url,
          requestBody,
          status: result.status || "",
          contentType: result.contentType || "",
          durationMs: Math.round(performance.now() - startedAt),
          relevant: true,
          responseText: result.responseText || ""
        });

        const parsed = parseJsonSafe(result.responseText);
        if (isAdReportFetchResultUsable(result, parsed)) {
          success += 1;
          directFailureStreak = 0;
        } else {
          directFailureStreak += 1;
          failures.push({
            target,
            reason: parsed?.errorMsg || parsed?.message || result.status || "无可用分时数据"
          });
        }
      } catch (error) {
        directFailureStreak += 1;
        failures.push({
          target,
          reason: error?.message || "请求失败"
        });
      }

      await sleep(120);
    }

    if (failures.length) {
      const fallback = await fetchYesterdayHourlyByOpeningPromotionDetails(
        failures.map((item) => item.target),
        button
      );
      success += fallback.success;
      failures = fallback.failures;
    }

    if (!success) {
      throw new Error(`昨日分时批量补采没有成功。失败样例：${formatPromotionFailures(failures) || "无响应"}`);
    }

    const exportResult = await exportAdHourlyCsvFromNetworkRecords({
      scope: "targets",
      targets: limitedTargets,
      filenamePrefix: "直通车_当前列表昨日分时"
    });
    const failNote = failures.length ? `，失败 ${failures.length} 个：${formatPromotionFailures(failures)}` : "";
    return {
      ...exportResult,
      message: `已补采并导出昨日分时：成功 ${success}/${limitedTargets.length}${limitedNote}${failNote}`,
      toastDurationMs: failures.length ? 14000 : 7000
    };
  }

  async function fetchYesterdayHourlyByOpeningPromotionDetails(targets, button) {
    const retryTargets = (targets || []).filter((target) => normalizeId(target?.goodsId));
    const failures = (targets || [])
      .filter((target) => !normalizeId(target?.goodsId))
      .map((target) => ({ target, reason: "缺少商品ID，无法自动打开详情" }));
    const yesterday = formatLocalDate(addLocalDays(new Date(), -1));
    let success = 0;

    for (let index = 0; index < retryTargets.length; index += 1) {
      await waitForForegroundTab("自动打开详情时请保持当前标签页在前台");
      const target = retryTargets[index];
      const goodsId = normalizeId(target.goodsId);
      if (button) {
        button.textContent = `详情 ${index + 1}/${retryTargets.length}`;
      }
      showToast(`自动打开详情补采昨日分时：${index + 1}/${retryTargets.length}`, 5000);

      try {
        await openPromotionDetailByGoodsId(goodsId, {
          maxAttempts: 3,
          label: `${index + 1}/${retryTargets.length}`
        });

        let context = await getCurrentPromotionContext({ requireReportTemplate: false });
        context = await waitForPromotionContextWithAdId(context, 6500);
        if (!context.adId) {
          throw new Error("没有识别到直通车计划请求参数");
        }
        context = {
          ...target,
          ...context,
          goodsId: context.goodsId || goodsId,
          goodsName: context.goodsName || target.goodsName || ""
        };
        pushPromotionContextMappingRecord(context);

        try {
          await waitForForegroundTab("切换详情日期时请保持当前标签页在前台");
          await switchPromotionDrawerMenu("推广详情", "0").catch(() => {});
          if (findPromotionAdReportRecord(yesterday, context.adId)) {
            success += 1;
            continue;
          }
          const beforeCount = runtime.networkRecords.length;
          const changed = await selectPromotionDetailDate(yesterday, { maxAttempts: 1 });
          if (!changed) {
            showToast("日期状态未确认，继续等待接口", 2200);
          }
          if (!findPromotionAdReportRecord(yesterday, context.adId)) {
            await waitForPromotionAdReportRecord(yesterday, beforeCount, context.adId, 4500, { intervalMs: 1000 });
          }
          success += 1;
          continue;
        } catch (pageError) {
          console.warn("[pdd-table-exporter] page-driven yesterday fetch failed", pageError);
        }

        const matchedTemplate = findAdReportTemplates(runtime.networkRecords)
          .find((item) => normalizeId(item.body?.entityId) === normalizeId(context.adId));
        const requestBody = buildAdReportRequestBody(context.templateBody || matchedTemplate?.body || {}, {
          adId: context.adId,
          startDate: yesterday,
          endDate: yesterday,
          endDayHour: 23
        });
        const url = context.url || matchedTemplate?.record?.url || "/mms-gateway/poseidon/api/report/queryHourlyRangeReport";
        const startedAt = performance.now();
        const result = await pageContextRequest({
          method: "POST",
          url,
          headers: {
            "content-type": "application/json;charset=UTF-8"
          },
          body: requestBody,
          timeoutMs: 18000
        });
        pushExtensionNetworkRecord({
          source: "extension-ad-yesterday-hourly-detail-fallback",
          method: "POST",
          url,
          requestBody,
          status: result.status || "",
          contentType: result.contentType || "",
          durationMs: Math.round(performance.now() - startedAt),
          relevant: true,
          responseText: result.responseText || ""
        });

        const parsed = parseJsonSafe(result.responseText);
        if (isAdReportFetchResultUsable(result, parsed)) {
          success += 1;
        } else {
          failures.push({
            target: context,
            reason: parsed?.errorMsg || parsed?.message || result.status || "无可用分时数据"
          });
        }
      } catch (error) {
        failures.push({
          target,
          reason: error?.message || "详情补采失败"
        });
      } finally {
        await sleep(700);
      }
    }

    return { success, failures };
  }

  function shouldTryDirectYesterdayHourlyFetch(target) {
    const requestId = normalizeId(target?.adId);
    const goodsId = normalizeId(target?.goodsId);
    if (!requestId) {
      return false;
    }
    return !(goodsId && requestId === goodsId);
  }

  async function openPromotionDetailByGoodsId(goodsId, options = {}) {
    const normalizedGoodsId = normalizeId(goodsId);
    const maxAttempts = options.maxAttempts || 3;
    let lastError = null;

    for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
      try {
        await waitForForegroundTab("打开计划详情时请保持当前标签页在前台");
        showToast(`打开计划详情 ${options.label || ""}${attempt ? `，重试 ${attempt + 1}/${maxAttempts}` : ""}`, 4000);
        if (attempt > 0) {
          const closed = await closePromotionDetailDrawer();
          if (!closed) {
            document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", code: "Escape", bubbles: true }));
            await sleep(900);
          }
        }

        await waitForCondition(
          () => Boolean(findPromotionSearchInput()),
          7000,
          180,
          "列表搜索框未恢复"
        );
        if (await openVisiblePromotionRowByGoodsId(normalizedGoodsId).catch(() => false)) {
          return true;
        }
        await searchPromotionGoodsId(normalizedGoodsId);
        await openPromotionSearchResult(normalizedGoodsId);
        await waitForPromotionDetailGoodsId(normalizedGoodsId, 18000);
        return true;
      } catch (error) {
        lastError = error;
        console.warn("[pdd-table-exporter] open promotion detail failed", normalizedGoodsId, error);
        await closePromotionDetailDrawer().catch(() => {});
        await sleep(1200 + attempt * 700);
      }
    }

    throw new Error(`打开商品ID ${normalizedGoodsId} 的推广详情失败：${lastError?.message || "未知错误"}`);
  }

  async function openVisiblePromotionRowByGoodsId(goodsId) {
    const title = findPromotionResultNameNode(goodsId, { allowFallback: false });
    if (!title) {
      return false;
    }
    title.scrollIntoView?.({ block: "center", inline: "nearest" });
    await sleep(180);
    triggerPreciseElementClick(title, { xRatio: 0.35, yRatio: 0.5 });
    await waitForPromotionDetailGoodsId(goodsId, 12000);
    return true;
  }

  function isAdReportFetchResultUsable(result, parsed) {
    const data = parsed?.result || {};
    return Boolean(
      result?.ok &&
      (
        (Array.isArray(data.hourlyReportList) && data.hourlyReportList.length > 0) ||
        Boolean(data.dailyReport) ||
        (Array.isArray(data.dailyReportList) && data.dailyReportList.length > 0)
      )
    );
  }

  function promotionTargetLabel(target) {
    return normalizeId(target?.goodsId) || normalizeId(target?.adId) || "未知计划";
  }

  function formatPromotionFailures(failures) {
    return (failures || [])
      .slice(0, 5)
      .map((item) => {
        const label = promotionTargetLabel(item.target);
        return item.reason ? `${label}(${item.reason})` : label;
      })
      .join("、");
  }

  function buildPromotionExportScope(targets) {
    const goodsIds = new Set();
    const adIds = new Set();
    (targets || []).forEach((target) => {
      const goodsId = normalizeId(target?.goodsId || target?.["商品ID"]);
      const adId = normalizeId(target?.adId || target?.["广告ID"]);
      if (goodsId) {
        goodsIds.add(goodsId);
      }
      if (adId) {
        adIds.add(adId);
      }
    });
    if (!goodsIds.size && !adIds.size) {
      return null;
    }
    return { goodsIds, adIds };
  }

  async function collectPromotionTargetsForYesterdayHourlyFetch(records, mappingMap = new Map()) {
    const latestListTargets = collectPromotionTargetsFromLatestListRecord(records, mappingMap);
    const tableTargets = await collectPromotionTargetsFromCurrentTable(mappingMap).catch(() => []);
    if (tableTargets.length && latestListTargets.length) {
      const tableGoodsIds = new Set(tableTargets.map((item) => normalizeId(item.goodsId)).filter(Boolean));
      const tableAdIds = new Set(tableTargets.map((item) => normalizeId(item.adId)).filter(Boolean));
      const matchingListTargets = latestListTargets.filter((item) =>
        (tableGoodsIds.size && tableGoodsIds.has(normalizeId(item.goodsId))) ||
        (tableAdIds.size && tableAdIds.has(normalizeId(item.adId)))
      );
      if (matchingListTargets.length) {
        return matchingListTargets;
      }
    }
    if (tableTargets.length) {
      return tableTargets;
    }

    if (latestListTargets.length) {
      return latestListTargets;
    }

    return collectPromotionTargetsFromMappings(mappingMap);
  }

  async function collectPromotionTargetsFromCurrentTable(mappingMap = new Map()) {
    const tableBodies = getPromotionTableBodies();
    const tableBody = tableBodies[0];
    if (!tableBody) {
      return [];
    }

    const metricHeaders = getPromotionMetricHeaders();
    const scrollTargets = findPromotionScrollTargets(tableBody);
    const targets = new Map();

    await collectVirtualRows(scrollTargets, tableBody, () => {
      const rows = tableBodies.flatMap((body) => Array.from(body.querySelectorAll("tr[data-row-key]")));
      rows.forEach((row) => {
        const parsed = parsePromotionRow(row, metricHeaders);
        const rowKey = normalizeId(row.dataset.rowKey || "");
        const goodsId = normalizeId(parsed?.["商品ID"]);
        const mapping = findPromotionMappingForRow(rowKey, goodsId, mappingMap);
        const adId = normalizeId(mapping.adId || rowKey);
        if (!adId) {
          return;
        }
        upsertPromotionTarget(targets, {
          adId,
          goodsId: goodsId || mapping.goodsId || "",
          goodsName: parsed?.["商品名称"] || mapping.goodsName || "",
          targetRoi: parsed?.["出价值"] || mapping.targetRoi || "",
          dailyLimit: parsed?.["预算日限额"] || mapping.dailyLimit || "",
          dailyCost: mapping.dailyCost || "",
          planId: mapping.planId || "",
          mallId: mapping.mallId || "",
          source: "current-table"
        });
      });
      return targets.size;
    });

    return Array.from(targets.values());
  }

  function collectPromotionTargetsFromLatestListRecord(records, mappingMap = new Map()) {
    const latest = (records || [])
      .slice()
      .reverse()
      .find((record) => (record.url || "").includes("/venus/api/goods/promotion/v3/list"));
    if (!latest?.responseJson?.result) {
      return [];
    }

    const targets = new Map();
    walkObjects(latest.responseJson.result, (node) => {
      const adId = normalizeId(node.adId || node.adInfo?.adId || node.promotionInfo?.adId);
      const goodsId = normalizeId(node.goodsId || node.goodsInfo?.goodsId || node.goods?.goodsId);
      if (!adId) {
        return;
      }
      const mapping = findPromotionMappingForRow(adId, goodsId, mappingMap);
      upsertPromotionTarget(targets, {
        adId,
        goodsId: goodsId || mapping.goodsId || "",
        goodsName: node.goodsName || node.goodsInfo?.goodsName || node.goods?.goodsName || mapping.goodsName || "",
        targetRoi: extractMetricValue(node.targetRoi || node.targetRoiValue || node.bidValue) || mapping.targetRoi || "",
        dailyLimit: extractMetricValue(node.maxCost || node.dailyCostLimit || node.budget) || mapping.dailyLimit || "",
        dailyCost: extractMetricValue(node.dailyCost || node.cost) || mapping.dailyCost || "",
        planId: normalizeId(node.planId || node.planInfo?.planId || mapping.planId),
        mallId: normalizeId(node.mallId || mapping.mallId),
        source: "latest-list-api"
      });
    });

    return Array.from(targets.values());
  }

  function collectPromotionTargetsFromMappings(mappingMap = new Map()) {
    const targets = new Map();
    mappingMap.forEach((item) => {
      if (!normalizeId(item.adId)) {
        return;
      }
      upsertPromotionTarget(targets, {
        adId: item.adId,
        goodsId: item.goodsId || "",
        goodsName: item.goodsName || "",
        targetRoi: item.targetRoi || "",
        dailyLimit: item.dailyLimit || "",
        dailyCost: item.dailyCost || "",
        planId: item.planId || "",
        mallId: item.mallId || "",
        source: item.source || "mapping-cache"
      });
    });
    return Array.from(targets.values());
  }

  function findPromotionMappingForRow(rowKey, goodsId, mappingMap = new Map()) {
    const normalizedRowKey = normalizeId(rowKey);
    const normalizedGoodsId = normalizeId(goodsId);
    if (normalizedRowKey && mappingMap.has(normalizedRowKey)) {
      return mappingMap.get(normalizedRowKey) || {};
    }
    if (normalizedGoodsId) {
      for (const item of mappingMap.values()) {
        if (normalizeId(item.goodsId) === normalizedGoodsId) {
          return item;
        }
      }
    }
    if (normalizedRowKey) {
      for (const item of mappingMap.values()) {
        if (normalizeId(item.goodsId) === normalizedRowKey || normalizeId(item.adId) === normalizedRowKey) {
          return item;
        }
      }
    }
    return {};
  }

  function upsertPromotionTarget(targets, item) {
    const adId = normalizeId(item.adId);
    if (!adId) {
      return;
    }
    const current = targets.get(adId) || { adId };
    const merged = { ...current, adId };
    Object.entries(item).forEach(([field, value]) => {
      const normalizedValue = field === "adId" || field === "goodsId" || field === "planId" || field === "mallId"
        ? normalizeId(value)
        : value;
      if (normalizedValue !== undefined && normalizedValue !== null && normalizedValue !== "" && !merged[field]) {
        merged[field] = normalizedValue;
      }
    });
    targets.set(adId, merged);
  }

  function getCurrentPromotionExportScope(records, mappingMap = new Map()) {
    const domInfo = findPromotionDetailInfoFromDom();
    const latestInfo = findLatestPromotionDetailInfo(records || []);
    const domGoodsId = normalizeId(domInfo.goodsId);
    const latestGoodsId = normalizeId(latestInfo.goodsId);
    const goodsId = domGoodsId || latestGoodsId;
    const adIds = new Set();
    const goodsIds = new Set();

    if (goodsId) {
      goodsIds.add(goodsId);
    }
    if (latestInfo.adId && (!domGoodsId || latestGoodsId === domGoodsId)) {
      adIds.add(normalizeId(latestInfo.adId));
    }
    if (goodsId && mappingMap?.size) {
      mappingMap.forEach((item) => {
        if (normalizeId(item.goodsId) === goodsId && normalizeId(item.adId)) {
          adIds.add(normalizeId(item.adId));
        }
      });
    }
    if (!goodsIds.size && latestInfo.adId) {
      adIds.add(normalizeId(latestInfo.adId));
    }
    if (!goodsIds.size && !adIds.size) {
      return null;
    }
    return { goodsIds, adIds };
  }

  function rowMatchesPromotionExportScope(row, scope) {
    if (!scope) {
      return true;
    }
    const goodsId = normalizeId(row["商品ID"] || row.goodsId);
    const adId = normalizeId(row["广告ID"] || row.adId);
    return Boolean((goodsId && scope.goodsIds.has(goodsId)) || (adId && scope.adIds.has(adId)));
  }

  function logMatchesPromotionExportScope(log, scope) {
    if (!scope) {
      return true;
    }
    const goodsId = normalizeId(log.goodsId || log["商品ID"]);
    const adId = normalizeId(log.adId || log["广告ID"]);
    return Boolean((goodsId && scope.goodsIds.has(goodsId)) || (adId && scope.adIds.has(adId)));
  }

  function addCsvFile(files, category, prefix, rows, headers, options = {}) {
    const deduped = sortQuantRows(dedupeRows(rows, headers), headers);
    if (!deduped.length && !options.force) {
      return;
    }
    files.push({
      category,
      filename: buildQuantCsvFilename(prefix, deduped),
      headers,
      rows: deduped
    });
  }

  function limitRowsToLatestDates(rows, dayLimit) {
    const dates = Array.from(new Set((rows || []).map((row) => normalizeDate(row["日期"])).filter(Boolean)))
      .sort((left, right) => left.localeCompare(right));
    if (dates.length <= dayLimit) {
      return rows;
    }
    const keepDates = new Set(dates.slice(-dayLimit));
    return rows.filter((row) => keepDates.has(normalizeDate(row["日期"])));
  }

  function buildQuantCsvFilename(prefix, rows) {
    const ids = Array.from(new Set((rows || []).map((row) => normalizeId(row["商品ID"] || row.goodsId || row.adId || row["广告ID"])).filter(Boolean)))
      .sort((left, right) => left.localeCompare(right));
    if (ids.length === 1) {
      return `${prefix}_${ids[0]}`;
    }
    if (ids.length > 1) {
      return `${prefix}_多商品${ids.length}`;
    }
    return prefix;
  }

  function extractPromotionOperationLogsFromRecords(records, mappingMap = new Map()) {
    const logs = [];
    records.forEach((record) => {
      const url = record.url || "";
      if (url.includes("/extension/promotion/operation-logs")) {
        const items = record.responseJson?.result?.operationLogs || [];
        items.forEach((item) => {
          logs.push({
            goodsId: normalizeId(item.goodsId),
            adId: normalizeId(item.adId),
            operationTime: normalizeOperationTime(item.operationTime),
            operation: item.operation || "",
            operationDetail: item.operationDetail || ""
          });
        });
      } else if (/queryAdOperationLogList/i.test(url)) {
        logs.push(...extractPromotionOperationLogsFromApiRecord(record, mappingMap));
      }
    });
    return dedupeOperationLogs(logs);
  }

  function extractPromotionOperationLogsFromApiRecord(record, mappingMap = new Map()) {
    const logs = [];
    const body = record.bodyJson || {};
    const bodyText = JSON.stringify(body || {});
    const fallbackAdId = normalizeId(
      firstObjectFieldValue(body, ["adId", "entityId", "promotionId", "ad_id"]) ||
      extractOperationLogBodyAdIds(body)[0] ||
      extractFirstMatch(bodyText, /"(?:adId|entityId|promotionId)"\s*:\s*"?([0-9]{6,20})"?/)
    );
    const fallbackGoodsId = normalizeId(
      firstObjectFieldValue(body, ["goodsId", "goods_id"]) ||
      goodsIdFromAdId(fallbackAdId, mappingMap) ||
      extractFirstMatch(bodyText, /"(?:goodsId|goods_id)"\s*:\s*"?([0-9]{8,20})"?/)
    );

    walkObjects(record.responseJson?.result || record.responseJson, (node) => {
      const operationTime = normalizeOperationTime(
        firstObjectFieldValue(node, [
          "operationTime",
          "操作时间",
          "operateTime",
          "operateTimeStr",
          "opTime",
          "createTime",
          "createTimeStr",
          "createdAt",
          "gmtCreate",
          "logTime",
          "time",
          "date",
          "datetime"
        ])
      );
      const operation = stringifyLogValue(
        firstObjectFieldValue(node, [
          "operation",
          "操作",
          "操作类型",
          "operate",
          "operateName",
          "operateTypeName",
          "operationName",
          "action",
          "actionName",
          "typeName",
          "eventName"
        ])
      ) || "更新推广";
      const directDetail = stringifyLogValue(
        firstObjectFieldValue(node, [
          "operationDetail",
          "操作详情",
          "详情",
          "operationContent",
          "operateDetail",
          "operateInfo",
          "detail",
          "details",
          "logDetail",
          "content",
          "message",
          "changeContent",
          "changeLog",
          "operateContent",
          "opContent",
          "bizContent",
          "logContent",
          "desc",
          "description",
          "remark"
        ])
      );
      const operationDetail = directDetail || stringifyOperationNode(node);
      const objectText = stringifyLogValue(
        firstObjectFieldValue(node, ["operationObject", "操作对象", "operateObject", "objectName", "targetName", "adName", "goodsName"])
      );
      const mergedText = `${objectText} ${operation} ${operationDetail}`;
      if (!operationTime || !operationDetail || !/(投产比|预算|日限额|出价|推广|更新)/.test(mergedText)) {
        return;
      }
      const adId = normalizeId(firstObjectFieldValue(node, ["adId", "entityId", "promotionId", "ad_id"]) || fallbackAdId);
      const goodsId = normalizeId(
        firstObjectFieldValue(node, ["goodsId", "goods_id"]) ||
        extractFirstMatch(mergedText, /商品ID[:：]?\s*([0-9]{8,20})/) ||
        goodsIdFromAdId(adId, mappingMap) ||
        fallbackGoodsId
      );
      logs.push({
        goodsId,
        adId,
        operationTime,
        operation,
        operationDetail
      });
    });

    if (!logs.length) {
      logs.push(...extractPromotionOperationLogsFromText(record.responseText || "", fallbackAdId, fallbackGoodsId));
    }
    return logs;
  }

  function extractPromotionOperationLogsFromText(text, fallbackAdId, fallbackGoodsId) {
    const logs = [];
    const source = String(text || "");
    const pattern = /(20\d{2}[-/]\d{1,2}[-/]\d{1,2}\s+\d{1,2}:\d{2}(?::\d{2})?)[\s\S]{0,500}?(更新[^"<>\\]{0,160}(?:投产比|日限额|预算|出价)[^"<>\\]{0,160})/g;
    let match;
    while ((match = pattern.exec(source))) {
      logs.push({
        goodsId: normalizeId(extractFirstMatch(match[0], /商品ID[:：]?\s*([0-9]{8,20})/) || fallbackGoodsId),
        adId: normalizeId(fallbackAdId),
        operationTime: normalizeOperationTime(match[1]),
        operation: "更新推广",
        operationDetail: cleanText(match[2])
      });
    }
    return logs;
  }

  function firstObjectFieldValue(node, names) {
    if (!node || typeof node !== "object") {
      return "";
    }
    const normalized = new Map();
    Object.keys(node).forEach((key) => normalized.set(String(key).toLowerCase().replace(/[_-]/g, ""), key));
    for (const name of names) {
      const key = normalized.get(String(name).toLowerCase().replace(/[_-]/g, ""));
      const value = key ? node[key] : undefined;
      if (value !== undefined && value !== null && value !== "") {
        return value;
      }
    }
    return "";
  }

  function stringifyLogValue(value) {
    if (value === undefined || value === null || value === "") {
      return "";
    }
    if (typeof value === "object") {
      return cleanText(JSON.stringify(value).replace(/[{}\[\]",]/g, " "));
    }
    return cleanText(String(value));
  }

  function stringifyOperationNode(node) {
    const text = stringifyLogValue(node);
    if (!/(投产比|预算|日限额|出价)/.test(text)) {
      return "";
    }
    return text.slice(0, 360);
  }

  function goodsIdFromAdId(adId, mappingMap = new Map()) {
    const normalizedAdId = normalizeId(adId);
    if (!normalizedAdId || !mappingMap?.size) {
      return "";
    }
    const direct = mappingMap.get(normalizedAdId);
    if (direct?.goodsId) {
      return normalizeId(direct.goodsId);
    }
    for (const item of mappingMap.values()) {
      if (normalizeId(item.adId) === normalizedAdId && item.goodsId) {
        return normalizeId(item.goodsId);
      }
    }
    return "";
  }

  function expandAdHourlyRowsToFullHours(rows, dailyRows = []) {
    const rowMap = new Map();
    const scopes = new Map();

    const rememberScope = (row) => {
      const goodsId = normalizeId(row["商品ID"]);
      const adId = normalizeId(row["广告ID"]);
      const date = normalizeDate(row["日期"]);
      if (!goodsId && !adId) {
        return;
      }
      const scopeKey = `${goodsId}|${adId}`;
      if (!scopes.has(scopeKey)) {
        scopes.set(scopeKey, {
          goodsId,
          adId,
          targetRoi: row["当前目标投产比"] || "",
          dailyLimit: row["当前日限额"] || "",
          dates: new Set()
        });
      }
      const scope = scopes.get(scopeKey);
      if (!scope.targetRoi && row["当前目标投产比"]) {
        scope.targetRoi = row["当前目标投产比"];
      }
      if (!scope.dailyLimit && row["当前日限额"]) {
        scope.dailyLimit = row["当前日限额"];
      }
      if (date) {
        scope.dates.add(date);
      }
    };

    dailyRows.forEach(rememberScope);
    rows.forEach((row) => {
      rememberScope(row);
      rowMap.set(adHourlyRowKey(row), row);
    });

    scopes.forEach((scope) => {
      Array.from(scope.dates)
        .sort((left, right) => left.localeCompare(right))
        .forEach((date) => {
          const maxHour = maxHourForAdHourlyDate(date);
          for (let hour = 0; hour <= maxHour; hour += 1) {
            const hourText = formatHour(hour);
            const key = [scope.goodsId, scope.adId, date, hourText].join("||");
            if (rowMap.has(key)) {
              return;
            }
            rowMap.set(key, {
              "商品ID": scope.goodsId,
              "日期": date,
              "小时": hourText,
              "小时区间": formatHourRange(date, hourText),
              "当前目标投产比": scope.targetRoi,
              "当前日限额": scope.dailyLimit,
              "花费": "0",
              "实际投产比": "0.00",
              "曝光": "0",
              "点击": "0",
              "成交笔数": "0",
              "直接成交笔数": "0",
              "点击率": "0.00",
              "操作时间": "",
              "操作": "",
              "操作详情": "",
              "广告ID": scope.adId,
              "口径": "补齐空小时；queryHourlyRangeReport.hourlyReportList 未返回该小时"
            });
          }
        });
    });

    return Array.from(rowMap.values());
  }

  function adHourlyRowKey(row) {
    return [
      normalizeId(row["商品ID"]),
      normalizeId(row["广告ID"]),
      normalizeDate(row["日期"]),
      row["小时"]
    ].join("||");
  }

  function maxHourForAdHourlyDate(dateText) {
    const date = normalizeDate(dateText);
    const today = formatLocalDate(new Date());
    if (date === today) {
      return Math.max(0, Math.min(23, new Date().getHours()));
    }
    return 23;
  }

  function applyOperationLogsToHourlyRows(rows, logs) {
    const rowGoodsIds = new Set(rows.map((row) => normalizeId(row["商品ID"])).filter(Boolean));
    const rowAdIds = new Set(rows.map((row) => normalizeId(row["广告ID"])).filter(Boolean));
    const singleScope = rowGoodsIds.size <= 1 && rowAdIds.size <= 1;
    const firstRow = rows[0] || {};
    const templateByGoods = new Map();
    const templateByAd = new Map();
    rows.forEach((row) => {
      const goodsId = normalizeId(row["商品ID"]);
      const adId = normalizeId(row["广告ID"]);
      if (goodsId && !templateByGoods.has(goodsId)) {
        templateByGoods.set(goodsId, row);
      }
      if (adId && !templateByAd.has(adId)) {
        templateByAd.set(adId, row);
      }
    });

    const grouped = new Map();
    logs.forEach((log) => {
      const slot = operationLogSlot(log.operationTime);
      if (!slot.date || !slot.hour) {
        return;
      }
      const keys = [];
      if (log.goodsId) {
        keys.push(`${log.goodsId}|${slot.date}|${slot.hour}`);
      }
      if (log.adId) {
        keys.push(`ad:${log.adId}|${slot.date}|${slot.hour}`);
      }
      if (singleScope) {
        keys.push(`slot:${slot.date}|${slot.hour}`);
      }
      keys.forEach((key) => {
        if (!grouped.has(key)) {
          grouped.set(key, []);
        }
        grouped.get(key).push(log);
      });
    });

    const matchedLogKeys = new Set();
    const mappedRows = rows.map((row) => {
      const goodsKey = `${normalizeId(row["商品ID"])}|${normalizeDate(row["日期"])}|${row["小时"]}`;
      const adKey = `ad:${normalizeId(row["广告ID"])}|${normalizeDate(row["日期"])}|${row["小时"]}`;
      const slotKey = `slot:${normalizeDate(row["日期"])}|${row["小时"]}`;
      const matched = [
        ...(grouped.get(goodsKey) || []),
        ...(grouped.get(adKey) || []),
        ...(singleScope ? grouped.get(slotKey) || [] : [])
      ];
      if (!matched.length) {
        return {
          ...row,
          "操作时间": "",
          "操作": "",
          "操作详情": ""
        };
      }
      const unique = dedupeOperationLogs(matched);
      unique.forEach((item) => matchedLogKeys.add(operationLogUniqueKey(item)));
      return {
        ...row,
        "操作时间": unique.map((item) => item.operationTime).join("；"),
        "操作": unique.map((item) => item.operation).join("；"),
        "操作详情": unique.map((item) => item.operationDetail).join("；")
      };
    });

    const missingGroups = new Map();
    logs.forEach((log) => {
      if (matchedLogKeys.has(operationLogUniqueKey(log))) {
        return;
      }
      const slot = operationLogSlot(log.operationTime);
      if (!slot.date || !slot.hour) {
        return;
      }
      const logGoodsId = normalizeId(log.goodsId) || (singleScope ? Array.from(rowGoodsIds)[0] || "" : "");
      const logAdId = normalizeId(log.adId) || (singleScope ? Array.from(rowAdIds)[0] || "" : "");
      const inScope =
        singleScope ||
        (logGoodsId && rowGoodsIds.has(logGoodsId)) ||
        (logAdId && rowAdIds.has(logAdId));
      if (!inScope) {
        return;
      }
      const groupKey = `${logGoodsId}|${logAdId}|${slot.date}|${slot.hour}`;
      if (!missingGroups.has(groupKey)) {
        missingGroups.set(groupKey, {
          goodsId: logGoodsId,
          adId: logAdId,
          slot,
          logs: []
        });
      }
      missingGroups.get(groupKey).logs.push(log);
    });

    const supplementalRows = Array.from(missingGroups.values()).map((group) => {
      const unique = dedupeOperationLogs(group.logs);
      const template =
        templateByGoods.get(group.goodsId) ||
        templateByAd.get(group.adId) ||
        firstRow ||
        {};
      return {
        "商品ID": group.goodsId || normalizeId(template["商品ID"]),
        "日期": group.slot.date,
        "小时": group.slot.hour,
        "小时区间": formatHourRange(group.slot.date, group.slot.hour),
        "当前目标投产比": template["当前目标投产比"] || "",
        "当前日限额": template["当前日限额"] || "",
        "花费": "",
        "实际投产比": "",
        "曝光": "",
        "点击": "",
        "成交笔数": "",
        "直接成交笔数": "",
        "点击率": "",
        "操作时间": unique.map((item) => item.operationTime).join("；"),
        "操作": unique.map((item) => item.operation).join("；"),
        "操作详情": unique.map((item) => item.operationDetail).join("；"),
        "广告ID": group.adId || normalizeId(template["广告ID"]),
        "口径": "操作记录补行；该小时直通车分时接口未返回数据"
      };
    });

    return [...mappedRows, ...supplementalRows];
  }

  function operationLogSlot(operationTime) {
    const match = String(operationTime || "").match(/(20\d{2}-\d{2}-\d{2})\s+(\d{1,2}):/);
    if (!match) {
      return { date: "", hour: "" };
    }
    return {
      date: match[1],
      hour: formatHour(match[2])
    };
  }

  function dedupeOperationLogs(logs) {
    const seen = new Set();
    return logs.filter((log) => {
      const key = operationLogUniqueKey(log);
      if (seen.has(key)) {
        return false;
      }
      seen.add(key);
      return true;
    });
  }

  function operationLogUniqueKey(log) {
    return `${log.operationTime}|${log.operation}|${log.operationDetail}`;
  }

  function buildQuantDiagnostics(base, files) {
    const countByPrefix = (prefix) => {
      const file = files.find((item) => item.filename.includes(prefix));
      return file?.rows?.length || 0;
    };
    return {
      ...base,
      mappingRows: countByPrefix("商品ID映射表"),
      adDailyRows: countByPrefix("直通车_商品日级历史"),
      adHourlyRows: countByPrefix("直通车_商品分时历史"),
      adOperationRows: countByPrefix("直通车_操作记录"),
      shopDailyRows: countByPrefix("店铺后台_商品日级历史"),
      shopHourlyRows: countByPrefix("店铺后台_商品分时历史"),
      afterSalesRows: countByPrefix("店铺后台_售后退款")
    };
  }

  function buildQuantExportHtml(files, diagnostics) {
    const sections = files
      .filter((file) => file.headers && file.rows)
      .map((file, index) => {
        const csv = buildCsv(file.headers, file.rows);
        const previewRows = file.rows.slice(0, 200);
        return `
          <section class="table-card">
            <div class="section-head">
              <div>
                <h2><span class="badge">${escapeHtml(file.category || "未分类")}</span>${escapeHtml(file.filename)}</h2>
                <p>${file.rows.length} 行，${file.headers.length} 列${file.rows.length > 200 ? "，预览前 200 行" : ""}</p>
              </div>
              <button onclick="downloadCsv(${index})">下载这张 CSV</button>
            </div>
            <div class="table-wrap">
              <table>
                <thead><tr>${file.headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("")}</tr></thead>
                <tbody>
                  ${previewRows
                    .map(
                      (row) =>
                        `<tr>${file.headers
                          .map((header) => `<td>${escapeHtml(row[header] ?? "")}</td>`)
                          .join("")}</tr>`
                    )
                    .join("")}
                </tbody>
              </table>
            </div>
          </section>
        `;
      })
      .join("");

    const payload = files
      .filter((file) => file.headers && file.rows)
      .map((file) => ({
        filename: file.filename,
        category: file.category || "",
        headers: file.headers,
        csv: buildCsv(file.headers, file.rows)
      }));

    return `<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <title>拼多多量化导出包</title>
  <style>
    :root { color-scheme: light; }
    body {
      margin: 0;
      padding: 32px;
      background: #f5f7fa;
      color: #111827;
      font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Microsoft YaHei", sans-serif;
    }
    h1 { margin: 0; font-size: 28px; letter-spacing: 0; }
    h2 { margin: 0; font-size: 18px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
    p { margin: 6px 0 0; color: #64748b; }
    .badge {
      display: inline-flex;
      align-items: center;
      height: 24px;
      padding: 0 9px;
      border-radius: 999px;
      background: #eef6ff;
      color: #0057d9;
      font-size: 12px;
      font-weight: 800;
    }
    .hero {
      background: white;
      border: 1px solid #e5e7eb;
      border-radius: 14px;
      padding: 22px;
      box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
    }
    .stats {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
      gap: 12px;
      margin-top: 18px;
    }
    .stat {
      background: #f8fafc;
      border: 1px solid #e5e7eb;
      border-radius: 10px;
      padding: 12px;
    }
    .stat b { display: block; font-size: 22px; margin-top: 4px; }
    .actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
    button {
      height: 38px;
      padding: 0 14px;
      border: 1px solid #007aff;
      border-radius: 10px;
      background: #007aff;
      color: white;
      font-weight: 700;
      cursor: pointer;
    }
    button.secondary { background: white; color: #1f2937; border-color: #d7dce5; }
    .table-card {
      margin-top: 18px;
      background: white;
      border: 1px solid #e5e7eb;
      border-radius: 14px;
      overflow: hidden;
      box-shadow: 0 12px 30px rgba(15, 23, 42, 0.05);
    }
    .section-head {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 16px;
      padding: 18px;
      border-bottom: 1px solid #e5e7eb;
    }
    .table-wrap { overflow: auto; max-height: 520px; }
    table { border-collapse: collapse; width: 100%; min-width: 980px; }
    th, td {
      border-bottom: 1px solid #edf2f7;
      padding: 9px 10px;
      text-align: left;
      white-space: nowrap;
      font-size: 13px;
    }
    th {
      position: sticky;
      top: 0;
      background: #f8fafc;
      z-index: 1;
    }
    .diagnostics {
      margin-top: 18px;
      background: #0f172a;
      color: #e5e7eb;
      border-radius: 14px;
      padding: 18px;
      overflow: auto;
    }
    pre { margin: 0; white-space: pre-wrap; font-size: 12px; line-height: 1.5; }
  </style>
</head>
<body>
  <div class="hero">
    <h1>拼多多量化导出包</h1>
    <p>导出时间：${escapeHtml(new Date().toLocaleString())}。这是核对版导出包：先确认字段口径，再升级为自动批量采集。</p>
    <div class="stats">
      <div class="stat">接口记录<b>${diagnostics.recordCount}</b></div>
      <div class="stat">可解析响应<b>${diagnostics.parsedRecordCount}</b></div>
      <div class="stat">映射表<b>${diagnostics.mappingRows}</b></div>
      <div class="stat">直通车日级<b>${diagnostics.adDailyRows}</b></div>
      <div class="stat">直通车分时<b>${diagnostics.adHourlyRows}</b></div>
      <div class="stat">操作记录<b>${diagnostics.adOperationRows || 0}</b></div>
      <div class="stat">店铺日级<b>${diagnostics.shopDailyRows}</b></div>
      <div class="stat">店铺分时<b>${diagnostics.shopHourlyRows}</b></div>
      <div class="stat">售后退款<b>${diagnostics.afterSalesRows}</b></div>
    </div>
    <div class="actions">
      <button onclick="downloadAll()">下载全部 CSV</button>
      <button class="secondary" onclick="document.getElementById('diagnostics').scrollIntoView()">查看诊断</button>
    </div>
  </div>
  ${sections || '<section class="table-card"><div class="section-head"><div><h2>暂无可转换表格</h2><p>请先开始抓接口，并打开商品详情、分时图或切换日期。</p></div></div></section>'}
  <div class="diagnostics" id="diagnostics">
    <pre>${escapeHtml(JSON.stringify(diagnostics, null, 2))}</pre>
  </div>
  <script>
    const files = ${JSON.stringify(payload).replace(/</g, "\\u003c")};
    function downloadCsv(index) {
      const file = files[index];
      if (!file) return;
      const blob = new Blob(["\\ufeff", file.csv], { type: "text/csv;charset=utf-8" });
      const url = URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = url;
      link.download = file.filename + ".csv";
      document.body.appendChild(link);
      link.click();
      link.remove();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
    }
    function downloadAll() {
      files.forEach((_, index) => setTimeout(() => downloadCsv(index), index * 350));
    }
  </script>
</body>
</html>`;
  }

  function collectPromotionMappings(records) {
    const map = new Map();
    records.forEach((record) => {
      const url = record.url || "";
      const result = record.responseJson?.result;
      if (!result) {
        return;
      }
      if (url.includes("/venus/api/goods/promotion/detail/query")) {
        const detail = result;
        const item = {
          adId: detail.adId,
          goodsId: detail.goodsId || detail.goodsInfo?.goodsId,
          planId: detail.planId,
          mallId: detail.mallId,
          goodsName: detail.goodsInfo?.goodsName,
          targetRoi: extractMetricValue(detail.targetRoi),
          dailyLimit: extractMetricValue(detail.maxCost),
          dailyCost: extractMetricValue(detail.dailyCost),
          source: "promotion_detail"
        };
        upsertPromotionMapping(map, item);
      } else if (url.includes("/venus/api/goods/promotion/v3/list")) {
        collectGoodsListMappings(record, map);
      }
    });
    return map;
  }

  function collectGoodsListMappings(record, map) {
    const result = record.responseJson?.result;
    if (!result) {
      return;
    }
    walkObjects(result, (node) => {
      const adId = node.adId || node.adInfo?.adId || node.promotionInfo?.adId;
      const goodsId = node.goodsId || node.goodsInfo?.goodsId || node.goods?.goodsId;
      if (!adId && !goodsId) {
        return;
      }
      const item = {
        adId,
        goodsId,
        planId: node.planId || node.planInfo?.planId,
        mallId: node.mallId,
        goodsName: node.goodsName || node.goodsInfo?.goodsName || node.goods?.goodsName,
        targetRoi: extractMetricValue(node.targetRoi || node.targetRoiValue || node.bidValue),
        dailyLimit: extractMetricValue(node.maxCost || node.dailyCostLimit || node.budget),
        dailyCost: extractMetricValue(node.dailyCost || node.cost),
        source: "promotion_list"
      };
      upsertPromotionMapping(map, item);
    });
  }

  function upsertPromotionMapping(map, item) {
    const adId = normalizeId(item.adId);
    const goodsId = normalizeId(item.goodsId);
    if (!adId && !goodsId) {
      return;
    }
    const key = adId || `goods:${goodsId}`;
    const current = map.get(key) || {};
    const merged = { ...current };
    Object.entries({ ...item, adId, goodsId }).forEach(([field, value]) => {
      if (value !== undefined && value !== null && value !== "" && !merged[field]) {
        merged[field] = value;
      }
    });
    map.set(key, merged);
  }

  function collectAdReportRows(record, mappingMap, dailyRows, hourlyRows) {
    const body = record.bodyJson || {};
    const result = record.responseJson?.result || {};
    const entityId = normalizeId(body.entityId);
    const dimension = Number(body.queryDimensionType);
    if (dimension !== 2) {
      return;
    }
    const mapping = mappingMap.get(entityId) || {};
    const goodsId = mapping.goodsId || entityId;
    const base = {
      goodsId,
      adId: entityId,
      targetRoi: mapping.targetRoi || "",
      dailyLimit: mapping.dailyLimit || ""
    };

    (result.dailyReportList || []).forEach((item) => {
      dailyRows.push({
        "商品ID": base.goodsId,
        "日期": normalizeDate(item.statDate || item.date),
        "当前目标投产比": base.targetRoi,
        "当前日限额": base.dailyLimit,
        "花费": extractMetricValue(item.spend || item.orderSpend),
        "实际投产比": extractMetricValue(item.orderSpendRoiUnified),
        "曝光": item.impression ?? "",
        "点击": item.click ?? "",
        "成交笔数": item.orderNum ?? "",
        "直接成交笔数": extractDirectOrderNum(item),
        "点击率": extractMetricValue(item.ctr),
        "广告ID": base.adId,
        "口径": "queryHourlyRangeReport.dailyReportList；当前目标投产比/当前日限额=导出时当前设置，非历史当天设置；直接成交=directOrderNum"
      });
    });

    if (!result.dailyReportList?.length && result.dailyReport) {
      const item = result.dailyReport;
      dailyRows.push({
        "商品ID": base.goodsId,
        "日期": normalizeDate(item.statDate || item.date || body.startDate),
        "当前目标投产比": base.targetRoi,
        "当前日限额": base.dailyLimit,
        "花费": extractMetricValue(item.spend || item.orderSpend),
        "实际投产比": extractMetricValue(item.orderSpendRoiUnified),
        "曝光": item.impression ?? "",
        "点击": item.click ?? "",
        "成交笔数": item.orderNum ?? "",
        "直接成交笔数": extractDirectOrderNum(item),
        "点击率": extractMetricValue(item.ctr),
        "广告ID": base.adId,
        "口径": "queryHourlyRangeReport.dailyReport；当前目标投产比/当前日限额=导出时当前设置，非历史当天设置；直接成交=directOrderNum"
      });
    }

    (result.hourlyReportList || []).forEach((item) => {
      const date = normalizeDate(item.statDate || item.date || body.startDate);
      const hour = formatHour(item.hour);
      hourlyRows.push({
        "商品ID": base.goodsId,
        "日期": date,
        "小时": hour,
        "小时区间": formatHourRange(date, hour),
        "当前目标投产比": base.targetRoi,
        "当前日限额": base.dailyLimit,
        "花费": extractMetricValue(item.spend || item.orderSpend),
        "实际投产比": extractMetricValue(item.orderSpendRoiUnified),
        "曝光": item.impression ?? "",
        "点击": item.click ?? "",
        "成交笔数": item.orderNum ?? "",
        "直接成交笔数": extractDirectOrderNum(item),
        "点击率": extractMetricValue(item.ctr),
        "广告ID": base.adId,
        "口径": "queryHourlyRangeReport.hourlyReportList；当前目标投产比/当前日限额=导出时当前设置，非历史当天设置；直接成交=directOrderNum；小时为左闭右开区间，当前小时截止到导出时刻"
      });
    });
  }

  function collectShopStatRows(record, dailyRows) {
    const body = record.bodyJson || {};
    const result = record.responseJson?.result || {};
    const goodsId = normalizeId(body.goodsId || result.yesterdayGoodsDetail?.goodsId || result.dayGoodsDetailList?.[0]?.goodsId);
    if (!goodsId || !Array.isArray(result.dayGoodsDetailList)) {
      return;
    }
    result.dayGoodsDetailList.forEach((item) => {
      dailyRows.push({
        "商品ID": normalizeId(item.goodsId || goodsId),
        "日期": normalizeDate(item.statDate),
        "访客": item.goodsUv ?? "",
        "成交": item.payOrdrCnt ?? "",
        "收藏": item.goodsFavCnt ?? "",
        "浏览量": item.goodsPv ?? "",
        "成交金额": item.payOrdrAmt ?? "",
        "商品名称": item.goodsName || result.yesterdayGoodsDetail?.goodsName || "",
        "口径": "queryGoodsStat.dayGoodsDetailList，近30天日级历史，不含今日实时；今日实时请看商品分时历史"
      });
    });
  }

  function collectShopDtrRows(record, hourlyRows) {
    const body = record.bodyJson || {};
    const result = record.responseJson?.result || {};
    const goodsId = normalizeId(body.goodsId || result.todayGoodsDetail?.goodsId || result.yesterdayGoodsDetail?.goodsId);
    if (!goodsId) {
      return;
    }
    addShopDtrSeries(hourlyRows, goodsId, result.todayGoodsDetail?.goodsName || "", result.todayGoodsDetailList || []);
    addShopDtrSeries(hourlyRows, goodsId, result.yesterdayGoodsDetail?.goodsName || "", result.yesterdayGoodsDetailList || []);
  }

  function addShopDtrSeries(hourlyRows, goodsId, goodsName, series) {
    const sorted = series
      .filter((item) => item && item.statDate && item.statHr !== undefined && item.statHr !== null)
      .slice()
      .sort((left, right) => Number(left.statHr) - Number(right.statHr));
    let prevUv = 0;
    let prevOrders = 0;
    let prevFav = 0;
    let prevPv = 0;
    let prevAmount = 0;
    sorted.forEach((item) => {
      const rawUv = numberOrZero(item.goodsUv);
      const rawOrders = numberOrZero(item.payOrdrCnt);
      const rawFav = numberOrZero(item.goodsFavCnt);
      const rawPv = numberOrZero(item.goodsPv);
      const rawAmount = numberOrZero(item.payOrdrAmt);
      const date = normalizeDate(item.statDate);
      const hour = formatHour(item.statHr);
      hourlyRows.push({
        "商品ID": goodsId,
        "日期": date,
        "小时": hour,
        "小时区间": formatHourRange(date, hour),
        "访客": deltaOrCurrent(rawUv, prevUv),
        "成交": deltaOrCurrent(rawOrders, prevOrders),
        "收藏": deltaOrCurrent(rawFav, prevFav),
        "浏览量": deltaOrCurrent(rawPv, prevPv),
        "成交金额": deltaOrCurrent(rawAmount, prevAmount),
        "累计访客": rawUv,
        "累计成交": rawOrders,
        "商品名称": item.goodsName || goodsName || "",
        "口径": "queryGoodsStatDtr，访客/成交为该小时区间发生值；累计访客/累计成交为截至该小时末累计值；15:00表示15:00-16:00，当前小时截止到导出时刻"
      });
      prevUv = rawUv;
      prevOrders = rawOrders;
      prevFav = rawFav;
      prevPv = rawPv;
      prevAmount = rawAmount;
    });
  }

  function collectAfterSalesRows(record, rows) {
    const result = record.responseJson?.result || {};
    if (!result.goodsId) {
      return;
    }
    rows.push({
      "商品ID": normalizeId(result.goodsId),
      "日期": normalizeDate(result.statDate),
      "商品名称": result.goodsName || "",
      "店铺ID": normalizeId(result.mallId),
      "店铺名称": result.mallName || "",
      "近1月退款成功订单数": result.rfSucOrdrCnt1m ?? "",
      "近1月退款金额(分)": result.rfSucOrdrAmt1m ?? "",
      "近1月退款金额(元)": result.rfSucOrdrAmt1m ? (Number(result.rfSucOrdrAmt1m) / 100).toFixed(2) : "",
      "平台介入订单数1m": result.pltInvlOrdrCnt1m ?? "",
      "平台介入率1m": result.pltInvlOrdrRto1m ?? "",
      "品质退款率1m": result.qltySucRfRto1m ?? "",
      "商品品质帮助率1m": result.goodsPtHelpRate1m ?? ""
    });
  }

  function collectShopGoodsIdsForHourlyFetch() {
    const ids = new Set();

    runtime.networkRecords.forEach((record) => {
      addGoodsIdsFromAny(parseJsonSafe(record.requestBody), ids);
      addGoodsIdsFromAny(parseJsonSafe(record.responseText), ids);
    });

    addGoodsIdsFromText(document.body?.innerText || "", ids, { labeledOnly: true });
    document.querySelectorAll("input, textarea").forEach((node) => {
      const value = node.value || node.getAttribute("value") || "";
      if (!value) {
        return;
      }
      if (isGoodsIdSearchInput(node) || countPossibleIds(value) >= 2) {
        addGoodsIdsFromText(value, ids, { allowUnlabeled: true });
      } else {
        addGoodsIdsFromText(value, ids, { labeledOnly: true });
      }
    });

    return Array.from(ids)
      .filter(isLikelyGoodsId)
      .sort((left, right) => left.localeCompare(right));
  }

  function collectShopGoodsIdsForCurrentView() {
    const currentTableIds = collectCurrentGoodsEffectTableGoodsIds();
    if (currentTableIds.length) {
      return {
        goodsIds: currentTableIds,
        source: "current-table"
      };
    }

    return {
      goodsIds: collectShopGoodsIdsForHourlyFetch(),
      source: "page-cache"
    };
  }

  function collectCurrentGoodsEffectTableGoodsIds() {
    const tableContext = getGoodsEffectTableContext();
    if (!tableContext?.body) {
      return [];
    }

    const ids = [];
    const seen = new Set();
    const rows = Array.from(tableContext.body.querySelectorAll("tr"))
      .filter((row) => isVisibleElement(row))
      .filter((row) => cleanText(row.innerText || row.textContent || ""));

    rows.forEach((row) => {
      const firstCell = row.querySelector("td");
      const rowText = cleanText((firstCell || row).innerText || (firstCell || row).textContent || "");
      const match = rowText.match(/ID\s*[:：]\s*([0-9]{8,20})/i);
      const goodsId = match && isLikelyGoodsId(match[1]) ? normalizeId(match[1]) : "";
      if (goodsId && !seen.has(goodsId)) {
        seen.add(goodsId);
        ids.push(goodsId);
      }
    });

    return ids;
  }

  function addGoodsIdsFromAny(value, ids) {
    if (value === undefined || value === null) {
      return;
    }
    if (typeof value === "string" || typeof value === "number") {
      addGoodsIdsFromText(String(value), ids, { labeledOnly: true });
      return;
    }
    if (typeof value !== "object") {
      return;
    }
    walkObjects(value, (node) => {
      [
        node.goodsId,
        node.goodsID,
        node.goods_id,
        node.productId,
        node.productID,
        node.product_id
      ].forEach((item) => addGoodsIdsFromStructuredField(item, ids));
      if (typeof node.goodsId === "string" && node.goodsId.includes(",")) {
        addGoodsIdsFromText(node.goodsId, ids, { allowUnlabeled: true });
      }
    });
  }

  function addGoodsIdsFromText(text, ids, options = {}) {
    options = options || {};
    const source = String(text || "");
    const labeledPattern = /(?:^|[^A-Za-z0-9\u4e00-\u9fa5])(?:商品\s*)?ID\s*[:：]\s*([0-9]{8,20})/gi;
    let match;
    while ((match = labeledPattern.exec(source))) {
      if (isLikelyGoodsId(match[1])) {
        ids.add(normalizeId(match[1]));
      }
    }
    if (options.labeledOnly || !options.allowUnlabeled) {
      return;
    }

    const generalPattern = /\b[0-9]{8,20}\b/g;
    while ((match = generalPattern.exec(source))) {
      if (isLikelyGoodsId(match[0])) {
        ids.add(normalizeId(match[0]));
      }
    }
  }

  function addGoodsIdsFromStructuredField(value, ids) {
    if (value === undefined || value === null || value === "") {
      return;
    }
    const text = normalizeId(value);
    if (isLikelyGoodsId(text)) {
      ids.add(text);
      return;
    }
    addGoodsIdsFromText(String(value), ids, { allowUnlabeled: true });
  }

  function isLikelyGoodsId(value) {
    return /^[0-9]{8,20}$/.test(normalizeId(value));
  }

  function isGoodsIdSearchInput(node) {
    const text = [
      node.getAttribute("placeholder"),
      node.getAttribute("aria-label"),
      node.getAttribute("name"),
      node.getAttribute("data-testid"),
      node.closest('[class*="search"], [class*="Search"], [class*="Input"], [class*="input"]')?.textContent
    ].filter(Boolean).join(" ");
    return /商品\s*ID|ID\s*查询|多个查询|goods\s*id/i.test(text);
  }

  function countPossibleIds(text) {
    const matches = String(text || "").match(/\b[0-9]{8,20}\b/g);
    return matches ? matches.length : 0;
  }

  function toRequestGoodsId(goodsId) {
    const normalized = normalizeId(goodsId);
    const numeric = Number(normalized);
    return Number.isSafeInteger(numeric) ? numeric : normalized;
  }

  function buildShopHourlyRequestBody(goodsId, targetDate) {
    const body = {
      goodsId: toRequestGoodsId(goodsId)
    };
    if (targetDate) {
      body.startDate = targetDate;
      body.endDate = targetDate;
      body.queryDate = targetDate;
    }
    return JSON.stringify(body);
  }

  function buildShopDailyRequestBody(goodsId) {
    return JSON.stringify({
      goodsId: toRequestGoodsId(goodsId)
    });
  }

  async function getCurrentAdReportContext() {
    return getCurrentPromotionContext({ requireReportTemplate: true });
  }

  async function getCurrentPromotionContext(options = {}) {
    if (window.location.hostname !== "yingxiao.pinduoduo.com") {
      throw new Error("直通车补采只能在拼多多推广平台使用。");
    }

    await ensurePageHookReady();
    postNetworkCommand({ type: "dump" });
    await sleep(220);

    const records = runtime.networkRecords.slice();
    const detailInfo = findLatestPromotionDetailInfo(records);
    const domInfo = findPromotionDetailInfoFromDom();
    // The drawer DOM is the active product. Network detail records can lag behind
    // when products are opened one after another, so use records only as fallback.
    const mergedInfo = { ...detailInfo, ...domInfo };
    const reportTemplates = findAdReportTemplates(records);

    if (options.requireReportTemplate && !reportTemplates.length) {
      throw new Error("还没有直通车数据趋势接口模板。请先点“开始抓接口”，打开一个推广详情，并切一次数据趋势日期。");
    }

    const selected =
      (mergedInfo.adId && reportTemplates.find((item) => normalizeId(item.body.entityId) === mergedInfo.adId)) ||
      reportTemplates[0] ||
      null;
    const adId = mergedInfo.adId || normalizeId(selected?.body?.entityId);

    if (options.requireReportTemplate && !adId) {
      throw new Error("没有识别到当前直通车广告ID。请打开推广详情后再试。");
    }

    return {
      adId,
      goodsId: mergedInfo.goodsId || "",
      planId: mergedInfo.planId || "",
      mallId: mergedInfo.mallId || "",
      goodsName: mergedInfo.goodsName || "",
      targetRoi: mergedInfo.targetRoi || "",
      dailyLimit: mergedInfo.dailyLimit || "",
      dailyCost: mergedInfo.dailyCost || "",
      templateBody: selected?.body || null,
      url: selected?.record?.url || "/mms-gateway/poseidon/api/report/queryHourlyRangeReport"
    };
  }

  function findAdReportTemplates(records) {
    return records
      .slice()
      .reverse()
      .filter((record) => (record.url || "").includes("/poseidon/api/report/queryHourlyRangeReport"))
      .map((record) => ({
        record,
        body: parseJsonSafe(record.requestBody)
      }))
      .filter((item) => item.body && Number(item.body.queryDimensionType) === 2 && item.body.entityId);
  }

  function findLatestPromotionDetailInfo(records) {
    for (let index = records.length - 1; index >= 0; index -= 1) {
      const record = records[index];
      const url = record.url || "";
      if (!url.includes("/venus/api/goods/promotion/detail/query")) {
        continue;
      }
      const body = parseJsonSafe(record.requestBody);
      const result = parseJsonSafe(record.responseText)?.result;
      const adId = normalizeId(result?.adId || body?.adId);
      if (adId) {
        return {
          adId,
          goodsId: normalizeId(result?.goodsId || result?.goodsInfo?.goodsId),
          planId: normalizeId(result?.planId),
          mallId: normalizeId(result?.mallId),
          goodsName: result?.goodsInfo?.goodsName || result?.adName || "",
          targetRoi: extractMetricValue(result?.targetRoi),
          dailyLimit: extractMetricValue(result?.maxCost),
          dailyCost: extractMetricValue(result?.dailyCost)
        };
      }
    }
    return {};
  }

  function findPromotionDetailInfoFromDom() {
    const root = findPromotionDetailRoot();
    if (!root) {
      return {};
    }
    const text = cleanText(root.innerText || "");
    return {
      goodsId: extractFirstMatch(text, /商品\s*ID[:：]\s*([0-9]{8,20})/),
      planId: extractFirstMatch(text, /计划\s*ID[:：]\s*([0-9]{6,20})/),
      mallId: extractFirstMatch(text, /店铺\s*ID[:：]\s*([0-9]{6,20})/),
      goodsName: extractPromotionDetailGoodsName(root),
      targetRoi: extractFirstMatch(text, /(?:净目标投产比|目标投产比|投产比)[:：]?\s*([0-9]+(?:\.[0-9]+)?)/),
      dailyLimit: extractFirstMatch(text, /(?:预算日限额|日限额)[:：]?\s*([0-9]+(?:\.[0-9]+)?)/)
    };
  }

  function extractPromotionDetailGoodsName(root) {
    const candidates = Array.from(root.querySelectorAll("span, div, a"))
      .map((node) => cleanText(node.innerText))
      .filter((text) => text && text.length >= 8 && !/商品\s*ID|预算|投产比|推广状态|数据趋势/.test(text))
      .sort((left, right) => right.length - left.length);
    return candidates[0] || "";
  }

  function pushPromotionContextMappingRecord(context) {
    if (!context?.adId || (!context.goodsId && !context.targetRoi && !context.dailyLimit)) {
      return;
    }
    const result = {
      adId: toRequestGoodsId(context.adId),
      goodsId: context.goodsId ? toRequestGoodsId(context.goodsId) : "",
      planId: context.planId ? toRequestGoodsId(context.planId) : "",
      mallId: context.mallId ? toRequestGoodsId(context.mallId) : "",
      goodsInfo: {
        goodsId: context.goodsId ? toRequestGoodsId(context.goodsId) : "",
        goodsName: context.goodsName || ""
      },
      targetRoi: context.targetRoi ? { value: context.targetRoi } : undefined,
      maxCost: context.dailyLimit ? { value: context.dailyLimit } : undefined,
      dailyCost: context.dailyCost ? { value: context.dailyCost } : undefined
    };
    pushExtensionNetworkRecord({
      source: "extension-promotion-context",
      method: "POST",
      url: "/mms-gateway/venus/api/goods/promotion/detail/query",
      requestBody: JSON.stringify({ adId: toRequestGoodsId(context.adId), clientType: 1 }),
      status: 200,
      contentType: "application/json",
      durationMs: 0,
      relevant: true,
      responseText: JSON.stringify({ success: true, result })
    });
  }

  function buildAdReportRequestBody(templateBody, options) {
    const source = templateBody || {};
    const body = {
      ...source,
      clientType: source.clientType || 1,
      entityId: toRequestGoodsId(options.adId),
      queryDimensionType: 2,
      endDayHour: options.endDayHour,
      startDate: `${options.startDate} 00:00:00`,
      endDate: `${options.endDate} 00:00:00`,
      reportPromotionType: source.reportPromotionType || 9,
      scenesModes: Array.isArray(source.scenesModes) ? source.scenesModes : [1],
      blockTypes: Array.isArray(source.blockTypes) ? source.blockTypes : [4],
      returnAnchorPoints: source.returnAnchorPoints !== false
    };
    return JSON.stringify(body);
  }

  function shopDtrResponseContainsDate(responseText, targetDate) {
    const parsed = parseJsonSafe(responseText);
    const result = parsed?.result;
    if (!result || !targetDate) {
      return false;
    }
    const dates = new Set();
    [
      result.todayGoodsDetail,
      result.yesterdayGoodsDetail,
      ...(result.todayGoodsDetailList || []),
      ...(result.yesterdayGoodsDetailList || [])
    ].forEach((item) => {
      const date = normalizeDate(item?.statDate);
      if (date) {
        dates.add(date);
      }
    });
    return dates.has(targetDate);
  }

  function getGoodsEffectStatsDate() {
    const text = getGoodsEffectStatsTimeText();
    const match = text.match(/20\d{2}[-/]\d{1,2}[-/]\d{1,2}/);
    return match ? normalizeDate(match[0]) : "";
  }

  function isTodayOrYesterday(dateText) {
    const now = new Date();
    const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
    return dateText === formatLocalDate(now) || dateText === formatLocalDate(yesterday);
  }

  function pushExtensionNetworkRecord(record) {
    const responseText = String(record.responseText || "");
    const compact = {
      id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
      capturedAt: new Date().toISOString(),
      source: record.source || "extension-fetch",
      method: record.method || "GET",
      url: record.url || "",
      requestBody: record.requestBody || "",
      status: record.status || "",
      contentType: record.contentType || "",
      durationMs: record.durationMs || "",
      relevant: Boolean(record.relevant),
      jsonSummary: summarizeJsonForRecord(responseText),
      responseText: responseText.slice(0, 500000)
    };
    runtime.networkRecords.push(compact);
    runtime.networkSeenTotal += 1;
    if (runtime.networkRecords.length > 800) {
      runtime.networkRecords.splice(0, runtime.networkRecords.length - 800);
    }
    updateNetworkButtonLabels();
  }

  function mergeNetworkRecords(existing, incoming) {
    const map = new Map();
    [...(existing || []), ...(incoming || [])].forEach((record) => {
      if (!record) {
        return;
      }
      const key = record.id || [
        record.source,
        record.method,
        record.url,
        record.requestBody,
        record.capturedAt
      ].join("||");
      map.set(key, record);
    });
    const merged = Array.from(map.values());
    return merged.length > 800 ? merged.slice(merged.length - 800) : merged;
  }

  function summarizeJsonForRecord(text) {
    const parsed = parseJsonSafe(text);
    if (Array.isArray(parsed)) {
      return `array(${parsed.length})`;
    }
    if (parsed && typeof parsed === "object") {
      return Object.keys(parsed).slice(0, 24).join(",");
    }
    return "";
  }

  async function ensurePageHookReady() {
    installNetworkHook();
    if (runtime.networkHookReady) {
      return;
    }
    for (let index = 0; index < 30; index += 1) {
      await sleep(100);
      if (runtime.networkHookReady) {
        return;
      }
    }
    throw new Error("接口请求桥接还没有就绪，请刷新页面后重试。");
  }

  function pageContextRequest(options) {
    const requestId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
    const timeoutMs = options.timeoutMs || 12000;
    return new Promise((resolve, reject) => {
      const timer = window.setTimeout(() => {
        runtime.pageRequestResolvers.delete(requestId);
        reject(new Error("页面接口请求超时"));
      }, timeoutMs);
      runtime.pageRequestResolvers.set(requestId, {
        resolve: (payload) => {
          window.clearTimeout(timer);
          if (payload.error) {
            reject(new Error(payload.error));
          } else {
            resolve(payload);
          }
        }
      });
      postNetworkCommand({
        type: "request",
        requestId,
        method: options.method || "GET",
        url: options.url,
        headers: options.headers || {},
        body: options.body || ""
      });
    });
  }

  function parseJsonSafe(text) {
    if (!text || typeof text !== "string") {
      return null;
    }
    try {
      return JSON.parse(text);
    } catch (error) {
      return null;
    }
  }

  function walkObjects(value, visitor, seen = new Set()) {
    if (!value || typeof value !== "object" || seen.has(value)) {
      return;
    }
    seen.add(value);
    if (!Array.isArray(value)) {
      visitor(value);
    }
    Object.values(value).forEach((child) => walkObjects(child, visitor, seen));
  }

  function extractMetricValue(value) {
    if (value === undefined || value === null) {
      return "";
    }
    if (typeof value === "object" && "value" in value) {
      return value.value ?? "";
    }
    return value;
  }

  function extractDirectOrderNum(item) {
    const direct = item?.directOrderNum ?? item?.directOrderCnt ?? item?.directOrder ?? item?.directDealOrderNum;
    if (direct !== undefined && direct !== null && direct !== "") {
      return direct;
    }
    const orderNum = Number(item?.orderNum);
    const indirectOrderNum = Number(item?.indirectOrderNum);
    if (Number.isFinite(orderNum) && Number.isFinite(indirectOrderNum)) {
      return Math.max(0, orderNum - indirectOrderNum);
    }
    return "";
  }

  function normalizeId(value) {
    if (value === undefined || value === null || value === "") {
      return "";
    }
    return String(value).replace(/\.0$/, "");
  }

  function normalizeDate(value) {
    const text = String(value || "").trim();
    const match = text.match(/20\d{2}[-/]\d{1,2}[-/]\d{1,2}/);
    if (!match) {
      return text;
    }
    const [year, month, day] = match[0].split(/[-/]/);
    return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
  }

  function formatHour(value) {
    const hour = Math.max(0, Math.min(23, Number.parseInt(value, 10) || 0));
    return `${String(hour).padStart(2, "0")}:00`;
  }

  function formatHourRange(dateText, hourText) {
    const hour = Math.max(0, Math.min(23, Number.parseInt(hourText, 10) || 0));
    const start = `${String(hour).padStart(2, "0")}:00`;
    const now = new Date();
    const today = formatLocalDate(now);
    if (dateText === today && hour === now.getHours()) {
      return `${start}-${formatClock(now)}`;
    }
    const nextHour = hour === 23 ? "24:00" : `${String(hour + 1).padStart(2, "0")}:00`;
    return `${start}-${nextHour}`;
  }

  function formatLocalDate(date) {
    return [
      date.getFullYear(),
      String(date.getMonth() + 1).padStart(2, "0"),
      String(date.getDate()).padStart(2, "0")
    ].join("-");
  }

  function recentDateRange(dayCount, options = {}) {
    const endDate = addLocalDays(new Date(), options.includeToday ? 0 : -1);
    const startDate = addLocalDays(endDate, -(Math.max(1, dayCount) - 1));
    return {
      start: formatLocalDate(startDate),
      end: formatLocalDate(endDate)
    };
  }

  function recentDates(dayCount, options = {}) {
    const range = recentDateRange(dayCount, options);
    const dates = [];
    let current = parseLocalDate(range.start);
    const end = parseLocalDate(range.end);
    while (current <= end) {
      dates.push(formatLocalDate(current));
      current = addLocalDays(current, 1);
    }
    return dates;
  }

  function parseLocalDate(dateText) {
    const match = String(dateText || "").match(/(20\d{2})-(\d{1,2})-(\d{1,2})/);
    if (!match) {
      return new Date();
    }
    return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
  }

  function addLocalDays(date, days) {
    return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
  }

  function formatClock(date) {
    return [
      String(date.getHours()).padStart(2, "0"),
      String(date.getMinutes()).padStart(2, "0"),
      String(date.getSeconds()).padStart(2, "0")
    ].join(":");
  }

  function numberOrZero(value) {
    const numeric = Number(value);
    return Number.isFinite(numeric) ? numeric : 0;
  }

  function deltaOrCurrent(current, previous) {
    const delta = current - previous;
    return delta >= 0 ? roundNumber(delta) : roundNumber(current);
  }

  function roundNumber(value) {
    return Math.round((Number(value) || 0) * 100) / 100;
  }

  function dedupeRows(rows, headers) {
    const map = new Map();
    rows.forEach((row) => {
      const key = buildNaturalRowKey(row, headers);
      const current = map.get(key);
      if (!current || rowCompleteness(row, headers) >= rowCompleteness(current, headers)) {
        map.set(key, row);
      }
    });
    return Array.from(map.values());
  }

  function buildNaturalRowKey(row, headers) {
    if (headers.includes("广告ID") && headers.includes("日期") && headers.includes("小时")) {
      return ["ad_hourly", row["商品ID"], row["广告ID"], row["日期"], row["小时"]].join("||");
    }
    if (headers.includes("广告ID") && headers.includes("日期")) {
      return ["ad_daily", row["商品ID"], row["广告ID"], row["日期"]].join("||");
    }
    if (headers.includes("日期") && headers.includes("小时")) {
      return ["shop_hourly", row["商品ID"], row["日期"], row["小时"]].join("||");
    }
    if (headers.includes("日期")) {
      return ["shop_daily", row["商品ID"], row["日期"]].join("||");
    }
    if (headers.includes("操作时间") && headers.includes("操作详情")) {
      return ["operation", row["商品ID"], row["广告ID"], row["操作时间"], row["操作"], row["操作详情"]].join("||");
    }
    if (headers.includes("广告ID")) {
      return ["mapping", row["商品ID"], row["广告ID"]].join("||");
    }
    return headers.map((header) => row[header] ?? "").join("||");
  }

  function rowCompleteness(row, headers) {
    return headers.reduce((score, header) => score + (row[header] !== undefined && row[header] !== null && row[header] !== "" ? 1 : 0), 0);
  }

  function sortQuantRows(rows, headers) {
    return rows.slice().sort((left, right) => {
      const leftGoods = String(left["商品ID"] || "");
      const rightGoods = String(right["商品ID"] || "");
      const goodsCompare = leftGoods.localeCompare(rightGoods);
      if (goodsCompare) {
        return goodsCompare;
      }

      const leftAd = String(left["广告ID"] || "");
      const rightAd = String(right["广告ID"] || "");
      const adCompare = leftAd.localeCompare(rightAd);
      if (adCompare) {
        return adCompare;
      }

      const leftDate = String(left["日期"] || "");
      const rightDate = String(right["日期"] || "");
      const dateCompare = leftDate.localeCompare(rightDate);
      if (dateCompare) {
        return dateCompare;
      }

      const leftHour = String(left["小时"] || "");
      const rightHour = String(right["小时"] || "");
      const hourCompare = leftHour.localeCompare(rightHour);
      if (hourCompare) {
        return hourCompare;
      }

      return headers.map((header) => String(left[header] || "")).join("|").localeCompare(
        headers.map((header) => String(right[header] || "")).join("|")
      );
    });
  }

  function compareByGoodsAndAd(left, right) {
    return String(left.goodsId || "").localeCompare(String(right.goodsId || "")) || String(left.adId || "").localeCompare(String(right.adId || ""));
  }

  function countBy(values) {
    const counts = {};
    values.forEach((value) => {
      counts[value] = (counts[value] || 0) + 1;
    });
    return counts;
  }

  function escapeHtml(value) {
    return String(value ?? "")
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
  }

  function postNetworkCommand(payload) {
    window.postMessage(
      {
        channel: "__PDD_TABLE_EXPORTER_NETWORK__",
        target: "page-hook",
        ...payload
      },
      "*"
    );
  }

  function updateNetworkButtonLabels() {
    networkCaptureAction.label = runtime.networkCaptureEnabled ? "停止抓接口" : "开始抓接口";
    const count = runtime.networkRecords.length || runtime.networkSeenTotal || 0;
    const countText = count ? `导出接口JSON(${count})` : "导出接口JSON";
    networkExportAction.label = countText;
    document.querySelectorAll('[data-action-key="network-capture"]').forEach((button) => {
      button.textContent = networkCaptureAction.label;
    });
    document.querySelectorAll('[data-action-key="network-export"]').forEach((button) => {
      button.textContent = networkExportAction.label;
    });
  }

  function isNetworkCapturePersisted() {
    try {
      return window.localStorage.getItem(NETWORK_CAPTURE_STORAGE_KEY) === "1";
    } catch (error) {
      return false;
    }
  }

  function persistNetworkCapture(enabled) {
    try {
      if (enabled) {
        window.localStorage.setItem(NETWORK_CAPTURE_STORAGE_KEY, "1");
      } else {
        window.localStorage.removeItem(NETWORK_CAPTURE_STORAGE_KEY);
      }
    } catch (error) {
      // localStorage may be unavailable in a restricted frame; runtime state still works.
    }
  }

  function findPromotionAnchor() {
    if (findPromotionBudgetTitleNode(document.body) || findPromotionDrawerMenu("推广详情")) {
      return null;
    }
    return findElementByText("span", "全店托管")?.closest('[class*="Tabs_option"]') || null;
  }

  function findPromotionDetailAnchor() {
    const globalBudgetTitle = findPromotionBudgetTitleNode(document.body);
    if (globalBudgetTitle) {
      return globalBudgetTitle;
    }

    const detailRoot = findPromotionDetailRoot();
    if (!detailRoot) {
      return null;
    }

    const budgetTitle = findPromotionBudgetTitleNode(detailRoot);
    if (budgetTitle) {
      return budgetTitle;
    }

    const preferredText = ["基本信息", "预算与出价", "数据趋势"];
    for (const text of preferredText) {
      const node = findElementByTextInRoot(detailRoot, "span, div, h1, h2, h3, h4, label", text);
      if (node) {
        return node.closest("div") || node;
      }
    }

    return detailRoot;
  }

  function findPromotionDetailRoot() {
    const candidates = Array.from(document.querySelectorAll("div, section, aside"))
      .filter((node) => isVisibleElement(node))
      .filter((node) => {
        const rect = node.getBoundingClientRect();
        if (rect.width < 420 || rect.height < 380 || rect.left < window.innerWidth * 0.14) {
          return false;
        }
        const text = cleanText(node.innerText).slice(0, 3000);
        return /推广详情|基本信息|预算与出价|数据趋势|目标及出价/.test(text);
      })
      .sort((left, right) => {
        const leftRect = left.getBoundingClientRect();
        const rightRect = right.getBoundingClientRect();
        return leftRect.left - rightRect.left || getElementArea(right) - getElementArea(left);
      });

    return candidates[0] || null;
  }

  function findPromotionDetailFallbackRoot() {
    const budgetTitle = findPromotionBudgetTitleNode(document.body);
    if (!budgetTitle) {
      return null;
    }
    let current = budgetTitle;
    for (let depth = 0; current && depth < 8; depth += 1) {
      const rect = current.getBoundingClientRect?.();
      const text = cleanText(current.innerText || current.textContent || "");
      if (
        rect &&
        rect.width >= 420 &&
        rect.height >= 300 &&
        /基本信息|预算与出价|数据趋势/.test(text)
      ) {
        return current;
      }
      current = current.parentElement;
    }
    return budgetTitle.closest("div") || budgetTitle;
  }

  function findPromotionBudgetTitleNode(root = document.body) {
    const nodes = Array.from((root || document.body).querySelectorAll('.DetailTitle_title___1O_h, [class*="DetailTitle_title"], div, span, h2, h3'))
      .filter((node) => isVisibleElement(node))
      .filter((node) => cleanText(node.textContent) === "预算与出价")
      .sort((left, right) => {
        const leftRect = left.getBoundingClientRect();
        const rightRect = right.getBoundingClientRect();
        return leftRect.top - rightRect.top || leftRect.left - rightRect.left;
      });
    return nodes[0] || null;
  }

  function findGoodsEffectAnchor() {
    if (findGoodsEffectDetailRoot()) {
      return null;
    }
    return (
      findElementByText("span", "成交转化榜")?.closest('[data-testid="beast-core-tab-itemLabel-wrapper"]') || null
    );
  }

  function findGoodsEffectDetailAnchor() {
    const detailRoot = findGoodsEffectDetailRoot();
    if (!detailRoot) {
      return null;
    }

    const preferredText = ["商品数据详情", "数据明细", "统计时间", "商品ID查询", "商品数据", "商品趋势"];
    for (const text of preferredText) {
      const node = findElementByTextInRoot(detailRoot, "span, div, h1, h2, h3, h4, label", text);
      if (node) {
        return node.closest("div") || node;
      }
    }

    const input = detailRoot.querySelector('input[placeholder*="商品ID"], input[value*=" "]');
    return input?.closest("div") || detailRoot;
  }

  function findGoodsEffectDetailRoot() {
    return findGoodsEffectRightDrawerRoot();
  }

  function getPromotionDetailToolbarStyle() {
    const root = findPromotionDetailRoot();
    const title = root ? findPromotionBudgetTitleNode(root) : findPromotionBudgetTitleNode(document.body);
    if (!root && !title) {
      return {};
    }
    const titleRect = title?.getBoundingClientRect();
    const rootRect = root?.getBoundingClientRect();
    const baseRect = titleRect || rootRect;
    const compact = (rootRect?.width || window.innerWidth) < 980 || window.innerWidth < 1280;
    let left = Math.max(72, (titleRect ? titleRect.right + 16 : (rootRect?.left || 0) + 24));
    let top = Math.max(64, (titleRect ? titleRect.top - 7 : (rootRect?.top || 0) + 58));
    if (window.innerWidth - left < 520 && baseRect) {
      left = Math.max(72, baseRect.left);
      top = Math.max(64, baseRect.bottom + 8);
    }
    return {
      left: `${left}px`,
      top: `${top}px`,
      right: "18px",
      maxWidth: `calc(100vw - ${left + 18}px)`,
      compact
    };
  }

  function findGoodsEffectRightDrawerRoot() {
    const minLeft = window.innerWidth * 0.38;
    const candidates = Array.from(document.querySelectorAll("div, section, aside"))
      .filter((node) => isVisibleElement(node))
      .filter((node) => {
        const rect = node.getBoundingClientRect();
        if (
          rect.left < minLeft ||
          rect.width < Math.min(520, window.innerWidth * 0.32) ||
          rect.height < window.innerHeight * 0.62 ||
          rect.top > 90 ||
          rect.right < window.innerWidth * 0.78
        ) {
          return false;
        }
        const text = cleanText(node.innerText).slice(0, 5000);
        return /商品数据详情/.test(text) && /数据明细|分析建议|评价数据|近30日售后数据/.test(text);
      })
      .sort((left, right) => {
        const leftRect = left.getBoundingClientRect();
        const rightRect = right.getBoundingClientRect();
        return (
          rightRect.left - leftRect.left ||
          leftRect.top - rightRect.top ||
          getElementArea(right) - getElementArea(left)
        );
      });

    return candidates[0] || null;
  }

  function getGoodsEffectDetailToolbarStyle() {
    const root = findGoodsEffectRightDrawerRoot();
    if (!root) {
      return {};
    }
    const rect = root.getBoundingClientRect();
    const drawerRightGap = Math.max(16, window.innerWidth - rect.right + 76);
    const compact = rect.width < 900;
    return {
      left: `${Math.max(16, rect.left + 16)}px`,
      top: `${Math.max(16, rect.top + 44)}px`,
      right: `${drawerRightGap}px`,
      maxWidth: `${Math.max(280, rect.width - 108)}px`,
      compact
    };
  }

  async function exportPromotionPage() {
    const tableBodies = getPromotionTableBodies();
    const tableBody = tableBodies[0];
    if (!tableBody) {
      throw new Error("没有找到推广列表表格");
    }

    const scrollTargets = findPromotionScrollTargets(tableBody);
    const metricHeaders = getPromotionMetricHeaders();
    const rowMap = new Map();
    runtime.lastPromotionDebug = {
      scrollTargets: scrollTargets.map(describeNode)
    };

    await collectVirtualRows(scrollTargets, tableBody, () => {
      const rows = tableBodies.flatMap((body) => Array.from(body.querySelectorAll("tr[data-row-key]")));
      rows.forEach((row) => {
        const rowKey = row.dataset.rowKey || "";
        const parsed = parsePromotionRow(row, metricHeaders);
        if (parsed && (rowKey || parsed["商品ID"])) {
          const mergedKey = rowKey || parsed["商品ID"];
          rowMap.set(mergedKey, mergePromotionRow(rowMap.get(mergedKey), parsed));
        }
      });
      return Array.from(rowMap.values()).filter((item) => item && item["商品ID"]).length;
    });

    const rows = Array.from(rowMap.values()).filter((item) => item && item["商品ID"]);
    if (!rows.length) {
      throw new Error("当前页没有可导出的数据");
    }

    const headers = collectHeadersFromRows(rows, [
      "商品ID",
      "商品名称",
      "拼单价",
      "推广状态",
      "今日预算日限额",
      "今日预算剩余",
      "出价类型",
      "出价值",
      "净目标投产比",
      ...metricHeaders
    ]);

    return {
      filename: buildFilename("直通车推广列表"),
      headers,
      rows
    };
  }

  function getPromotionMetricHeaders() {
    const headerTexts = Array.from(document.querySelectorAll("thead th, .anq-table-header th"))
      .map((cell) => cleanText(cell.innerText))
      .filter(Boolean);

    if (headerTexts.length >= DEFAULT_P1_METRIC_HEADERS.length) {
      const filtered = headerTexts.filter((text) => !isPromotionIgnoredHeader(text));
      if (filtered.length >= DEFAULT_P1_METRIC_HEADERS.length) {
        if (filtered.every((text) => !isPromotionIgnoredHeader(text))) {
          return filtered;
        }
      }
    }

    return DEFAULT_P1_METRIC_HEADERS.slice();
  }

  function parsePromotionRow(row, metricHeaders) {
    const cells = Array.from(row.querySelectorAll(":scope > td")).filter((cell) => isVisibleElement(cell));
    if (!cells.length) {
      return null;
    }

    const productCell = findPromotionProductCell(cells);
    const statusCell = findPromotionStatusCell(cells);
    const bidCell = findPromotionBidCell(cells);
    const metricCells = getPromotionMetricCells(cells, metricHeaders.length);

    const productText = cleanText(productCell?.innerText);
    const budgetText = cleanText(bidCell?.innerText);
    const productName =
      cleanText(productCell?.querySelector('[data-testid="Name"]')?.innerText) ||
      cleanText(productCell?.querySelector('[class*="NameRow_title"]')?.innerText);
    const productId = extractFirstMatch(productText, /商品\s*ID[:：]\s*(\d+)/);
    const groupPrice = extractFirstMatch(productText, /拼单价[:：]\s*([0-9.,]+(?:\s*元)?)/);
    const promotionStatus = extractPromotionStatus(statusCell);
    const budgetFields = extractPromotionBudgetFields(bidCell, budgetText);
    const netTargetRoi = extractPromotionNetTargetRoi(budgetText);
    const bidLabelNode = bidCell?.querySelector('[class*="BidTypeLabel"]');
    const bidLabel = cleanText(bidLabelNode?.innerText).replace(/[：:]$/, "");
    const bidValue =
      cleanText(bidCell?.querySelector('[class*="BidRow_bidValue"]')?.innerText) ||
      extractFirstMatch(budgetText, /(净目标投产比|目标投产比|出价)[：:]?\s*([0-9.]+)/, 2);

    const result = {};

    if (productId) {
      result["商品ID"] = productId;
    }
    if (productName) {
      result["商品名称"] = productName;
    }
    if (groupPrice) {
      result["拼单价"] = groupPrice;
    }
    if (promotionStatus) {
      result["推广状态"] = promotionStatus;
    }
    if (budgetFields.dailyLimit) {
      result["今日预算日限额"] = budgetFields.dailyLimit;
    }
    if (budgetFields.remaining) {
      result["今日预算剩余"] = budgetFields.remaining;
    }
    if (bidLabel) {
      result["出价类型"] = bidLabel;
    }
    if (bidValue) {
      result["出价值"] = extractFirstMatch(bidValue, /([0-9]+(?:\.[0-9]+)?)/) || bidValue;
    }
    if (netTargetRoi) {
      result["净目标投产比"] = netTargetRoi;
    }

    metricCells.forEach((cell, index) => {
      result[metricHeaders[index] || `字段${index + 1}`] = cleanText(cell.innerText);
    });

    return Object.keys(result).length ? result : null;
  }

  function getPromotionTableBodies() {
    return Array.from(document.querySelectorAll(".anq-table-body"))
      .filter((node) => isVisibleElement(node))
      .sort((left, right) => getElementArea(right) - getElementArea(left));
  }

  function mergePromotionRow(current, incoming) {
    if (!current) {
      return { ...incoming };
    }

    const merged = { ...current };
    Object.entries(incoming).forEach(([key, value]) => {
      if (value === undefined || value === null || value === "") {
        return;
      }

      if (!merged[key] || String(merged[key]).length < String(value).length) {
        merged[key] = value;
      }
    });
    return merged;
  }

  function findPromotionProductCell(cells) {
    return (
      cells.find(
        (cell) =>
          /商品\s*ID/.test(cleanText(cell.innerText)) ||
          cell.querySelector('[data-testid="Name"], [class*="NameRow_title"]')
      ) || null
    );
  }

  function findPromotionStatusCell(cells) {
    return (
      cells.find(
        (cell) =>
          cell.querySelector('[data-testid^="AdStatus_"], [class*="AdStatus_adStatus"], [class*="AdStatus_hasBorder"]') ||
          /(推广中|手动暂停|到达日限额|已暂停|暂停推广|已结束|待开启|已下架|审核中|已驳回)/.test(
            cleanText(cell.innerText)
          )
      ) || null
    );
  }

  function extractPromotionStatus(statusCell) {
    if (!statusCell) {
      return "";
    }

    const explicitNodes = Array.from(
      statusCell.querySelectorAll('[data-testid^="AdStatus_"], [class*="AdStatus_hasBorder"]')
    );
    const explicitTexts = explicitNodes
      .map((node) => cleanText(node.textContent))
      .filter((text) => text && !/^(去提升|详情|更多)$/.test(text));
    if (explicitTexts.length) {
      return explicitTexts[0];
    }

    const statusText = cleanText(statusCell.innerText);
    return (
      extractFirstMatch(
        statusText,
        /(推广中|手动暂停|到达日限额|已暂停|暂停推广|已结束|待开启|已下架|审核中|已驳回)/
      ) || statusText.replace(/\s*去提升.*$/, "")
    );
  }

  function extractPromotionBudgetFields(bidCell, budgetText) {
    const dailyLimitNode = bidCell?.querySelector('[class*="useMaxCostValue_value"]');
    const remainingNode = bidCell?.querySelector('[class*="useMaxCostValue_remain"]');
    const insufficientNode = bidCell?.querySelector('[class*="useMaxCostValue_tag"]');
    const text = cleanText(budgetText);

    const dailyLimit =
      cleanText(dailyLimitNode?.textContent) ||
      extractFirstMatch(text, /预算日限额[:：]?\s*([0-9.,]+\s*元)/);

    const insufficient = cleanText(insufficientNode?.textContent);
    const remainingText = cleanText(remainingNode?.textContent);
    let remaining = "";
    if (/预算不足/.test(`${insufficient} ${text}`)) {
      remaining = "预算不足";
    } else if (remainingText) {
      remaining = remainingText.replace(/[()（）]/g, "").replace(/^剩\s*/, "");
    } else {
      remaining = extractFirstMatch(text, /\(\s*剩\s*([0-9.]+%)\s*\)/);
    }

    return {
      dailyLimit: cleanText(dailyLimit),
      remaining: cleanText(remaining)
    };
  }

  function extractPromotionNetTargetRoi(budgetText) {
    const text = cleanText(budgetText);
    const match = text.match(/(?:净目标投产比|目标投产比)[:：\s]*(.+)$/);
    if (!match) {
      return "";
    }
    return cleanText(match[1])
      .replace(/^[:：\s]+/, "")
      .replace(/\s*(?:编辑|修改)\s*$/, "");
  }

  function findPromotionBidCell(cells) {
    return (
      cells.find(
        (cell) =>
          cell.querySelector('[class*="BidTypeLabel"], [class*="BidRow_bidValue"]') ||
          /(净目标投产比|目标投产比|出价|预算日限额)/.test(cleanText(cell.innerText))
      ) || null
    );
  }

  function getPromotionMetricCells(cells, metricCount) {
    if (!metricCount || cells.length < metricCount) {
      return [];
    }

    return cells.slice(-metricCount);
  }

  async function exportGoodsEffectPage() {
    runtime.decodeCache.clear();
    await waitForFontReady();

    const tableContext = getGoodsEffectTableContext();
    if (!tableContext) {
      throw new Error("没有找到商品效果表格");
    }

    const bodyRows = Array.from(tableContext.body.querySelectorAll("tr")).filter((row) => isVisibleElement(row));
    if (!bodyRows.length) {
      throw new Error("没有找到商品效果表格");
    }

    const headerTexts = Array.from(tableContext.header.querySelectorAll("th"))
      .map((cell) => cleanText(cell.innerText))
      .filter(Boolean);
    const metricHeaders = headerTexts.slice(1, -1);

    const rows = bodyRows
      .map((row) => parseGoodsEffectRow(row, metricHeaders))
      .filter((item) => item && item["商品ID"]);

    if (!rows.length) {
      throw new Error("当前页没有可导出的数据");
    }

    const headers = collectHeadersFromRows(rows, ["商品ID", "商品名称", ...metricHeaders]);

    return {
      filename: buildFilename("商品数据明细"),
      headers,
      rows
    };
  }

  async function exportPromotionSummaryPage() {
    const periods = [
      { optionText: "今日", prefix: "今日" },
      { optionText: "昨日", prefix: "昨日" },
      { optionText: "近 7 日", prefix: "近7日" }
    ];
    const excludedFields = new Set([
      "商品名称",
      "拼单价",
      "推广状态",
      "出价类型",
      "预算剩余",
      "预算及出价",
      "净成交笔数占比",
      "每笔净成交金额(元)",
      "每笔结算成交金额(元)",
      "每笔成交花费(元)"
    ]);
    const originalOption = getActivePromotionQuickOptionText();
    const datasets = [];

    try {
      for (const period of periods) {
        const exported = await exportPromotionPeriod(period.optionText);
        datasets.push({
          prefix: period.prefix,
          rows: exported.rows
        });
        await sleep(320);
      }
    } finally {
      if (originalOption && !periods.some((period) => period.optionText === originalOption)) {
        await switchPromotionQuickDate(originalOption).catch(() => {});
      } else if (originalOption) {
        await switchPromotionQuickDate(originalOption).catch(() => {});
      }
    }

    return buildSummaryExportResult("直通车数据：今天截止到现在的-昨天-近七天", datasets, {
      keyField: "商品ID",
      baseHeaders: ["商品ID"],
      excludedFields,
      includeField: ({ dataset, header }) =>
        (header !== "出价值" || dataset.prefix === "今日") &&
        (header !== "推广 分组 推广添加时间 ~" || dataset.prefix === "今日")
    });
  }

  async function exportPromotionPeriod(optionText) {
    let lastError = null;

    for (let attempt = 0; attempt < 3; attempt += 1) {
      try {
        await switchPromotionQuickDate(optionText);
        await waitForPromotionDataReady();
        return await exportPromotionPage();
      } catch (error) {
        lastError = error;
        await sleep(420 + attempt * 220);
      }
    }

    throw lastError || new Error(`直通车数据采集失败：${optionText}`);
  }

  async function exportGoodsEffectSummaryPage() {
    const periods = [
      { optionText: "实时", prefix: "实时" },
      { optionText: "昨日", prefix: "昨日" },
      { optionText: "7日", prefix: "7日" }
    ];
    const excludedFields = new Set(["商品名称"]);
    const originalOption = getActiveGoodsEffectDateText();
    const datasets = [];

    try {
      for (const period of periods) {
        const exported = await exportGoodsEffectPeriod(period.optionText);
        datasets.push({
          prefix: period.prefix,
          rows: exported.rows
        });
        await sleep(320);
      }
    } finally {
      if (originalOption) {
        await switchGoodsEffectDate(originalOption).catch(() => {});
      }
    }

    return buildSummaryExportResult("店铺后台数据：今天截止到现在的-昨天-近七天", datasets, {
      keyField: "商品ID",
      baseHeaders: ["商品ID"],
      excludedFields
    });
  }

  async function exportGoodsEffectPeriod(optionText) {
    let lastError = null;

    for (let attempt = 0; attempt < 3; attempt += 1) {
      try {
        await switchGoodsEffectDate(optionText);
        await waitForGoodsEffectDataReady();
        return await exportGoodsEffectPage();
      } catch (error) {
        lastError = error;
        await sleep(450 + attempt * 220);
      }
    }

    throw lastError || new Error(`店铺后台数据采集失败：${optionText}`);
  }

  function buildSummaryExportResult(filenamePrefix, datasets, options) {
    const { keyField, baseHeaders, excludedFields, includeField } = options;
    const rowMap = new Map();
    const metricOrder = [];
    const metricSet = new Set();

    datasets.forEach((dataset) => {
      dataset.rows.forEach((row) => {
        const rowKey = row[keyField];
        if (!rowKey) {
          return;
        }

        if (!rowMap.has(rowKey)) {
          rowMap.set(rowKey, { [keyField]: rowKey });
        }

        const targetRow = rowMap.get(rowKey);
        Object.entries(row).forEach(([header, value]) => {
          if (header === keyField || excludedFields.has(header)) {
            return;
          }
          if (includeField && !includeField({ dataset, header, value, row })) {
            return;
          }

          const summaryHeader = `${dataset.prefix}${header}`;
          targetRow[summaryHeader] = value;

          if (!metricSet.has(summaryHeader)) {
            metricSet.add(summaryHeader);
            metricOrder.push(summaryHeader);
          }
        });
      });
    });

    const rows = Array.from(rowMap.values());
    if (!rows.length) {
      throw new Error("当前页没有可导出的数据");
    }

    const headers = collectHeadersFromRows(rows, [...baseHeaders, ...metricOrder]);
    return {
      filename: filenamePrefix,
      headers,
      rows
    };
  }

  function parseGoodsEffectRow(row, metricHeaders) {
    const cells = Array.from(row.querySelectorAll(":scope > td"));
    if (cells.length < 3) {
      return null;
    }

    const infoCell = cells[0];
    const metricCells = cells.slice(1, -1);
    const titleNode = infoCell.querySelector('[data-testid="beast-core-ellipsis"]');
    const infoText = cleanText(infoCell.innerText);
    const productId = extractFirstMatch(infoText, /ID[:：]\s*(\d+)/);
    const result = {
      商品ID: productId,
      商品名称: cleanText(titleNode?.innerText)
    };

    metricCells.forEach((cell, index) => {
      result[metricHeaders[index] || `字段${index + 1}`] = extractGoodsEffectMetricValue(cell);
    });

    return result;
  }

  async function switchPromotionQuickDate(optionText) {
    const option = findPromotionQuickOption(optionText);
    if (!option) {
      throw new Error(`没有找到直通车日期选项：${optionText}`);
    }

    const beforeSignature = getPromotionTableContentSignature();
    const beforeActiveText = getActivePromotionQuickOptionText();

    if (beforeActiveText === optionText) {
      resetPromotionScrollPosition();
      await waitForPromotionDataReady();
      return;
    }

    let switched = false;
    for (let attempt = 0; attempt < 3; attempt += 1) {
      triggerClick(option);
      try {
        await waitForCondition(
          () => getActivePromotionQuickOptionText() === optionText,
          2500,
          120,
          `切换直通车日期失败：${optionText}`
        );
        switched = true;
        break;
      } catch (error) {
        await sleep(260 + attempt * 160);
      }
    }

    if (!switched) {
      throw new Error(`切换直通车日期失败：${optionText}`);
    }

    await sleep(520);
    resetPromotionScrollPosition();
    await sleep(220);
    await waitForSignatureRefresh(getPromotionTableContentSignature, beforeSignature, 7000);
    await waitForPromotionDataReady();
    await waitForStableSignature(getPromotionTableSignature, 3, 220, 7000);
  }

  function findPromotionQuickOption(optionText) {
    return Array.from(document.querySelectorAll('[data-testid^="DateAreaQuickOption_"]')).find(
      (node) => cleanText(node.textContent) === optionText
    );
  }

  function getActivePromotionQuickOptionText() {
    const activeNode = document.querySelector('[data-testid^="DateAreaQuickOption_"][class*="isActive"]');
    return cleanText(activeNode?.textContent);
  }

  function getPromotionTableSignature() {
    const activeText = getActivePromotionQuickOptionText();
    const rows = getPromotionTableBodies().flatMap((body) =>
      Array.from(body.querySelectorAll("tr[data-row-key]")).slice(0, 6).map((row) => cleanText(row.innerText))
    );
    return [activeText, ...rows].join("||");
  }

  function getPromotionTableContentSignature() {
    return getPromotionTableBodies()
      .flatMap((body) =>
        Array.from(body.querySelectorAll("tr[data-row-key]")).slice(0, 8).map((row) => cleanText(row.innerText))
      )
      .join("||");
  }

  function resetPromotionScrollPosition() {
    const tableBody = getPromotionTableBodies()[0];
    if (!tableBody) {
      return;
    }

    const scrollTargets = findPromotionScrollTargets(tableBody);
    scrollTargets.forEach((node) => {
      setScrollerTop(node, 0);
    });
  }

  async function waitForPromotionDataReady() {
    await waitForCondition(
      () => {
        const rows = getPromotionTableBodies().flatMap((body) =>
          Array.from(body.querySelectorAll("tr[data-row-key]")).filter((row) => isVisibleElement(row))
        );
        if (!rows.length) {
          return false;
        }

        return Boolean(cleanText(rows[0].innerText));
      },
      7000,
      150,
      "直通车表格数据加载超时"
    );
  }

  async function switchGoodsEffectDate(optionText) {
    const option = findGoodsEffectDateOption(optionText);
    if (!option) {
      throw new Error(`没有找到店铺后台日期选项：${optionText}`);
    }

    const beforeSignature = getGoodsEffectTableSignature();
    const beforeActiveText = getActiveGoodsEffectDateText();
    const beforeStatsTime = getGoodsEffectStatsTimeText();

    if (beforeActiveText === optionText) {
      await waitForGoodsEffectDataReady();
      await waitForFontReady();
      return;
    }

    let switched = false;
    for (let attempt = 0; attempt < 3; attempt += 1) {
      triggerClick(option);
      try {
        await waitForCondition(
          () => getActiveGoodsEffectDateText() === optionText,
          2500,
          120,
          `切换店铺后台日期失败：${optionText}`
        );
        switched = true;
        break;
      } catch (error) {
        await sleep(260 + attempt * 160);
      }
    }

    if (!switched) {
      throw new Error(`切换店铺后台日期失败：${optionText}`);
    }

    await sleep(480);
    await waitForSignatureRefresh(
      getGoodsEffectTableSignature,
      beforeSignature,
      6500,
      () => getGoodsEffectStatsTimeText() !== beforeStatsTime
    );
    await waitForStableSignature(getGoodsEffectTableSignature, 3, 220, 6500);
    await waitForGoodsEffectDataReady();
    await waitForFontReady();
  }

  function findGoodsEffectDateOption(optionText) {
    return getGoodsEffectDateOptions().find((node) => getGoodsEffectDateOptionText(node) === optionText) || null;
  }

  function getActiveGoodsEffectDateText() {
    const activeNode = getGoodsEffectDateOptions().find(
      (node) => node.getAttribute("data-checked") === "true" || node.querySelector('input[type="radio"]')?.checked
    );
    return getGoodsEffectDateOptionText(activeNode);
  }

  function getGoodsEffectDateOptions() {
    const preferredRoots = Array.from(
      document.querySelectorAll('[class*="goods-detail-optimization-ui_filterWrapper"]')
    ).filter((node) => isVisibleElement(node));

    const sourceNodes = preferredRoots.length
      ? preferredRoots.flatMap((root) => Array.from(root.querySelectorAll('[data-testid="beast-core-radio"]')))
      : Array.from(document.querySelectorAll('[data-testid="beast-core-radio"]'));

    const radios = sourceNodes
      .filter((node) => isVisibleElement(node))
      .filter((node) => isElementInViewport(node));

    const deduped = new Map();
    radios.forEach((node) => {
      const text = getGoodsEffectDateOptionText(node);
      if (text && !deduped.has(text)) {
        deduped.set(text, node);
      }
    });

    return Array.from(deduped.values());
  }

  function getGoodsEffectDateOptionText(node) {
    const textNode =
      node?.querySelector(".data-pick-group_dateItemStyle__1bRXr") ||
      node?.querySelector('[class*="RD_textWrapper"]');
    return cleanText(textNode?.textContent || node?.textContent);
  }

  function getGoodsEffectTableSignature() {
    const activeText = getActiveGoodsEffectDateText();
    const statsTime = getGoodsEffectStatsTimeText();
    const tableContext = getGoodsEffectTableContext();
    if (!tableContext) {
      return [activeText, statsTime].filter(Boolean).join("||");
    }

    const headerTexts = Array.from(tableContext.header.querySelectorAll("th"))
      .slice(0, 8)
      .map((node) => cleanText(node.textContent))
      .join("|");
    const rows = Array.from(tableContext.body.querySelectorAll("tr"))
      .slice(0, 8)
      .map((row) => cleanText(row.innerText));
    return [activeText, statsTime, headerTexts, ...rows].join("||");
  }

  function getGoodsEffectStatsTimeText() {
    const node = Array.from(document.querySelectorAll('[class*="goods-detail-optimization-ui_subTitle"]'))
      .find((item) => isVisibleElement(item));
    return cleanText(node?.textContent);
  }

  function getGoodsEffectTableContext() {
    const bodies = Array.from(document.querySelectorAll('[data-testid="beast-core-table-middle-tbody"]'))
      .filter((node) => isVisibleElement(node))
      .sort((left, right) => getElementArea(right) - getElementArea(left));
    const headers = Array.from(document.querySelectorAll('[data-testid="beast-core-table-middle-header"]'))
      .filter((node) => isVisibleElement(node))
      .sort((left, right) => getElementArea(right) - getElementArea(left));

    if (!bodies.length || !headers.length) {
      return null;
    }

    return {
      body: bodies[0],
      header: headers[0]
    };
  }

  function extractGoodsEffectMetricValue(cell) {
    const valueNode =
      cell.querySelector('[class*="list_value"] .__spider_font') ||
      cell.querySelector('[class*="list_value"]') ||
      cell;
    const raw = cleanText(valueNode.innerText || valueNode.textContent);

    if (!raw) {
      return "";
    }

    if (!containsSpiderChars(raw)) {
      return raw;
    }

    const decoded = decodeSpiderText(valueNode, raw);
    return decoded || raw;
  }

  function containsSpiderChars(text) {
    return /[\uE000-\uF8FF]/.test(text);
  }

  function decodeSpiderText(node, rawText) {
    const style = window.getComputedStyle(node);
    const cacheKey = [
      style.fontStyle,
      style.fontWeight,
      style.fontSize,
      style.fontFamily,
      style.letterSpacing
    ].join("|");

    if (!runtime.decodeCache.has(cacheKey)) {
      runtime.decodeCache.set(cacheKey, buildSpiderCharMap(node));
    }

    let charMap = runtime.decodeCache.get(cacheKey);
    const missingChars = getMissingSpiderChars(rawText, charMap);
    if (missingChars.length) {
      charMap = {
        ...charMap,
        ...buildSpiderCharMap(node, missingChars)
      };
      runtime.decodeCache.set(cacheKey, charMap);
    }

    return Array.from(rawText)
      .map((char) => charMap[char] || char)
      .join("");
  }

  function buildSpiderCharMap(node, requiredChars = []) {
    const style = window.getComputedStyle(node);
    const font = `${style.fontStyle} ${style.fontWeight} 56px ${style.fontFamily}`;
    const candidates = Array.from("0123456789.,%-+");
    const valueNodes = Array.from(
      document.querySelectorAll('[class*="list_value"] .__spider_font, [class*="list_value"]')
    );
    const uniqueChars = new Set();

    valueNodes.forEach((valueNode) => {
      const text = cleanText(valueNode.innerText || valueNode.textContent);
      Array.from(text).forEach((char) => {
        if (containsSpiderChars(char)) {
          uniqueChars.add(char);
        }
      });
    });
    requiredChars.forEach((char) => {
      if (containsSpiderChars(char)) {
        uniqueChars.add(char);
      }
    });

    const candidateSignatures = new Map();
    candidates.forEach((char) => {
      candidateSignatures.set(char, renderCharSignature(char, font));
    });

    const mapped = {};
    uniqueChars.forEach((char) => {
      const signature = renderCharSignature(char, font);
      let bestChar = "";
      let bestScore = Number.POSITIVE_INFINITY;
      let secondBestScore = Number.POSITIVE_INFINITY;

      candidates.forEach((candidate) => {
        const score = compareSignatures(signature, candidateSignatures.get(candidate));
        if (score < bestScore) {
          secondBestScore = bestScore;
          bestScore = score;
          bestChar = candidate;
        } else if (score < secondBestScore) {
          secondBestScore = score;
        }
      });

      if (bestChar && isSpiderMatchReliable(bestScore, secondBestScore, signature)) {
        mapped[char] = bestChar;
      }
    });

    return mapped;
  }

  function getMissingSpiderChars(rawText, charMap) {
    return Array.from(new Set(Array.from(rawText).filter((char) => containsSpiderChars(char) && !charMap[char])));
  }

  function renderCharSignature(char, font) {
    const sourceCanvas = document.createElement("canvas");
    const sourceSize = 96;
    sourceCanvas.width = sourceSize;
    sourceCanvas.height = sourceSize;

    const ctx = sourceCanvas.getContext("2d", { willReadFrequently: true });
    ctx.clearRect(0, 0, sourceSize, sourceSize);
    ctx.fillStyle = "#000";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.font = font;
    ctx.fillText(char, sourceSize / 2, sourceSize / 2 + 3);

    const sourceImage = ctx.getImageData(0, 0, sourceSize, sourceSize);
    const bounds = findGlyphBounds(sourceImage.data, sourceSize, sourceSize);
    const normalizedSize = 36;

    if (!bounds) {
      return {
        pixels: new Uint8Array(normalizedSize * normalizedSize),
        aspectRatio: 0,
        density: 0
      };
    }

    const normalizedCanvas = document.createElement("canvas");
    normalizedCanvas.width = normalizedSize;
    normalizedCanvas.height = normalizedSize;
    const normalizedCtx = normalizedCanvas.getContext("2d", { willReadFrequently: true });
    normalizedCtx.clearRect(0, 0, normalizedSize, normalizedSize);

    const innerSize = normalizedSize - 8;
    const scale = Math.min(innerSize / bounds.width, innerSize / bounds.height);
    const drawWidth = Math.max(1, Math.round(bounds.width * scale));
    const drawHeight = Math.max(1, Math.round(bounds.height * scale));
    const offsetX = Math.floor((normalizedSize - drawWidth) / 2);
    const offsetY = Math.floor((normalizedSize - drawHeight) / 2);

    normalizedCtx.drawImage(
      sourceCanvas,
      bounds.left,
      bounds.top,
      bounds.width,
      bounds.height,
      offsetX,
      offsetY,
      drawWidth,
      drawHeight
    );

    const { data } = normalizedCtx.getImageData(0, 0, normalizedSize, normalizedSize);
    const pixels = new Uint8Array(normalizedSize * normalizedSize);
    let activePixels = 0;

    for (let pixelIndex = 0; pixelIndex < pixels.length; pixelIndex += 1) {
      const value = data[pixelIndex * 4 + 3] > 24 ? 1 : 0;
      pixels[pixelIndex] = value;
      activePixels += value;
    }

    return {
      pixels,
      aspectRatio: bounds.width / Math.max(bounds.height, 1),
      density: activePixels / pixels.length
    };
  }

  function compareSignatures(left, right) {
    let diff = 0;
    for (let index = 0; index < left.pixels.length; index += 1) {
      if (left.pixels[index] !== right.pixels[index]) {
        diff += 1;
      }
    }

    diff += Math.round(Math.abs(left.aspectRatio - right.aspectRatio) * 180);
    diff += Math.round(Math.abs(left.density - right.density) * 1200);
    return diff;
  }

  function isSpiderMatchReliable(bestScore, secondBestScore, signature) {
    if (!signature || signature.density <= 0) {
      return false;
    }

    if (bestScore <= 260) {
      return true;
    }

    return bestScore * 1.12 < secondBestScore;
  }

  function findGlyphBounds(data, width, height) {
    let left = width;
    let right = -1;
    let top = height;
    let bottom = -1;

    for (let y = 0; y < height; y += 1) {
      for (let x = 0; x < width; x += 1) {
        const alpha = data[(y * width + x) * 4 + 3];
        if (alpha <= 24) {
          continue;
        }

        left = Math.min(left, x);
        right = Math.max(right, x);
        top = Math.min(top, y);
        bottom = Math.max(bottom, y);
      }
    }

    if (right < left || bottom < top) {
      return null;
    }

    return {
      left,
      top,
      width: right - left + 1,
      height: bottom - top + 1
    };
  }

  async function waitForFontReady() {
    if (!document.fonts?.ready) {
      await sleep(120);
      return;
    }

    try {
      await Promise.race([
        document.fonts.ready,
        sleep(1200)
      ]);
    } catch (error) {
      console.warn("[pdd-table-exporter] font ready wait failed", error);
    }
  }

  async function collectVirtualRows(scrollTargets, rowContainer, collect) {
    const originalPositions = scrollTargets.map((node) => ({
      node,
      scrollTop: getNodeScrollTop(node)
    }));
    let stableRounds = 0;
    let previousSnapshot;

    scrollTargets.forEach((node) => {
      setScrollerTop(node, 0);
    });
    await sleep(220);
    previousSnapshot = getVirtualScrollerSnapshot(scrollTargets, rowContainer);

    while (stableRounds < 4) {
      const collectedCount = Number(collect()) || 0;
      const advanced = advanceVirtualRows(scrollTargets, rowContainer);
      const settledSnapshot = await waitForVirtualScrollerChange(
        scrollTargets,
        rowContainer,
        previousSnapshot,
        advanced ? 1000 : 1400
      );
      const latestCount = Number(collect()) || collectedCount;
      const hasProgress =
        settledSnapshot.scrollMetrics !== previousSnapshot.scrollMetrics ||
        settledSnapshot.rowKeys !== previousSnapshot.rowKeys ||
        latestCount > collectedCount;

      stableRounds = hasProgress ? 0 : stableRounds + 1;
      previousSnapshot = settledSnapshot;
    }

    collect();
    originalPositions.forEach(({ node, scrollTop }) => {
      setScrollerTop(node, scrollTop);
    });
    await sleep(60);

    runtime.lastPromotionDebug = {
      ...(runtime.lastPromotionDebug || {}),
      rowCount: Number(collect()) || 0,
      finalSnapshot: previousSnapshot
    };
  }

  function findPromotionScrollTargets(tableBody) {
    const targets = [];
    let current = tableBody;
    let depth = 0;

    while (current && depth < 10) {
      if (isPromotionScrollCandidate(current)) {
        targets.push(current);
      }
      current = current.parentElement;
      depth += 1;
    }

    const docScroller = document.scrollingElement || document.documentElement;
    if (docScroller && !targets.includes(docScroller)) {
      targets.push(docScroller);
    }

    if (!targets.length) {
      targets.push(tableBody);
    }

    return targets;
  }

  function isPromotionScrollCandidate(node) {
    if (!(node instanceof HTMLElement)) {
      return false;
    }

    const style = window.getComputedStyle(node);
    if (style.display === "none" || style.visibility === "hidden") {
      return false;
    }

    const overflowY = style.overflowY || style.overflow;
    return (
      node.clientHeight > 0 &&
      (/(auto|scroll|overlay)/.test(overflowY) || node.scrollHeight > node.clientHeight + 8)
    );
  }

  function advanceVirtualRows(scrollTargets, rowContainer) {
    const tailRow = getTailRow(rowContainer);
    const delta = Math.max(
      180,
      ...scrollTargets.map((node) => Math.max(0, Math.floor(getNodeClientHeight(node) * 0.85)))
    );
    let moved = false;

    if (tailRow) {
      tailRow.scrollIntoView({
        block: "end",
        inline: "nearest",
        behavior: "instant"
      });
      dispatchSyntheticWheel(tailRow, delta);
      moved = true;
    }

    scrollTargets.forEach((node) => {
      const beforeTop = getNodeScrollTop(node);
      focusScrollableNode(node);
      dispatchSyntheticWheel(node, delta);
      const maxScrollTop = Math.max(0, getNodeScrollHeight(node) - getNodeClientHeight(node));
      const nextTop = Math.min(maxScrollTop, beforeTop + delta);
      if (nextTop > beforeTop + 1) {
        setScrollerTop(node, nextTop);
        moved = true;
      }
    });

    return moved;
  }

  function getTailRow(rowContainer) {
    const rows = Array.from(rowContainer.querySelectorAll("tr[data-row-key]"));
    return rows[rows.length - 1] || null;
  }

  function focusScrollableNode(node) {
    if (!(node instanceof HTMLElement)) {
      return;
    }

    if (!node.hasAttribute("tabindex")) {
      node.dataset.pddExporterTabindex = "true";
      node.setAttribute("tabindex", "-1");
    }

    if (typeof node.focus === "function") {
      node.focus({ preventScroll: true });
    }
  }

  function dispatchSyntheticWheel(node, deltaY) {
    ["wheel", "mousewheel"].forEach((type) => {
      node.dispatchEvent(
        new WheelEvent(type, {
          bubbles: true,
          cancelable: true,
          deltaY,
          wheelDelta: -deltaY
        })
      );
    });
  }

  function getVirtualScrollerSnapshot(scrollTargets, rowContainer) {
    const rowKeys = Array.from(rowContainer.querySelectorAll("tr[data-row-key]"))
      .map((row) => row.dataset.rowKey || "")
      .join("|");
    const scrollMetrics = scrollTargets
      .map(
        (node) =>
          `${getNodeScrollTop(node)}:${getNodeClientHeight(node)}:${getNodeScrollHeight(node)}`
      )
      .join("|");

    return {
      rowKeys,
      scrollMetrics
    };
  }

  async function waitForVirtualScrollerChange(scrollTargets, rowContainer, previousSnapshot, timeoutMs) {
    const deadline = Date.now() + timeoutMs;
    let snapshot = getVirtualScrollerSnapshot(scrollTargets, rowContainer);

    while (Date.now() < deadline) {
      if (hasVirtualScrollerChanged(previousSnapshot, snapshot)) {
        await sleep(180);
        return getVirtualScrollerSnapshot(scrollTargets, rowContainer);
      }

      await sleep(120);
      snapshot = getVirtualScrollerSnapshot(scrollTargets, rowContainer);
    }

    return snapshot;
  }

  function hasVirtualScrollerChanged(previousSnapshot, nextSnapshot) {
    return previousSnapshot.scrollMetrics !== nextSnapshot.scrollMetrics || previousSnapshot.rowKeys !== nextSnapshot.rowKeys;
  }

  function setScrollerTop(node, top) {
    if (node === document.body || node === document.documentElement || node === document.scrollingElement) {
      window.scrollTo({
        top,
        behavior: "auto"
      });
    } else {
      node.scrollTop = top;
    }
    node.dispatchEvent(new Event("scroll", { bubbles: true }));
  }

  function getNodeScrollTop(node) {
    if (node === document.body || node === document.documentElement || node === document.scrollingElement) {
      return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
    }
    return node.scrollTop || 0;
  }

  function getNodeClientHeight(node) {
    if (node === document.body || node === document.documentElement || node === document.scrollingElement) {
      return window.innerHeight || document.documentElement.clientHeight || 0;
    }
    return node.clientHeight || 0;
  }

  function getNodeScrollHeight(node) {
    if (node === document.body || node === document.documentElement || node === document.scrollingElement) {
      return Math.max(
        document.body.scrollHeight || 0,
        document.documentElement.scrollHeight || 0
      );
    }
    return node.scrollHeight || 0;
  }

  function describeNode(node) {
    return {
      tag: node.tagName || "UNKNOWN",
      className: typeof node.className === "string" ? node.className : "",
      scrollTop: getNodeScrollTop(node),
      clientHeight: getNodeClientHeight(node),
      scrollHeight: getNodeScrollHeight(node)
    };
  }

  function collectHeadersFromRows(rows, preferredOrder) {
    const existing = new Set();
    const headers = [];

    preferredOrder.forEach((header) => {
      if (rows.some((row) => row[header] !== undefined)) {
        headers.push(header);
        existing.add(header);
      }
    });

    rows.forEach((row) => {
      Object.keys(row).forEach((key) => {
        if (!existing.has(key)) {
          headers.push(key);
          existing.add(key);
        }
      });
    });

    return headers;
  }

  function downloadCsv(filename, headers, rows) {
    const csv = buildCsv(headers, rows);
    const blob = new Blob(["\ufeff", csv], {
      type: "text/csv;charset=utf-8"
    });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = `${filename}.csv`;
    document.body.appendChild(link);
    link.click();
    link.remove();
    window.setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  function downloadText(filename, text, extension, mime) {
    const blob = new Blob([text], {
      type: mime || "text/plain;charset=utf-8"
    });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = `${filename}.${extension || "txt"}`;
    document.body.appendChild(link);
    link.click();
    link.remove();
    window.setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  function buildCsv(headers, rows) {
    const headerRow = headers.map((header) => csvCell(header)).join(",");
    const bodyRows = rows
      .map((row) => {
        return headers.map((header) => csvCell(row[header] ?? "")).join(",");
      })
      .join("\r\n");

    return [headerRow, bodyRows].filter(Boolean).join("\r\n");
  }

  function csvCell(value) {
    const normalized = String(value).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
    return `"${normalized.replace(/"/g, '""')}"`;
  }

  function showToast(message, durationMs = 2400) {
    let toast = document.getElementById(TOAST_ID);
    if (!toast) {
      toast = document.createElement("div");
      toast.id = TOAST_ID;
      toast.style.cssText = [
        "position: fixed",
        "right: 24px",
        "bottom: 24px",
        "z-index: 2147483647",
        "max-width: 320px",
        "padding: 12px 16px",
        "border-radius: 8px",
        "background: rgba(17, 24, 39, 0.92)",
        "color: #fff",
        "font-size: 14px",
        "line-height: 1.5",
        "box-shadow: 0 12px 28px rgba(0,0,0,0.22)"
      ].join("; ");
      document.body.appendChild(toast);
    }

    toast.textContent = message;
    toast.style.opacity = "1";

    window.clearTimeout(showToast.timerId);
    showToast.timerId = window.setTimeout(() => {
      if (toast) {
        toast.style.opacity = "0";
      }
    }, durationMs);
  }

  function triggerClick(node) {
    if (!node) {
      return;
    }

    const targets = [
      node,
      node.querySelector(".data-pick-group_dateItemStyle__1bRXr"),
      node.querySelector('[class*="RD_textWrapper"]'),
      node.querySelector('input[type="radio"]')
    ].filter(Boolean);

    const hitTarget = getHitTarget(node);
    if (hitTarget) {
      targets.unshift(hitTarget);
    }

    targets.forEach((target) => {
      ["pointerdown", "mousedown", "mouseup", "click"].forEach((eventName) => {
        const EventClass =
          eventName === "pointerdown" && typeof PointerEvent === "function" ? PointerEvent : MouseEvent;
        target.dispatchEvent(
          new EventClass(eventName, {
            bubbles: true,
            cancelable: true,
            composed: true
          })
        );
      });

      if (typeof target.click === "function") {
        target.click();
      }
    });

    const input = node.querySelector('input[type="radio"]');
    if (input) {
      input.checked = true;
      input.dispatchEvent(new Event("input", { bubbles: true }));
      input.dispatchEvent(new Event("change", { bubbles: true }));
    }
  }

  function getHitTarget(node) {
    if (!(node instanceof HTMLElement)) {
      return null;
    }

    const rect = node.getBoundingClientRect();
    const x = Math.min(rect.right - 2, Math.max(rect.left + 2, rect.left + rect.width / 2));
    const y = Math.min(rect.bottom - 2, Math.max(rect.top + 2, rect.top + rect.height / 2));
    const hit = document.elementFromPoint(x, y);
    return hit && (node.contains(hit) || hit.contains(node)) ? hit : null;
  }

  function isElementInViewport(node) {
    if (!(node instanceof HTMLElement)) {
      return false;
    }

    const rect = node.getBoundingClientRect();
    return rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth;
  }

  function isInteractableOnScreen(node) {
    if (!(node instanceof HTMLElement)) {
      return false;
    }

    const rect = node.getBoundingClientRect();
    const x = Math.min(rect.right - 2, Math.max(rect.left + 2, rect.left + rect.width / 2));
    const y = Math.min(rect.bottom - 2, Math.max(rect.top + 2, rect.top + rect.height / 2));
    const hit = document.elementFromPoint(x, y);
    return Boolean(hit && (node.contains(hit) || hit.contains(node)));
  }

  async function waitForCondition(checker, timeoutMs, intervalMs, errorMessage) {
    const deadline = Date.now() + timeoutMs;

    while (Date.now() < deadline) {
      if (checker()) {
        return;
      }
      await sleep(intervalMs);
    }

    throw new Error(errorMessage);
  }

  async function waitForForegroundTab(message) {
    if (!document.hidden && document.visibilityState === "visible") {
      return true;
    }

    console.warn("[pdd-table-exporter] paused while tab is hidden", message || "");
    return new Promise((resolve) => {
      let timerId = 0;
      const cleanup = () => {
        document.removeEventListener("visibilitychange", check);
        window.removeEventListener("focus", check);
        if (timerId) {
          window.clearInterval(timerId);
        }
      };
      const check = () => {
        if (!document.hidden && document.visibilityState === "visible") {
          cleanup();
          showToast("已回到当前标签页，继续批量补采", 3000);
          resolve(true);
        }
      };
      document.addEventListener("visibilitychange", check);
      window.addEventListener("focus", check);
      timerId = window.setInterval(check, 1000);
      check();
    });
  }

  async function waitForSignatureRefresh(getSignature, previousSignature, timeoutMs, alternateChecker) {
    const deadline = Date.now() + timeoutMs;

    while (Date.now() < deadline) {
      const currentSignature = getSignature();
      if ((currentSignature && currentSignature !== previousSignature) || alternateChecker?.()) {
        return;
      }
      await sleep(120);
    }
  }

  async function waitForStableSignature(getSignature, stableRounds, intervalMs, timeoutMs) {
    const deadline = Date.now() + timeoutMs;
    let previousSignature = "";
    let stableCount = 0;

    while (Date.now() < deadline) {
      const currentSignature = getSignature();
      if (currentSignature && currentSignature === previousSignature) {
        stableCount += 1;
        if (stableCount >= stableRounds) {
          return currentSignature;
        }
      } else {
        stableCount = 0;
        previousSignature = currentSignature;
      }
      await sleep(intervalMs);
    }

    return previousSignature;
  }

  async function waitForGoodsEffectDataReady() {
    await waitForCondition(
      () => {
        const tableContext = getGoodsEffectTableContext();
        if (!tableContext) {
          return false;
        }

        const bodyRows = Array.from(tableContext.body.querySelectorAll("tr")).filter((row) => isVisibleElement(row));
        if (!bodyRows.length) {
          return false;
        }

        return Boolean(cleanText(bodyRows[0].innerText));
      },
      7000,
      150,
      "店铺后台表格数据加载超时"
    );

    await waitForStableSignature(getGoodsEffectTableSignature, 2, 220, 5000);
  }

  function cleanText(value) {
    return (value || "").replace(/\s+/g, " ").trim();
  }

  function isPromotionIgnoredHeader(text) {
    return (
      [
        "操作",
        "推广",
        "商品信息",
        "推广状态",
        "顶限及出价",
        "预算及出价",
        "预算与出价",
        "预算日限额"
      ].includes(text) ||
      /^推广(?:\s|分组|添加时间|$)/.test(text) ||
      /^[一二三四五六日天]$/.test(text) ||
      /^(周|星期)[一二三四五六日天]$/.test(text) ||
      /^\d{1,2}[-/月]\d{1,2}(日)?$/.test(text) ||
      /^\d{4}[-/]\d{1,2}[-/]\d{1,2}$/.test(text)
    );
  }

  function isVisibleElement(node) {
    if (!(node instanceof HTMLElement)) {
      return false;
    }

    const style = window.getComputedStyle(node);
    if (style.display === "none" || style.visibility === "hidden") {
      return false;
    }

    const rect = node.getBoundingClientRect();
    return rect.width > 0 && rect.height > 0;
  }

  function getElementArea(node) {
    const rect = node.getBoundingClientRect();
    return rect.width * rect.height;
  }

  function extractFirstMatch(text, pattern, groupIndex = 1) {
    const match = text.match(pattern);
    return match ? cleanText(match[groupIndex]) : "";
  }

  function findElementByText(selector, expectedText) {
    return Array.from(document.querySelectorAll(selector)).find(
      (node) => cleanText(node.textContent) === expectedText
    );
  }

  function findElementByTextInRoot(root, selector, expectedText) {
    if (!root) {
      return null;
    }
    return Array.from(root.querySelectorAll(selector)).find(
      (node) => cleanText(node.textContent) === expectedText
    ) || null;
  }

  function buildFilename(prefix) {
    const now = new Date();
    const parts = [
      now.getFullYear(),
      String(now.getMonth() + 1).padStart(2, "0"),
      String(now.getDate()).padStart(2, "0"),
      "_",
      String(now.getHours()).padStart(2, "0"),
      String(now.getMinutes()).padStart(2, "0"),
      String(now.getSeconds()).padStart(2, "0")
    ];
    return `${prefix}_${parts.join("")}`;
  }

  function getButtonId(key) {
    return `${BUTTON_BASE_ID}-${key}`;
  }

  function sleep(ms) {
    return new Promise((resolve) => window.setTimeout(resolve, ms));
  }
})();
