(() => {
  if (window.__MMW_QIANCHUAN_LAUNCH_MAIN__) return;
  window.__MMW_QIANCHUAN_LAUNCH_MAIN__ = true;

  const ROOT_ID = "mmw-qianchuan-launch-root";
  const OPEN_EVENT = "MMW_QIANCHUAN_LAUNCH_OPEN";
  const HOST_RE = /(^|\.)qianchuan\.jinritemai\.com$/i;
  const API_BASE = "https://qianchuan.jinritemai.com";
  const GF_VERSION = "1.0.0.5534";
  const LIST_GF_VERSION = "1.0.0.4748";
  const BID_COST_CONTROL = 0;
  const BID_NO_COST_CONTROL = 7;
  const MAX_RENDER_GOODS = 500;
  const MAX_UPLOAD_BATCH_SIZE = 20;
  const MAX_TITLE_COUNT = 30;
  const MIN_TITLE_LENGTH = 5;
  const MAX_TITLE_LENGTH = 55;
  const MIN_PRODUCT_CARD_TITLE_LENGTH = 1;
  const MAX_PRODUCT_CARD_TITLE_LENGTH = 30;
  const BILLING_REQUEST_SOURCE = "mmw-qianchuan-billing-main";
  const BILLING_RESPONSE_SOURCE = "mmw-qianchuan-billing-extension";
  const BILLING_TIMEOUT_MS = 18000;
  const PREFERENCE_STORAGE_PREFIX = "mmw-qianchuan-launch-preferences-v1";
  const REMEMBERED_FIELDS = new Set(["budgetCost", "budgetVolume", "ecpRoi2Goal", "titlePrefix"]);
  const scrollPositions = new Map();

  if (!HOST_RE.test(location.hostname)) return;

  const state = {
    open: false,
    busy: false,
    loadingGoods: false,
    creating: false,
    stopping: false,
    message: "",
    messageType: "info",
    aavid: "",
    awemeOptions: [],
    goods: [],
    selected: new Set(),
    failedGoods: [],
    materialBindings: {},
    materialSerial: 0,
    materialBatchSerial: 0,
    activeMaterialGoodsId: "",
    revealMaterialIssueGoodsId: "",
    importIds: "",
    access: {
      checking: false,
      allowed: false,
      message: "",
      billing: null
    },
    form: {
      awemeId: "",
      creativeSource: "",
      searchKeyWords: "",
      loadMode: "all",
      keyword: "",
      smartBidType: BID_COST_CONTROL,
      budgetCost: 300,
      budgetVolume: 300,
      ecpRoi2Goal: 1.8,
      goodsCount: 1,
      scheduleType: 1,
      titleMakeType: 1,
      titlePrefix: "",
      activityIntervalSecond: 5,
      useCustomMaterials: false,
      enableAigcCreative: true,
      isAllowMall: true
    }
  };

  const root = document.createElement("div");
  root.id = ROOT_ID;
  const shadow = root.attachShadow({ mode: "open" });
  document.documentElement.appendChild(root);

  window.addEventListener(OPEN_EVENT, () => openPanel());

  if (new URLSearchParams(location.search).get("mmw_qc_launch") === "1") {
    setTimeout(openPanel, 800);
  }

  function openPanel() {
    if (!state.open) scrollPositions.clear();
    state.open = true;
    state.aavid = getAavid();
    restorePlanningPreferences();
    state.access.checking = true;
    state.access.allowed = false;
    state.access.message = "正在校验会员状态...";
    render();
    refreshQianchuanAccess()
      .then(() => {
        if (state.aavid) {
          hydrateAwemeOptions().catch((error) => setMessage(error?.message || "抖音号加载失败", "error"));
        } else {
          setMessage("未获取到千川账户 ID，请先进入千川全域页面。", "error");
        }
      })
      .catch(() => {});
  }

  function closePanel() {
    state.open = false;
    state.message = "";
    render();
  }

  function setMessage(message, type = "info") {
    state.message = message || "";
    state.messageType = type;
    render();
  }

  function getAavid() {
    const direct = new URLSearchParams(location.search).get("aavid");
    if (direct) return direct;
    const match = decodeURIComponent(location.href).match(/aavid["'=:%]+(\d{8,})/i);
    return match?.[1] || "";
  }

  function preferenceStorageKey() {
    return `${PREFERENCE_STORAGE_PREFIX}:${String(state.aavid || "default").trim() || "default"}`;
  }

  function readPlanningPreferences() {
    try {
      const value = JSON.parse(window.localStorage?.getItem(preferenceStorageKey()) || "{}");
      return value && typeof value === "object" && !Array.isArray(value) ? value : {};
    } catch {
      return {};
    }
  }

  function validRememberedValue(field, value) {
    if (field === "titlePrefix") return normalizePlanNamePrefix(value);
    const number = Number(value);
    if (!Number.isFinite(number)) return null;
    if (field === "ecpRoi2Goal") return number >= 0.01 && number <= 40 ? number : null;
    return number >= 300 ? number : null;
  }

  function restorePlanningPreferences() {
    const preferences = readPlanningPreferences();
    REMEMBERED_FIELDS.forEach((field) => {
      const value = validRememberedValue(field, preferences[field]);
      if (value !== null) state.form[field] = value;
    });
  }

  function persistPlanningPreference(field) {
    if (!REMEMBERED_FIELDS.has(field)) return;
    const value = validRememberedValue(field, state.form[field]);
    if (value === null) return;
    try {
      window.localStorage?.setItem(preferenceStorageKey(), JSON.stringify({
        ...readPlanningPreferences(),
        [field]: value
      }));
    } catch {}
  }

  function normalizeFeatureName(value) {
    return String(value || "").replace(/千川投放/g, "千川全域");
  }

  function billingRequest(action, payload = {}) {
    const requestId = `qc-bridge-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
    return new Promise((resolve, reject) => {
      const timer = window.setTimeout(() => {
        window.removeEventListener("message", onMessage);
        reject(new Error("会员与计费服务连接超时，请确认插件已重新加载"));
      }, BILLING_TIMEOUT_MS);
      const onMessage = (event) => {
        if (event.source !== window || event.origin !== location.origin) return;
        const message = event.data;
        if (!message || message.source !== BILLING_RESPONSE_SOURCE || message.requestId !== requestId) return;
        window.clearTimeout(timer);
        window.removeEventListener("message", onMessage);
        const response = message.response || {};
        if (!response.ok) {
          reject(new Error(normalizeFeatureName(response.error || "千川全域会员或计费校验失败")));
          return;
        }
        resolve(response.payload || {});
      };
      window.addEventListener("message", onMessage);
      window.postMessage({
        source: BILLING_REQUEST_SOURCE,
        requestId,
        action,
        payload
      }, location.origin);
    });
  }

  async function refreshQianchuanAccess() {
    state.access.checking = true;
    state.access.message = "正在校验会员状态...";
    render();
    try {
      const billing = await billingRequest("status");
      state.access.allowed = billing.member_active === true;
      state.access.billing = billing;
      state.access.message = normalizeFeatureName(billing.message || "会员校验通过");
      if (!state.access.allowed) throw new Error("千川全域仅限有效会员使用，请先开通或续费会员");
      return billing;
    } catch (error) {
      state.access.allowed = false;
      state.access.billing = null;
      state.access.message = normalizeFeatureName(error?.message || "千川全域仅限有效会员使用");
      throw error;
    } finally {
      state.access.checking = false;
      render();
    }
  }

  async function ensureQianchuanAccess(force = false) {
    if (!force && state.access.allowed && state.access.billing) return state.access.billing;
    return refreshQianchuanAccess();
  }

  function captureScrollState() {
    shadow.querySelectorAll("[data-scroll-key]").forEach((node) => {
      const key = String(node.dataset.scrollKey || "");
      if (!key) return;
      scrollPositions.set(key, { top: node.scrollTop, left: node.scrollLeft });
    });
  }

  function restoreScrollState() {
    shadow.querySelectorAll("[data-scroll-key]").forEach((node) => {
      const position = scrollPositions.get(String(node.dataset.scrollKey || ""));
      if (!position) return;
      node.scrollTop = position.top;
      node.scrollLeft = position.left;
    });
  }

  function render() {
    captureScrollState();
    shadow.innerHTML = `
      <style>
        :host{all:initial}
        .mask{position:fixed;inset:0;z-index:2147483640;background:rgba(5,12,24,.48);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
        .panel{position:fixed;left:50%;top:38px;z-index:2147483641;width:min(1480px,calc(100vw - 48px));max-height:calc(100vh - 76px);transform:translateX(-50%);display:flex;flex-direction:column;border:1px solid rgba(159,198,255,.42);border-radius:18px;background:linear-gradient(145deg,rgba(248,251,255,.92),rgba(225,235,249,.84));box-shadow:0 28px 90px rgba(12,31,68,.34),inset 0 1px 0 rgba(255,255,255,.78);backdrop-filter:blur(18px) saturate(145%);-webkit-backdrop-filter:blur(18px) saturate(145%);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:#0e2442;overflow:hidden}
        .head{display:flex;align-items:center;justify-content:space-between;padding:16px 18px;border-bottom:1px solid rgba(84,128,190,.2)}
        .title{font-size:20px;font-weight:850;letter-spacing:-.01em}
        .sub{font-size:12px;color:#557095}
        .close{width:34px;height:34px;border:0;border-radius:10px;background:rgba(15,39,76,.08);color:#173252;cursor:pointer;font-size:22px;line-height:1}
        .body{display:grid;grid-template-columns:320px minmax(0,1fr);gap:14px;padding:14px 18px 18px;min-height:0;overflow:auto}
        .body.hasMaterial{grid-template-columns:320px minmax(520px,1fr) 360px}
        .card{border:1px solid rgba(90,135,198,.22);border-radius:14px;background:rgba(255,255,255,.62);box-shadow:0 12px 34px rgba(20,57,106,.08)}
        .form{padding:14px;display:grid;gap:12px;align-content:start}
        .sectionTitle{font-size:13px;font-weight:850;color:#19375d}
        .row{display:grid;gap:6px}
        label{font-size:12px;font-weight:760;color:#3f5c7c}
        .fieldHint{color:#607896;font-size:11px;line-height:1.4;overflow-wrap:anywhere}
        .fieldHint b{color:#17416e;font-weight:760}
        input,select,textarea{box-sizing:border-box;width:100%;min-height:34px;border:1px solid rgba(83,127,188,.28);border-radius:9px;background:rgba(255,255,255,.86);padding:7px 10px;color:#0f2544;font:inherit;outline:none}
        textarea{min-height:72px;resize:vertical}
        input:focus,select:focus,textarea:focus{border-color:#1f6fd1;box-shadow:0 0 0 3px rgba(31,111,209,.14)}
        .inline{display:grid;grid-template-columns:1fr 1fr;gap:8px}
        .check{display:flex;align-items:center;gap:8px;font-size:12px;color:#365676}
        .check input{width:16px;min-height:16px}
        .actions{display:flex;gap:8px;flex-wrap:wrap}
        .btn{height:34px;border:1px solid rgba(70,118,187,.26);border-radius:9px;background:rgba(255,255,255,.8);color:#17416e;cursor:pointer;padding:0 12px;font-weight:800}
        .btn.primary{border-color:#1f6fd1;background:#1f6fd1;color:#fff}
        .btn.danger{border-color:#e16a7b;background:#fff5f6;color:#b4263c}
        .btn:disabled{opacity:.55;cursor:not-allowed}
        .status{min-height:20px;font-size:12px;color:#557095}
        .status.error{color:#c92a3d}.status.success{color:#087a43}
        .content{display:flex;flex-direction:column;min-width:0;min-height:460px}
        .toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 14px;border-bottom:1px solid rgba(90,135,198,.18)}
        .summary{display:flex;gap:10px;flex-wrap:wrap;font-size:12px;color:#4e6c90}
        .summary b{color:#12345e}
        .tableWrap{overflow:auto;min-height:360px}
        table{width:100%;border-collapse:collapse;min-width:860px}
        .goodsTable col.colSelect{width:46px}
        .goodsTable col.colImage{width:58px}
        .goodsTable col.colProduct{width:auto}
        .goodsTable col.colId{width:150px}
        .goodsTable col.colPrice{width:90px}
        .goodsTable col.colSales{width:90px}
        .goodsTable col.colCategory{width:100px}
        .goodsTable col.colMaterial{width:118px}
        .goodsTable col.colResult{width:90px}
        .goodsTable.compact{min-width:680px;table-layout:fixed}
        .goodsTable.compact col.colSelect{width:38px}
        .goodsTable.compact col.colImage{width:50px}
        .goodsTable.compact col.colProduct{width:150px}
        .goodsTable.compact col.colId{width:145px}
        .goodsTable.compact col.colPrice{width:56px}
        .goodsTable.compact col.colSales{width:54px}
        .goodsTable.compact col.colCategory{width:62px}
        .goodsTable.compact col.colMaterial{width:74px}
        .goodsTable.compact col.colResult{width:60px}
        .goodsTable.compact th,.goodsTable.compact td{padding:8px 5px}
        .goodsTable.compact .goodsName{max-width:150px}
        .goodsTable.compact td:nth-child(4){white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
        .goodsTable.compact .materialChip{box-sizing:border-box;width:100%;justify-content:center;padding-inline:4px;line-height:1.25}
        th,td{padding:10px 8px;border-bottom:1px solid rgba(90,135,198,.14);font-size:12px;text-align:left;vertical-align:middle}
        th{position:sticky;top:0;background:rgba(242,247,255,.96);z-index:1;color:#365676;font-weight:850}
        td{color:#173252}
        .goodsRow{cursor:pointer}
        .goodsRow:hover td{background:rgba(238,246,255,.64)}
        .goodsRow.active td{background:rgba(225,239,255,.82)}
        .goodsRow.hasIssue td{background:#fff4f6}
        .goodsRow.hasIssue:hover td{background:#ffecef}
        .thumb{width:42px;height:42px;border-radius:8px;object-fit:cover;background:#eef4fb;border:1px solid rgba(85,126,180,.18)}
        .goodsName{max-width:300px;font-weight:750;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
        .muted{color:#7188a4}
        .tag{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:999px;background:#eef5ff;color:#1f6fd1;font-size:12px;font-weight:780}
        .tinyBtn{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;height:28px;border:1px solid rgba(70,118,187,.24);border-radius:8px;background:#fff;color:#17416e;cursor:pointer;padding:0 9px;font-size:12px;font-weight:780;line-height:1;white-space:nowrap}
        .tinyBtn:hover{border-color:#1f6fd1;color:#1f6fd1}
        .tinyBtn:disabled{opacity:.48;cursor:not-allowed}
        .titleAddBtn{width:76px;padding:0}
        .materialState{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
        .materialBadge{display:inline-flex;align-items:center;height:22px;padding:0 7px;border-radius:999px;background:#eef5ff;color:#1f5fb8;font-weight:780}
        .materialBadge.warn{background:#fff6e8;color:#a86200}
        .materialChip{display:inline-flex;align-items:center;gap:5px;min-height:28px;border:1px solid rgba(70,118,187,.2);border-radius:9px;background:#fff;color:#17416e;cursor:pointer;padding:0 8px;font-size:12px;font-weight:780}
        .materialChip:hover{border-color:#1f6fd1;color:#1f6fd1}
        .materialChip.warn{border-color:#d13a51;background:#fff1f3;color:#b4263c}
        .materialPanelHead{display:flex;align-items:center;justify-content:space-between;gap:10px}
        .materialPanelTitle{font-size:13px;font-weight:850;color:#19375d}
        .materialPanelHint{font-size:12px;color:#5d7594}
        .materialChannels{display:grid;grid-template-columns:1fr 1fr;gap:8px}
        .dropZone{position:relative;display:flex;min-height:96px;align-items:center;justify-content:center;text-align:center;border:1px dashed rgba(31,111,209,.38);border-radius:12px;background:rgba(239,246,255,.82);color:#426384;transition:border-color .16s ease,background .16s ease}
        .dropZone.image{border-color:rgba(32,145,122,.38);background:rgba(238,251,247,.82)}
        .dropZone.image .dropTitle{color:#176d5d}
        .dropZone.image.isDrag{border-color:#208f79;background:#e5f8f2}
        .dropZone.isDrag{border-color:#1f6fd1;background:#e9f2ff}
        .dropZone input{position:absolute;inset:0;opacity:0;cursor:pointer}
        .dropTitle{font-size:13px;font-weight:850;color:#17416e}
        .materialList{display:grid;gap:8px;max-height:220px;overflow:auto}
        .materialItem{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center;padding:8px 10px;border:1px solid rgba(90,135,198,.18);border-radius:10px;background:#f8fbff}
        .materialItem.errorFlash{border-color:#d13a51;background:#fff1f3;animation:materialIssueFlash 1.1s ease-in-out infinite}
        .materialItemError{margin-top:3px;color:#b4263c;font-size:11px;font-weight:760;overflow-wrap:anywhere}
        .materialName{font-weight:760;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#19375d}
        .materialMeta{margin-top:2px;color:#6b819f;font-size:11px}
        .titleBox{display:grid;gap:8px}
        .titleIssueSummary{display:grid;gap:8px;padding:10px;border:1px solid #d13a51;border-radius:10px;background:#fff1f3;color:#8f1f34}
        .titleIssueSummary[hidden]{display:none}
        .titleIssueHead{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;font-weight:850}
        .titleIssueServer{font-size:12px;line-height:1.45;overflow-wrap:anywhere}
        .titleIssueList{display:grid;gap:6px;max-height:150px;overflow:auto}
        .titleIssueItem{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;align-items:center;padding:7px 8px;border-radius:8px;background:#fff;color:#6f1930}
        .titleIssueText{min-width:0;font-size:12px;line-height:1.4;overflow-wrap:anywhere}
        .titleIssueReason{display:block;color:#b4263c;font-weight:760}
        .titleHead{display:flex;align-items:center;justify-content:space-between;gap:8px}
        .titleHeading{display:flex;align-items:center;gap:7px;min-width:0}
        .titleRule{color:#7188a4;font-size:12px;font-weight:700}
        .titleDrop{position:relative;display:flex;min-height:38px;align-items:center;justify-content:center;border:1px dashed rgba(31,111,209,.34);border-radius:10px;background:rgba(239,246,255,.66);color:#315f91;font-size:12px;font-weight:780;transition:border-color .16s ease,background .16s ease,color .16s ease}
        .titleDrop:hover,.titleDrop.isDrag{border-color:#1f6fd1;background:#e9f2ff;color:#17416e}
        .titleDrop input{position:absolute;inset:0;opacity:0;cursor:pointer}
        .titleRow{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center}
        .titleRow.invalid{padding:6px;border:1px solid #d13a51;border-radius:10px;background:#fff5f6}
        .titleField{position:relative;min-width:0}
        .titleInput{min-height:32px;padding-right:54px}
        .titleInput[aria-invalid="true"]{border-color:#d94b5f;background:#fff8f9}
        .titleInput[aria-invalid="true"]:focus{border-color:#d13a51;box-shadow:0 0 0 3px rgba(209,58,81,.12)}
        .titleCounter{position:absolute;top:8px;right:10px;color:#7188a4;font-size:12px;font-weight:700;line-height:18px;pointer-events:none}
        .titleCounter.invalid{color:#c92a3d}
        .titleIssue{margin-top:4px;color:#b4263c;font-size:12px;line-height:1.35}
        .titleIssue[hidden]{display:none}
        .materialSide{padding:14px;display:flex;flex-direction:column;gap:12px;align-self:start;max-height:calc(100vh - 205px);overflow:auto}
        .materialSideEmpty{display:flex;min-height:300px;align-items:center;justify-content:center;text-align:center;color:#607896;padding:24px}
        .sideProduct{display:grid;grid-template-columns:52px minmax(0,1fr);gap:10px;align-items:center;padding:10px;border:1px solid rgba(90,135,198,.18);border-radius:12px;background:rgba(248,251,255,.78)}
        .sideProductName{font-size:13px;font-weight:850;color:#173252;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
        .sideMetrics{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
        .sideMetric{padding:9px 10px;border:1px solid rgba(90,135,198,.18);border-radius:10px;background:#f8fbff;color:#365676;font-size:12px}
        .sideMetric b{display:block;color:#12345e;font-size:16px}
        .sideMetric.warn{border-color:rgba(201,42,61,.24);background:#fff7f8;color:#9f2940}
        .sideMetric.warn b{color:#b4263c}
        .captureNote{padding:9px 10px;border-radius:10px;background:#f6f8fb;color:#526b88;font-size:12px}
        .empty{padding:58px 20px;text-align:center;color:#6b819f}
        .failBox{margin:10px 14px 0;padding:10px;border-radius:10px;background:#fff5f6;color:#b4263c;font-size:12px}
        .accessGate{display:flex;min-height:360px;align-items:center;justify-content:center;padding:28px}
        .accessCard{width:min(460px,100%);padding:34px;border:1px solid rgba(90,135,198,.22);border-radius:16px;background:rgba(255,255,255,.72);text-align:center;box-shadow:0 16px 40px rgba(20,57,106,.1)}
        .accessTitle{font-size:18px;font-weight:850;color:#173252}
        .accessMessage{margin-top:10px;color:#526b88}
        .accessCard .btn{margin-top:20px}
        @keyframes materialIssueFlash{0%,100%{background:#fff1f3}50%{background:#ffdce2}}
        @media (prefers-reduced-motion:reduce){.dropZone,.titleDrop{transition:none}.materialItem.errorFlash{animation:none}}
        @media (max-width:1180px){.body.hasMaterial{grid-template-columns:320px minmax(0,1fr)}.materialSide{grid-column:1/-1;max-height:none}}
        @media (max-width:860px){.body,.body.hasMaterial{grid-template-columns:1fr}.panel{top:16px;max-height:calc(100vh - 32px)}}
      </style>
      ${state.open ? `
        <div class="mask" aria-hidden="true"></div>
        <section class="panel" role="dialog" aria-modal="true" aria-label="千川全域">
          <header class="head">
            <div>
              <div class="title">千川全域</div>
              <div class="sub">加载商品、批量创建计划，直接调用当前千川登录态。</div>
            </div>
            <button class="close" data-action="close-panel" type="button" aria-label="关闭">×</button>
          </header>
          ${state.access.allowed ? `
            ${createHtml()}
          ` : accessGateHtml()}
        </section>
      ` : ""}
    `;
    bindEvents();
    restoreScrollState();
    revealPendingMaterialIssue();
  }

  function accessGateHtml() {
    return `
      <div class="accessGate">
        <div class="accessCard">
          <div class="accessTitle">${state.access.checking ? "正在校验会员" : "千川全域为会员功能"}</div>
          <div class="accessMessage">${escapeHtml(state.access.message || "请使用有效会员账号后重试")}</div>
          ${state.access.checking ? "" : `<button class="btn primary" data-action="retry-access" type="button">重新校验</button>`}
        </div>
      </div>
    `;
  }

  function createHtml() {
    const selectedCount = state.selected.size;
    const goodsRows = state.goods.slice(0, MAX_RENDER_GOODS).map(goodsRowHtml).join("");
    const statusClass = state.messageType === "error" ? " error" : state.messageType === "success" ? " success" : "";
    const materialStats = materialBindingStats();
    const hasMaterialSide = state.form.useCustomMaterials === true;
    return `
      <div class="body${hasMaterialSide ? " hasMaterial" : ""}" data-scroll-key="create-body">
        <aside class="card form">
          <div class="sectionTitle">投放设置</div>
          <div class="row">
            <label>千川账户</label>
            <input value="${escapeHtml(state.aavid || "未获取")}" disabled>
          </div>
          <div class="row">
            <label>抖音号</label>
            <select data-field="awemeId">
              <option value="">自动/暂不选择</option>
              ${state.awemeOptions.map((item) => `<option value="${escapeHtml(item.id)}"${state.form.awemeId === item.id ? " selected" : ""}>${escapeHtml(item.name || item.id)}</option>`).join("")}
            </select>
          </div>
          <div class="row">
            <label>投放方式</label>
            <select data-field="smartBidType">
              <option value="${BID_COST_CONTROL}"${state.form.smartBidType === BID_COST_CONTROL ? " selected" : ""}>控成本投放</option>
              <option value="${BID_NO_COST_CONTROL}"${state.form.smartBidType === BID_NO_COST_CONTROL ? " selected" : ""}>放量投放</option>
            </select>
          </div>
          ${state.form.smartBidType === BID_NO_COST_CONTROL ? `
            <div class="row">
              <label>日预算（元）</label>
              <input data-field="budgetVolume" type="number" min="300" step="1" value="${escapeHtml(state.form.budgetVolume)}">
            </div>
          ` : `
            <div class="inline">
              <div class="row">
                <label>日预算（元）</label>
                <input data-field="budgetCost" type="number" min="300" step="1" value="${escapeHtml(state.form.budgetCost)}">
              </div>
              <div class="row">
                <label>净成交 ROI 目标</label>
                <input data-field="ecpRoi2Goal" type="number" min="0.01" max="40" step="0.01" value="${escapeHtml(state.form.ecpRoi2Goal)}">
              </div>
            </div>
          `}
          <div class="inline">
            <div class="row">
              <label>每计划商品数</label>
              <input data-field="goodsCount" type="number" min="1" max="30" step="1" value="${escapeHtml(state.form.goodsCount)}">
            </div>
            <div class="row">
              <label>创建间隔（秒）</label>
              <input data-field="activityIntervalSecond" type="number" min="0" max="60" step="1" value="${escapeHtml(state.form.activityIntervalSecond)}">
            </div>
          </div>
          <div class="inline">
            <div class="row">
              <label>加载方式</label>
              <select data-field="loadMode">
                <option value="all"${state.form.loadMode === "all" ? " selected" : ""}>加载全部未投放</option>
                <option value="all_sell_num"${state.form.loadMode === "all_sell_num" ? " selected" : ""}>销量优先</option>
                <option value="new"${state.form.loadMode === "new" ? " selected" : ""}>店铺新品</option>
                <option value="top"${state.form.loadMode === "top" ? " selected" : ""}>破圈尖货</option>
                <option value="search"${state.form.loadMode === "search" ? " selected" : ""}>搜索趋势</option>
                <option value="specify"${state.form.loadMode === "specify" ? " selected" : ""}>导入商品ID</option>
              </select>
            </div>
            <div class="row">
              <label>关键词</label>
              <input data-field="keyword" value="${escapeHtml(state.form.keyword)}" placeholder="可选">
            </div>
          </div>
          ${state.form.loadMode === "specify" ? `
            <div class="row">
              <label>商品ID，一行一个或逗号分隔</label>
              <textarea data-field="importIds">${escapeHtml(state.importIds)}</textarea>
            </div>
          ` : ""}
          <div class="row">
            <label>自定义计划名前缀</label>
            <input data-field="titlePrefix" maxlength="24" value="${escapeAttribute(state.form.titlePrefix)}" placeholder="留空使用 MMWGZT快速计划-">
            <div class="fieldHint">计划名预览：<b data-plan-name-preview>${escapeHtml(makePlanNamePreview())}</b></div>
          </div>
          <label class="check">
            <input data-field="isAllowMall" type="checkbox"${state.form.isAllowMall ? " checked" : ""}>
            支持商城流量包
          </label>
          <label class="check">
            <input data-field="enableAigcCreative" type="checkbox"${state.form.enableAigcCreative ? " checked" : ""}>
            AIGC 动态创意
          </label>
          <label class="check">
            <input data-field="useCustomMaterials" type="checkbox"${state.form.useCustomMaterials ? " checked" : ""}>
            自选投放素材（视频/图片）
          </label>
          <div class="actions">
            <button class="btn primary" data-action="load-goods" type="button"${state.loadingGoods || state.creating ? " disabled" : ""}>${state.loadingGoods ? "加载中..." : "加载商品"}</button>
            <button class="btn primary" data-action="create-plan" type="button"${state.creating || !selectedCount ? " disabled" : ""}>${state.creating ? "创建中..." : "批量创建计划"}</button>
            <button class="btn danger" data-action="stop" type="button"${state.loadingGoods || state.creating ? "" : " disabled"}>停止</button>
          </div>
          <div class="status${statusClass}" aria-live="${state.messageType === "error" ? "assertive" : "polite"}"${state.messageType === "error" ? ' role="alert"' : ""}>${escapeHtml(state.message || "建议先少量选择商品测试创建，确认账户预算和ROI设置正确。")}</div>
        </aside>
        <main class="card content">
          <div class="toolbar">
            <div class="summary">
              <span>商品：<b>${state.goods.length}</b></span>
              <span>已选：<b data-selected-count>${selectedCount}</b></span>
              <span>失败/过滤：<b>${state.failedGoods.length}</b></span>
              ${hasMaterialSide ? `<span>已绑素材：<b>${materialStats.products}</b> 个商品 / <b>${materialStats.files}</b> 个文件</span>` : ""}
            </div>
            <div class="actions">
              <button class="btn" data-action="select-all" type="button"${state.goods.length ? "" : " disabled"}>全选</button>
              <button class="btn" data-action="select-none" type="button"${state.goods.length ? "" : " disabled"}>取消选择</button>
              <button class="btn" data-action="clear-goods" type="button"${state.goods.length || state.failedGoods.length ? "" : " disabled"}>清空</button>
            </div>
          </div>
          ${state.failedGoods.length ? `<div class="failBox">${escapeHtml(state.failedGoods.slice(0, 5).map((item) => `${item.id || item.good_id || "-"}：${item.reason || "不可投放"}`).join("；"))}${state.failedGoods.length > 5 ? " ..." : ""}</div>` : ""}
          <div class="tableWrap" data-scroll-key="create-goods-table">
            ${state.goods.length ? `
              <table class="goodsTable${hasMaterialSide ? " compact" : ""}">
                <colgroup>
                  <col class="colSelect">
                  <col class="colImage">
                  <col class="colProduct">
                  <col class="colId">
                  <col class="colPrice">
                  <col class="colSales">
                  <col class="colCategory">
                  ${hasMaterialSide ? `<col class="colMaterial">` : ""}
                  <col class="colResult">
                </colgroup>
                <thead>
                  <tr>
                    <th>选择</th>
                    <th>图</th>
                    <th>商品</th>
                    <th>商品ID</th>
                    <th>价格</th>
                    <th>销量</th>
                    <th>类目</th>
                    ${hasMaterialSide ? `<th>素材</th>` : ""}
                    <th>结果</th>
                  </tr>
                </thead>
                <tbody>${goodsRows}</tbody>
              </table>
            ` : `<div class="empty">暂无商品。先点击“加载商品”，或选择“导入商品ID”。</div>`}
          </div>
        </main>
        ${hasMaterialSide ? materialSideHtml() : ""}
      </div>
    `;
  }

  function goodsRowHtml(goods) {
    const id = String(goods.id || "");
    const checked = state.selected.has(id) ? " checked" : "";
    const price = formatMoney(Number(goods.priceLower || goods.min_price || goods.price || 0) / 100);
    const sell = goods.sellNum ?? goods.sell_num ?? "-";
    const category = goods.categoryDetail?.firstCname || goods.thirdCname || goods.categoryName || "-";
    const result = goods.__dealText || "-";
    const binding = state.materialBindings[id] || null;
    const fileCount = binding?.files?.length || 0;
    const titleCount = countFilledTitles(binding);
    const issueState = materialBindingIssueState(binding);
    const active = state.form.useCustomMaterials === true && state.activeMaterialGoodsId === id;
    return `
      <tr class="goodsRow${active ? " active" : ""}${issueState.total ? " hasIssue" : ""}" data-row-goods-id="${escapeHtml(id)}">
        <td><input data-goods-id="${escapeHtml(id)}" type="checkbox"${checked}></td>
        <td><img class="thumb" src="${escapeAttribute(goods.mainImageUrl || goods.img || "")}" alt=""></td>
        <td><div class="goodsName" title="${escapeAttribute(goods.name || goods.title || "")}">${escapeHtml(goods.name || goods.title || "-")}</div></td>
        <td><span class="muted">${escapeHtml(id)}</span></td>
        <td>${escapeHtml(price)}</td>
        <td>${escapeHtml(sell)}</td>
        <td>${escapeHtml(category)}</td>
        ${state.form.useCustomMaterials ? `
          <td>
            <button class="materialChip${issueState.total ? " warn" : ""}" data-action="open-material" data-material-goods-id="${escapeHtml(id)}" type="button">
              ${fileCount} 素材 · ${titleCount} 标题${issueState.total ? ` · ${issueState.total} 异常` : ""}
            </button>
          </td>
        ` : ""}
        <td><span class="tag">${escapeHtml(result)}</span></td>
      </tr>
    `;
  }

  function activeMaterialGoods() {
    const id = String(state.activeMaterialGoodsId || "");
    if (!id) return null;
    return state.goods.find((item) => String(item.id || "") === id) || null;
  }

  function materialSideHtml() {
    const goods = activeMaterialGoods();
    if (!state.goods.length) {
      return `
        <aside class="card materialSide" data-scroll-key="create-material-side">
          <div class="materialSideEmpty">
            <div>
              <div class="materialPanelTitle">商品素材配置</div>
              <div class="materialPanelHint" style="margin-top:6px">点击商品行配置素材。</div>
            </div>
          </div>
        </aside>
      `;
    }
    if (!goods) {
      return `
        <aside class="card materialSide" data-scroll-key="create-material-side">
          <div class="materialSideEmpty">
            <div>
              <div class="materialPanelTitle">选择一个商品</div>
              <div class="materialPanelHint" style="margin-top:6px">点击商品行开始配置。</div>
            </div>
          </div>
        </aside>
      `;
    }
    const id = String(goods.id || "");
    const binding = ensureMaterialBinding(id);
    const files = Array.isArray(binding?.files) ? binding.files : [];
    const videoFiles = files.filter((item) => materialKind(item) === "video");
    const imageFiles = files.filter((item) => materialKind(item) === "image");
    const titles = normalizedTitles(binding);
    const titleStats = materialTitleStats(binding);
    const productCardTitle = effectiveProductCardTitle(binding, goods);
    const productCardTitleStatus = productCardTitleValidation(productCardTitle, imageFiles.length > 0);
    const videoErrorCount = videoFiles.filter((item) => item.status === "error").length;
    const imageErrorCount = imageFiles.filter((item) => item.status === "error").length;
    const submitIssue = binding?.submitIssue || null;
    return `
      <aside class="card materialSide" data-scroll-key="create-material-side" aria-label="商品素材配置">
        <div class="materialPanelHead">
          <div>
            <div class="materialPanelTitle">商品素材配置</div>
            <div class="materialPanelHint">可多选，后台 ${MAX_UPLOAD_BATCH_SIZE} 条/批。</div>
          </div>
          <button class="tinyBtn" data-action="close-material-side" type="button">关闭</button>
        </div>
        <div class="sideProduct">
          <img class="thumb" src="${escapeAttribute(goods.mainImageUrl || goods.img || "")}" alt="">
          <div>
            <div class="sideProductName" title="${escapeAttribute(goods.name || goods.title || "")}">${escapeHtml(goods.name || goods.title || "-")}</div>
            <div class="materialPanelHint">ID：${escapeHtml(id)}</div>
          </div>
        </div>
        <div class="sideMetrics">
          <div class="sideMetric${videoErrorCount || submitIssue?.kind === "video" ? " warn" : ""}"><b>${videoFiles.length}</b>${videoErrorCount ? `${videoErrorCount} 个异常` : "本地视频"}</div>
          <div class="sideMetric${imageErrorCount || submitIssue?.kind === "image" ? " warn" : ""}"><b>${imageFiles.length}</b>${imageErrorCount ? `${imageErrorCount} 张异常` : "本地图片"}</div>
          <div class="sideMetric${titleStats.invalid ? " warn" : ""}"><b>${titleStats.valid}</b>${titleStats.invalid ? `${titleStats.invalid} 条无效` : "有效标题"}</div>
        </div>
        <div>
          <div class="materialChannels">
            <div class="dropZone" data-video-drop-goods-id="${escapeHtml(id)}">
              <input data-video-file-goods-id="${escapeHtml(id)}" type="file" accept="video/mp4,video/quicktime,video/webm,video/*" multiple>
              <div>
                <div class="dropTitle">选择视频</div>
                <div class="materialPanelHint">拖入或点击</div>
              </div>
            </div>
            <div class="dropZone image" data-image-drop-goods-id="${escapeHtml(id)}">
              <input data-image-file-goods-id="${escapeHtml(id)}" type="file" accept="image/jpeg,image/png,.jpg,.jpeg,.png" multiple>
              <div>
                <div class="dropTitle">选择商品方图</div>
                <div class="materialPanelHint">1:1 · 600-3000px</div>
              </div>
            </div>
          </div>
          <div class="materialList" data-scroll-key="create-material-files" style="margin-top:10px">
            ${files.length ? files.map(materialFileHtml).join("") : `<div class="empty" style="padding:22px">暂无素材</div>`}
          </div>
        </div>
        ${imageFiles.length ? productCardTitleHtml(id, productCardTitle, productCardTitleStatus, submitIssue) : ""}
        <div class="titleBox">
          <div class="titleHead">
            <div class="titleHeading">
              <div class="materialPanelTitle">标题</div>
              <span class="titleRule">${MIN_TITLE_LENGTH}-${MAX_TITLE_LENGTH} 字 · 最多 ${MAX_TITLE_COUNT} 条</span>
            </div>
            <button class="tinyBtn titleAddBtn" data-action="add-title" data-material-goods-id="${escapeHtml(id)}" type="button"${titles.length >= MAX_TITLE_COUNT ? " disabled" : ""}>添加标题</button>
          </div>
          ${titleIssueSummaryHtml(id, binding)}
          <div class="titleDrop" data-title-drop-goods-id="${escapeHtml(id)}">
            <input data-title-file-goods-id="${escapeHtml(id)}" type="file" accept=".txt,text/plain" aria-label="批量导入标题文本" multiple>
            <span>拖入或选择标题文本</span>
          </div>
          ${titles.map((title, index) => titleRowHtml(id, title, index, titles.length)).join("")}
        </div>
        <div class="actions">
          <button class="tinyBtn" data-action="clear-material" data-material-goods-id="${escapeHtml(id)}" type="button">清空当前商品素材</button>
        </div>
      </aside>
    `;
  }

  function materialFileHtml(item) {
    const binding = state.materialBindings[String(item.goodsId || "")] || null;
    const kind = materialKind(item);
    const kindLabel = kind === "image" ? "图片" : "视频";
    const submitIssue = binding?.submitIssue?.kind === kind ? binding.submitIssue : null;
    const hasError = item.status === "error" || Boolean(submitIssue);
    const statusText = submitIssue
      ? `创建失败关联素材：${submitIssue.message || "请检查视频素材"}`
      : item.status === "success"
      ? "已上传"
      : item.status === "uploading"
        ? `上传中 ${Math.round(Number(item.progress) || 0)}%`
        : item.status === "error"
          ? `失败：${item.message || "请重试"}`
          : "待上传";
    return `
      <div class="materialItem${hasError ? " errorFlash" : ""}"${hasError ? ` role="alert" aria-label="异常${kindLabel} ${escapeAttribute(item.name)}"` : ""}>
        <div>
          <div class="materialName" title="${escapeAttribute(item.name)}">${escapeHtml(item.name)}</div>
          <div class="materialMeta">${kindLabel} · 批次 ${escapeHtml(item.batchNo)} · ${escapeHtml(formatFileSize(item.size))} · ${escapeHtml(statusText)}</div>
          ${hasError ? `<div class="materialItemError">异常${kindLabel}：${escapeHtml(submitIssue?.message || item.message || "上传失败，请移除或重新上传")}</div>` : ""}
        </div>
        <button class="tinyBtn" data-action="remove-material-file" data-material-goods-id="${escapeHtml(item.goodsId)}" data-file-id="${escapeHtml(item.id)}" type="button"${state.creating ? " disabled" : ""}>移除</button>
      </div>
    `;
  }

  function titleRowHtml(goodsId, title, index, total) {
    const status = titleValidation(title);
    const issueId = `title-issue-${goodsId}-${index}`;
    return `
      <div class="titleRow${status.invalid ? " invalid" : ""}" data-title-row-index="${index}">
        <div class="titleField">
          <input class="titleInput" data-title-goods-id="${escapeHtml(goodsId)}" data-title-index="${index}" minlength="${MIN_TITLE_LENGTH}" maxlength="${MAX_TITLE_LENGTH}" value="${escapeAttribute(title)}" placeholder="请输入标题或使用系统推荐标题" aria-invalid="${status.invalid ? "true" : "false"}"${status.invalid ? ` aria-describedby="${issueId}"` : ""}>
          <span class="titleCounter${status.invalid ? " invalid" : ""}" data-title-counter>${status.length}/${MAX_TITLE_LENGTH}</span>
          <div class="titleIssue" id="${issueId}" data-title-issue${status.invalid ? "" : " hidden"}>${escapeHtml(status.message)}</div>
        </div>
        <button class="tinyBtn" data-action="remove-title" data-material-goods-id="${escapeHtml(goodsId)}" data-title-index="${index}" type="button"${total <= 1 ? " disabled" : ""}>删除</button>
      </div>
    `;
  }

  function productCardTitleHtml(goodsId, title, status, submitIssue) {
    const issueId = `product-card-title-issue-${goodsId}`;
    const serverIssue = submitIssue?.kind === "card-title" ? submitIssue.message : "";
    const message = serverIssue || status.message;
    const invalid = Boolean(serverIssue) || status.invalid;
    return `
      <div class="titleBox productCardTitleBox">
        <div class="titleHead">
          <div class="titleHeading">
            <div class="materialPanelTitle">商品卡标题</div>
            <span class="titleRule">${MIN_PRODUCT_CARD_TITLE_LENGTH}-${MAX_PRODUCT_CARD_TITLE_LENGTH} 字</span>
          </div>
        </div>
        <div class="titleRow${invalid ? " invalid" : ""}">
          <div class="titleField">
            <input class="titleInput" data-product-card-title-goods-id="${escapeHtml(goodsId)}" maxlength="${MAX_PRODUCT_CARD_TITLE_LENGTH}" value="${escapeAttribute(title)}" placeholder="使用商品图片时必填，默认取商品名称" aria-invalid="${invalid ? "true" : "false"}"${invalid ? ` aria-describedby="${issueId}"` : ""}>
            <span class="titleCounter${invalid ? " invalid" : ""}" data-product-card-title-counter>${status.length}/${MAX_PRODUCT_CARD_TITLE_LENGTH}</span>
            <div class="titleIssue" id="${issueId}" data-product-card-title-issue${invalid ? "" : " hidden"}>${escapeHtml(message)}</div>
          </div>
        </div>
      </div>
    `;
  }

  function ensureMaterialBinding(goodsId) {
    const id = String(goodsId || "");
    if (!id) return null;
    if (!state.materialBindings[id]) {
      state.materialBindings[id] = {
        goodsId: id,
        files: [],
        titles: [""],
        productCardTitle: defaultProductCardTitle(id),
        updatedAt: Date.now()
      };
    }
    return state.materialBindings[id];
  }

  function normalizedTitles(binding) {
    const source = Array.isArray(binding?.titles) ? binding.titles : [""];
    const clean = source.map((item) => String(item || ""));
    return clean.length ? clean : [""];
  }

  function defaultProductCardTitle(goodsOrId) {
    const goods = typeof goodsOrId === "object" && goodsOrId
      ? goodsOrId
      : state.goods.find((item) => String(item.id || "") === String(goodsOrId || ""));
    const value = String(goods?.name || goods?.title || "")
      .replace(/[\p{Extended_Pictographic}\p{Regional_Indicator}\u{1F3FB}-\u{1F3FF}\uFE0E\uFE0F\u20E3]/gu, "")
      .replace(/[\u0000-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF\uFFFD]/gu, "")
      .replace(/\s+/g, " ")
      .trim();
    return Array.from(value).slice(0, MAX_PRODUCT_CARD_TITLE_LENGTH).join("");
  }

  function effectiveProductCardTitle(binding, goods) {
    if (binding && Object.prototype.hasOwnProperty.call(binding, "productCardTitle")) {
      return String(binding.productCardTitle || "");
    }
    return defaultProductCardTitle(goods || binding?.goodsId);
  }

  function productCardTitleValidation(value, required = true) {
    const title = String(value || "").trim();
    const length = Array.from(title).length;
    if (!length) {
      return {
        title,
        length,
        valid: !required,
        invalid: required,
        message: required ? "使用商品图片时必须填写商品卡标题" : "",
        issues: required ? ["使用商品图片时必须填写商品卡标题"] : []
      };
    }
    const issues = [];
    if (length > MAX_PRODUCT_CARD_TITLE_LENGTH) issues.push(`已超出 ${length - MAX_PRODUCT_CARD_TITLE_LENGTH} 字`);
    issues.push(...titleCharacterIssues(title));
    return { title, length, valid: issues.length === 0, invalid: issues.length > 0, message: issues.join("；"), issues };
  }

  function countFilledTitles(binding) {
    return materialTitleStats(binding).valid;
  }

  function materialTitleStats(binding) {
    return normalizedTitles(binding).reduce((stats, title) => {
      const status = titleValidation(title);
      if (status.valid) stats.valid += 1;
      else if (status.invalid) stats.invalid += 1;
      else stats.empty += 1;
      return stats;
    }, { valid: 0, invalid: 0, empty: 0 });
  }

  function invalidTitleEntries(binding) {
    return normalizedTitles(binding)
      .map((title, index) => ({ ...titleValidation(title), index }))
      .filter((item) => item.invalid);
  }

  function materialBindingIssueState(binding) {
    if (!binding) return { titles: 0, cardTitles: 0, videos: 0, images: 0, server: 0, total: 0 };
    const titles = invalidTitleEntries(binding).length;
    const videos = (binding.files || []).filter((item) => materialKind(item) === "video" && item.status === "error").length;
    const images = (binding.files || []).filter((item) => materialKind(item) === "image" && item.status === "error").length;
    const hasImages = (binding.files || []).some((item) => materialKind(item) === "image");
    const cardTitles = productCardTitleValidation(effectiveProductCardTitle(binding), hasImages).invalid ? 1 : 0;
    const submitIssue = binding.submitIssue;
    const server = submitIssue && (
      (submitIssue.kind === "title" && !titles)
      || (submitIssue.kind === "card-title" && !cardTitles)
      || (submitIssue.kind === "video" && !videos)
      || (submitIssue.kind === "image" && !images)
    ) ? 1 : 0;
    return { titles, cardTitles, videos, images, server, total: titles + cardTitles + videos + images + server };
  }

  function titleIssueSummaryHtml(goodsId, binding) {
    const entries = invalidTitleEntries(binding);
    const submitIssue = binding?.submitIssue?.kind === "title" ? binding.submitIssue : null;
    const visible = entries.length > 0 || Boolean(submitIssue);
    const list = entries.length
      ? entries.map((entry) => `
          <div class="titleIssueItem">
            <div class="titleIssueText">
              第 ${entry.index + 1} 条：${escapeHtml(entry.title || "空标题")}
              <span class="titleIssueReason">${escapeHtml(entry.message)}</span>
            </div>
            <button class="tinyBtn" data-action="focus-title-issue" data-material-goods-id="${escapeHtml(goodsId)}" data-title-index="${entry.index}" type="button">定位</button>
          </div>
        `).join("")
      : `
          <div class="titleIssueItem">
            <div class="titleIssueText">
              平台未返回具体标题序号
              <span class="titleIssueReason">请重点检查 Emoji、不可见字符及从聊天软件复制的特殊符号</span>
            </div>
          </div>
        `;
    return `
      <div class="titleIssueSummary" data-title-issue-summary="${escapeHtml(goodsId)}"${visible ? ` role="alert"` : " hidden"}>
        <div class="titleIssueHead"><span>异常标题</span><span>${entries.length || "待检查"}</span></div>
        ${submitIssue ? `<div class="titleIssueServer">${escapeHtml(submitIssue.message || "千川拒绝了当前标题")}</div>` : ""}
        <div class="titleIssueList" data-scroll-key="create-title-issues">${list}</div>
      </div>
    `;
  }

  function titleValidation(value) {
    const title = String(value || "").trim();
    const length = Array.from(title).length;
    if (!length) return { title, length, valid: false, invalid: false, message: "", issues: [] };
    const issues = [];
    if (length < MIN_TITLE_LENGTH) {
      issues.push(`至少还需 ${MIN_TITLE_LENGTH - length} 字`);
    }
    if (length > MAX_TITLE_LENGTH) {
      issues.push(`已超出 ${length - MAX_TITLE_LENGTH} 字`);
    }
    issues.push(...titleCharacterIssues(title));
    return {
      title,
      length,
      valid: issues.length === 0,
      invalid: issues.length > 0,
      message: issues.join("；"),
      issues
    };
  }

  function titleCharacterIssues(title) {
    const issues = [];
    if (/[\p{Extended_Pictographic}\p{Regional_Indicator}\u{1F3FB}-\u{1F3FF}\uFE0E\uFE0F\u20E3]/u.test(title)) {
      issues.push("包含平台不支持的 Emoji/表情符号");
    }
    if (/[\u0000-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/u.test(title)) {
      issues.push("包含不可见或控制字符");
    }
    if (/[\uD800-\uDFFF]/u.test(title.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "")) || title.includes("\uFFFD")) {
      issues.push("包含损坏或无法识别的字符");
    }
    return [...new Set(issues)];
  }

  function syncTitleValidation(node) {
    const status = titleValidation(node?.value);
    const field = node?.closest?.(".titleField");
    const row = node?.closest?.(".titleRow");
    const counter = field?.querySelector?.("[data-title-counter]");
    const issue = field?.querySelector?.("[data-title-issue]");
    node?.setAttribute?.("aria-invalid", status.invalid ? "true" : "false");
    row?.classList?.toggle?.("invalid", status.invalid);
    if (counter) {
      counter.textContent = `${status.length}/${MAX_TITLE_LENGTH}`;
      counter.classList.toggle("invalid", status.invalid);
    }
    if (issue) {
      issue.textContent = status.message;
      issue.hidden = !status.invalid;
      if (status.invalid) node?.setAttribute?.("aria-describedby", issue.id);
      else node?.removeAttribute?.("aria-describedby");
    }
    syncTitleIssueSummary(node?.dataset?.titleGoodsId);
  }

  function syncProductCardTitleValidation(node) {
    const status = productCardTitleValidation(node?.value, true);
    const field = node?.closest?.(".titleField");
    const row = node?.closest?.(".titleRow");
    const counter = field?.querySelector?.("[data-product-card-title-counter]");
    const issue = field?.querySelector?.("[data-product-card-title-issue]");
    node?.setAttribute?.("aria-invalid", status.invalid ? "true" : "false");
    row?.classList?.toggle?.("invalid", status.invalid);
    if (counter) {
      counter.textContent = `${status.length}/${MAX_PRODUCT_CARD_TITLE_LENGTH}`;
      counter.classList.toggle("invalid", status.invalid);
    }
    if (issue) {
      issue.textContent = status.message;
      issue.hidden = !status.invalid;
    }
  }

  function syncTitleIssueSummary(goodsId) {
    const id = String(goodsId || "");
    const binding = state.materialBindings[id];
    const current = Array.from(shadow.querySelectorAll("[data-title-issue-summary]")).find(
      (node) => String(node.dataset.titleIssueSummary || "") === id
    );
    if (!binding || !current) return;
    captureScrollState();
    current.outerHTML = titleIssueSummaryHtml(id, binding);
    bindTitleIssueFocusEvents();
    restoreScrollState();
  }

  function materialBindingStats() {
    const bindings = Object.values(state.materialBindings || {});
    return bindings.reduce((acc, item) => {
      const fileCount = Array.isArray(item.files) ? item.files.length : 0;
      const titleCount = countFilledTitles(item);
      if (fileCount || titleCount) acc.products += 1;
      acc.files += fileCount;
      acc.titles += titleCount;
      return acc;
    }, { products: 0, files: 0, titles: 0 });
  }

  function addMaterialFiles(goodsId, fileList, kind = "video") {
    const binding = ensureMaterialBinding(goodsId);
    if (!binding) return;
    const image = kind === "image";
    const files = Array.from(fileList || []).filter((file) => file && (image ? isImageFile(file) : isVideoFile(file)));
    if (!files.length) {
      setMessage(image ? "请选择 JPG、JPEG 或 PNG 商品方图。" : "请拖入视频文件。", "error", true);
      return;
    }
    const batchCount = Math.ceil(files.length / MAX_UPLOAD_BATCH_SIZE);
    const batchStart = state.materialBatchSerial + 1;
    state.materialBatchSerial += batchCount;
    files.forEach((file, index) => {
      binding.files.push({
        id: `m-${Date.now()}-${++state.materialSerial}`,
        goodsId: binding.goodsId,
        batchNo: batchStart + Math.floor(index / MAX_UPLOAD_BATCH_SIZE),
        name: file.name || (image ? `image-${state.materialSerial}.jpg` : `video-${state.materialSerial}.mp4`),
        kind: image ? "image" : "video",
        type: file.type || (image ? "image/jpeg" : "video/mp4"),
        size: file.size || 0,
        lastModified: file.lastModified || 0,
        file,
        status: "local",
        message: "待上传"
      });
    });
    binding.updatedAt = Date.now();
    setMessage(`已为商品 ${binding.goodsId} 绑定 ${files.length} 个${image ? "图片" : "视频"}素材，后台按 ${MAX_UPLOAD_BATCH_SIZE} 条/批处理，共 ${batchCount} 批。`, "success", true);
  }

  function materialKind(item) {
    return item?.kind === "image" ? "image" : "video";
  }

  function isVideoFile(file) {
    const type = String(file.type || "").toLowerCase();
    const name = String(file.name || "").toLowerCase();
    return type.startsWith("video/") || /\.(mp4|mov|m4v|webm|avi|mkv)$/i.test(name);
  }

  function isImageFile(file) {
    const type = String(file?.type || "").toLowerCase();
    const name = String(file?.name || "").toLowerCase();
    return ["image/jpeg", "image/png"].includes(type) || /\.(jpe?g|png)$/i.test(name);
  }

  function removeMaterialFile(goodsId, fileId) {
    const binding = ensureMaterialBinding(goodsId);
    if (!binding) return;
    binding.files = binding.files.filter((item) => item.id !== fileId);
    binding.updatedAt = Date.now();
    render();
  }

  function clearMaterialBinding(goodsId) {
    const binding = ensureMaterialBinding(goodsId);
    if (!binding) return;
    binding.files = [];
    binding.titles = [""];
    binding.productCardTitle = defaultProductCardTitle(goodsId);
    binding.submitIssue = null;
    binding.updatedAt = Date.now();
    render();
  }

  function updateMaterialTitle(goodsId, index, value) {
    const binding = ensureMaterialBinding(goodsId);
    if (!binding) return;
    const titles = normalizedTitles(binding);
    titles[index] = String(value || "");
    binding.titles = titles;
    binding.submitIssue = null;
    binding.updatedAt = Date.now();
  }

  function updateProductCardTitle(goodsId, value) {
    const binding = ensureMaterialBinding(goodsId);
    if (!binding) return;
    binding.productCardTitle = String(value || "");
    if (binding.submitIssue?.kind === "card-title") binding.submitIssue = null;
    binding.updatedAt = Date.now();
  }

  function addMaterialTitle(goodsId) {
    const binding = ensureMaterialBinding(goodsId);
    if (!binding) return;
    const titles = normalizedTitles(binding);
    if (titles.length >= MAX_TITLE_COUNT) {
      setMessage(`每个商品最多配置 ${MAX_TITLE_COUNT} 条标题（千川官方上限）。`, "error");
      return;
    }
    titles.push("");
    binding.titles = titles;
    binding.updatedAt = Date.now();
    render();
  }

  async function importMaterialTitles(goodsId, fileList) {
    const binding = ensureMaterialBinding(goodsId);
    if (!binding) return;
    const files = Array.from(fileList || []);
    if (!files.length) return;

    const textFiles = files.filter(isTextFile);
    const unsupported = files.length - textFiles.length;
    if (!textFiles.length) {
      setMessage("请选择 TXT 标题文本。", "error", true);
      return;
    }

    const reads = await Promise.all(textFiles.map(async (file) => {
      try {
        return parseImportedTitleText(await file.text());
      } catch {
        return [];
      }
    }));
    const current = normalizedTitles(binding);
    const retained = current.filter((title) => String(title || "").trim()).slice(0, MAX_TITLE_COUNT);
    const seen = new Set(retained.map(titleDedupeKey).filter(Boolean));
    const imported = [];
    let invalid = unsupported;
    let duplicate = 0;
    let overflow = Math.max(0, current.filter((title) => String(title || "").trim()).length - retained.length);

    reads.flat().forEach((title) => {
      const status = titleValidation(title);
      if (!status.valid) {
        invalid += 1;
        return;
      }
      const key = titleDedupeKey(status.title);
      if (!key || seen.has(key)) {
        duplicate += 1;
        return;
      }
      if (retained.length + imported.length >= MAX_TITLE_COUNT) {
        overflow += 1;
        return;
      }
      seen.add(key);
      imported.push(status.title);
    });

    if (!imported.length) {
      const reasons = importTitleSkipSummary({ invalid, duplicate, overflow });
      setMessage(`未导入标题${reasons ? `：${reasons}` : ""}`, "error", true);
      return;
    }

    binding.titles = [...retained, ...imported];
    binding.updatedAt = Date.now();
    const skipped = importTitleSkipSummary({ invalid, duplicate, overflow });
    setMessage(`已导入 ${imported.length} 条标题${skipped ? `，${skipped}` : ""}`, "success", true);
  }

  function isTextFile(file) {
    const type = String(file?.type || "").toLowerCase();
    const name = String(file?.name || "").toLowerCase();
    return type === "text/plain" || /\.txt$/i.test(name);
  }

  function normalizeImportedTitle(value) {
    return String(value || "")
      .replace(/[\u200B-\u200D\u2060\uFEFF]/g, "")
      .replace(/\s+/g, " ")
      .trim();
  }

  function parseImportedTitleText(value) {
    return String(value || "")
      .split(/\r?\n/)
      .map(normalizeImportedTitle)
      .filter(Boolean);
  }

  function titleDedupeKey(value) {
    return normalizeImportedTitle(value).toLocaleLowerCase();
  }

  function importTitleSkipSummary({ invalid = 0, duplicate = 0, overflow = 0 } = {}) {
    const reasons = [];
    if (invalid) reasons.push(`${invalid} 条无效`);
    if (duplicate) reasons.push(`${duplicate} 条重复`);
    if (overflow) reasons.push(`${overflow} 条超过 ${MAX_TITLE_COUNT} 条上限`);
    return reasons.length ? `跳过 ${reasons.join("、")}` : "";
  }

  function removeMaterialTitle(goodsId, index) {
    const binding = ensureMaterialBinding(goodsId);
    if (!binding) return;
    const titles = normalizedTitles(binding);
    if (titles.length <= 1) return;
    titles.splice(index, 1);
    binding.titles = titles.length ? titles : [""];
    binding.submitIssue = null;
    binding.updatedAt = Date.now();
    render();
  }

  function openMaterialSide(goodsId) {
    const binding = ensureMaterialBinding(goodsId);
    if (!binding) return;
    state.activeMaterialGoodsId = binding.goodsId;
    binding.updatedAt = Date.now();
    render();
  }

  function closeMaterialSide() {
    state.activeMaterialGoodsId = "";
    render();
  }

  function syncSelectionUi() {
    const selectedCount = state.selected.size;
    const countNode = shadow.querySelector("[data-selected-count]");
    if (countNode) countNode.textContent = String(selectedCount);
    const createButton = shadow.querySelector("[data-action='create-plan']");
    if (createButton) createButton.disabled = state.creating || selectedCount === 0;
    shadow.querySelectorAll("[data-goods-id]").forEach((node) => {
      node.checked = state.selected.has(String(node.dataset.goodsId || ""));
    });
  }

  function focusMaterialTitleIssue(goodsId, index) {
    const target = Array.from(shadow.querySelectorAll("[data-title-goods-id]")).find((node) => (
      String(node.dataset.titleGoodsId || "") === String(goodsId || "")
      && Number(node.dataset.titleIndex || 0) === Number(index)
    ));
    if (!target) return;
    target.scrollIntoView({ block: "nearest" });
    target.focus();
    target.select?.();
  }

  function bindTitleIssueFocusEvents() {
    shadow.querySelectorAll("[data-action='focus-title-issue']").forEach((node) => {
      if (node.dataset.bound === "1") return;
      node.dataset.bound = "1";
      node.addEventListener("click", () => focusMaterialTitleIssue(
        node.dataset.materialGoodsId,
        Number(node.dataset.titleIndex || 0)
      ));
    });
  }

  function revealPendingMaterialIssue() {
    const goodsId = String(state.revealMaterialIssueGoodsId || "");
    if (!goodsId) return;
    state.revealMaterialIssueGoodsId = "";
    window.setTimeout(() => {
      const summary = Array.from(shadow.querySelectorAll("[data-title-issue-summary]")).find(
        (node) => String(node.dataset.titleIssueSummary || "") === goodsId && !node.hidden
      );
      const abnormalVideo = shadow.querySelector(".materialItem.errorFlash");
      const target = summary || abnormalVideo;
      const side = target?.closest?.(".materialSide");
      if (target && side) side.scrollTop = Math.max(0, target.offsetTop - 16);
    }, 0);
  }

  function bindEvents() {
    shadow.querySelector("[data-action='close-panel']")?.addEventListener("click", closePanel);
    shadow.querySelector("[data-action='retry-access']")?.addEventListener("click", () => {
      refreshQianchuanAccess().catch(() => {});
    });
    shadow.querySelectorAll("[data-field]").forEach((node) => {
      const update = () => updateField(node);
      node.addEventListener("change", update);
      if (node.tagName === "TEXTAREA" || node.tagName === "INPUT") node.addEventListener("input", update);
    });
    shadow.querySelector("[data-action='load-goods']")?.addEventListener("click", () => loadGoods().catch((error) => setMessage(error?.message || "加载商品失败", "error")));
    shadow.querySelector("[data-action='create-plan']")?.addEventListener("click", () => createPlans().catch((error) => setMessage(error?.message || "创建计划失败", "error", true)));
    shadow.querySelector("[data-action='stop']")?.addEventListener("click", () => {
      state.stopping = true;
      setMessage("已请求停止，当前请求完成后停止。", "info", true);
    });
    shadow.querySelector("[data-action='select-all']")?.addEventListener("click", () => {
      state.goods.forEach((item) => state.selected.add(String(item.id)));
      syncSelectionUi();
    });
    shadow.querySelector("[data-action='select-none']")?.addEventListener("click", () => {
      state.selected.clear();
      syncSelectionUi();
    });
    shadow.querySelector("[data-action='clear-goods']")?.addEventListener("click", () => {
      state.goods = [];
      state.failedGoods = [];
      state.selected.clear();
      state.materialBindings = {};
      state.activeMaterialGoodsId = "";
      render();
    });
    shadow.querySelectorAll("[data-goods-id]").forEach((node) => {
      node.addEventListener("change", () => {
        const id = String(node.dataset.goodsId || "");
        if (!id) return;
        if (node.checked) state.selected.add(id);
        else state.selected.delete(id);
        syncSelectionUi();
      });
    });
    shadow.querySelectorAll("[data-row-goods-id]").forEach((node) => {
      node.addEventListener("click", (event) => {
        if (!state.form.useCustomMaterials) return;
        if (event.target?.closest?.("input,button,a,select,textarea,label")) return;
        openMaterialSide(node.dataset.rowGoodsId);
      });
    });
    shadow.querySelectorAll("[data-action='open-material']").forEach((node) => {
      node.addEventListener("click", () => openMaterialSide(node.dataset.materialGoodsId));
    });
    shadow.querySelector("[data-action='close-material-side']")?.addEventListener("click", closeMaterialSide);
    shadow.querySelectorAll("[data-video-file-goods-id]").forEach((node) => {
      node.addEventListener("change", () => {
        addMaterialFiles(node.dataset.videoFileGoodsId, node.files, "video");
        node.value = "";
      });
    });
    shadow.querySelectorAll("[data-image-file-goods-id]").forEach((node) => {
      node.addEventListener("change", () => {
        addMaterialFiles(node.dataset.imageFileGoodsId, node.files, "image");
        node.value = "";
      });
    });
    const bindMaterialDrop = (selector, dataKey, kind) => shadow.querySelectorAll(selector).forEach((node) => {
      const prevent = (event) => {
        event.preventDefault();
        event.stopPropagation();
      };
      node.addEventListener("dragenter", (event) => {
        prevent(event);
        node.classList.add("isDrag");
      });
      node.addEventListener("dragover", (event) => {
        prevent(event);
        node.classList.add("isDrag");
      });
      node.addEventListener("dragleave", (event) => {
        prevent(event);
        node.classList.remove("isDrag");
      });
      node.addEventListener("drop", (event) => {
        prevent(event);
        node.classList.remove("isDrag");
        addMaterialFiles(node.dataset[dataKey], event.dataTransfer?.files || [], kind);
      });
    });
    bindMaterialDrop("[data-video-drop-goods-id]", "videoDropGoodsId", "video");
    bindMaterialDrop("[data-image-drop-goods-id]", "imageDropGoodsId", "image");
    shadow.querySelectorAll("[data-action='remove-material-file']").forEach((node) => {
      node.addEventListener("click", () => removeMaterialFile(node.dataset.materialGoodsId, node.dataset.fileId));
    });
    shadow.querySelectorAll("[data-action='clear-material']").forEach((node) => {
      node.addEventListener("click", () => clearMaterialBinding(node.dataset.materialGoodsId));
    });
    shadow.querySelectorAll("[data-action='add-title']").forEach((node) => {
      node.addEventListener("click", () => addMaterialTitle(node.dataset.materialGoodsId));
    });
    shadow.querySelectorAll("[data-title-file-goods-id]").forEach((node) => {
      node.addEventListener("change", () => {
        importMaterialTitles(node.dataset.titleFileGoodsId, node.files);
        node.value = "";
      });
    });
    shadow.querySelectorAll("[data-title-drop-goods-id]").forEach((node) => {
      const prevent = (event) => {
        event.preventDefault();
        event.stopPropagation();
      };
      node.addEventListener("dragenter", (event) => {
        prevent(event);
        node.classList.add("isDrag");
      });
      node.addEventListener("dragover", (event) => {
        prevent(event);
        node.classList.add("isDrag");
      });
      node.addEventListener("dragleave", (event) => {
        prevent(event);
        node.classList.remove("isDrag");
      });
      node.addEventListener("drop", (event) => {
        prevent(event);
        node.classList.remove("isDrag");
        importMaterialTitles(node.dataset.titleDropGoodsId, event.dataTransfer?.files || []);
      });
    });
    shadow.querySelectorAll("[data-action='remove-title']").forEach((node) => {
      node.addEventListener("click", () => removeMaterialTitle(node.dataset.materialGoodsId, Number(node.dataset.titleIndex || 0)));
    });
    bindTitleIssueFocusEvents();
    shadow.querySelectorAll("[data-title-goods-id]").forEach((node) => {
      node.addEventListener("input", () => {
        updateMaterialTitle(node.dataset.titleGoodsId, Number(node.dataset.titleIndex || 0), node.value);
        syncTitleValidation(node);
      });
      node.addEventListener("change", render);
    });
    shadow.querySelectorAll("[data-product-card-title-goods-id]").forEach((node) => {
      node.addEventListener("input", () => {
        updateProductCardTitle(node.dataset.productCardTitleGoodsId, node.value);
        syncProductCardTitleValidation(node);
      });
      node.addEventListener("change", render);
    });
  }

  function updateField(node) {
    const field = node.dataset.field;
    if (!field) return;
    if (field === "importIds") {
      state.importIds = node.value || "";
      return;
    }
    if (node.type === "checkbox") {
      state.form[field] = node.checked === true;
      if (field === "useCustomMaterials") {
        if (!state.form.useCustomMaterials) state.activeMaterialGoodsId = "";
        render();
      }
      return;
    }
    const numericFields = new Set(["smartBidType", "budgetCost", "budgetVolume", "ecpRoi2Goal", "goodsCount", "activityIntervalSecond"]);
    state.form[field] = numericFields.has(field) ? Number(node.value) : node.value;
    persistPlanningPreference(field);
    if (field === "titlePrefix") {
      const preview = shadow.querySelector("[data-plan-name-preview]");
      if (preview) preview.textContent = makePlanNamePreview();
    }
    if (field === "smartBidType") render();
    if (field === "loadMode") render();
  }

  async function hydrateAwemeOptions() {
    if (!state.aavid) return;
    const options = [];
    try {
      const defaultUser = await apiGet("/ad/api/creation/v1/uni-prom-product/default-aweme-user", { aavid: state.aavid });
      collectAwemeOptions(defaultUser, options);
    } catch {}
    try {
      const list = await apiGet("/ad/api/creation/v1/aweme/get-aweme-list", {
        aavid: state.aavid,
        searchKeyWords: undefined,
        awemeSourceMode: "roi2",
        needSingleVideoAuth: 0,
        page: 1,
        pageSize: 100,
        needLiveLimitInfo: 0,
        needRoi2DeliveryLimit: 0,
        needCommissionGray: 0,
        needPagination: 1,
        needIsExcellentLiveRoom: 0,
        needIsAwemeC: 1,
        needAwemePostPunish: 1,
        marGoal: 1,
        needProductRoi2Info: 1,
        needProductUniPromApplyInfo: 1,
        needUniPromPredictInfo: 0,
        needCanApply: 0,
        needAllianceInfo: true,
        gfversion: GF_VERSION,
        authTypes: "1,2,3"
      });
      collectAwemeOptions(list, options);
    } catch {}
    const unique = [];
    const seen = new Set();
    for (const item of options) {
      if (!item.id || seen.has(item.id)) continue;
      seen.add(item.id);
      unique.push(item);
    }
    state.awemeOptions = unique;
    if (!state.form.awemeId && unique[0]) state.form.awemeId = unique[0].id;
    render();
  }

  function collectAwemeOptions(value, out) {
    if (!value || out.length > 200) return;
    if (Array.isArray(value)) {
      value.forEach((item) => collectAwemeOptions(item, out));
      return;
    }
    if (typeof value !== "object") return;
    const id = value.userId || value.awemeId || value.aweme_id || value.id || value.openId;
    const name = value.nickName || value.nickname || value.name || value.awemeName || value.accountName || value.userName;
    if (id && (value.hasShopPermission !== false)) out.push({ id: String(id), name: String(name || id) });
    Object.values(value).forEach((item) => {
      if (item && typeof item === "object") collectAwemeOptions(item, out);
    });
  }

  async function loadGoods() {
    await ensureQianchuanAccess(true);
    validateBaseForm(false);
    state.loadingGoods = true;
    state.stopping = false;
    state.goods = [];
    state.failedGoods = [];
    state.materialBindings = {};
    state.activeMaterialGoodsId = "";
    state.selected.clear();
    setMessage("开始加载商品...");
    try {
      if (state.form.loadMode === "specify") await loadSpecifiedGoods();
      else await loadPagedGoods();
      state.goods.forEach((item) => state.selected.add(String(item.id)));
      setMessage(`已加载 ${state.goods.length} 个可投放商品。`, "success");
    } finally {
      state.loadingGoods = false;
      state.stopping = false;
      render();
    }
  }

  async function loadPagedGoods() {
    let page = 1;
    let totalPage = 1;
    do {
      if (state.stopping) break;
      setMessage(`加载中第 ${page}${totalPage > 1 ? `/${totalPage}` : ""} 页...`);
      const data = await listProducts({
        page,
        loadMode: state.form.loadMode,
        keyword: state.form.keyword,
        sortField: state.form.loadMode === "all_sell_num" ? "sell_num" : undefined,
        sortType: state.form.loadMode === "all_sell_num" ? "desc" : undefined,
        createRoi2LimitProduct: true
      });
      addProductListInfos(data.productListInfos || []);
      totalPage = Number(data.pagination?.totalPage || totalPage || page);
      page += 1;
      await delay(200);
    } while (page <= totalPage);
  }

  async function loadSpecifiedGoods() {
    const ids = parseImportIds(state.importIds);
    if (!ids.length) throw new Error("请先输入商品ID");
    let done = 0;
    for (const group of chunk(ids, 20)) {
      if (state.stopping) break;
      setMessage(`导入中 ${done}/${ids.length}...`);
      const results = await Promise.all(group.map(async (id) => {
        try {
          const data = await listProducts({ page: 1, loadMode: "all", keyword: id, createRoi2LimitProduct: false });
          return { id, list: data.productListInfos || [] };
        } catch (error) {
          return { id, error: error?.message || "导入失败" };
        }
      }));
      results.forEach((result) => {
        if (result.error) {
          state.failedGoods.push({ id: result.id, reason: result.error });
          return;
        }
        const hit = result.list.find((item) => String(item.productInfo?.id || "") === String(result.id));
        if (hit) addProductListInfos([hit]);
        else state.failedGoods.push({ id: result.id, reason: "导入的商品ID不存在或不可投放" });
      });
      done += group.length;
      await delay(200);
    }
  }

  async function listProducts({ page, loadMode, keyword = "", sortField, sortType, createRoi2LimitProduct }) {
    const tabMap = { all: 1, all_sell_num: 1, new: 2, top: 15, search: 14, specify: 1 };
    const tab = tabMap[loadMode] || 1;
    const params = [2, 15].includes(tab)
      ? {
        keyword: keyword || "",
        cursor: (page - 1) * 20,
        tab,
        aavid: state.aavid,
        campaignTypeView: 1,
        campaignScene: 0,
        enableAutoManage: false,
        needRoi2LimitStatus: false,
        needCreateRoi2LimitStatus: true,
        adlabMode: 1,
        needShopLimitStatus: true,
        awemeId: "",
        isAwemeSingleVideoAuth: 0,
        isNeedImageAggregateProduct: true,
        useProductListInfo: true,
        needUnadvertisedDays: false,
        needRoi2ShopAuthInitStatus: true,
        gfversion: LIST_GF_VERSION
      }
      : {
        keyword: keyword || "",
        page,
        tab,
        aavid: state.aavid,
        pageSize: 30,
        sortField: sortField || "audit_time",
        sortType: sortType || "desc",
        campaignTypeView: 1,
        campaignScene: 0,
        enableAutoManage: false,
        needRoi2LimitStatus: false,
        needCreateRoi2LimitStatus: true,
        adlabMode: 1,
        needShopLimitStatus: true,
        awemeId: state.form.awemeId || "",
        isAwemeSingleVideoAuth: 0,
        isNeedImageAggregateProduct: true,
        useProductListInfo: true,
        createRoi2LimitProduct: createRoi2LimitProduct !== false,
        needUnadvertisedDays: false,
        gfversion: LIST_GF_VERSION
      };
    return apiGet("/ad/api/creation/v1/product/list-products", params);
  }

  function addProductListInfos(list) {
    const existing = new Set(state.goods.map((item) => String(item.id)));
    for (const item of list) {
      const product = item.productInfo || item;
      if (!product?.id || existing.has(String(product.id))) continue;
      existing.add(String(product.id));
      const gray = Array.isArray(item.grayOutCheckResults) ? item.grayOutCheckResults : [];
      if (gray.length) {
        state.failedGoods.push({
          id: product.id,
          good_id: product.id,
          img_url: product.mainImageUrl,
          name: product.name,
          reason: gray[0]?.desc || "不可投放"
        });
        continue;
      }
      state.goods.push({ ...product, __dealText: "-" });
    }
  }

  async function createPlans() {
    const billing = await ensureQianchuanAccess(true);
    validateBaseForm(true);
    const selectedGoods = state.goods.filter((item) => state.selected.has(String(item.id)));
    if (!selectedGoods.length) throw new Error("没有可处理的商品，请先勾选商品");
    validateCustomMaterialBindings(selectedGoods);
    if (state.form.useCustomMaterials) {
      const freeRemaining = Math.max(0, Number(billing.free_remaining || 0));
      const pointsPerLink = Math.max(1, Number(billing.video_plan_points_per_extra_link || 1));
      const projectedCharge = Math.max(0, selectedGoods.length - freeRemaining) * pointsPerLink;
      const pointsBalance = Math.max(0, Number(billing.points_balance || 0));
      if (projectedCharge > pointsBalance) {
        throw new Error(`积分不足：本次视频计划预计需 ${projectedCharge} 积分，当前剩余 ${pointsBalance} 积分`);
      }
    }
    state.creating = true;
    state.stopping = false;
    setMessage("开始创建计划...", "info", true);
    let refreshPlanList = false;
    try {
      if (state.form.useCustomMaterials) await uploadCustomMaterials(selectedGoods);
      let processed = 0;
      let succeeded = 0;
      let failed = 0;
      let chargedPoints = 0;
      let billingWarnings = 0;
      const createdIds = [];
      const groups = chunk(selectedGoods, normalizeInt(state.form.goodsCount, 1, 30));
      for (const group of groups) {
        if (state.stopping) break;
        setMessage(`创建中 ${processed}/${selectedGoods.length}...`, "info", true);
        let authorization = null;
        let createResult = null;
        let createError = null;
        try {
          const planInput = await collectPlanInput(group);
          authorization = await authorizeQianchuanTask(makeBillingRequestId(), planInput);
          if (authorization.billing) state.access.billing = authorization.billing;
          chargedPoints += Math.max(0, Number(authorization.billing?.charged_points || 0));
          const submit = authorizedPlanSubmit(authorization);
          createResult = await apiPost(submit.endpoint, submit.query, submit.body, true);
        } catch (error) {
          createError = error;
        }
        if (!createError) {
          const createIds = extractCreateIdentifiers(createResult);
          createdIds.push(...createIds);
          group.forEach((item) => {
            item.__dealText = "创建成功";
            state.selected.delete(String(item.id));
            const binding = state.materialBindings[String(item.id || "")];
            if (binding) binding.submitIssue = null;
          });
          succeeded += group.length;
          if (authorization && !(await reportQianchuanTask(authorization, "succeeded", {
            created_ids: createIds,
            resource_count: group.length
          }))) {
            billingWarnings += group.length;
          }
        } else {
          if (authorization && !(await reportQianchuanTask(authorization, "failed", {
            error: normalizeQianchuanError(createError),
            resource_count: group.length
          }))) {
            billingWarnings += group.length;
          }
          const charged = Math.max(0, Number(authorization?.billing?.charged_points || 0));
          const chargeNotice = charged
            ? `；本批任务已在发起前由服务端结算 ${charged} 积分，如需复核请联系客服`
            : "";
          const msg = `${normalizeQianchuanError(createError)}${chargeNotice}`;
          markMaterialSubmitIssue(group, msg);
          group.forEach((item) => {
            item.__dealText = "失败";
            state.failedGoods.push({ id: item.id, name: item.name, reason: msg });
          });
          failed += group.length;
        }
        processed += group.length;
        render();
        const seconds = normalizeInt(state.form.activityIntervalSecond, 0, 60);
        for (let index = 0; index < seconds && !state.stopping; index += 1) {
          setMessage(`创建中 ${processed}/${selectedGoods.length}，${seconds - index} 秒后继续...`, "info", true);
          await delay(1000);
        }
      }
      const idText = createdIds.length ? `，计划 ID：${createdIds.slice(0, 3).join("、")}` : "";
      const billingText = state.form.useCustomMaterials
        ? `，本次扣除 ${chargedPoints} 积分${billingWarnings ? `，${billingWarnings} 条结果回执待同步` : ""}`
        : "";
      if (state.stopping) {
        setMessage(`已停止：成功 ${succeeded}，失败 ${failed}，已处理 ${processed}/${selectedGoods.length}${idText}${billingText}`, failed ? "error" : "info", true);
      } else {
        setMessage(`创建完成：成功 ${succeeded}，失败 ${failed}${idText}${billingText}`, failed || billingWarnings ? "error" : "success", true);
      }
      refreshPlanList = !state.stopping && succeeded > 0 && failed === 0 && billingWarnings === 0 && processed === selectedGoods.length;
    } finally {
      state.creating = false;
      state.stopping = false;
      render();
      if (refreshPlanList) {
        window.setTimeout(() => window.location.reload(), 1200);
      }
    }
  }

  function classifyMaterialSubmitIssue(message) {
    const text = String(message || "");
    if (/商品卡标题|商品图必须同时使用|product\s*card\s*title/i.test(text)) return "card-title";
    if (/错误码\s*3001|特殊字符|emoji|表情/i.test(text)) return "title";
    if (/图片|方图|尺寸|比例|image/i.test(text)) return "image";
    if (/视频|素材|video|material|转码|编码|封面/i.test(text)) return "video";
    return "";
  }

  function markMaterialSubmitIssue(goodsList, message) {
    const kind = classifyMaterialSubmitIssue(message);
    if (!["title", "card-title", "image"].includes(kind)) return;
    let firstGoodsId = "";
    for (const goods of goodsList || []) {
      const goodsId = String(goods?.id || "");
      const binding = state.materialBindings[goodsId];
      if (!binding) continue;
      binding.submitIssue = {
        kind,
        message: String(message || "素材提交失败"),
        createdAt: Date.now()
      };
      firstGoodsId ||= goodsId;
    }
    if (!firstGoodsId) return;
    state.activeMaterialGoodsId = firstGoodsId;
    state.revealMaterialIssueGoodsId = firstGoodsId;
  }

  function makeBillingRequestId() {
    return `qc-plan-${Date.now()}-${globalThis.crypto?.randomUUID?.() || Math.random().toString(36).slice(2, 12)}`;
  }

  async function authorizeQianchuanTask(requestId, planInput) {
    return billingRequest("authorize", {
      task_id: requestId,
      task_payload: planInput
    });
  }

  function authorizedPlanSubmit(authorization) {
    const action = authorization?.action;
    const submit = action?.kind === "qianchuan_create_plan" ? action.submit : null;
    if (!submit || action.input_digest !== authorization.request_digest) {
      throw new Error("服务端未返回当前任务的计划结果，已停止提交");
    }
    const endpoint = String(submit.endpoint || "");
    const query = submit.query && typeof submit.query === "object" ? submit.query : null;
    const body = submit.body && typeof submit.body === "object" ? submit.body : null;
    if (endpoint !== "/ad/api/creation/v1/ad/create" || !query || !body) {
      throw new Error("服务端返回的千川计划结果无效，已停止提交");
    }
    if (String(query.aavid || "") !== String(state.aavid || "")) {
      throw new Error("千川计划结果与当前账号不匹配，已停止提交");
    }
    return { endpoint, query, body };
  }

  async function reportQianchuanTask(authorization, resultStatus, resultPayload) {
    if (!authorization?.task_id || !authorization?.request_digest) return false;
    try {
      await billingRequest("report", {
        task_id: authorization.task_id,
        request_digest: authorization.request_digest,
        result_status: resultStatus,
        result_payload: resultPayload
      });
      return true;
    } catch {
      return false;
    }
  }

  async function collectPlanInput(goods) {
    const cardDefaults = await getPromotionCardDefaults().catch(() => ({}));
    return {
      account_id: String(state.aavid || ""),
      aweme: {
        id: String(state.form.awemeId || ""),
        name: selectedAwemeName()
      },
      settings: {
        smart_bid_type: Number(state.form.smartBidType),
        budget_yuan: activeBudget(),
        roi_goal: Number(state.form.ecpRoi2Goal),
        plan_name_prefix: String(state.form.titlePrefix || ""),
        use_custom_materials: state.form.useCustomMaterials === true,
        enable_aigc_creative: state.form.enableAigcCreative === true
      },
      promotion_card_defaults: cardDefaults,
      goods: goods.map(collectGoodsInput)
    };
  }

  function collectGoodsInput(item) {
    const binding = state.materialBindings[String(item.id || "")];
    const videos = state.form.useCustomMaterials
      ? (binding?.files || [])
        .filter((entry) => materialKind(entry) === "video" && entry.status === "success" && entry.videoInfo?.videoId)
        .map((entry) => collectUploadedVideoInput(entry.videoInfo))
      : [];
    const images = state.form.useCustomMaterials
      ? (binding?.files || [])
        .filter((entry) => materialKind(entry) === "image" && entry.status === "success" && entry.imageInfo?.materialId)
        .map((entry) => collectUploadedImageInput(entry.imageInfo))
      : [];
    const titles = state.form.useCustomMaterials
      ? normalizedTitles(binding).map((title) => titleValidation(title)).filter((entry) => entry.valid).map((entry) => entry.title).slice(0, MAX_TITLE_COUNT)
      : [];
    const productCardTitle = productCardTitleValidation(effectiveProductCardTitle(binding, item), images.length > 0);
    const productCardTitles = state.form.useCustomMaterials && images.length > 0 && productCardTitle.valid
      ? [productCardTitle.title]
      : [];
    return {
      product_id: String(item.id || ""),
      tags: {
        product_new: Boolean(item.isProductNew),
        blue_sea: Boolean(item.isBlueSea),
        cai_xi: Boolean(item.doudianInPool),
        mall_recommended: Boolean(item.isRecommended),
        high_sales_potential: Boolean(item.isHighSalesPotential),
        key_explosive_pool: Boolean(item.isKeyExplosivePool),
        discounted_pool: Boolean(item.isDiscountedPool),
        price_level: item.priceLevel
      },
      product_tag_list: Array.isArray(item.ecpProductTagList) ? item.ecpProductTagList.slice(0, 60) : [],
      image: {
        uri: String(item.image?.uri || item.imageUri || ""),
        url: String(item.mainImageUrl || "")
      },
      materials: { videos, images, titles, product_card_titles: productCardTitles }
    };
  }

  function collectUploadedVideoInput(videoInfo) {
    const imageInfo = videoInfo?.imageInfo || {};
    return {
      video_id: String(videoInfo?.videoId || ""),
      material_id: String(videoInfo?.materialId || videoInfo?.videoId || ""),
      image_mode: videoInfo?.imageMode,
      play_url: String(videoInfo?.playUrl || ""),
      cover: {
        uri: String(imageInfo.webUri || ""),
        url: String(imageInfo.imageUrl || ""),
        height: Number(imageInfo.height) || 0,
        width: Number(imageInfo.width) || 0
      }
    };
  }

  function collectUploadedImageInput(imageInfo) {
    return {
      material_id: String(imageInfo?.materialId || ""),
      image_mode: Number(imageInfo?.imageMode) || 155,
      images: [{
        uri: String(imageInfo?.webUri || ""),
        url: String(imageInfo?.imageUrl || ""),
        height: Number(imageInfo?.height) || 0,
        width: Number(imageInfo?.width) || 0
      }]
    };
  }

  function selectedAwemeName() {
    const selected = state.awemeOptions.find((item) => String(item.id) === String(state.form.awemeId || ""));
    return selected?.name || state.form.creativeSource || "";
  }

  async function uploadCustomMaterials(selectedGoods) {
    const files = selectedGoods.flatMap((goods) => {
      const binding = state.materialBindings[String(goods.id || "")];
      return binding?.files || [];
    });
    const pending = files.filter((item) => !materialUploadComplete(item));
    if (!pending.length) return;

    let completed = 0;
    const failedUploads = [];
    for (const kind of ["video", "image"]) {
      const kindItems = pending.filter((item) => materialKind(item) === kind);
      if (!kindItems.length) continue;
      const kindLabel = kind === "image" ? "图片" : "视频";
      const uploader = kind === "image"
        ? window.MMW_QIANCHUAN_IMAGE_UPLOADER
        : window.MMW_QIANCHUAN_MATERIAL_UPLOADER;
      if (!uploader?.uploadBatch) throw new Error(`千川${kindLabel}上传组件未就绪，请刷新页面后重试`);
      for (let offset = 0; offset < kindItems.length; offset += MAX_UPLOAD_BATCH_SIZE) {
        if (state.stopping) break;
        const batch = kindItems.slice(offset, offset + MAX_UPLOAD_BATCH_SIZE);
        const batchNo = Math.floor(offset / MAX_UPLOAD_BATCH_SIZE) + 1;
        const batchTotal = Math.ceil(kindItems.length / MAX_UPLOAD_BATCH_SIZE);
        batch.forEach((item) => {
          item.status = "uploading";
          item.progress = 0;
          item.message = "上传中";
        });
        setMessage(`正在上传${kindLabel}：第 ${batchNo}/${batchTotal} 批，已处理 ${completed + failedUploads.length}/${pending.length}${failedUploads.length ? `，失败 ${failedUploads.length}` : ""}`, "info", true);
        let results;
        try {
          results = await uploader.uploadBatch(batch, {
            aavid: state.aavid,
            onProgress(item, progress) {
              item.status = "uploading";
              item.progress = progress;
            },
            onStall(item, message) {
              item.status = "error";
              item.message = message;
              setMessage(`${message}；其他素材会继续处理，已上传成功的素材会保留。`, "error", true);
            }
          });
        } catch (error) {
          results = batch.map(() => ({ status: "rejected", reason: error }));
        }
        batch.forEach((item, index) => {
          const result = results[index];
          if (result?.status === "fulfilled") {
            if (kind === "image") item.imageInfo = result.value;
            else item.videoInfo = result.value;
            item.status = "success";
            item.progress = 100;
            item.message = "已上传";
            completed += 1;
            return;
          }
          const error = result?.reason || new Error("上传失败");
          item.status = "error";
          item.message = error?.message || "上传失败";
          failedUploads.push({ item, error });
        });
        const failedGoodsId = String(batch.find((item) => item.status === "error")?.goodsId || "");
        if (failedGoodsId) {
          state.activeMaterialGoodsId = failedGoodsId;
          state.revealMaterialIssueGoodsId = failedGoodsId;
        }
        render();
        if (batch.some((item) => item.status === "error")) {
          setMessage(`${kindLabel}第 ${batchNo}/${batchTotal} 批存在失败项，已继续处理下一批；累计成功 ${completed}，失败 ${failedUploads.length}。`, "error", true);
        }
      }
    }
    if (state.stopping) {
      throw new Error(`素材上传已停止：成功 ${completed}，失败 ${failedUploads.length}，未处理 ${pending.length - completed - failedUploads.length}`);
    }
    if (failedUploads.length) {
      const first = failedUploads[0];
      const failedGoodsId = String(first.item?.goodsId || "");
      if (failedGoodsId) {
        state.activeMaterialGoodsId = failedGoodsId;
        state.revealMaterialIssueGoodsId = failedGoodsId;
      }
      throw new Error(`素材上传结束：成功 ${completed}，失败 ${failedUploads.length}；其余素材已继续处理。首个失败：${first.item?.name || "素材"}，${first.error?.message || "请重试"}`);
    }
    setMessage(`素材上传完成：${completed}/${pending.length}`, "success", true);
  }

  function materialUploadComplete(item) {
    if (item?.status !== "success") return false;
    return materialKind(item) === "image"
      ? Boolean(item.imageInfo?.materialId)
      : Boolean(item.videoInfo?.videoId);
  }

  async function getPromotionCardDefaults() {
    const link = await apiGet("/ad/api/creation/v1/config-manage/get-oeconfig-creation-link", { aavid: state.aavid });
    const creationUrl = link?.cdnLink?.creation;
    if (!creationUrl) return {};
    const text = await fetch(creationUrl, { credentials: "include" }).then((res) => res.text());
    const json = JSON.parse(text);
    const feed = json.formatConfigData?.find((item) => item.key === "feedVideoAuto");
    const creative = feed?.moduleList?.find((item) => item.key === "Creative");
    const procedure = creative?.fieldList?.find((item) => item.key === "procedureCreative");
    const defaults = JSON.parse(procedure?.defaultValue || "{}");
    return defaults.promotionCard || {};
  }

  async function apiGet(path, params = {}) {
    return apiRequest("GET", path, params);
  }

  async function apiPost(path, params = {}, body, returnAll = false) {
    return apiRequest("POST", path, params, body, returnAll);
  }

  async function apiRequest(method, path, params = {}, body, returnAll = false) {
    const url = new URL(path, API_BASE);
    Object.entries(params || {}).forEach(([key, value]) => {
      if (value !== undefined && value !== null && value !== "") url.searchParams.set(key, String(value));
    });
    const response = await fetch(url.toString(), {
      method,
      credentials: "include",
      headers: body ? { "content-type": "application/json" } : undefined,
      body: body ? JSON.stringify(body) : undefined
    });
    const text = await response.text();
    if (text.trim() === "Too Many Requests") throw new Error("请求过于频繁，千川暂时限制了创建活动计划，请稍后再试。");
    let json = null;
    try {
      json = text ? JSON.parse(text) : null;
    } catch {
      if (!response.ok) throw new Error(text.slice(0, 200) || response.statusText || "接口请求失败");
      return text;
    }
    if (!response.ok) throw new Error(json?.message || json?.msg || response.statusText || "接口请求失败");
    if (json && typeof json === "object" && "status_code" in json) {
      const statusCode = Number(json.status_code);
      if (statusCode !== 0) throw new Error(formatQianchuanApiError(json, statusCode));
      return returnAll ? json : (json.data ?? json);
    }
    if (json && typeof json === "object" && "code" in json) {
      const code = Number(json.code);
      if (![0, 1].includes(code)) throw new Error(formatQianchuanApiError(json, code));
      return returnAll ? json : (json.data ?? json);
    }
    return returnAll ? json : (json?.data ?? json);
  }

  function formatQianchuanApiError(json, code) {
    const message = json?.status_message
      || json?.message
      || json?.msg
      || json?.data?.status_message
      || json?.data?.message
      || json?.data?.msg
      || "千川接口返回异常";
    return Number.isFinite(Number(code)) ? `${message}（错误码 ${code}）` : message;
  }

  function extractCreateIdentifiers(response) {
    const data = response?.data && typeof response.data === "object" ? response.data : {};
    const values = [
      data.orderId,
      data.orderID,
      data.order_id,
      data.adId,
      data.adID,
      data.ad_id,
      data.taskId,
      data.task_id,
      data.orderIds,
      data.adIds,
      data.ids
    ].flatMap((value) => Array.isArray(value) ? value : [value]);
    return [...new Set(values
      .filter((value) => ["string", "number", "bigint"].includes(typeof value))
      .map((value) => String(value).trim())
      .filter(Boolean))];
  }

  function validateBaseForm(forCreate) {
    state.aavid = getAavid();
    if (!state.aavid) throw new Error("未获取到千川账户 ID，请刷新千川页面后重试");
    const smartBidType = Number(state.form.smartBidType);
    const budget = activeBudget();
    if (!Number.isFinite(budget) || budget < 300) throw new Error("日预算不能低于 300 元");
    if (forCreate && smartBidType === BID_COST_CONTROL) {
      const roi = Number(state.form.ecpRoi2Goal);
      if (!Number.isFinite(roi) || roi < 0.01 || roi > 40) throw new Error("ROI 目标范围为 0.01 ~ 40");
    }
    const rawPrefix = String(state.form.titlePrefix || "").trim();
    const prefixIssues = titleCharacterIssues(rawPrefix);
    if (prefixIssues.length) throw new Error(`自定义计划名前缀无效：${prefixIssues.join("；")}`);
    state.form.titlePrefix = normalizePlanNamePrefix(rawPrefix);
    const count = normalizeInt(state.form.goodsCount, 1, 30);
    state.form.goodsCount = count;
  }

  function validateCustomMaterialBindings(selectedGoods) {
    if (!state.form.useCustomMaterials) return;
    const missing = [];
    const invalidTitles = [];
    const invalidProductCardTitles = [];
    let firstInvalidTitleGoodsId = "";
    let firstInvalidProductCardGoodsId = "";
    for (const goods of selectedGoods) {
      const id = String(goods.id || "");
      const binding = state.materialBindings[id];
      const fileCount = Array.isArray(binding?.files) ? binding.files.length : 0;
      const imageCount = (binding?.files || []).filter((item) => materialKind(item) === "image").length;
      const titleCount = countFilledTitles(binding);
      normalizedTitles(binding).forEach((title, index) => {
        const status = titleValidation(title);
        if (status.invalid) {
          invalidTitles.push(`${goods.name || goods.title || id}（第 ${index + 1} 条：${status.message}）`);
          firstInvalidTitleGoodsId ||= id;
        }
      });
      if (imageCount) {
        const cardTitleStatus = productCardTitleValidation(effectiveProductCardTitle(binding, goods), true);
        if (!cardTitleStatus.valid) {
          invalidProductCardTitles.push(`${goods.name || goods.title || id}（${cardTitleStatus.message}）`);
          firstInvalidProductCardGoodsId ||= id;
        }
      }
      if (!fileCount || !titleCount) {
        missing.push(`${goods.name || goods.title || id}（${fileCount}素材/${titleCount}标题）`);
      }
    }
    if (invalidProductCardTitles.length) {
      if (firstInvalidProductCardGoodsId) {
        const binding = state.materialBindings[firstInvalidProductCardGoodsId];
        if (binding) {
          binding.submitIssue = {
            kind: "card-title",
            message: "商品图片必须同时配置有效的商品卡标题",
            createdAt: Date.now()
          };
        }
        state.activeMaterialGoodsId = firstInvalidProductCardGoodsId;
        state.revealMaterialIssueGoodsId = firstInvalidProductCardGoodsId;
      }
      throw new Error(`商品卡标题未配置完整：${invalidProductCardTitles.slice(0, 3).join("；")}${invalidProductCardTitles.length > 3 ? " ..." : ""}`);
    }
    if (invalidTitles.length) {
      if (firstInvalidTitleGoodsId) {
        const binding = state.materialBindings[firstInvalidTitleGoodsId];
        if (binding) {
          binding.submitIssue = {
            kind: "title",
            message: "提交前检测到异常标题，请根据异常标题栏逐条修正",
            createdAt: Date.now()
          };
        }
        state.activeMaterialGoodsId = firstInvalidTitleGoodsId;
        state.revealMaterialIssueGoodsId = firstInvalidTitleGoodsId;
      }
      throw new Error(`检测到异常标题：${invalidTitles.slice(0, 3).join("；")}${invalidTitles.length > 3 ? " ..." : ""}`);
    }
    if (missing.length) {
      throw new Error(`自选素材未配置完整：${missing.slice(0, 3).join("；")}${missing.length > 3 ? " ..." : ""}`);
    }
  }

  function activeBudget() {
    return Number(state.form.smartBidType) === BID_NO_COST_CONTROL ? Number(state.form.budgetVolume) : Number(state.form.budgetCost);
  }

  function makePlanNamePreview(date = new Date()) {
    const prefix = normalizePlanNamePrefix(state.form.titlePrefix) || "MMWGZT快速计划-";
    const pad = (num) => String(num).padStart(2, "0");
    return `${prefix}${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
  }

  function normalizePlanNamePrefix(value) {
    return String(value || "")
      .replace(/\s+/g, " ")
      .trim()
      .slice(0, 24);
  }

  function randomTitlePrefix() {
    const chars = "全域推广热门推荐精选好流量加持多渠道曝光精准触达高效引流实力好货平台扶持智能投放站内流量好评热卖";
    return Array.from({ length: 5 }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
  }

  function parseImportIds(text) {
    return [...new Set(String(text || "").split(/[\s,，;；]+/).map((item) => item.trim()).filter(Boolean))];
  }

  function normalizeInt(value, min, max) {
    const num = Math.floor(Number(value));
    if (!Number.isFinite(num)) return min;
    return Math.min(max, Math.max(min, num));
  }

  function normalizeQianchuanError(error) {
    const msg = error?.message || String(error || "");
    if (msg.includes("Too Many Requests")) return "请求过于频繁，请稍后再试";
    return msg || "创建失败";
  }

  function chunk(list, size) {
    const result = [];
    for (let index = 0; index < list.length; index += size) result.push(list.slice(index, index + size));
    return result;
  }

  function delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  function formatMoney(value) {
    if (!Number.isFinite(value) || value <= 0) return "-";
    return value.toFixed(2).replace(/\.00$/, "");
  }

  function formatFileSize(bytes) {
    const value = Number(bytes || 0);
    if (!Number.isFinite(value) || value <= 0) return "0 B";
    const units = ["B", "KB", "MB", "GB"];
    let size = value;
    let unitIndex = 0;
    while (size >= 1024 && unitIndex < units.length - 1) {
      size /= 1024;
      unitIndex += 1;
    }
    return `${size >= 10 || unitIndex === 0 ? size.toFixed(0) : size.toFixed(1)} ${units[unitIndex]}`;
  }

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

  function escapeAttribute(value) {
    return escapeHtml(value).replace(/`/g, "&#96;");
  }
})();
