    window.__formalBootErrors = [];
    window.addEventListener("error", event => {
      const errorType = event?.error?.name || "JavaScriptError";
      const record = { type:errorType, line:Number(event?.lineno || 0), column:Number(event?.colno || 0) };
      window.__formalBootErrors.push(record);
      const message = document.getElementById("authMessage");
      if (message && typeof window.submitFormalAuth !== "function") {
        message.textContent = `登录界面没有加载完整（${errorType}），请重新打开软件。`;
      }
    });
    (function () {
      const apiBase = "";
      const isStaticFileMode = window.location.protocol === "file:";
      const desktopAppUrl = "http://127.0.0.1:8765/";
      async function leaveStaticPreview() {
        try {
          await fetch(desktopAppUrl + "api/health", { mode: "no-cors", cache: "no-store" });
          window.location.replace(desktopAppUrl);
        } catch (_) {
          notify("这是开发预览页，不会写入正式聊天记录。请从桌面打开“将莱数字员工”。");
          renderFormalStatus("开发预览页 · 请从桌面打开将莱数字员工正式版");
        }
      }
      if (!isStaticFileMode && "serviceWorker" in navigator) {
        navigator.serviceWorker.register("/sw.js").catch(() => {});
        navigator.serviceWorker.addEventListener("message", event => { if (event.data?.type === "OPEN_REMINDERS") showPage("提醒中心"); });
      }
      let apiToken = "";
      try {
        apiToken = window.localStorage?.getItem?.("formal_api_token") || "";
      } catch (_) {}
      window.__currentFormalApiToken = () => String(apiToken || "");
      let formalTaskLoadRequestId = 0;
      let formalTaskMutationGeneration = 0;
      window.__formalTaskCacheCompanyId = "";
      let uploadedFileIds = [];
      let uploadedHomeFileRecords = [];
      let latestUploadPromise = null;
      let latestAnalysisPromise = null;
      let latestAnalysisSignature = "";
      let lastAnalysisData = null;
      let lastAnalysisSignature = "";
      const homeSessionUploadPromises = new Map();
      const homeSessionAnalysisRequests = new Map();
      const homeSessionAnalysisCache = new Map();
      const homeSessionVisualJobs = new Map();
      const homeSessionThinkingTimers = new Map();
      let latestVectorUploadPromise = null;
      let latestVectorFileId = null;
      let latestVisualUploadPromise = null;
      let latestVisualFileId = null;
      function resetBusinessTenantRuntime() {
        formalTaskLoadRequestId += 1;
        formalTaskMutationGeneration += 1;
        window.__formalTaskCacheCompanyId = "";
        uploadedFileIds = [];
        uploadedHomeFileRecords = [];
        latestUploadPromise = null;
        latestAnalysisPromise = null;
        latestAnalysisSignature = "";
        lastAnalysisData = null;
        lastAnalysisSignature = "";
        homeSessionUploadPromises.clear();
        homeSessionAnalysisRequests.clear();
        homeSessionAnalysisCache.clear();
        homeSessionVisualJobs.clear();
        homeSessionThinkingTimers.forEach(timer => window.clearInterval(timer));
        homeSessionThinkingTimers.clear();
        latestVectorUploadPromise = null;
        latestVectorFileId = null;
        latestVisualUploadPromise = null;
        latestVisualFileId = null;
        teamCatalog = { members:[], permissions:[], can_manage:false };
        notificationPreferences = {};
        latestNotificationData = { items:[], unread:0, preferences:{}, business_sms:{} };
        customerMemoryLoadVersion += 1;
        lastPreflightUpload = null;
        detailPackageResultsBySession.forEach(packageResult => {
          if (packageResult?.previewUrl) URL.revokeObjectURL(packageResult.previewUrl);
          (packageResult?.panels || []).forEach(panel => {
            if (panel?.previewUrl) URL.revokeObjectURL(panel.previewUrl);
          });
        });
        detailPackageResultsBySession.clear();
        activeDetailPackage = null;
        currentVectorJobs = [];
        window.__latestAiAnalysis = null;
        window.__latestAiSourceText = "";
        window.__homeConversationContext = "";
        window.__activeHomePrompt = "";
        window.__homeVisiblePrompt = "";
        window.__lastHomeVisualUserPrompt = "";
        window.__homeUploadFailures = [];
        window.__homeVisualGenerating = false;
        revokeHomeUserAttachmentBlobUrls();
        const session = activeCustomerSession();
        applyHomeSessionRuntime(session);
        restoreCustomerSession(session);
        renderCustomerWorkbench();
        renderHomeCommandCenter();
      }
      window.addEventListener("jianglai:business-tenant-changed", resetBusinessTenantRuntime);
      function captureHomeSessionRuntime(session) {
        if (!session) return;
        session.pendingFileIds = [...uploadedFileIds];
        session.pendingFileRecords = uploadedHomeFileRecords.map(record => ({ ...record }));
        session.uploadFailures = Array.isArray(window.__homeUploadFailures) ? window.__homeUploadFailures.map(item => ({ ...item })) : [];
        session.conversationContext = String(window.__homeConversationContext || "");
        session.activePrompt = String(window.__activeHomePrompt || "");
        session.visiblePrompt = String(window.__homeVisiblePrompt || "");
        session.lastVisualUserPrompt = String(window.__lastHomeVisualUserPrompt || "");
      }
      function invalidateFormalTaskViews() {
        formalTaskLoadRequestId += 1;
        formalTaskMutationGeneration += 1;
        window.__formalTaskCacheCompanyId = "";
        formalTaskRecycleItems = [];
        taskRecycleItems = [];
        resetTaskDetailOutput();
        renderTaskRecycleBin();
        renderTasks();
        renderDashboard();
      }
      function applyHomeSessionRuntime(session) {
        uploadedFileIds = Array.isArray(session?.pendingFileIds) ? [...session.pendingFileIds] : [];
        uploadedHomeFileRecords = Array.isArray(session?.pendingFileRecords) ? session.pendingFileRecords.map(record => ({ ...record })) : [];
        window.__homeUploadFailures = Array.isArray(session?.uploadFailures) ? session.uploadFailures.map(item => ({ ...item })) : [];
        latestUploadPromise = session?.id ? (homeSessionUploadPromises.get(session.id) || null) : null;
        window.__homeConversationContext = String(session?.conversationContext || "");
        window.__activeHomePrompt = String(session?.activePrompt || "");
        window.__homeVisiblePrompt = String(session?.visiblePrompt || "");
        window.__lastHomeVisualUserPrompt = String(session?.lastVisualUserPrompt || "");
        window.__homeVisualGenerating = Boolean(session?.id && homeSessionVisualJobs.has(session.id));
      }
      window.__captureHomeSessionUploadState = captureHomeSessionRuntime;
      window.__applyHomeSessionUploadState = applyHomeSessionRuntime;
      window.__removeRestoredHomePendingFile = async function (fileId) {
        const id = Number(fileId || 0);
        const session = activeCustomerSession();
        if (!session || id <= 0) return;
        const record = (Array.isArray(session.pendingFileRecords) ? session.pendingFileRecords : [])
          .find(item => Number(item?.id || 0) === id);
        session.pendingFileIds = (Array.isArray(session.pendingFileIds) ? session.pendingFileIds : []).filter(item => Number(item) !== id);
        session.pendingFileRecords = (Array.isArray(session.pendingFileRecords) ? session.pendingFileRecords : []).filter(item => Number(item?.id || 0) !== id);
        uploadedFileIds = uploadedFileIds.filter(item => Number(item) !== id);
        uploadedHomeFileRecords = uploadedHomeFileRecords.filter(item => Number(item?.id || 0) !== id);
        session.updatedAt = new Date().toISOString();
        saveState();
        updateHomeFileName();
        captureActiveCustomerSession(true);
        if (!isStaticFileMode) {
          try {
            await api("/api/files/remove", { method: "POST", body: JSON.stringify({ file_id: id }) });
          } catch (err) {
            renderFormalStatus(`附件已从当前输入区移除 · ${err.message}`);
          }
        }
        notify(`已移除${record?.name ? `：${record.name}` : "这个附件"}`);
      };
      const homeUserAttachmentBlobUrls = new Set();
      const homeUserAttachmentBlobCache = new Map();
      function revokeHomeUserAttachmentBlobUrls() {
        homeUserAttachmentBlobUrls.forEach(url => URL.revokeObjectURL(url));
        homeUserAttachmentBlobUrls.clear();
        homeUserAttachmentBlobCache.clear();
      }
      function attachmentFilenameFromHeaders(response, fallback = "客户上传图片") {
        const disposition = response.headers.get("content-disposition") || "";
        const encoded = disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1];
        const simple = disposition.match(/filename="?([^";]+)"?/i)?.[1];
        try { return decodeURIComponent(encoded || simple || fallback); }
        catch (_) { return simple || fallback; }
      }
      function applyHydratedUserAttachment(button, record) {
        if (!button || !record) return false;
        button.dataset.imageName = record.name || button.dataset.imageName || "客户上传图片";
        const label = button.querySelector(".chat-user-attachment-name");
        if (label) {
          label.textContent = button.dataset.imageName;
          label.title = button.dataset.imageName;
        }
        if (!String(record.type || "").startsWith("image/") || !record.url) {
          button.classList.add("is-not-image");
          button.dataset.persistentThumb = "not-image";
          button.disabled = true;
          button.dataset.imageUrl = "";
          button.querySelector("img")?.remove();
          const placeholder = button.querySelector(".chat-user-attachment-placeholder") || document.createElement("span");
          placeholder.className = "chat-user-attachment-placeholder";
          placeholder.textContent = "文件附件";
          if (!placeholder.isConnected) button.prepend(placeholder);
          return false;
        }
        button.classList.remove("is-not-image");
        button.dataset.persistentThumb = "true";
        button.disabled = false;
        button.dataset.imageUrl = record.url;
        button.querySelector(".chat-user-attachment-placeholder")?.remove();
        let image = button.querySelector("img");
        if (!image) {
          image = document.createElement("img");
          button.prepend(image);
        }
        image.src = record.url;
        image.alt = button.dataset.imageName;
        return true;
      }
      async function hydrateHomeUserAttachmentThumbs(root = document, options = {}) {
        if (options.reset) revokeHomeUserAttachmentBlobUrls();
        const scope = root?.querySelectorAll ? root : document;
        const buttons = Array.from(scope.querySelectorAll(".chat-user-attachment[data-user-file-id]"))
          .filter(button => Number(button.dataset.userFileId || 0) > 0 && !button.classList.contains("is-not-image"));
        if (!buttons.length) return { requested: 0, hydrated: 0, failed: 0, allSucceeded: true };
        if (isStaticFileMode) return { requested: buttons.length, hydrated: 0, failed: buttons.length, allSucceeded: false };
        if (!apiToken) await ensureFormalLogin().catch(() => null);
        if (!apiToken) return { requested: buttons.length, hydrated: 0, failed: buttons.length, allSucceeded: false };
        const byId = new Map();
        buttons.forEach(button => {
          const id = Number(button.dataset.userFileId || 0);
          if (!byId.has(id)) byId.set(id, []);
          byId.get(id).push(button);
        });
        let hydrated = 0;
        let failed = 0;
        await Promise.all([...byId.entries()].map(async ([id, targets]) => {
          let record = homeUserAttachmentBlobCache.get(id);
          if (!record) {
            try {
              const response = await fetch(`/api/files/${id}/content`, {
                headers: { Authorization: `Bearer ${apiToken}` },
                cache: "no-store"
              });
              if (!response.ok) {
                failed += targets.length;
                targets.forEach(button => { button.dataset.persistentThumb = "false"; });
                return;
              }
              const blob = await response.blob();
              const fallback = targets[0]?.dataset.imageName || `历史图片 ${id}`;
              const name = attachmentFilenameFromHeaders(response, fallback);
              const type = blob.type || response.headers.get("content-type") || "application/octet-stream";
              const url = type.startsWith("image/") ? URL.createObjectURL(blob) : "";
              if (url) homeUserAttachmentBlobUrls.add(url);
              record = { id, name, type, url };
              homeUserAttachmentBlobCache.set(id, record);
            } catch (_) {
              failed += targets.length;
              targets.forEach(button => { button.dataset.persistentThumb = "false"; });
              return;
            }
          }
          targets.forEach(button => {
            if (applyHydratedUserAttachment(button, record)) hydrated += 1;
          });
        }));
        return { requested: buttons.length, hydrated, failed, allSucceeded: failed === 0 };
      }
      async function bindCurrentTurnUserAttachments(uploadRecords = [], sessionId = activeCustomerSession()?.id || "") {
        const records = (Array.isArray(uploadRecords) ? uploadRecords : [])
          .filter(record => Number(record?.id || 0) > 0 && record?.key)
          .map(record => ({ ...record, id: Number(record.id), key: String(record.key) }));
        const targetSession = homeSessionById(sessionId);
        if (targetSession) {
          targetSession.userFileIds = records.map(record => record.id);
          targetSession.userFileNames = records.map(record => record.name || "客户上传附件");
          targetSession.userFileRecords = records.map(record => ({
            id: record.id,
            key: record.key,
            name: record.name || "客户上传附件",
            mimeType: String(record.mimeType || "")
          }));
        }
        if (sessionId !== state.activeCustomerSessionId) {
          return { allAttachmentsPersistent: records.length > 0, allImagesPersistent: records.length > 0, attachmentCount: records.length, boundCount: records.length };
        }
        window.__homeCurrentTurnFileIds = records.map(record => record.id);
        window.__homeCurrentTurnFileNames = records.map(record => record.name || "客户上传附件");
        const bubble = document.getElementById("homeUserMessage");
        if (!bubble) return { allAttachmentsPersistent: false, allImagesPersistent: false, attachmentCount: 0, boundCount: 0 };
        const buttons = Array.from(bubble.querySelectorAll(".chat-user-attachment"));
        const recordsByKey = new Map(records.map(record => [record.key, record]));
        buttons.forEach(button => {
          const record = recordsByKey.get(String(button.dataset.userFileKey || ""));
          if (!record) return;
          button.dataset.userFileId = String(record.id);
          if (record.name) button.dataset.imageName = record.name;
          if (record.mimeType) button.dataset.attachmentMime = String(record.mimeType);
          const mimeType = String(record.mimeType || "").toLowerCase();
          if (mimeType && !mimeType.startsWith("image/")) {
            button.classList.add("is-not-image");
            button.dataset.persistentThumb = "not-image";
            button.disabled = true;
          }
        });
        await hydrateHomeUserAttachmentThumbs(bubble);
        const boundButtons = buttons.filter(button => Number(button.dataset.userFileId || 0) > 0);
        const allAttachmentsPersistent = buttons.length === records.length && buttons.every(button => {
          if (Number(button.dataset.userFileId || 0) <= 0) return false;
          return button.classList.contains("is-not-image")
            ? button.dataset.persistentThumb === "not-image"
            : button.dataset.persistentThumb === "true";
        });
        return {
          allAttachmentsPersistent,
          allImagesPersistent: allAttachmentsPersistent,
          attachmentCount: buttons.length,
          boundCount: boundButtons.length
        };
      }
      async function importSharedIntake() {
        if (isStaticFileMode || !new URLSearchParams(location.search).has("shared")) return;
        try {
          const response = await fetch("/_shared/meta", { cache:"no-store" });
          if (!response.ok) throw new Error("没有读取到分享内容");
          const metadata = await response.json();
          const sharedText = [metadata.title, metadata.text, metadata.url].map(item => String(item || "").trim()).filter(Boolean).join("\n");
          const input = document.getElementById("homeFiles");
          let importedFiles = 0;
          if (input && typeof DataTransfer !== "undefined") {
            const transfer = new DataTransfer();
            for (const item of (metadata.files || []).slice(0, 10)) {
              const fileResponse = await fetch(`/_shared/file-${Number(item.index)}`, { cache:"no-store" });
              if (!fileResponse.ok) continue;
              const blob = await fileResponse.blob();
              transfer.items.add(new File([blob], item.name || `客户图片-${Number(item.index) + 1}`, { type:item.type || blob.type || "application/octet-stream" }));
              importedFiles += 1;
            }
            input.files = transfer.files;
            updateHomeFileName();
          }
          const textBox = document.getElementById("aiQuestion");
          if (textBox && sharedText) {
            textBox.value = sharedText;
            autoGrowHomeQuestion(textBox);
          }
          showPage("图片制作");
          setTextIfExists("homePromptHint", `已从手机分享接收${importedFiles ? ` ${importedFiles} 个文件` : "内容"}，核对客户名称后点击发送即可。`);
          notify(`已接收手机分享${importedFiles ? `：${importedFiles} 个文件` : "内容"}，尚未发送给AI。`);
          history.replaceState({}, "", "/");
          navigator.serviceWorker?.controller?.postMessage({ type:"CLEAR_SHARED_INTAKE" });
        } catch (err) {
          notify(`手机分享接收失败：${err.message}`);
        }
      }
      async function api(path, options = {}) {
        if (isStaticFileMode) throw new Error("当前是本地预览模式，未连接正式服务");
        const headers = { "Content-Type": "application/json", ...(options.headers || {}) };
        if (apiToken) headers.Authorization = "Bearer " + apiToken;
        const controller = new AbortController();
        const timeoutMs = Number(options.timeoutMs || 120000);
        const timer = window.setTimeout(() => controller.abort(), timeoutMs);
        try {
          const { timeoutMs: _timeout, ...fetchOptions } = options;
          const res = await fetch(apiBase + path, { ...fetchOptions, headers, signal: controller.signal });
          let data = {};
          try { data = await res.json(); }
          catch { throw new Error(`服务返回了无法识别的结果（${res.status}），请稍后重试。`); }
          if (!res.ok || data.ok === false) {
            const message = data.error || (res.status >= 500 ? "服务暂时繁忙，请稍后重试。" : "请求没有完成，请检查填写内容后重试。");
            const error = new Error(message);
            error.status = res.status;
            error.retryAfter = Math.max(0, Number(data.retry_after || data.resend_after || 0));
            throw error;
          }
          if (data.membership) syncFormalMembership(data.membership);
          return data;
        } catch (err) {
          if (err?.name === "AbortError") throw new Error("AI处理超时，请稍后再试。");
          if (err instanceof TypeError || /failed to fetch|networkerror|load failed/i.test(String(err?.message || ""))) {
            throw new Error("本机服务暂时未连接，请确认软件服务已启动后重试。");
          }
          throw err;
        } finally {
          window.clearTimeout(timer);
        }
      }
      window.__jianglaiAppApi = api;
      window.__loadCustomerAssetFilesConnected = async function (customer) {
        await ensureFormalLogin();
        return api(`/api/files?customer=${encodeURIComponent(String(customer || "").trim())}`, { method:"GET" });
      };
      window.__uploadCustomerAssetFilesConnected = async function (files, customer, kind) {
        await ensureFormalLogin();
        const uploaded = [];
        for (const file of Array.from(files || [])) {
          const base64 = await fileToBase64(file);
          const data = await api("/api/files", {
            method:"POST",
            body:JSON.stringify({
              name:file.name,
              mime_type:normalizedUploadMime(file),
              base64,
              customer:String(customer || "").trim(),
              kind:String(kind || "客户文件").trim()
            })
          });
          if (data?.file) uploaded.push(data.file);
        }
        return uploaded;
      };
      window.__analyzeInquiryAttachments = async function (payload = {}) {
        await ensureFormalLogin();
        const files = Array.from(payload.files || []);
        const customer = String(payload.customer || "").trim();
        const reportProgress = update => {
          if (typeof payload.onProgress === "function") payload.onProgress(update);
        };
        const existingByKey = new Map(
          (Array.isArray(payload.existingRecords) ? payload.existingRecords : [])
            .filter(record => Number(record?.id || 0) > 0 && record?.key)
            .map(record => [String(record.key), { ...record, id:Number(record.id) }])
        );
        const uploadedRecords = [];
        for (const [index, file] of files.entries()) {
          reportProgress({
            stage:"upload",
            detail:`正在上传附件 ${index + 1}/${files.length}：${file.name || "客户附件"}`
          });
          const key = inquiryAttachmentKey(file);
          const existing = existingByKey.get(key);
          if (existing) {
            uploadedRecords.push(existing);
            continue;
          }
          if (Number(file.size || 0) > 18 * 1024 * 1024) {
            throw new Error(`${file.name || "客户附件"}超过18MB，无法上传`);
          }
          const base64 = await fileToBase64(file);
          const uploaded = await api("/api/files", {
            method:"POST",
            body:JSON.stringify({
              name:file.name || "客户附件",
              mime_type:normalizedUploadMime(file),
              base64,
              customer,
              kind:fileKind(file) === "PDF" || fileKind(file).includes("表格") ? "报价与订单" : "客户文件"
            })
          });
          if (!uploaded?.file?.id) throw new Error(`${file.name || "客户附件"}上传后没有返回文件编号`);
          uploadedRecords.push({
            id:Number(uploaded.file.id),
            key,
            name:file.name || uploaded.file.name || "客户附件",
            size:Number(file.size || uploaded.file.size || 0),
            mimeType:normalizedUploadMime(file)
          });
        }
        const hasImages = files.some(file => fileKind(file).startsWith("图片"));
        reportProgress({
          stage:"vision",
          detail:hasImages
            ? "附件已上传，视觉 AI 正在识别产品、文字、尺寸、材质和工艺。"
            : "附件已上传，AI 正在提取文件内容并理解客户要求。"
        });
        const result = await api("/api/ai/analyze", {
          method:"POST",
          timeoutMs:180000,
          body:JSON.stringify({
            text:String(payload.text || "").trim(),
            customer,
            file_ids:uploadedRecords.map(record => Number(record.id)).filter(id => id > 0),
            use_memory:true,
            analysis_mode:"standard"
          })
        });
        reportProgress({
          stage:"organize",
          detail:"识别已返回，正在整理接单字段、缺失资料和客户回复。"
        });
        return { ...result, uploadedRecords };
      };
      window.__downloadCustomerAssetFileConnected = async function (fileId, filename) {
        await ensureFormalLogin();
        const response = await fetch(`${apiBase}/api/files/${Number(fileId)}/content?download=1`, {
          headers:{ Authorization:`Bearer ${apiToken}` },
          cache:"no-store"
        });
        if (!response.ok) {
          let message = "文件下载失败";
          try { message = (await response.json())?.error || message; } catch (_) {}
          throw new Error(message);
        }
        const blob = await response.blob();
        const url = URL.createObjectURL(blob);
        const link = document.createElement("a");
        link.href = url;
        link.download = String(filename || `客户文件-${Number(fileId)}`);
        document.body.appendChild(link);
        link.click();
        link.remove();
        window.setTimeout(() => URL.revokeObjectURL(url), 1000);
      };
      window.__removeCustomerAssetFileConnected = async function (fileId) {
        await ensureFormalLogin();
        return api("/api/files/remove", { method:"POST", body:JSON.stringify({ file_id:Number(fileId) }) });
      };
      let formalAuthMode = "login";
      let formalAuthSubmitting = false;
      let formalVerificationStatus = { required:true, available:false, resend_seconds:60, code_expires_minutes:5 };
      const formalVerificationCooldowns = new Map();
      function setFormalAuthMessage(message = "") {
        const box = document.getElementById("authMessage");
        if (box) box.textContent = String(message || "");
      }
      function updateFormalVerificationUi() {
        document.getElementById("authRegisterVerificationRow")?.classList.remove("is-hidden");
      }
      async function refreshFormalVerificationStatus() {
        try {
          const result = await api("/api/auth/verification/status", { method:"GET", timeoutMs:10000 });
          formalVerificationStatus = {
            required:true,
            available:result.available === true,
            resend_seconds:Math.max(30, Number(result.resend_seconds || 60)),
            code_expires_minutes:Math.max(1, Number(result.code_expires_minutes || 5))
          };
        } catch (_) {
          formalVerificationStatus = { required:true, available:false, resend_seconds:60, code_expires_minutes:5 };
        }
        updateFormalVerificationUi();
        return formalVerificationStatus;
      }
      function startFormalVerificationCooldown(buttonId, seconds = 60) {
        const button = document.getElementById(buttonId);
        if (!button) return;
        const existing = formalVerificationCooldowns.get(buttonId);
        if (existing) window.clearInterval(existing);
        let remaining = Math.max(1, Number(seconds || 60));
        button.disabled = true;
        button.textContent = `${remaining}秒后重发`;
        const timer = window.setInterval(() => {
          remaining -= 1;
          if (remaining <= 0) {
            window.clearInterval(timer);
            formalVerificationCooldowns.delete(buttonId);
            button.disabled = false;
            button.textContent = "重新获取";
          } else button.textContent = `${remaining}秒后重发`;
        }, 1000);
        formalVerificationCooldowns.set(buttonId, timer);
      }
      window.requestFormalVerificationCode = async function (purpose) {
        const registering = purpose === "register";
        const phone = val(registering ? "authRegisterPhone" : "authResetPhone");
        const buttonId = registering ? "authRegisterCodeButton" : "authResetCodeButton";
        if (!/^1[3-9]\d{9}$/.test(phone)) return setFormalAuthMessage("请输入正确的11位中国大陆手机号");
        const button = document.getElementById(buttonId);
        if (button) button.disabled = true;
        setFormalAuthMessage("正在发送验证码…");
        try {
          const result = await api("/api/auth/verification/send", {
            method:"POST",
            body:JSON.stringify({ phone, purpose }),
            timeoutMs:15000
          });
          setFormalAuthMessage(result.message || "验证码已发送，请查看手机短信");
          startFormalVerificationCooldown(buttonId, result.resend_after || formalVerificationStatus.resend_seconds);
        } catch (err) {
          const retryAfter = Math.max(0, Number(err?.retryAfter || 0));
          if (retryAfter > 0) startFormalVerificationCooldown(buttonId, retryAfter);
          else if (button) button.disabled = false;
          setFormalAuthMessage(err.message || "验证码暂时没有发送成功");
        }
      };
      window.setFormalAuthMode = function (mode = "login") {
        formalAuthMode = mode === "register" ? "register" : mode === "reset" ? "reset" : "login";
        const registering = formalAuthMode === "register";
        const resetting = formalAuthMode === "reset";
        document.getElementById("authLoginFields")?.classList.toggle("is-hidden", registering || resetting);
        document.getElementById("authRegisterFields")?.classList.toggle("is-hidden", !registering);
        document.getElementById("authResetFields")?.classList.toggle("is-hidden", !resetting);
        document.getElementById("authLoginTab")?.classList.toggle("active", !registering && !resetting);
        document.getElementById("authRegisterTab")?.classList.toggle("active", registering);
        document.getElementById("authLoginTab")?.setAttribute("aria-selected", String(!registering && !resetting));
        document.getElementById("authRegisterTab")?.setAttribute("aria-selected", String(registering));
        setTextIfExists("authTitle", registering ? "注册工厂账号" : resetting ? "短信找回密码" : "登录工厂账号");
        setTextIfExists("authDescription", registering
          ? "使用本人手机号完成验证，创建本厂独立数据空间。"
          : resetting ? "验证注册手机号后设置新密码，原有登录设备会自动退出。"
          : "登录后继续使用本厂的报价、订单、客户和生产数据。");
        setTextIfExists("authSubmitButton", registering ? "注册并进入工作台" : resetting ? "确认重置密码" : "登录并进入工作台");
        updateFormalVerificationUi();
        setFormalAuthMessage("");
        window.setTimeout(() => document.getElementById(registering ? "authRegisterCompany" : resetting ? "authResetPhone" : "authLoginPhone")?.focus(), 50);
      };
      document.getElementById("authForgotPasswordButton")?.addEventListener("click", event => {
        event.preventDefault();
        window.setFormalAuthMode("reset");
      });
      document.getElementById("authBackToLoginButton")?.addEventListener("click", event => {
        event.preventDefault();
        window.setFormalAuthMode("login");
      });
      function showFormalAuthGate(message = "", mode = "login") {
        const gate = document.getElementById("authGate");
        if (!gate || isStaticFileMode) return;
        document.body.classList.add("auth-required");
        gate.classList.remove("is-hidden");
        gate.setAttribute("aria-hidden", "false");
        window.setFormalAuthMode(mode);
        const lastAccount = localStorage.getItem("formal_last_login_account") || "";
        if (lastAccount && !val("authLoginPhone")) setIfExists("authLoginPhone", lastAccount);
        setFormalAuthMessage(message);
        refreshFormalVerificationStatus().catch(() => null);
      }
      function hideFormalAuthGate() {
        const gate = document.getElementById("authGate");
        document.body.classList.remove("auth-required");
        gate?.classList.add("is-hidden");
        gate?.setAttribute("aria-hidden", "true");
        setFormalAuthMessage("");
      }
      async function loadSignedInWorkspace() {
        const companyId = String(window.__currentUser?.company_id || "").trim();
        if (!companyId) throw new Error("登录信息缺少工厂编号，无法隔离本机业务数据");
        window.__activateBusinessTenant?.(companyId);
        markBusinessSyncAttempt();
        const syncedDomains = [];
        const failures = [];
        const settingsResult = await Promise.allSettled([loadFormalSettings()]);
        if (settingsResult[0].status === "fulfilled") syncedDomains.push("价格设置");
        else failures.push(`价格设置：${settingsResult[0].reason?.message || "读取失败"}`);
        renderCurrentStaff();
        const results = await Promise.allSettled([
          loadIntegrationStatus(),
          loadCloudCustomers(),
          loadFormalQuotes(),
          loadFormalTasks(),
          loadTeamManagement(),
          loadNotifications(false)
        ]);
        const coreDomains = [null, "客户与聊天", "报价", "跟单", null, null];
        results.forEach((result, index) => {
          const domain = coreDomains[index];
          if (!domain) return;
          if (result.status === "fulfilled") syncedDomains.push(domain);
          else failures.push(`${domain}：${result.reason?.message || "读取失败"}`);
        });
        await selectHomeCustomerConnected().catch(err => {
          if (!failures.some(item => item.startsWith("客户与聊天："))) failures.push(`客户与聊天：${err?.message || "历史沟通读取失败"}`);
        });
        if (failures.length) {
          markBusinessSyncFailure(failures.join("；"), syncedDomains);
          renderFormalStatus(`云端部分未同步 · ${failures[0]}`);
        } else {
          markBusinessSyncSuccess(syncedDomains);
          renderFormalStatus(window.__baseServiceStatus || "AI已连接 · 云端共享");
        }
        return { ok:failures.length === 0, failures, syncedDomains };
      }
      window.retryBusinessCloudSync = async function () {
        if (!apiToken) {
          markBusinessSyncFailure("当前尚未登录正式工厂账号，请先登录后再同步。", normalizedBusinessSyncStatus().syncedDomains);
          return notify("请先在“更多 → 当前账号”登录，再重试云端同步。");
        }
        try {
          const result = await loadSignedInWorkspace();
          notify(result.ok ? "本机与云端已经同步完成。" : "部分数据仍未同步，页面已显示具体原因，可以稍后再次重试。");
        } catch (err) {
          markBusinessSyncFailure(err, normalizedBusinessSyncStatus().syncedDomains);
          notify(`同步失败：${err.message || err}`);
        }
      };
      window.submitFormalAuth = async function () {
        if (formalAuthSubmitting) return;
        const registering = formalAuthMode === "register";
        const resetting = formalAuthMode === "reset";
        let phone = "";
        let password = "";
        let payload = {};
        if (registering) {
          const company = val("authRegisterCompany");
          const name = val("authRegisterName");
          const referralCode = val("authRegisterReferralCode").replace(/[^A-Za-z0-9]/g, "").toUpperCase();
          const businessIdentity = document.querySelector('input[name="authBusinessIdentity"]:checked')?.value || "";
          phone = val("authRegisterPhone");
          password = val("authRegisterPassword");
          const confirmPassword = val("authRegisterPasswordConfirm");
          if (!company || !name || !phone) return setFormalAuthMessage("请完整填写工厂名称、负责人和登录账号");
          if (!businessIdentity) return setFormalAuthMessage("请选择接单商家、生产厂家或两种业务都有");
          if (!/^1[3-9]\d{9}$/.test(phone)) return setFormalAuthMessage("请输入正确的11位中国大陆手机号");
          if (password.length < 8) return setFormalAuthMessage("密码至少8位");
          if (password !== confirmPassword) return setFormalAuthMessage("两次输入的密码不一致");
          const verificationCode = val("authRegisterVerificationCode");
          if (!/^\d{6}$/.test(verificationCode)) return setFormalAuthMessage("请输入手机收到的6位验证码");
          if (!document.getElementById("authRegisterAgreement")?.checked) return setFormalAuthMessage("请先阅读并同意用户协议、隐私政策和AI服务说明");
          payload = {
            company, name, phone, password, verification_code:verificationCode, referral_code:referralCode,
            business_identity:businessIdentity,
            agreement_accepted:true,
            terms_version:"2026-07-22",
            privacy_version:"2026-07-22",
            ai_disclosure_version:"2026-07-22"
          };
        } else if (resetting) {
          phone = val("authResetPhone");
          const code = val("authResetVerificationCode");
          password = val("authResetPassword");
          const confirmPassword = val("authResetPasswordConfirm");
          if (!/^1[3-9]\d{9}$/.test(phone)) return setFormalAuthMessage("请输入正确的11位中国大陆手机号");
          if (!/^\d{6}$/.test(code)) return setFormalAuthMessage("请输入手机收到的6位验证码");
          if (password.length < 8) return setFormalAuthMessage("新密码至少8位");
          if (password !== confirmPassword) return setFormalAuthMessage("两次输入的新密码不一致");
          payload = { phone, code, new_password:password };
        } else {
          phone = val("authLoginPhone");
          password = val("authLoginPassword");
          if (!phone || !password) return setFormalAuthMessage("请输入账号和密码");
          payload = { phone, password };
        }
        formalAuthSubmitting = true;
        const button = document.getElementById("authSubmitButton");
        if (button) button.disabled = true;
        setFormalAuthMessage(registering ? "正在创建工厂账号…" : resetting ? "正在重置密码…" : "正在登录…");
        try {
          apiToken = "";
          if (resetting) {
            const result = await api("/api/auth/password/reset", { method:"POST", body:JSON.stringify(payload), timeoutMs:15000 });
            setIfExists("authLoginPhone", phone);
            setIfExists("authLoginPassword", "");
            window.setFormalAuthMode("login");
            setFormalAuthMessage(result.message || "密码已重置，请使用新密码登录");
            return;
          }
          let result;
          if (!registering) {
            result = await api("/api/auth/login", { method:"POST", body:JSON.stringify(payload) });
          } else {
            result = await api("/api/auth/register", { method:"POST", body:JSON.stringify(payload) });
          }
          apiToken = result.token || "";
          if (!apiToken) throw new Error("服务没有返回登录凭证");
          localStorage.setItem("formal_api_token", apiToken);
          localStorage.setItem("formal_api_bootstrapped", "1");
          localStorage.setItem("formal_last_login_account", phone);
          const me = await api("/api/me");
          window.__currentUser = me.user || null;
          window.__activateBusinessTenant?.(window.__currentUser?.company_id || "");
          hideFormalAuthGate();
          if (requireTemporaryPasswordChange()) return;
          renderFormalStatus("账号已登录 · 正在同步本厂数据");
          const syncResult = await loadSignedInWorkspace();
          showPage("工作台");
          notify(syncResult.ok
            ? (registering ? "工厂账号已创建，已进入工作台。" : "登录成功，已同步本厂数据。")
            : "账号已登录，部分云端数据暂未同步；可到“更多 → 数据与服务”查看原因并重试。");
        } catch (err) {
          apiToken = "";
          localStorage.removeItem("formal_api_token");
          window.__currentUser = null;
          window.__activateBusinessTenant?.("");
          if (registering && /短信|验证码|手机号/.test(String(err.message || ""))) {
            formalVerificationStatus.required = true;
            updateFormalVerificationUi();
          }
          setFormalAuthMessage(err.message || "登录没有完成，请重试");
        } finally {
          formalAuthSubmitting = false;
          if (button) button.disabled = false;
        }
      };
      document.getElementById("formalAuthForm")?.addEventListener("submit", event => {
        event.preventDefault();
        window.submitFormalAuth();
      });
      document.getElementById("authSubmitButton")?.addEventListener("click", event => {
        event.preventDefault();
        window.submitFormalAuth();
      });
      async function ensureFormalLogin() {
        if (isStaticFileMode) throw new Error("当前是本地预览模式，未连接正式服务");
        if (apiToken) {
          try {
            const me = await api("/api/me");
            window.__currentUser = me.user || null;
            window.__activateBusinessTenant?.(window.__currentUser?.company_id || "");
            return apiToken;
          } catch (err) {
            apiToken = "";
            localStorage.removeItem("formal_api_token");
            window.__currentUser = null;
            window.__activateBusinessTenant?.("");
            invalidateFormalTaskViews();
            if (localStorage.getItem("formal_api_bootstrapped") === "1") {
              const message = "登录已失效，请到“更多 → 当前账号”重新登录";
              showFormalAuthGate(message, "login");
              throw new Error(message);
            }
          }
        }
        const message = "请先登录或注册工厂账号";
        showFormalAuthGate(message, "login");
        throw new Error(message);
      }
      function tenantSettingsStorageKey(companyId) {
        return `jianglai_settings_company_${String(companyId || "").trim()}`;
      }
      function freshDefaultSettings() {
        return JSON.parse(JSON.stringify(defaults.settings));
      }
      async function loadFormalSettings() {
        const user = window.__currentUser;
        const companyId = String(user?.company_id || "").trim();
        if (!companyId) throw new Error("登录信息缺少工厂编号，无法加载价格表");
        window.__formalSettingsReady = false;
        state.settings = freshDefaultSettings();
        restoreSettings();
        renderQuoteRules();
        const activeCompanyId = String(localStorage.getItem("jianglai_active_company_id") || "");
        const tenantKey = tenantSettingsStorageKey(companyId);
        if (!hasPermission("price.view")) {
          window.__quoteCatalogSettingsUnavailable = true;
          localStorage.setItem("jianglai_active_company_id", companyId);
          window.__formalSettingsReady = true;
          restoreSettings();
          initializeQuoteCatalog();
          renderQuoteCatalogSetup();
          window.__legacyCachedSettingsForMigration = null;
          return { source: "permission-hidden" };
        }
        window.__quoteCatalogSettingsUnavailable = false;
        const response = await api("/api/settings");
        const remoteSettings = response?.settings && typeof response.settings === "object" ? response.settings : {};
        let nextSettings = null;
        if (response?.exists) {
          nextSettings = { ...freshDefaultSettings(), ...remoteSettings };
          if (!Object.prototype.hasOwnProperty.call(remoteSettings, "quoteCatalogPreferences")) {
            nextSettings.quoteCatalogPreferences = { schemaVersion:1, selectionMode:"all", selectedProductIds:[], setupCompletedAt:"legacy", order:[], hidden:[], customCategories:[] };
          }
        } else {
          let cached = null;
          try { cached = JSON.parse(localStorage.getItem(tenantKey) || "null"); } catch (_) {}
          if (cached && typeof cached === "object" && !Array.isArray(cached)) {
            nextSettings = { ...freshDefaultSettings(), ...cached };
            if (!Object.prototype.hasOwnProperty.call(cached, "quoteCatalogPreferences")) {
              nextSettings.quoteCatalogPreferences = { schemaVersion:1, selectionMode:"all", selectedProductIds:[], setupCompletedAt:"legacy", order:[], hidden:[], customCategories:[] };
            }
          } else if (!activeCompanyId && hasPermission("price.edit")) {
            const legacySettings = window.__legacyCachedSettingsForMigration;
            nextSettings = { ...freshDefaultSettings(), ...(legacySettings && typeof legacySettings === "object" ? legacySettings : {}) };
            if (legacySettings && typeof legacySettings === "object" && !Object.prototype.hasOwnProperty.call(legacySettings, "quoteCatalogPreferences")) {
              nextSettings.quoteCatalogPreferences = { schemaVersion:1, selectionMode:"all", selectedProductIds:[], setupCompletedAt:"legacy", order:[], hidden:[], customCategories:[] };
            }
            const migrated = await api("/api/settings", { method:"PUT", body:JSON.stringify(nextSettings) });
            if (!migrated?.ok) throw new Error("本机价格表首次迁移没有完成");
          } else {
            nextSettings = freshDefaultSettings();
          }
        }
        nextSettings.costLibrary = normalizeCostLibrary(nextSettings.costLibrary);
        if (!response?.exists && hasPermission("price.edit") && nextSettings.referencePricePack?.status === "active") {
          const initialized = await api("/api/settings", { method:"PUT", body:JSON.stringify(nextSettings) });
          if (!initialized?.ok) throw new Error("新账号参考价包没有完成云端初始化");
        }
        const promotion = promoteDueCostLibraryVersion(nextSettings.costLibrary);
        if (promotion.changed) {
          projectCostLibraryLegacySettings(nextSettings, promotion.previousItems, promotion.nextItems);
          nextSettings.costRulesConfirmedAt = nextSettings.costLibrary.updatedAt || new Date().toISOString();
          nextSettings.quoteRulesUpdatedAt = nextSettings.costLibrary.updatedAt || new Date().toISOString();
          if (hasPermission("price.edit")) {
            const promoted = await api("/api/settings", { method:"PUT", body:JSON.stringify(nextSettings) });
            if (!promoted?.ok) throw new Error("到期成本版本没有完成启用");
          }
        }
        state.settings = nextSettings;
        localStorage.setItem(tenantKey, JSON.stringify(nextSettings));
        localStorage.setItem("jianglai_active_company_id", companyId);
        saveState();
        window.__formalSettingsReady = true;
        window.__legacyCachedSettingsForMigration = null;
        restoreSettings();
        initializeQuoteCatalog();
        renderIndustryCategories();
        renderQuoteRules();
        return { source: response?.exists ? "server" : "local" };
      }
      function hasPermission(permission) {
        return !!window.__currentUser?.permissions?.includes(permission);
      }
      window.__hasPermission = hasPermission;
      function renderCurrentStaff() {
        const user = window.__currentUser;
        const box = document.getElementById("currentStaffSummary");
        if (box) box.textContent = user ? `${user.name} · ${user.phone || "未设置登录账号"} · ${user.role_label || user.role} · ${user.is_owner ? "拥有全部权限" : `已分配 ${(user.permission_labels || []).length} 项权限`}` : "尚未登录";
        document.getElementById("temporaryPasswordNotice")?.classList.toggle(
          "is-hidden",
          !user?.password_reset_required
        );
        document.querySelectorAll("#nav button").forEach(button => {
          const required = { "询盘处理":"task.view", "报价参数":"price.view", "经营报表":"price.view", "跟单工单":"task.view", "生产中心":"task.view", "图片制作":"design.use", "团队权限":"task.manage" }[button.dataset.page];
          const planLocked = button.dataset.page === "团队权限" && !hasEnterpriseTeamAccess();
          const permissionLocked = !!required && !!user && !hasPermission(required);
          button.classList.toggle("is-hidden", planLocked || permissionLocked);
        });
        document.getElementById("serviceAdminCard")?.classList.toggle("is-hidden", !!user && !user.is_owner);
        renderPlanVisibility();
        renderCostLibrary();
        renderQuoteCategoryManager();
        renderTasks();
        renderProductionCenter();
        refreshSelectedTaskDetail();
      }
      function requireTemporaryPasswordChange() {
        if (!window.__currentUser?.password_reset_required) return false;
        renderCurrentStaff();
        showPage("更多");
        const details = document.getElementById("accountSecurityDetails");
        if (details) details.open = true;
        window.requestAnimationFrame(() => {
          document.getElementById("accountAccessCard")?.scrollIntoView({ block:"start", behavior:"smooth" });
          document.getElementById("currentAccountPassword")?.focus();
        });
        renderFormalStatus("临时密码已登录 · 请先设置新密码");
        notify("这是一次性临时密码，请先在“密码与登录安全”中设置自己的新密码。");
        return true;
      }
      window.__renderCurrentStaff = renderCurrentStaff;
      window.switchFormalAccount = async function () {
        const phone = val("staffLoginPhone");
        const password = val("staffLoginPassword");
        if (!phone || password.length < 6) return notify("请输入员工账号和至少6位密码。" );
        const previousUser = window.__currentUser;
        const previousToken = apiToken;
        invalidateFormalTaskViews();
        showPage("工作台");
        try {
          apiToken = "";
          const logged = await api("/api/auth/login", { method:"POST", body:JSON.stringify({ phone, password }) });
          apiToken = logged.token;
          localStorage.setItem("formal_api_token", apiToken);
          localStorage.setItem("formal_api_bootstrapped", "1");
          localStorage.setItem("formal_last_login_account", phone);
          const me = await api("/api/me");
          window.__currentUser = me.user || null;
          window.__activateBusinessTenant?.(window.__currentUser?.company_id || "");
          hideFormalAuthGate();
          if (requireTemporaryPasswordChange()) return;
          setIfExists("staffLoginPassword", "");
          const syncResult = await loadSignedInWorkspace();
          notify(syncResult.ok
            ? `已切换为${window.__currentUser?.name || "员工"}账号。`
            : `已切换为${window.__currentUser?.name || "员工"}账号；部分云端数据暂未同步。` );
          showPage("工作台");
        } catch (err) {
          apiToken = previousToken || "";
          if (apiToken) localStorage.setItem("formal_api_token", apiToken);
          else localStorage.removeItem("formal_api_token");
          window.__currentUser = previousUser || null;
          window.__activateBusinessTenant?.(window.__currentUser?.company_id || "");
          await loadFormalTasks().catch(() => null);
          renderCurrentStaff();
          notify(err.message);
        }
      };
      window.changeFormalPassword = async function () {
        const currentPassword = val("currentAccountPassword");
        const newPassword = val("newAccountPassword");
        const confirmPassword = val("confirmAccountPassword");
        if (!currentPassword) return notify("请输入当前密码。");
        if (newPassword.length < 8) return notify("新密码至少8位。");
        if (newPassword !== confirmPassword) return notify("两次输入的新密码不一致。");
        try {
          await ensureFormalLogin();
          const result = await api("/api/auth/change-password", {
            method: "POST",
            body: JSON.stringify({ current_password: currentPassword, new_password: newPassword })
          });
          setIfExists("currentAccountPassword", "");
          setIfExists("newAccountPassword", "");
          setIfExists("confirmAccountPassword", "");
          const me = await api("/api/me");
          window.__currentUser = me.user || null;
          renderCurrentStaff();
          if (!window.__currentUser?.password_reset_required) {
            renderFormalStatus("密码已更新 · 正在同步本厂数据");
            const syncResult = await loadSignedInWorkspace();
            showPage("工作台");
            if (!syncResult.ok) notify("密码已更新；部分云端数据暂未同步，可稍后重试。");
          }
          notify(result.message || "密码已更新。");
        } catch (err) {
          notify(err.message || "密码修改失败，请重试。");
        }
      };
      window.changeFormalLoginAccount = async function () {
        const newAccount = val("newAccountLogin");
        const currentPassword = val("currentAccountPassword");
        if (newAccount.length < 3 || /\s/.test(newAccount)) return notify("新登录账号至少3位，且不能包含空格。");
        if (!currentPassword) return notify("修改登录账号前请输入当前密码。");
        try {
          await ensureFormalLogin();
          const result = await api("/api/auth/change-account", {
            method: "POST",
            body: JSON.stringify({ current_password: currentPassword, new_account: newAccount })
          });
          localStorage.setItem("formal_last_login_account", result.account || newAccount);
          setIfExists("staffLoginPhone", result.account || newAccount);
          setIfExists("authLoginPhone", result.account || newAccount);
          setIfExists("newAccountLogin", "");
          setIfExists("currentAccountPassword", "");
          const me = await api("/api/me");
          window.__currentUser = me.user || null;
          renderCurrentStaff();
          notify(result.message || "登录账号已更新。");
        } catch (err) {
          notify(err.message || "登录账号修改失败，请重试。");
        }
      };
      window.logoutFormalAccount = async function () {
        const tokenBeforeLogout = apiToken;
        try {
          if (tokenBeforeLogout) await api("/api/auth/logout", { method: "POST", body: "{}" });
        } catch (_) {
          // 即使服务暂时不可用，也清除本机登录凭证，避免继续显示旧账号数据。
        }
        apiToken = "";
        localStorage.removeItem("formal_api_token");
        localStorage.removeItem("formal_api_bootstrapped");
        window.__currentUser = null;
        window.__activateBusinessTenant?.("");
        invalidateFormalTaskViews();
        renderCurrentStaff();
        showPage("工作台");
        showFormalAuthGate("已退出当前账号，请重新登录。", "login");
      };
      const rolePermissionDefaults = {
        sales: ["price.view", "customer.view", "customer.manage", "quote.view", "quote.create", "task.view", "task.manage", "design.use", "file.upload", "notification.view", "ai.use"],
        design: ["customer.view", "task.view", "task.manage", "design.use", "file.upload", "notification.view", "ai.use"],
        production: ["customer.view", "quote.view", "task.view", "task.manage", "notification.view"],
        finance: ["customer.view", "quote.view", "task.view", "task.manage", "finance.manage", "notification.view"]
      };
      let teamCatalog = { members: [], permissions: [], can_manage: false };
      function selectedTeamPermissions() {
        return Array.from(document.querySelectorAll("[data-team-permission]:checked")).map(input => input.value);
      }
      function renderTeamPermissionGrid(selected = []) {
        const box = document.getElementById("teamPermissionGrid");
        if (!box) return;
        const active = new Set(selected);
        box.innerHTML = (teamCatalog.permissions || []).map(item => `<label><input type="checkbox" data-team-permission value="${escapeHtml(item.id)}"${active.has(item.id) ? " checked" : ""}><span>${escapeHtml(item.label)}</span></label>`).join("");
      }
      window.applyRolePermissionDefaults = function () {
        const role = document.getElementById("teamMemberRole")?.value || "sales";
        renderTeamPermissionGrid(rolePermissionDefaults[role] || []);
      };
      window.clearTeamMemberEditor = function () {
        setIfExists("teamMemberId", ""); setIfExists("teamMemberName", ""); setIfExists("teamMemberPhone", ""); setIfExists("teamMemberPassword", ""); setIfExists("teamMemberRole", "sales");
        setTextIfExists("teamEditorTitle", "添加员工");
        window.__teamEditorOpen = false;
        document.getElementById("teamEditorCard")?.classList.add("is-hidden");
        window.applyRolePermissionDefaults();
      };
      window.openTeamMemberEditor = function (id = 0) {
        if (!teamCatalog.can_manage) return notify("只有老板可以添加员工或修改权限。");
        const member = (teamCatalog.members || []).find(item => Number(item.id) === Number(id));
        window.clearTeamMemberEditor();
        window.__teamEditorOpen = true;
        document.getElementById("teamEditorCard")?.classList.remove("is-hidden");
        if (member) {
          setIfExists("teamMemberId", member.id); setIfExists("teamMemberName", member.name); setIfExists("teamMemberRole", member.role); setIfExists("teamMemberPhone", "");
          setTextIfExists("teamEditorTitle", `修改 ${member.name} 的权限`);
          renderTeamPermissionGrid(member.permissions || []);
        }
        document.getElementById("teamEditorCard")?.scrollIntoView({ behavior: "smooth", block: "start" });
      };
      function renderTeamMembers() {
        const box = document.getElementById("teamMemberList");
        if (!box) return;
        document.getElementById("teamOwnerActions")?.classList.toggle("is-hidden", !teamCatalog.can_manage);
        document.getElementById("teamEditorCard")?.classList.toggle("is-hidden", !teamCatalog.can_manage || window.__teamEditorOpen !== true);
        box.innerHTML = (teamCatalog.members || []).map(member => {
          const locked = member.role === "owner";
          const actions = teamCatalog.can_manage && !locked ? `<div class="actions"><button class="btn light" type="button" onclick="openTeamMemberEditor(${Number(member.id)})">修改权限</button><button class="btn light" type="button" onclick="toggleTeamMemberStatus(${Number(member.id)}, '${member.status === "active" ? "disabled" : "active"}')">${member.status === "active" ? "停用" : "启用"}</button></div>` : "";
          return `<div class="team-member${member.status !== "active" ? " is-disabled" : ""}"><div class="team-member-main"><strong>${escapeHtml(member.name)} · ${escapeHtml(member.role_label || member.role)}</strong><span>${escapeHtml(member.phone_masked || "")} · ${member.status === "active" ? "正常使用" : "已停用"}${locked ? " · 最高权限" : ""}</span><div class="team-member-permissions">${(member.permission_labels || []).slice(0, 10).map(label => `<i>${escapeHtml(label)}</i>`).join("")}</div></div>${actions}</div>`;
        }).join("") || `<div class="notification-empty">还没有员工账号</div>`;
      }
      async function loadAudit() {
        const box = document.getElementById("auditList");
        if (!hasPermission("audit.view")) { if (box) box.innerHTML = `<div class="notification-empty">当前账号没有查看全部操作记录的权限</div>`; return; }
        const data = await api("/api/audit");
        const entityLabels = { task:"跟单任务", quote:"报价单", customer:"客户资料", member:"员工账号", user:"员工账号", file:"客户文件", settings:"价格设置", notification:"提醒设置", preflight:"印前体检", vector:"矢量任务" };
        if (box) box.innerHTML = (data.items || []).map(item => {
          const auditDate = new Date(item.created_at || "");
          const timeText = Number.isNaN(auditDate.getTime()) ? String(item.created_at || "") : auditDate.toLocaleString("zh-CN", { year:"numeric", month:"numeric", day:"numeric", hour:"2-digit", minute:"2-digit", hour12:false });
          const entityText = entityLabels[item.entity_type] || item.entity_type || "系统记录";
          return `<div class="audit-item"><strong>${escapeHtml(item.user_name)}（${escapeHtml(item.role_label)}）· ${escapeHtml(item.action)}</strong><div class="muted">${escapeHtml(timeText)} · ${escapeHtml(entityText)}${item.entity_id ? ` · 记录 ${escapeHtml(String(item.entity_id))}` : ""}</div></div>`;
        }).join("") || `<div class="notification-empty">还没有关键操作记录</div>`;
      }
      async function loadTeamManagement() {
        if (!hasEnterpriseTeamAccess()) {
          renderPlanVisibility();
          return null;
        }
        try {
          await ensureFormalLogin();
          const data = await api("/api/team");
          teamCatalog = data;
          window.__teamMembers = data.members || [];
          renderCurrentStaff();
          renderTeamMembers();
          window.clearTeamMemberEditor();
          await loadAudit().catch(() => {});
        } catch (err) {
          const box = document.getElementById("teamMemberList"); if (box) box.innerHTML = `<div class="notification-empty">${escapeHtml(err.message)}</div>`;
        }
      }
      window.saveTeamMember = async function () {
        try {
          const id = Number(document.getElementById("teamMemberId")?.value || 0);
          const body = { id, name: val("teamMemberName"), phone: val("teamMemberPhone"), password: val("teamMemberPassword"), role: val("teamMemberRole"), permissions: selectedTeamPermissions() };
          await api("/api/team/member", { method: "POST", body: JSON.stringify(body) });
          notify(id ? "员工权限已更新，操作已留痕。" : "员工账号已创建，可以用账号和临时密码登录。" );
          await loadTeamManagement();
        } catch (err) { notify(err.message); }
      };
      window.toggleTeamMemberStatus = async function (id, status) {
        try { await api("/api/team/member/status", { method: "POST", body: JSON.stringify({ id, status }) }); await loadTeamManagement(); notify(status === "active" ? "员工账号已启用。" : "员工账号已停用并退出登录。" ); }
        catch (err) { notify(err.message); }
      };
      window.__loadTeamManagement = loadTeamManagement;
      const notificationKindLabels = { customer_reply:"客户回复", quote_approval:"报价审批", quote_cost_pending:"实际成本待补", order_update:"订单进度", quote_overdue:"报价待跟进", sample_pending:"样品待确认", delivery_due:"交期临近", payment_due:"尾款未收", task_assigned:"任务分配", task_due:"任务到期", vector_pending:"矢量待处理", vector_completed:"矢量已完成" };
      const customerNotificationKinds = new Set(["customer_reply", "order_update", "quote_overdue", "sample_pending", "delivery_due", "payment_due", "task_due"]);
      let notificationPreferences = {};
      let activeNotificationCategory = "customer";
      let latestNotificationData = { items: [], unread: 0, preferences: {}, business_sms: {} };
      async function renderNotificationChannelStatus() {
        const box = document.getElementById("notificationChannelStatus");
        if (!box) return;
        const permission = typeof Notification === "undefined" ? "unsupported" : Notification.permission;
        const registration = navigator.serviceWorker?.getRegistration
          ? await navigator.serviceWorker.getRegistration().catch(() => null)
          : null;
        const subscription = registration?.pushManager ? await registration.pushManager.getSubscription().catch(() => null) : null;
        const deviceReady = permission === "granted";
        const pushReady = Boolean(subscription);
        const sms = latestNotificationData.business_sms || {};
        const smsReady = Boolean(sms.configured);
        const smsOn = notificationPreferences.sms_enabled === true;
        const smsState = smsOn ? (smsReady ? "已开启" : "等待模板") : "未开启";
        box.innerHTML = `
          <div class="notification-channel"><div><strong>软件内提醒</strong><span>网页打开时每分钟同步一次待办</span></div><em>已开启</em></div>
          <div class="notification-channel"><div><strong>手机/电脑系统通知</strong><span>${permission === "denied" ? "浏览器已拒绝，可到网站设置重新允许" : permission === "unsupported" ? "当前浏览器不支持系统通知" : deviceReady ? "网页打开或驻留后台时可显示系统通知" : "点击上方按钮允许通知"}</span></div><em class="${deviceReady ? "" : "pending"}">${deviceReady ? "已授权" : "待开启"}</em></div>
          <div class="notification-channel"><div><strong>完全关闭后的后台推送</strong><span>${pushReady ? "推送订阅已建立，关闭网页后仍可接收" : "尚未配置推送订阅；当前不能承诺关闭网页后送达"}</span></div><em class="${pushReady ? "" : "pending"}">${pushReady ? "已接入" : "待部署"}</em></div>
          <div class="notification-channel"><div><strong>业务短信提醒</strong><span>${escapeHtml(sms.message || "订单、交期和回款短信状态正在读取")}；仅发当前账号手机号，${escapeHtml(sms.send_window || "08:00-21:00")}发送</span></div><em class="${smsOn && smsReady ? "" : "pending"}">${smsState}</em></div>
        `;
      }
      async function showNativeNotification(item) {
        if (Notification.permission !== "granted" || notificationPreferences.browser === false || notificationPreferences[item.kind] === false) return;
        const lastShown = Number(localStorage.getItem("last_native_notification_id") || 0);
        if (Number(item.id) <= lastShown || item.read_at) return;
        const registration = await navigator.serviceWorker?.ready.catch(() => null);
        if (registration) await registration.showNotification(item.title, { body:item.body, tag:`factory-ai-${item.id}`, data:{ url:"/?page=reminders" } });
        else new Notification(item.title, { body:item.body, tag:`factory-ai-${item.id}` });
        localStorage.setItem("last_native_notification_id", String(item.id));
      }
      function formatNotificationTime(value) {
        const date = new Date(value || "");
        return Number.isNaN(date.getTime()) ? String(value || "") : date.toLocaleString("zh-CN", { month:"numeric", day:"numeric", hour:"2-digit", minute:"2-digit", hour12:false });
      }
      function notificationCategoryFor(item = {}) {
        if (["customer", "system"].includes(item.category)) return item.category;
        return customerNotificationKinds.has(String(item.kind || "")) ? "customer" : "system";
      }
      function notificationItemMarkup(item) {
        const unread = !item.read_at;
        return `<button class="notification-item${unread ? " unread" : ""}" data-notification-category="${notificationCategoryFor(item)}" type="button" onclick="openNotification(${Number(item.id)},'${encodeURIComponent(item.entity_type || "")}', '${encodeURIComponent(String(item.entity_id || ""))}')"><div class="notification-item-content"><div class="notification-item-head"><strong>${escapeHtml(item.title)}</strong><span class="notification-kind">${escapeHtml(notificationKindLabels[item.kind] || item.kind || "系统消息")}</span></div><div class="muted" style="margin-top:6px">${escapeHtml(item.body || "")}</div><div class="notification-item-foot"><span>${escapeHtml(formatNotificationTime(item.created_at))}${item.read_at ? " · 已读" : " · 未读"}</span><span class="notification-item-action">${item.entity_type ? "查看并处理 →" : unread ? "查看消息 →" : "已查看"}</span></div></div></button>`;
      }
      function renderNotificationCategory() {
        const items = Array.isArray(latestNotificationData.items) ? latestNotificationData.items : [];
        const groups = {
          customer: items.filter(item => notificationCategoryFor(item) === "customer"),
          system: items.filter(item => notificationCategoryFor(item) === "system")
        };
        const currentItems = groups[activeNotificationCategory] || [];
        const currentUnread = currentItems.filter(item => !item.read_at).length;
        const customerTab = document.getElementById("notificationCustomerTab");
        const systemTab = document.getElementById("notificationSystemTab");
        customerTab?.classList.toggle("active", activeNotificationCategory === "customer");
        systemTab?.classList.toggle("active", activeNotificationCategory === "system");
        customerTab?.setAttribute("aria-selected", activeNotificationCategory === "customer" ? "true" : "false");
        systemTab?.setAttribute("aria-selected", activeNotificationCategory === "system" ? "true" : "false");
        setTextIfExists("notificationCustomerCount", String(groups.customer.length));
        setTextIfExists("notificationSystemCount", String(groups.system.length));
        const summary = document.getElementById("notificationListSummary");
        if (summary) summary.innerHTML = `<strong>${activeNotificationCategory === "customer" ? "客户提醒" : "系统消息"}</strong><span>${currentUnread ? `${currentUnread} 条未读 · ` : ""}共 ${currentItems.length} 条</span>`;
        const box = document.getElementById("notificationList");
        if (box) {
          const emptyText = activeNotificationCategory === "customer"
            ? "目前没有客户业务提醒"
            : "目前没有系统消息";
          box.innerHTML = currentItems.map(notificationItemMarkup).join("") || `<div class="notification-empty">${emptyText}</div>`;
        }
      }
      window.setNotificationCategory = function (category) {
        activeNotificationCategory = category === "system" ? "system" : "customer";
        renderNotificationCategory();
      };
      function renderNotifications(data) {
        const count = document.getElementById("notificationCount");
        if (count) { count.textContent = String(data.unread || 0); count.classList.toggle("show", Number(data.unread || 0) > 0); }
        latestNotificationData = {
          items: Array.isArray(data.items) ? data.items : [],
          unread: Number(data.unread || 0),
          preferences: data.preferences || {},
          business_sms: data.business_sms || {}
        };
        notificationPreferences = data.preferences || {};
        document.querySelectorAll("[data-notice-pref]").forEach(input => {
          const key = input.dataset.noticePref;
          input.checked = key === "sms_enabled"
            ? notificationPreferences[key] === true
            : notificationPreferences[key] !== false;
        });
        renderNotificationCategory();
        (data.items || []).filter(item => !item.read_at).slice().reverse().forEach(item => showNativeNotification(item).catch(() => {}));
        renderNotificationChannelStatus().catch(() => {});
      }
      async function loadNotifications(force = false) {
        if (isStaticFileMode || document.hidden && !force) return;
        try { await ensureFormalLogin(); if (!hasPermission("notification.view")) return; renderNotifications(await api("/api/notifications")); }
        catch (err) { if (force) { const box = document.getElementById("notificationList"); if (box) box.innerHTML = `<div class="notification-empty">${escapeHtml(err.message)}</div>`; } }
      }
      window.readNotification = async function (id) { await api("/api/notifications/read", { method:"POST", body:JSON.stringify({ id }) }); await loadNotifications(true); };
      window.openNotification = async function (id, entityTypeCode, entityIdCode) {
        const entityType = decodeURIComponent(entityTypeCode || "");
        const entityId = decodeURIComponent(entityIdCode || "");
        await api("/api/notifications/read", { method:"POST", body:JSON.stringify({ id }) });
        if (entityType === "task") {
          await loadFormalTasks().catch(() => null);
          const task = (state.tasks || []).find(item => String(item.serverId || "") === entityId);
          if (task) window.setTimeout(() => openTaskDetail(task.id), 60);
          else showPage("跟单工单");
        } else if (entityType === "quote") {
          await loadFormalQuotes().catch(() => null);
          showPage("报价系统");
          const record = (state.quoteHistory || []).find(item => String(item.serverId || "") === entityId);
          if (record) window.setTimeout(() => viewSavedQuote(encodeURIComponent(quoteRecordKey(record))), 60);
          else document.getElementById("quoteHistoryBody")?.closest("details")?.setAttribute("open", "");
        } else if (entityType === "customer") {
          await loadCloudCustomers().catch(() => null);
          showPage("客户档案");
          const profile = (state.customerProfiles || []).find(item => String(item.serverId || "") === entityId);
          if (profile) window.setTimeout(() => editCustomerProfile(encodeURIComponent(profile.company)), 60);
        } else if (entityType === "vector_job") {
          showPage("图片转矢量");
          await loadVectorJobs().catch(() => null);
        } else {
          showPage("提醒中心");
        }
        await loadNotifications(false);
      };
      window.markAllNotificationsRead = async function () { await api("/api/notifications/read", { method:"POST", body:JSON.stringify({ all:true }) }); await loadNotifications(true); };
      window.saveNotificationPreferences = async function () {
        const body = { browser: Notification.permission === "granted" };
        document.querySelectorAll("[data-notice-pref]").forEach(input => { body[input.dataset.noticePref] = input.checked; });
        try { const data = await api("/api/notifications/preferences", { method:"POST", body:JSON.stringify(body) }); notificationPreferences = data.preferences || body; notify("提醒设置已保存。" ); }
        catch (err) { notify(err.message); }
      };
      window.enableBrowserNotifications = async function () {
        if (!("Notification" in window)) return notify("当前浏览器不支持系统通知，请使用最新版 Chrome、Edge 或手机浏览器。" );
        const result = await Notification.requestPermission();
        if (result !== "granted") return notify("通知权限未开启，可以在浏览器网站设置中重新允许。" );
        await window.saveNotificationPreferences();
        const registration = await navigator.serviceWorker?.ready.catch(() => null);
        if (registration) await registration.showNotification("将莱数字员工提醒已开启", { body:"客户回复、报价、样品、交期和尾款将按你的设置提醒。", tag:"factory-ai-enabled" });
        await renderNotificationChannelStatus();
        notify("手机/电脑通知已开启。" );
      };
      async function assignFormalTask(task, userId) {
        try {
          const data = await api(`/api/tasks/${task.serverId}/assign`, { method:"POST", body:JSON.stringify({ owner_user_id:userId }) });
          if (data.task) mergeFormalTasks([data.task]);
          showTaskDetail(task.id);
          notify("负责人已分配，对方会收到提醒。" );
        } catch (err) { notify(err.message); }
      }
      window.__assignFormalTask = assignFormalTask;
      window.__loadNotifications = loadNotifications;
      window.setInterval(() => loadNotifications(false), 60000);
      function paintHeaderServiceStatus(text) {
        const badge = document.getElementById("serviceStatusBadge");
        if (!badge) return;
        const detail = String(text || "正在连接服务").trim();
        const healthy = /^(?:AI已连接|AI可试用)/.test(detail);
        const connecting = /正在连接|连接中/.test(detail);
        badge.textContent = healthy ? "服务正常" : connecting ? "连接中" : "服务提醒";
        badge.title = `${detail}；点击查看本机保存、云端同步和备份状态`;
        badge.setAttribute("aria-label", badge.title);
        badge.classList.toggle("is-attention", !healthy);
      }
      function renderFormalStatus(text) {
        window.__formalStatusText = text;
        paintHeaderServiceStatus(text);
        window.clearTimeout(window.__serviceStatusTimer);
        if (/^(?:AI已连接|AI可试用)/.test(String(text || ""))) {
          window.__baseServiceStatus = text;
          return;
        }
        window.__serviceStatusTimer = window.setTimeout(() => {
          if (window.__baseServiceStatus) paintHeaderServiceStatus(window.__baseServiceStatus);
        }, 4500);
      }
      let customerMemoryLoadVersion = 0;
      function cloudCustomerToProfile(item) {
        const profile = {
          serverId: Number(item.id || item.serverId || 0),
          company: item.name || item.company || "新客户",
          contact: item.contact || "未填写",
          level: item.level || "普通客户",
          dealStatus: item.deal_status || item.dealStatus || "未报价",
          need: item.need || "待沉淀",
          preference: item.preference || "待沉淀",
          note: item.notes || item.note || "",
          memorySummary: item.memory_summary || item.memorySummary || "",
          designLearningEnabled: (item.design_learning_enabled ?? item.designLearningEnabled) !== false && Number(item.design_learning_enabled ?? 1) !== 0,
          designPreferenceSummary: item.design_preference_summary || item.designPreferenceSummary || "",
          pricing: item.pricing && typeof item.pricing === "object" ? item.pricing : {},
          createdAt: item.created_at || item.createdAt || new Date().toISOString(),
          updatedAt: item.updated_at || item.updatedAt || ""
        };
        const quoteCount = item.quote_count ?? item.quoteCount;
        const lastQuoteTotal = item.last_quote_total ?? item.lastQuoteTotal;
        const lastQuoteUnit = item.last_quote_unit ?? item.lastQuoteUnit;
        const lastQuoteAt = item.last_quote_at ?? item.lastQuoteAt;
        const lastProduct = item.last_quote_product ?? item.lastProduct;
        if (quoteCount != null) profile.quoteCount = Number(quoteCount || 0);
        if (lastQuoteTotal != null) profile.lastQuoteTotal = Number(lastQuoteTotal || 0);
        if (lastQuoteUnit != null) profile.lastQuoteUnit = Number(lastQuoteUnit || 0);
        if (lastQuoteAt != null) profile.lastQuoteAt = lastQuoteAt || "";
        if (lastProduct != null) profile.lastProduct = lastProduct || "";
        const dealTotal = item.actual_deal_total ?? item.actualDealTotal;
        const dealQty = item.actual_deal_qty ?? item.actualDealQty;
        const outcomeReason = item.outcome_reason ?? item.outcomeReason;
        const dealNotes = item.deal_notes ?? item.dealNotes;
        const lastDealAt = item.last_deal_at ?? item.lastDealAt;
        if (dealTotal != null) profile.actualDealTotal = Number(dealTotal || 0);
        if (dealQty != null) profile.actualDealQty = Number(dealQty || 0);
        if (outcomeReason != null) profile.outcomeReason = outcomeReason || "";
        if (dealNotes != null) profile.dealNotes = dealNotes || "";
        if (lastDealAt != null) profile.lastDealAt = lastDealAt || "";
        if (item.conversation_count != null) profile.conversationCount = Number(item.conversation_count || 0);
        if (item.design_feedback_count != null) profile.designFeedbackCount = Number(item.design_feedback_count || 0);
        return profile;
      }
      function mergeCloudCustomerProfiles(items = []) {
        const existing = new Map((state.customerProfiles || []).map(item => [item.company, item]));
        items.forEach(item => {
          const profile = cloudCustomerToProfile(item);
          existing.set(profile.company, { ...(existing.get(profile.company) || {}), ...profile });
        });
        state.customerProfiles = [...existing.values()]
          .sort((a, b) => (Date.parse(b.updatedAt || "") || 0) - (Date.parse(a.updatedAt || "") || 0))
          .slice(0, 100);
        saveState();
        renderCustomerProfiles();
        renderCustomerWorkbench();
        const options = document.getElementById("homeCustomerOptions");
        if (options) {
          options.innerHTML = businessProfiles()
            .filter(item => item.company && item.company !== "新客户")
            .map(item => `<option value="${escapeHtml(item.company)}"></option>`)
            .join("");
        }
      }
      function renderConnectedMemoryStatus(data, cloudOk = null) {
        const status = document.getElementById("homeMemoryStatus");
        if (!status) return;
        const customer = data?.customer;
        const messageCount = Number(data?.memory?.conversation_count ?? data?.messages?.length ?? customer?.conversation_count ?? 0);
        const summary = data?.memory?.summary || customer?.memory_summary || "";
        if (!customer && !data?.memory?.stored) {
          const currentName = (document.getElementById("homeCustomerName")?.value || "新客户").trim();
          status.textContent = isPendingCustomerName(currentName)
            ? `${currentName} 的资料已独立暂存；发送后会同步云端，之后可补姓名`
            : currentName && !["新客户", "未命名客户"].includes(currentName)
              ? `这是新客户；发送后会以“${currentName}”建立独立档案和聊天记忆`
              : "这是新客户；发送后会自动建立待命名档案和聊天记忆";
          status.classList.remove("is-saved");
          status.title = "";
          return;
        }
        const syncText = cloudOk === true ? " · 云端已同步" : cloudOk === false ? " · 本机已保存" : " · 服务端已保存";
        status.textContent = `已读取 ${messageCount} 条历史沟通${syncText}${summary ? ` · ${summary}` : ""}`;
        status.title = summary || data?.memory_text || "客户历史记忆已载入";
        status.classList.add("is-saved");
      }
      function normalizedConversationMessageText(value = "") {
        return String(value || "").replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
      }
      function localUserBubbleSourceText(bubble) {
        if (!bubble) return "";
        const copy = bubble.cloneNode(true);
        copy.querySelectorAll(".chat-user-attachments, .chat-user-files").forEach(item => item.remove());
        return normalizedConversationMessageText(copy.textContent || "");
      }
      function legacyUserAttachmentSummary(bubble, count = 0, serverNames = []) {
        const summary = bubble?.querySelector(".chat-user-files");
        const summaryText = normalizedConversationMessageText(summary?.textContent || "");
        const label = /^继续参考\s*[：:]/.test(summaryText) ? "继续参考" : "附件";
        const legacyNames = summaryText
          .replace(/^(?:附件|继续参考)\s*[：:]\s*/, "")
          .split(/[、，,]/)
          .map(item => item.trim())
          .filter(Boolean);
        const names = Array.from({ length: count }, (_, index) => (
          legacyNames[index] || serverNames[index] || `历史附件 ${index + 1}`
        ));
        return { label, names };
      }
      function backfillLocalConversationAttachmentsFromServer(messages = [], customerName = "") {
        if (!Array.isArray(messages) || !messages.length) return 0;
        const session = activeCustomerSession();
        const currentName = (document.getElementById("homeCustomerName")?.value || "").trim();
        if (!session || currentName !== customerName || session.customer !== customerName) return 0;
        const history = document.getElementById("homeChatHistory");
        const currentBubble = document.getElementById("homeUserMessage");
        const localBubbles = [
          ...Array.from(history?.querySelectorAll(".chat-user-bubble") || []),
          ...(currentBubble && normalizedConversationMessageText(currentBubble.textContent) ? [currentBubble] : [])
        ].filter(bubble => {
          const durableButtons = Array.from(bubble.querySelectorAll(".chat-user-attachment[data-user-file-id]"))
            .some(button => Number(button.dataset.userFileId || 0) > 0);
          return !durableButtons && (bubble.querySelector(".chat-user-files") || bubble.querySelector("[data-user-attachments]"));
        });
        if (!localBubbles.length) return 0;
        const serverTurns = messages.map((item, messageIndex) => {
          if (item.role !== "customer" && item.role !== "user") return null;
          const ids = (Array.isArray(item.metadata?.file_ids) ? item.metadata.file_ids : []).map(Number).filter(id => id > 0);
          if (!ids.length) return null;
          return {
            messageIndex,
            ids,
            names: Array.isArray(item.metadata?.file_names) ? item.metadata.file_names : [],
            text: normalizedConversationMessageText(item.content || "")
          };
        }).filter(Boolean);
        if (!serverTurns.length) return 0;
        const usedBubbles = new Set();
        const matches = [];
        serverTurns.forEach(turn => {
          const bubble = localBubbles.find(candidate => {
            if (usedBubbles.has(candidate)) return false;
            const localText = localUserBubbleSourceText(candidate);
            return localText && turn.text && localText === turn.text;
          });
          if (!bubble) return;
          usedBubbles.add(bubble);
          matches.push({ bubble, turn });
        });
        const unmatchedBubbles = localBubbles.filter(bubble => !usedBubbles.has(bubble));
        const matchedTurnIndexes = new Set(matches.map(item => item.turn.messageIndex));
        const unmatchedTurns = serverTurns.filter(turn => !matchedTurnIndexes.has(turn.messageIndex));
        // 旧版纯附件消息的本地提示和服务端占位文案可能不同；仅在一一对应时按时间顺序安全回填。
        if (unmatchedBubbles.length && unmatchedBubbles.length === unmatchedTurns.length) {
          unmatchedTurns.forEach((turn, index) => matches.push({ bubble: unmatchedBubbles[index], turn }));
        }
        if (!matches.length) return 0;
        let currentTurnBackfilled = false;
        matches.forEach(({ bubble, turn }) => {
          const summary = legacyUserAttachmentSummary(bubble, turn.ids.length, turn.names);
          if (bubble.querySelector("[data-user-attachments]")) {
            bubble.innerHTML = restoreSessionUserAttachmentMarkup(bubble.innerHTML, turn.ids, summary.names);
          } else {
            bubble.querySelector(".chat-user-files")?.remove();
            bubble.insertAdjacentHTML("beforeend", homeUserAttachmentMarkup(
              turn.ids.map((id, index) => ({ id, name: summary.names[index], imageLike: attachmentNameLooksLikeImage(summary.names[index]) })),
              summary.label
            ));
          }
          if (bubble === currentBubble) {
            currentTurnBackfilled = true;
            window.__homeCurrentTurnFileIds = [...turn.ids];
            window.__homeCurrentTurnFileNames = [...summary.names];
            session.userFileIds = [...turn.ids];
            session.userFileNames = [...summary.names];
          }
        });
        session.chatHistoryHtml = history?.innerHTML || session.chatHistoryHtml || "";
        if (currentTurnBackfilled) session.userMessageHtml = currentBubble?.innerHTML || session.userMessageHtml || "";
        saveState();
        window.requestAnimationFrame(() => {
          const hydration = window.__hydrateHomeUserAttachmentThumbs?.(document.querySelector(".gpt-home"));
          Promise.resolve(hydration).catch(() => null).finally(scrollHomeConversationToLatest);
        });
        return matches.length;
      }
      function restoreServerConversationIntoHome(messages = [], customerName = "") {
        if (!Array.isArray(messages) || !messages.length) return;
        const session = activeCustomerSession();
        const currentName = (document.getElementById("homeCustomerName")?.value || "").trim();
        if (!session || currentName !== customerName || session.customer !== customerName) return;
        backfillLocalConversationAttachmentsFromServer(messages, customerName);
        if (session.hasConversation || String(session.chatHistoryHtml || "").trim()) return;
        const latestMessageIsStaleImageSetup = isStaleImageSetupFailureText(messages[messages.length - 1]?.content || "");
        const historyHtml = messages.map(item => {
          const content = escapeHtml(String(item.content || "")).replace(/\n/g, "<br>");
          if (!content) return "";
          if (item.role === "customer" || item.role === "user") {
            const fileIds = (Array.isArray(item.metadata?.file_ids) ? item.metadata.file_ids : []).map(Number).filter(id => id > 0);
            const fileNames = Array.isArray(item.metadata?.file_names) ? item.metadata.file_names : [];
            const attachments = homeUserAttachmentMarkup(fileIds.map((id, index) => {
              const name = fileNames[index] || `历史附件 ${index + 1}`;
              return { id, name, imageLike: attachmentNameLooksLikeImage(name) };
            }), "附件");
            return `<div class="chat-user-row"><div class="chat-user-bubble">${content}${attachments}</div></div>`;
          }
          const speaker = item.role === "internal" ? "内部记录" : "将莱数字员工";
          const generated = item.metadata?.generated_image;
          const generatedImage = generated?.url
            ? `<div class="home-inline-visual">${generatedImagePreviewMarkup(generated.url, generated.filename || "历史效果图.png", "历史生成效果图", generated.file_id)}<div class="home-inline-visual-meta">已从客户记录恢复</div><div class="actions">${generatedImageDownloadButton(generated.url, generated.filename || "历史效果图.png")}</div></div>`
            : "";
          const staleImageSetup = isStaleImageSetupFailureText(item.content);
          const restoredContent = isHistoricalServiceFailureText(item.content)
            ? `<details class="historical-service-note${staleImageSetup ? " stale-image-setup-note" : ""}"${staleImageSetup ? ' data-stale-image-setup-record="true"' : ""}><summary>${staleImageSetup ? "旧版图片服务异常记录（不代表当前状态）" : "当时的服务异常记录（已折叠）"}</summary><div><p>${content}</p></div></details>`
            : `<p>${content}</p>`;
          return `<div class="chat-assistant-row"><div class="chat-assistant-avatar" aria-hidden="true"><img src="assets/报价跟单助手-logo.png" alt=""></div><div class="chat-assistant-content"><div class="chat-assistant-name">${speaker}</div><div class="output"><div class="ai-answer-body">${restoredContent}${generatedImage}</div></div></div></div>`;
        }).join("");
        if (!historyHtml) return;
        const history = document.getElementById("homeChatHistory");
        const answer = document.getElementById("aiAnswer");
        if (history) history.innerHTML = historyHtml;
        document.getElementById("homeUserRow")?.classList.add("is-hidden");
        if (answer) answer.innerHTML = `<div class="ai-answer-body"><p>已恢复 ${messages.length} 条历史沟通，可以在下面继续聊。</p>${latestMessageIsStaleImageSetup ? staleImageSetupRecoveryMarkup() : ""}</div>`;
        document.getElementById("aiNextActions")?.classList.add("is-hidden");
        document.getElementById("homeResult")?.classList.remove("is-empty");
        document.querySelector(".gpt-home")?.classList.add("has-conversation");
        session.chatHistoryHtml = historyHtml;
        session.answerHtml = answer?.innerHTML || "";
        session.userHidden = true;
        session.resultEmpty = false;
        session.hasConversation = true;
        session.lastMessage = `已恢复 ${messages.length} 条历史沟通`;
        saveState();
        renderCustomerWorkbench();
        window.requestAnimationFrame(syncHomeComposerClearance);
        window.requestAnimationFrame(() => {
          const hydration = window.__hydrateHomeUserAttachmentThumbs?.(history, { reset: true });
          scrollHomeConversationToLatest(true);
          Promise.resolve(hydration).catch(() => null).finally(scrollHomeConversationToLatest);
        });
      }
      async function loadCloudCustomers() {
        await ensureFormalLogin();
        const data = await api("/api/customers");
        mergeCloudCustomerProfiles(data.customers || []);
        return data;
      }
      function cloudQuoteToLocal(item) {
        const result = item.result || {};
        const outcome = result.outcome || {};
        const estimatedMaterialCost = Number(result.estimated_material_cost || 0);
        const estimatedProcessCost = Number(result.estimated_process_cost || 0);
        const estimatedOtherCost = Number(result.estimated_other_cost || 0);
        const estimatedCost = Number(result.estimated_cost || (estimatedMaterialCost + estimatedProcessCost + estimatedOtherCost));
        const total = Number(item.total || result.internal?.suggested_total || 0);
        const estimatedProfit = Number(result.estimated_profit ?? Math.max(0, total - estimatedCost));
        const marginRate = Number(result.margin_rate || (total > 0 ? estimatedProfit / total * 100 : 0));
        return {
          serverId: Number(item.id || 0),
          quoteNo: result.quote_no || `YDBJ-${item.id}`,
          version: Number(result.version || 1),
          customer: item.customer || "新客户",
          product: item.product || "定制产品",
          size: item.size || "",
          qty: Number(item.qty || 0),
          material: item.material || "",
          process: item.process || "",
          total,
          unit: Number(item.unit || result.internal?.suggested_unit || 0),
          minAcceptTotal: Number(result.min_accept_total || 0),
          urgentTotal: Number(result.urgent_total || 0),
          estimatedCost,
          estimatedProfit,
          marginRate,
          materialUnitCost: Number(result.material_unit_cost || 0),
          estimatedMaterialCost,
          estimatedProcessCost,
          estimatedOtherCost,
          costMode: String(result.cost_mode || ""),
          materialCostDraft: result.material_cost_draft && typeof result.material_cost_draft === "object" ? cloneQuoteSnapshot(result.material_cost_draft) : null,
          safetyDetails: result.safety_details && typeof result.safety_details === "object" ? result.safety_details : {},
          costVersionId: String(result.cost_version_id || result.cost_snapshot?.versionId || ""),
          costSnapshot: result.cost_snapshot && typeof result.cost_snapshot === "object" ? cloneQuoteSnapshot(result.cost_snapshot) : null,
          formulaSnapshot: result.formula_snapshot && typeof result.formula_snapshot === "object" ? cloneQuoteSnapshot(result.formula_snapshot) : null,
          costLineItems: Array.isArray(result.cost_line_items?.items) ? cloneQuoteSnapshot(result.cost_line_items.items) : [],
          standardUnit: Number(result.standard_unit || 0),
          customerPriceSource: String(result.customer_price_source || "standard_price"),
          customerPriceSourceLabel: String(result.customer_price_source_label || "系统标准报价"),
          industryCategory: result.industry_category || item.industry_category || result.cost_snapshot?.industryCategory || "",
          industryStrategyId: result.industry_strategy_id || item.industry_strategy_id || result.formula_snapshot?.strategyId || "",
          productConfiguration: result.product_configuration && typeof result.product_configuration === "object" ? result.product_configuration : null,
          approvalStatus: result.approval_status || "可发送",
          approvalReason: result.approval_reason || "",
          approvedAt: result.approved_at || "",
          approvedBy: result.approved_by || "",
          approvalNote: result.approval_note || "",
          createdBy: result.created_by || "",
          caseSource: result.case_source || "",
          followupTaskId: Number(result.followup_task_id || 0),
          customerText: result.customer_text || "",
          internalText: result.internal_text || "",
          freightRule: "统一不含运费",
          freightMode: "不含运",
          freight: 0,
          createdAt: item.created_at || new Date().toISOString(),
          time: item.created_at || "",
          status: "已报价",
          outcomeStatus: outcome.status || "",
          actualDealTotal: Number(outcome.actual_deal_total || 0),
          actualDealQty: Number(outcome.actual_deal_qty || 0),
          actualMaterialCost: Number(outcome.actual_material_cost || 0),
          actualProcessCost: Number(outcome.actual_process_cost || 0),
          actualOtherCost: Number(outcome.actual_other_cost || 0),
          actualTotalCost: Number(outcome.actual_total_cost || 0),
          actualProfit: outcome.actual_cost_recorded ? Number(outcome.actual_profit || 0) : null,
          actualMarginRate: outcome.actual_cost_recorded ? Number(outcome.actual_margin_rate || 0) : null,
          outcomeReason: outcome.reason || "",
          goldenSample: result.golden_sample && typeof result.golden_sample === "object" ? cloneQuoteSnapshot(result.golden_sample) : null, goldenValidation: result.golden_validation && typeof result.golden_validation === "object" ? cloneQuoteSnapshot(result.golden_validation) : null,
          routeCandidates: Array.isArray(result.route_candidates) ? cloneQuoteSnapshot(result.route_candidates) : [], routeComparison: result.route_comparison && typeof result.route_comparison === "object" ? cloneQuoteSnapshot(result.route_comparison) : null
        };
      }
      function mergeFormalQuotes(items = []) {
        const local = state.quoteHistory || [];
        const byServerId = new Map(local.filter(item => item.serverId).map(item => [Number(item.serverId), item]));
        const byQuoteNo = new Map(local.filter(item => item.quoteNo).map(item => [item.quoteNo, item]));
        items.forEach(item => {
          const cloud = cloudQuoteToLocal(item);
          const existing = byServerId.get(cloud.serverId) || byQuoteNo.get(cloud.quoteNo);
          if (existing) Object.assign(existing, cloud);
          else local.push(cloud);
        });
        state.quoteHistory = local
          .sort((a, b) => (Date.parse(b.createdAt || b.time || "") || 0) - (Date.parse(a.createdAt || a.time || "") || 0))
          .slice(0, 100);
        saveState();
        renderQuoteHistory();
        renderDashboard();
        renderQuoteProfessionalRecord(activeQuoteRecord()); renderQuoteReliabilityPanel(activeQuoteRecord());
      }
      async function loadFormalQuotes() {
        await ensureFormalLogin();
        const data = await api("/api/quotes?limit=100");
        mergeFormalQuotes(data.quotes || []);
        return data;
      }
      function formalTaskToLocal(item) {
        const product = String(item.title || "跟进任务").replace(/\s*报价后跟进$/, "").replace(/\s*跟进$/, "");
        return {
          id: `cloud-${item.id}`,
          serverId: Number(item.id),
          customer: product && !String(item.customer || "").includes(product)
            ? `${item.customer || "新客户"} · ${product}`
            : (item.customer || product || "未命名客户"),
          cloudCustomer: item.customer || "新客户",
          title: item.title || `${product} 跟进`,
          status: item.status || "询盘",
          nextAction: item.next_action || "继续跟进下一步确认",
          owner: item.owner || "业务",
          ownerUserId: Number(item.owner_user_id || 0),
          due: item.due || "待排",
          nextFollow: item.next_follow || item.due || "待设置",
          amount: Number(item.amount || 0),
          risk: item.risk || "暂无风险备注",
          source: item.source || "cloud",
          quoteId: item.quote_id || null,
          industryCategory: item.industry_category || "",
          industryStrategyId: item.industry_strategy_id || "",
          industryFollowupPlan: item.industry_followup_plan && typeof item.industry_followup_plan === "object" ? item.industry_followup_plan : null,
          currentIndustryStage: item.current_industry_stage && typeof item.current_industry_stage === "object" ? item.current_industry_stage : null,
          productionProduct: item.production_product || product || "",
          productionProcess: item.production_process || "",
          process: item.production_process || "",
          productionSteps: Array.isArray(item.production_steps) ? item.production_steps : [],
          purchaseOrders: Array.isArray(item.purchase_orders) ? item.purchase_orders : [],
          purchaseSummary: item.purchase_summary && typeof item.purchase_summary === "object" ? item.purchase_summary : {},
          inventorySummary: item.inventory_summary && typeof item.inventory_summary === "object" ? item.inventory_summary : {},
          factoryOperations: item.factory_operations && typeof item.factory_operations === "object" ? item.factory_operations : {},
          factorySummary: item.factory_summary && typeof item.factory_summary === "object" ? item.factory_summary : {},
          profitReview: item.profit_review && typeof item.profit_review === "object" ? item.profit_review : {},
          factorySchedule: item.factory_schedule && typeof item.factory_schedule === "object" ? item.factory_schedule : {},
          scheduleAlerts: item.schedule_alerts && typeof item.schedule_alerts === "object" ? item.schedule_alerts : {},
          deletedAt: item.deleted_at || "",
          deletedBy: item.deleted_by_name || "",
          trashExpiresAt: item.trash_expires_at || "",
          trashDaysRemaining: Number(item.trash_days_remaining ?? TASK_RECYCLE_RETENTION_DAYS),
          createdAt: item.created_at || new Date().toISOString(),
          updatedAt: item.updated_at || "",
          logs: Array.isArray(item.logs) && item.logs.length
            ? item.logs
            : [`${item.updated_at || item.created_at || ""} 云端跟单已同步`]
        };
      }
      function mergeFormalTasks(items = []) {
        items.forEach(item => {
          const mapped = formalTaskToLocal(item);
          const existing = (state.tasks || []).find(task =>
            Number(task.serverId || 0) === mapped.serverId ||
            (taskBelongsToCurrentCompany(task) && mapped.source === "quote_auto" && task.source === "quote_auto" && task.title === mapped.title && String(task.customer || "").includes(mapped.cloudCustomer))
          );
          if (existing) {
            const localLogs = existing.logs || [];
            const localProductionProcess = existing.productionProcess || existing.process || "";
            const localProductionProduct = existing.productionProduct || "";
            Object.assign(existing, mapped, { logs: dedupeTaskLogs([...mapped.logs, ...localLogs]).slice(0, 20) });
            if (!mapped.productionProcess && localProductionProcess) {
              existing.productionProcess = localProductionProcess;
              existing.process = localProductionProcess;
            }
            if (!mapped.productionProduct && localProductionProduct) existing.productionProduct = localProductionProduct;
          } else {
            state.tasks.unshift(mapped);
          }
        });
        const seen = new Set();
        state.tasks = (state.tasks || []).filter(task => {
          const key = task.serverId
            ? `server-${task.serverId}`
            : `local-${task.localCompanyId || "legacy"}-${task.source || "local"}-${task.customer}-${task.title}-${task.status}`;
          if (seen.has(key)) return false;
          seen.add(key);
          return true;
        });
        saveState();
        renderTasks();
        renderDashboard();
        renderCustomerWorkbench();
      }
      async function loadFormalTasks() {
        const requestId = ++formalTaskLoadRequestId;
        await ensureFormalLogin();
        const companyId = String(window.__currentUser?.company_id || "");
        const snapshot = await api("/api/tasks/snapshot");
        const data = { ok:snapshot.ok, tasks:snapshot.tasks || [], snapshot_at:snapshot.snapshot_at || "" };
        if (requestId !== formalTaskLoadRequestId || companyId !== String(window.__currentUser?.company_id || "")) {
          return { ...data, stale:true };
        }
        const recycleData = { ok:true, tasks:canUseTaskRecycleBin() ? (snapshot.trash || []) : [] };
        // 服务端任务已按当前工厂隔离并持久化；刷新时移除本机已经被服务端删除的旧任务，
        // 但保留尚未同步、没有 serverId 的离线手工任务。
        const currentServerIds = new Set((data.tasks || []).map(item => Number(item.id || 0)).filter(Boolean));
        state.tasks = (state.tasks || []).filter(task => !task.serverId || currentServerIds.has(Number(task.serverId)));
        window.__formalTaskCacheCompanyId = companyId;
        formalTaskRecycleItems = (recycleData.tasks || []).map(formalTaskToLocal);
        refreshCombinedTaskRecycleItems();
        mergeFormalTasks(data.tasks || []);
        renderTaskRecycleBin();
        return data;
      }
      window.__saveHomeChatTaskConnected = async function (session = activeCustomerSession(), options = {}) {
        if (!session) throw new Error("没有找到当前客户会话");
        const silent = options?.silent === true;
        await ensureFormalLogin();
        // 保存前刷新云端跟单，避免本机缓存未加载完整时为已报价客户
        // 再建一条低阶段的重复任务。
        await loadFormalTasks().catch(() => null);
        const source = homeChatTaskSource(session);
        const existing = homeChatTaskForSession(session);
        if (existing) {
          session.followupTaskId = Number(existing.serverId || session.followupTaskId || 0);
          session.chatActionStatus = "已自动记录，报价和生产进度都会更新到这里";
          session.chatAutoRecordPending = false;
          session.chatAutoRecordFailed = false;
          saveState();
          renderHomeChatRecordActions();
          return existing;
        }
        const latestBubbleText = localUserBubbleSourceText(document.getElementById("homeUserMessage"));
        const sourceText = String(
          session.businessSourceText
          || window.__latestAiSourceText
          || latestBubbleText
          || session.lastMessage
          || ""
        ).trim();
        const storedAnalysis = session.businessAnalysis || window.__latestAiAnalysis;
        const analysis = storedAnalysis && typeof storedAnalysis === "object"
          ? { ...storedAnalysis }
          : analyze(sourceText || "客户沟通后需要继续跟进");
        analysis.source_text = analysis.source_text || sourceText;
        analysis.missing = Array.isArray(analysis.missing) ? analysis.missing : [];
        analysis.risk = Array.isArray(analysis.risk) ? analysis.risk : [];
        const customer = String(session.customer || currentCustomerName() || "新客户").trim() || "新客户";
        const currentCustomerStage = customerSessionStatus(customer, session);
        const newTaskStatus = customerTaskStageRank(currentCustomerStage) >= customerTaskStageRank("已报价")
          ? currentCustomerStage
          : analysis.missing.length ? "待补资料" : "询盘";
        const data = await api("/api/tasks", {
          method: "POST",
          body: JSON.stringify({
            analysis,
            customer,
            status: newTaskStatus,
            next_action: "查看本次聊天并安排下一步跟进",
            source
          })
        });
        if (data.task) mergeFormalTasks([data.task]);
        else await loadFormalTasks();
        const saved = homeChatTaskForSession(session)
          || (data.task_id ? (state.tasks || []).find(task => Number(task.serverId || 0) === Number(data.task_id)) : null);
        const serverId = Number(data.task?.id || data.task_id || saved?.serverId || 0);
        if (serverId) session.followupTaskId = serverId;
        session.chatActionStatus = "已自动记录，报价和生产进度都会更新到这里";
        session.chatAutoRecordPending = false;
        session.chatAutoRecordFailed = false;
        saveState();
        renderCustomerWorkbench();
        renderFormalStatus(data.cloud?.ok ? "客户沟通和生意进度已同步云端" : "客户沟通和生意进度已保存");
        if (!silent) notify(data.created === false ? "这笔生意已经记录，不会重复创建。" : "生意进度已记录，仍可在首页继续聊天。" );
        return saved || data.task || { serverId };
      };
      window.__clearHomeTodayChatConnected = async function (session = activeCustomerSession()) {
        if (!session) throw new Error("没有找到当前客户会话");
        await ensureFormalLogin();
        const sessionId = session.id;
        const customer = String(session.customer || currentCustomerName() || "新客户").trim() || "新客户";
        const draft = document.getElementById("aiQuestion")?.value || session.draft || "";
        const preservedFiles = new Map();
        const rememberFiles = (ids = [], names = []) => {
          (Array.isArray(ids) ? ids : []).forEach((value, index) => {
            const id = Number(value || 0);
            if (id <= 0) return;
            const name = String((Array.isArray(names) ? names[index] : "") || "").trim();
            const existing = preservedFiles.get(id) || "";
            preservedFiles.set(id, existing || name);
          });
        };
        rememberFiles(session.retainedFileIds, session.retainedFileNames);
        rememberFiles(session.userFileIds, session.userFileNames);
        rememberFiles(homeRetainedFileIds(), homeRetainedFileNames());
        const retainedFileIds = Array.from(preservedFiles.keys());
        const retainedFileNames = retainedFileIds.map((id, index) => preservedFiles.get(id) || `历史图片 ${index + 1}`);
        const data = await api("/api/conversations/clear-today", {
          method: "POST",
          body: JSON.stringify({ customer, confirm: true })
        });
        const remainingMessages = Array.isArray(data.messages)
          ? data.messages
          : Array.isArray(data.remaining_messages)
            ? data.remaining_messages
            : [];
        const targetSession = (state.customerSessions || []).find(item => item.id === sessionId) || session;
        const followupTaskId = targetSession.followupTaskId;
        targetSession.chatHistoryHtml = "";
        targetSession.userMessageHtml = "";
        targetSession.userFileIds = [];
        targetSession.userFileNames = [];
        targetSession.userFileRecords = [];
        targetSession.userHidden = true;
        targetSession.answerHtml = "";
        targetSession.nextActionsHtml = "";
        targetSession.nextActionsHidden = true;
        targetSession.resultEmpty = true;
        targetSession.hasConversation = false;
        targetSession.chatScrollTop = 0;
        targetSession.businessAnalysis = null;
        targetSession.businessSourceText = "";
        targetSession.retainedFileIds = [...retainedFileIds];
        targetSession.retainedFileNames = [...retainedFileNames];
        targetSession.draft = draft;
        targetSession.lastMessage = remainingMessages.length ? "已保留更早的历史沟通" : "等待客户消息";
        targetSession.followupTaskId = followupTaskId;
        targetSession.chatActionStatus = `已清空今天聊天（${Number(data.deleted || 0)} 条）；档案、报价、跟单和文件均保留`;
        targetSession.updatedAt = new Date().toISOString();
        saveState();
        if (activeCustomerSession()?.id === sessionId) {
          startNewCustomerConversation(true, true);
          // startNewCustomerConversation 会先归档旧 DOM；随后再次清理当前会话缓存，
          // 避免旧的当天聊天被 captureActiveCustomerSession 写回本机。
          Object.assign(targetSession, {
            chatHistoryHtml: "",
            userMessageHtml: "",
            userFileIds: [],
            userFileNames: [],
            userFileRecords: [],
            userHidden: true,
            answerHtml: "",
            nextActionsHtml: "",
            nextActionsHidden: true,
            resultEmpty: true,
            hasConversation: false,
            chatScrollTop: 0,
            businessAnalysis: null,
            businessSourceText: "",
            retainedFileIds: [...retainedFileIds],
            retainedFileNames: [...retainedFileNames],
            draft,
            lastMessage: remainingMessages.length ? "已保留更早的历史沟通" : "等待客户消息",
            followupTaskId,
            chatActionStatus: `已清空今天聊天（${Number(data.deleted || 0)} 条）；档案、报价、跟单和文件均保留`
          });
          window.__lastRecognizedHomeFileIds = [...retainedFileIds];
          window.__homeRetainedFileNames = [...retainedFileNames];
          renderHomeRetainedFileContext();
          setIfExists("aiQuestion", draft);
          autoGrowHomeQuestion(document.getElementById("aiQuestion"));
          if (remainingMessages.length) restoreServerConversationIntoHome(remainingMessages, customer);
          else {
            renderConnectedMemoryStatus(data, data.cloud?.ok === true);
            captureActiveCustomerSession();
          }
        }
        saveState();
        renderCustomerWorkbench();
        renderFormalStatus(`今天聊天已清空 ${Number(data.deleted || 0)} 条 · 客户档案、报价、跟单和文件均保留`);
        notify(Number(data.deleted || 0) > 0 ? "今天的聊天已清空，其他业务资料都已保留。" : "今天没有可清空的聊天，其他资料未做改动。" );
        return data;
      };
      async function moveFormalTask(task, action) {
        if (!task?.serverId || isStaticFileMode) return;
        try {
          const data = await api(`/api/tasks/${task.serverId}/${action}`, { method: "POST", body: "{}" });
          if (data.task) mergeFormalTasks([data.task]);
          else if (data.status) task.status = data.status;
          renderFormalStatus(data.cloud?.ok ? "跟单状态已同步云端" : "跟单状态已保存到服务端");
          if (!data.task) await loadFormalTasks();
          await loadNotifications(true).catch(() => null);
        } catch (err) {
          await loadFormalTasks().catch(() => null);
          renderFormalStatus("跟单状态同步提醒：" + err.message);
          notify("跟单状态未保存：" + err.message);
        }
      }
      async function trashFormalTask(task) {
        if (!task?.serverId || isStaticFileMode) return;
        const mutationCompanyId = String(window.__currentUser?.company_id || "");
        const mutationGeneration = ++formalTaskMutationGeneration;
        let data;
        try {
          data = await api(`/api/tasks/${task.serverId}/trash`, {
            method: "POST",
            body: JSON.stringify({ confirm:true })
          });
        } catch (err) {
          if (mutationGeneration !== formalTaskMutationGeneration || mutationCompanyId !== String(window.__currentUser?.company_id || "")) {
            loadFormalTasks().catch(() => null);
            return;
          }
          renderFormalStatus("跟单删除失败：" + err.message);
          notify("没有删除：" + err.message);
          return;
        }
        if (mutationGeneration !== formalTaskMutationGeneration || mutationCompanyId !== String(window.__currentUser?.company_id || "")) {
          loadFormalTasks().catch(() => null);
          return;
        }
        formalTaskLoadRequestId += 1;
        state.tasks = (state.tasks || []).filter(item => Number(item.serverId || 0) !== Number(task.serverId));
        const deleted = data.task ? formalTaskToLocal(data.task) : { ...task, deletedAt:new Date().toISOString() };
        formalTaskRecycleItems = [deleted, ...formalTaskRecycleItems.filter(item => Number(item.serverId || 0) !== Number(task.serverId))];
        refreshCombinedTaskRecycleItems();
        saveState();
        renderTasks();
        renderDashboard();
        resetTaskDetailOutput("跟单已移入回收站，可在本页底部恢复。客户聊天、报价、图片和文件都没有删除。");
        renderFormalStatus("跟单已移入回收站");
        notify(data.idempotent ? "这条跟单已经在回收站中。" : "已移入回收站，需要时可以恢复。" );
        try {
          await loadFormalTasks();
        } catch (err) {
          renderFormalStatus("跟单已移入回收站，但列表刷新失败：" + err.message);
          notify("删除已完成，列表暂时没有刷新；稍后点“刷新任务与回收站”即可。" );
        }
      }
      async function restoreFormalTask(task) {
        if (!task?.serverId || isStaticFileMode) return;
        const mutationCompanyId = String(window.__currentUser?.company_id || "");
        const mutationGeneration = ++formalTaskMutationGeneration;
        let data;
        try {
          data = await api(`/api/tasks/${task.serverId}/restore`, {
            method: "POST",
            body: JSON.stringify({ confirm:true })
          });
        } catch (err) {
          if (mutationGeneration !== formalTaskMutationGeneration || mutationCompanyId !== String(window.__currentUser?.company_id || "")) {
            loadFormalTasks().catch(() => null);
            return;
          }
          renderFormalStatus("跟单恢复失败：" + err.message);
          notify("没有恢复：" + err.message);
          return;
        }
        if (mutationGeneration !== formalTaskMutationGeneration || mutationCompanyId !== String(window.__currentUser?.company_id || "")) {
          loadFormalTasks().catch(() => null);
          return;
        }
        formalTaskLoadRequestId += 1;
        formalTaskRecycleItems = formalTaskRecycleItems.filter(item => Number(item.serverId || 0) !== Number(task.serverId));
        refreshCombinedTaskRecycleItems();
        if (data.task) mergeFormalTasks([data.task]);
        const restored = (state.tasks || []).find(item => Number(item.serverId || 0) === Number(task.serverId));
        if (restored) {
          taskFilter = taskFilterForStatus(restored.status);
          renderTasks();
          showTaskDetail(restored.id);
        }
        renderFormalStatus("跟单已从回收站恢复");
        notify(data.idempotent ? "这条跟单已经在任务列表中。" : "跟单已恢复到原来的位置。" );
        try {
          await loadFormalTasks();
        } catch (err) {
          renderFormalStatus("跟单已恢复，但列表刷新失败：" + err.message);
          notify("恢复已完成，列表暂时没有刷新；稍后点“刷新任务与回收站”即可。" );
        }
      }
      async function updateFormalTaskStep(task, action, payload = {}) {
        if (!task?.serverId || isStaticFileMode) return { ok:false, skipped:true };
        try {
          const data = await api(`/api/tasks/${task.serverId}/steps/${action}`, {
            method: "POST",
            body: JSON.stringify(payload)
          });
          if (data.task) mergeFormalTasks([data.task]);
          renderFormalStatus(data.cloud?.ok ? "生产进度已同步云端" : "生产进度已保存到服务端");
          return data;
        } catch (err) {
          await loadFormalTasks().catch(() => null);
          renderFormalStatus("生产进度同步提醒：" + err.message);
          notify("生产进度未保存：" + err.message);
          return { ok:false, error:err.message };
        }
      }
      async function updateFormalTaskPurchase(task, action, payload = {}) {
        if (!task?.serverId || isStaticFileMode) return { ok:false, skipped:true };
        try {
          const data = await api(`/api/tasks/${task.serverId}/purchases/${action}`, {
            method: "POST",
            body: JSON.stringify(payload)
          });
          if (data.task) mergeFormalTasks([data.task]);
          renderFormalStatus(data.cloud?.ok ? "按单采购已同步云端" : "按单采购已保存到服务端");
          if (data.task && selectedTaskDetailId) showTaskDetail(selectedTaskDetailId);
          return data;
        } catch (err) {
          await loadFormalTasks().catch(() => null);
          renderFormalStatus("采购记录同步提醒：" + err.message);
          throw err;
        }
      }
      async function updateFormalTaskOperations(task, section, payload = {}) {
        if (!task?.serverId || isStaticFileMode) return { ok:false, skipped:true };
        try {
          const data = await api(`/api/tasks/${task.serverId}/operations/${section}`, {
            method:"POST",
            body:JSON.stringify(payload)
          });
          if (data.task) mergeFormalTasks([data.task]);
          renderFormalStatus(data.cloud?.ok ? "生产经营记录已同步云端" : "生产经营记录已保存到服务端");
          return data;
        } catch (err) {
          await loadFormalTasks().catch(() => null);
          renderFormalStatus("生产经营记录同步提醒：" + err.message);
          notify("记录未保存：" + err.message);
          return { ok:false, error:err.message };
        }
      }
      async function reverseFormalTaskPayment(task, paymentId, reason) {
        if (!task?.serverId || isStaticFileMode) return { ok:false, skipped:true };
        try {
          const data = await api(`/api/tasks/${task.serverId}/payments/reverse`, {
            method:"POST",
            body:JSON.stringify({ payment_id:paymentId, reason, confirm:true })
          });
          if (data.task) mergeFormalTasks([data.task]);
          return data;
        } catch (err) {
          notify("收款冲正失败：" + err.message);
          return { ok:false, error:err.message };
        }
      }
      async function loadTaskConfirmations(task) {
        if (!task?.serverId || isStaticFileMode) return [];
        const data = await api(`/api/tasks/${task.serverId}/confirmations`);
        return data.confirmations || [];
      }
      async function createTaskConfirmation(task, expiresDays = 7) {
        if (!task?.serverId || isStaticFileMode) return { ok:false, skipped:true };
        try {
          return await api(`/api/tasks/${task.serverId}/confirmations`, {
            method:"POST",
            body:JSON.stringify({ expires_days:expiresDays })
          });
        } catch (err) {
          notify("确认链接生成失败：" + err.message);
          return { ok:false, error:err.message };
        }
      }
      async function saveFormalTask(task) {
        if (!task || isStaticFileMode) return;
        try {
          await ensureFormalLogin();
          const product = task.title || task.customer || "手动跟单";
          const data = await api("/api/tasks", {
            method: "POST",
            body: JSON.stringify({
              analysis: {
                product,
                process: task.process || "",
                industry_category: task.industryCategory || "",
                industry_strategy_id: task.industryStrategyId || "",
                missing: [],
                risk: task.risk ? [task.risk] : []
              },
              customer: task.cloudCustomer || task.customer || "新客户",
              status: task.status,
              next_action: task.nextAction,
              owner: task.owner,
              due: task.due,
              next_follow: task.nextFollow,
              amount: Number(task.amount || 0),
              source: task.source || "manual",
              process: task.process || "",
              industry_category: task.industryCategory || "",
              industry_strategy_id: task.industryStrategyId || "",
              production_steps: taskProductionSteps(task),
              purchase_orders: typeof window.purchaseOrdersForTask === "function" ? window.purchaseOrdersForTask(task) : [],
              factory_operations: factoryOperationsForTask(task)
            })
          });
          if (data.task) {
            // 这条本机任务就是刚提交的对象：直接把服务端编号和标准化字段
            // 回填到它身上，避免异步保存完成时列表短暂出现一条本机副本和
            // 一条云端副本。随后再走统一合并逻辑，刷新后也只保留一条。
            const mapped = formalTaskToLocal(data.task);
            const localLogs = task.logs || [];
            Object.assign(task, mapped, {
              logs: dedupeTaskLogs([...mapped.logs, ...localLogs]).slice(0, 20)
            });
            mergeFormalTasks([data.task]);
          }
          renderFormalStatus(data.cloud?.ok ? "跟单已同步云端" : "跟单已保存到服务端");
        } catch (err) {
          renderFormalStatus("跟单保存提醒：" + err.message);
        }
      }
      async function setFormalTaskOutcome(task) {
        if (!task?.serverId || isStaticFileMode) return;
        try {
          const data = await api(`/api/tasks/${task.serverId}/outcome`, {
            method: "POST",
            body: JSON.stringify({
              status: task.status,
              next_action: task.nextAction,
              owner: task.owner,
              due: task.due,
              next_follow: task.nextFollow,
              amount: Number(task.amount || 0),
              note: (task.logs || []).slice(-1)[0] || "客户成交结果已更新"
            })
          });
          if (data.task) mergeFormalTasks([data.task]);
          renderFormalStatus(data.cloud?.ok ? "成交结果与跟单已同步云端" : "成交结果已保存到服务端");
        } catch (err) {
          renderFormalStatus("成交结果已保存在本机 · 云端同步提醒：" + err.message);
        }
      }
      async function saveQuoteOutcomeCloud(record, outcome) {
        if (!record?.serverId || isStaticFileMode) return;
        try {
          const data = await api(`/api/quotes/${record.serverId}/outcome`, {
            method: "POST",
            body: JSON.stringify(outcome)
          });
          renderFormalStatus(data.cloud?.ok ? "报价结果已同步云端" : "报价结果已保存到服务端");
        } catch (err) {
          renderFormalStatus("报价结果已保存在本机 · 云端同步提醒：" + err.message);
        }
      }
      async function approveQuoteCloud(record) {
        if (isStaticFileMode || !record?.serverId) throw new Error("报价尚未保存到服务端");
        try {
          await ensureFormalLogin();
          const data = await api(`/api/quotes/${record.serverId}/approve`, {
            method: "POST",
            body: JSON.stringify({ approved_by: record.approvedBy || "老板", approval_note: record.approvalNote || "" })
          });
          renderFormalStatus(data.cloud?.ok ? "报价确认已同步云端" : "报价确认已保存到服务端");
          return data;
        } catch (err) {
          renderFormalStatus("报价确认同步提醒：" + err.message);
          throw err;
        }
      }
      async function selectHomeCustomerConnected() {
        const version = ++customerMemoryLoadVersion;
        const name = (document.getElementById("homeCustomerName")?.value || "新客户").trim() || "新客户";
        const activeSession = activeCustomerSession();
        const activeSessionId = String(activeSession?.id || "");
        const activeSessionName = String(activeSession?.customer || "新客户").trim() || "新客户";
        const existingTargetSession = (state.customerSessions || []).find(
          item => item.id !== activeSession?.id && item.customer === name
        );
        const activeSessionHasLocalWork = Boolean(
          activeSession?.hasConversation
          || String(activeSession?.draft || "").trim()
          || String(activeSession?.chatHistoryHtml || "").trim()
          || (Array.isArray(activeSession?.userFileIds) && activeSession.userFileIds.length)
          || document.getElementById("aiQuestion")?.value.trim()
          || document.getElementById("homeFiles")?.files?.length
        );
        // Selecting a name that already owns a conversation must activate that
        // conversation, not rename the current formal customer and create a
        // second session with the same name. A pending inquiry with actual
        // content still follows the merge flow below so its messages are kept.
        if (
          existingTargetSession
          && activeSessionName !== name
          && (!isPendingCustomerName(activeSessionName) || !activeSessionHasLocalWork)
        ) {
          captureActiveCustomerSession(false, activeSessionName);
          delete existingTargetSession.archivedAt;
          existingTargetSession.updatedAt = new Date().toISOString();
          state.activeCustomerSessionId = existingTargetSession.id;
          saveState();
          restoreCustomerSession(existingTargetSession);
          renderCustomerWorkbench();
          notify(`已切换到“${name}”的已有会话。`);
          return;
        }
        if (
          activeSession
          && isPendingCustomerName(activeSessionName)
          && activeSessionName !== name
          && name !== "新客户"
          && !isPendingCustomerName(name)
        ) {
          activeSession.renameFrom = activeSessionName;
          activeSession.customer = name;
        }
        const renameRequestIsCurrent = () => (
          version === customerMemoryLoadVersion
          && (!activeSessionId || state.activeCustomerSessionId === activeSessionId)
        );
        const renameFrom = String(activeSession?.renameFrom || "").trim();
        let renamedSameSession = false;
        if (renameFrom && renameFrom !== name && !isPendingCustomerName(name) && !["新客户", "未命名客户"].includes(name)) {
          try {
            await ensureFormalLogin();
            const renamed = await api("/api/customers/rename", {
              method: "POST",
              body: JSON.stringify({ old_name: renameFrom, new_name: name })
            });
            const renameStillCurrent = renameRequestIsCurrent();
            if (activeSession) delete activeSession.renameFrom;
            state.customerSessions = existingTargetSession && !renameStillCurrent
              ? (state.customerSessions || []).filter(item => item.id !== activeSession?.id)
              : (state.customerSessions || []).filter(item => item.id === activeSession?.id || item.customer !== name);
            state.customerProfiles = (state.customerProfiles || []).filter(item => item.company !== renameFrom);
            state.quoteHistory = (state.quoteHistory || []).map(item => item.customer === renameFrom ? { ...item, customer: name } : item);
            state.tasks = (state.tasks || []).map(item => {
              if ((item.cloudCustomer || String(item.customer || "").split(" · ")[0]) !== renameFrom) return item;
              return { ...item, cloudCustomer: name, customer: String(item.customer || "").replace(renameFrom, name) };
            });
            if (renamed.customer) mergeCloudCustomerProfiles([renamed.customer]);
            renamedSameSession = true;
            saveState();
            renderCustomerWorkbench();
            renderDashboard();
            if (renameRequestIsCurrent()) notify(`已将 ${renameFrom} 的全部资料合并到 ${name}。`);
          } catch (err) {
            if (/没有找到待命名客户资料|客户记录不存在/.test(String(err?.message || ""))) {
              // 客户在第一条消息发送前就填写了姓名：云端还没有临时档案可改名，
              // 这不是失败，直接以正式姓名创建即可，避免会话又退回“待命名客户”。
              if (activeSession) {
                activeSession.customer = name;
                delete activeSession.renameFrom;
              }
              if (existingTargetSession && renameRequestIsCurrent()) {
                state.customerSessions = (state.customerSessions || []).filter(
                  item => item.id === activeSession?.id || item.customer !== name
                );
              }
              renamedSameSession = true;
              saveState();
              renderCustomerWorkbench();
              if (!renameRequestIsCurrent()) return;
              setIfExists("homeCustomerName", name);
              setIfExists("inquiryCustomerName", name);
            } else {
              if (activeSession && String(activeSession.customer || "") === name) activeSession.customer = renameFrom;
              saveState();
              renderCustomerWorkbench();
              if (!renameRequestIsCurrent()) return;
              setIfExists("homeCustomerName", renameFrom);
              setIfExists("inquiryCustomerName", renameFrom);
              const failedStatus = document.getElementById("homeMemoryStatus");
              if (failedStatus) failedStatus.textContent = `补充姓名失败：${err.message}`;
              return;
            }
          }
          if (!renameRequestIsCurrent()) return;
        }
        setIfExists("inquiryCustomerName", name);
        // 用户可能刚填完客户名就立刻发送。此时客户记忆的防抖读取会稍后触发，
        // 不能在 AI 正在处理时把本次原话和附件清空，否则“下一步报价”会失去来源。
        const aiIsSending = !!document.querySelector(".gpt-send")?.disabled;
        const customerChanged = name !== String(window.__selectedHomeCustomerName || "新客户");
        window.__selectedHomeCustomerName = name;
        if (!aiIsSending && customerChanged && !renamedSameSession) {
          const hasDraft = !!document.getElementById("aiQuestion")?.value.trim();
          const hasFiles = !!document.getElementById("homeFiles")?.files?.length;
          startNewCustomerConversation(true, hasDraft || hasFiles);
        }
        const status = document.getElementById("homeMemoryStatus");
        if (status) {
          status.textContent = name === "新客户" ? "新客户模式：发送后再建立档案" : "正在读取云端客户记忆…";
          status.classList.remove("is-saved");
        }
        if (name === "新客户") return;
        try {
          await ensureFormalLogin();
          const data = await api(`/api/customers/memory?name=${encodeURIComponent(name)}&limit=30`);
          if (version !== customerMemoryLoadVersion) return;
          if (data.customer) mergeCloudCustomerProfiles([data.customer]);
          window.__activeCustomerMemory = data;
          renderConnectedMemoryStatus(data, data.cloud?.ok ?? null);
          restoreServerConversationIntoHome(data.messages || [], name);
        } catch (err) {
          if (version !== customerMemoryLoadVersion) return;
          if (status) status.textContent = `客户记忆暂时读取失败：${err.message}`;
        }
      }
      async function saveCustomerProfileCloud(profile) {
        if (isStaticFileMode || !profile?.company) return;
        try {
          await ensureFormalLogin();
          const data = await api("/api/customers", {
            method: "POST",
            body: JSON.stringify({
              name: profile.company,
              contact: profile.contact,
              level: profile.level,
              deal_status: profile.dealStatus,
              need: profile.need,
              preference: profile.preference,
              design_learning_enabled: profile.designLearningEnabled !== false,
              notes: profile.note,
              actual_deal_total: Number(profile.actualDealTotal || 0),
              actual_deal_qty: Number(profile.actualDealQty || 0),
              outcome_reason: profile.outcomeReason || "",
              deal_notes: profile.dealNotes || "",
              last_deal_at: profile.lastDealAt || ""
              ,pricing: profile.pricing || {}
            })
          });
          mergeCloudCustomerProfiles([data.customer]);
          renderFormalStatus(data.cloud?.ok ? "客户档案已保存到云端" : "客户档案已保存到本机 · 云端暂不可用");
        } catch (err) {
          renderFormalStatus("客户档案已保存在本机 · 云端同步提醒：" + err.message);
        }
      }
      async function recordCustomerDesignFeedback(payload = {}) {
        const customer = String(payload.customer || currentCustomerName() || "新客户").trim();
        if (!customer || ["新客户", "未命名客户"].includes(customer)) return null;
        await ensureFormalLogin();
        const data = await api("/api/customers/design-feedback", {
          method: "POST",
          body: JSON.stringify({ ...payload, customer })
        });
        if (data.customer) mergeCloudCustomerProfiles([data.customer]);
        return data;
      }
      async function clearCustomerDesignLearningConnected(customer) {
        await ensureFormalLogin();
        const data = await api("/api/customers/design-feedback", {
          method: "POST",
          body: JSON.stringify({ customer, action: "clear" })
        });
        if (data.customer) mergeCloudCustomerProfiles([data.customer]);
        return data;
      }
      async function loadCustomerProfileMemory(company) {
        const output = document.getElementById("profileOutput");
        if (output) output.textContent = `正在读取 ${company} 的云端聊天与报价记忆…`;
        try {
          await ensureFormalLogin();
          const data = await api(`/api/customers/memory?name=${encodeURIComponent(company)}&limit=30`);
          const messages = data.messages || [];
          const quotes = data.quotes || [];
          const designLearning = data.design_learning || {};
          const transcript = messages.slice(-10).map(item => {
            const speaker = item.role === "customer" ? "客户" : item.role === "internal" ? "内部记录" : "AI助手";
            return `${speaker}：${item.content}`;
          }).join("\n");
          if (output) {
            const dealText = Number(data.customer?.actual_deal_total || 0) > 0
              ? `\n实际成交：¥${Number(data.customer.actual_deal_total).toFixed(2)} · ${Number(data.customer.actual_deal_qty || 0).toLocaleString()}个`
              : "";
            const reasonText = data.customer?.outcome_reason ? `\n结果原因：${data.customer.outcome_reason}` : "";
            const designText = designLearning.enabled === false
              ? "制图偏好学习：已关闭"
              : `制图偏好学习：已开启 · ${designLearning.summary || "等待客户改图或确认结果"} · ${Number(designLearning.feedback_count || 0)}条制图反馈`;
            output.textContent = `${company} · 长期记忆\n\n${data.customer?.memory_summary || "尚未形成摘要"}${dealText}${reasonText}\n${designText}\n\n沟通记录 ${messages.length} 条 · 历史报价 ${quotes.length} 次\n\n${transcript || "还没有聊天记录。回到首页选择该客户并发送消息后，系统会自动保存。"}`;
          }
          if (data.customer) mergeCloudCustomerProfiles([data.customer]);
        } catch (err) {
          if (output) output.textContent = `客户记忆读取失败：${err.message}`;
        }
      }
      function ensureFormalOpsPanel() {
        if (document.getElementById("formalOpsPanel")) return;
        const style = document.createElement("style");
        style.textContent = `
          .formal-ops-mask{position:fixed;inset:0;background:rgba(15,23,42,.22);z-index:60;display:none;align-items:flex-start;justify-content:flex-end;padding:76px 24px}
          .formal-ops-mask.show{display:flex}
          .formal-ops-panel{width:min(440px,calc(100vw - 32px));background:#fff;border:1px solid #e5e7eb;border-radius:12px;box-shadow:0 24px 70px rgba(15,23,42,.18);padding:18px}
          .formal-ops-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}
          .formal-ops-head strong{font-size:18px}
          .formal-ops-close{border:0;background:#f3f4f6;width:34px;height:34px;border-radius:8px;cursor:pointer;font-size:20px}
          .formal-ops-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:12px 0}
          .formal-ops-actions button{border:1px solid #dbe3ef;background:#f8fafc;border-radius:8px;padding:11px 10px;font-weight:800;cursor:pointer;color:#0f172a}
          .formal-ops-actions button.primary{background:#0b74de;color:#fff;border-color:#0b74de}
          .huoshen-image-config{display:none;margin:12px 0;padding:13px;border:1px solid #bfdbfe;background:#eff6ff;border-radius:10px}
          .huoshen-image-config.show{display:block}
          .huoshen-image-config label{display:block;margin:9px 0 4px;font-size:12px;font-weight:800;color:#334155}
          .huoshen-image-config input,.huoshen-image-config select{width:100%;box-sizing:border-box;border:1px solid #cbd5e1;border-radius:8px;background:#fff;padding:10px 11px;font-size:13px}
          .huoshen-image-config .hint{margin:7px 0;color:#64748b;font-size:11px;line-height:1.5}
          .huoshen-image-warning{margin:10px 0;padding:10px 11px;border:1px solid #fdba74;border-radius:9px;background:#fff7ed;color:#8a4307;font-size:11px;line-height:1.55}
          .huoshen-image-warning strong{display:block;margin-bottom:3px;color:#7c2d12;font-size:12px}
          .formal-admin{margin:10px 0;color:#6b7280;font-size:12px}
          .formal-admin summary{cursor:pointer;font-weight:800;list-style:none;text-align:center}
          .formal-admin summary::-webkit-details-marker{display:none}
          .formal-ops-output{white-space:pre-wrap;background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:12px;min-height:120px;max-height:340px;overflow:auto;font-size:13px;line-height:1.65;color:#273142}
          .formal-ops-scores{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-bottom:12px}
          .formal-ops-score{padding:12px;border:1px solid #dbe3ef;border-radius:10px;background:#fff}
          .formal-ops-score span,.formal-ops-score strong,.formal-ops-score em{display:block}
          .formal-ops-score span{color:#64748b;font-size:11px}.formal-ops-score strong{margin:4px 0;font-size:25px}.formal-ops-score em{color:#64748b;font-size:10px;font-style:normal}
          .formal-check-list{display:grid;gap:7px}.formal-check{padding:9px 10px;border-radius:8px;background:#fff;border:1px solid #e5e7eb}
          .formal-check strong{display:block;font-size:12px}.formal-check span{display:block;margin-top:2px;color:#64748b;font-size:10px}.formal-check.pending{border-color:#fed7aa;background:#fff7ed}
          .formal-action-list{margin:10px 0 0;padding-left:18px;color:#9a5700;font-size:11px}
        `;
        document.head.appendChild(style);
        const panel = document.createElement("div");
        panel.id = "formalOpsPanel";
        panel.className = "formal-ops-mask";
        panel.innerHTML = `
          <div class="formal-ops-panel">
            <div class="formal-ops-head">
              <div><strong>系统状态</strong><div class="muted">查看 AI、云端数据、套餐和备份状态。</div></div>
              <button class="formal-ops-close" type="button" aria-label="关闭" onclick="closeFormalOps()">×</button>
            </div>
            <div class="formal-ops-actions">
              <button class="primary" type="button" onclick="activateLicense()">输入激活码</button>
              <button class="primary" type="button" onclick="runSystemCheck()">系统体检</button>
              <button type="button" onclick="downloadFormalBackup()">导出备份</button>
              <button type="button" onclick="createSafetySnapshot()">立即安全快照</button>
              <button type="button" onclick="refreshFormalStatus()">刷新状态</button>
            </div>
            <div class="muted" style="margin:8px 0 12px">图片生成、识图和分析能力由将莱云端统一运维，客户端无需配置接口，也不会显示供应商或密钥信息。</div>
            <div class="formal-ops-output" id="formalOpsOutput">点击“系统体检”可以检查 AI 接口、会员状态、上传目录、数据库和后续 API 接入情况。</div>
          </div>
        `;
        panel.addEventListener("click", event => {
          if (event.target === panel) closeFormalOps();
        });
        document.body.appendChild(panel);
      }
      function openFormalOps() {
        ensureFormalOpsPanel();
        document.getElementById("formalOpsPanel")?.classList.add("show");
        runSystemCheck();
      }
      function closeFormalOps() {
        document.getElementById("formalOpsPanel")?.classList.remove("show");
      }
      function setFormalOpsOutput(text) {
        ensureFormalOpsPanel();
        const output = document.getElementById("formalOpsOutput");
        if (output) {
          output.style.whiteSpace = "pre-wrap";
          output.textContent = text;
        }
      }
      let appUpdatePromptedBuild = "";
      const appUpdateSleep = milliseconds => new Promise(resolve => window.setTimeout(resolve, milliseconds));
      function appUpdateSizeText(bytes = 0) {
        const value = Math.max(0, Number(bytes || 0));
        if (!value) return "";
        return value >= 1024 * 1024 * 1024
          ? `${(value / (1024 * 1024 * 1024)).toFixed(1)} GB`
          : `${Math.max(1, Math.round(value / (1024 * 1024)))} MB`;
      }
      async function installAvailableAppUpdate(version) {
        setFormalOpsOutput(`正在准备更新到 ${version.latest || version.latest_build}…\n\n请保持软件打开，下载、校验完成后会自动重启。客户资料和本机配置不会被删除。`);
        notify("正在后台下载安全更新包…");
        await api("/api/app/update/install", { method:"POST", body:"{}" });
        let restartingSeen = false;
        let missed = 0;
        for (let attempt = 0; attempt < 900; attempt += 1) {
          await appUpdateSleep(1000);
          try {
            const status = await api("/api/app/update/status");
            missed = 0;
            const job = status.update || {};
            const progress = Math.max(0, Math.min(100, Number(job.progress || 0)));
            setFormalOpsOutput(`${job.message || "正在准备更新"}\n\n进度：${progress}%\n目标版本：${status.latest || status.latest_build || "新版本"}\n\n更新完成后软件会自动重启。`);
            if (job.state === "failed") throw new Error(job.message || "更新失败，请稍后重试");
            if (job.state === "restarting") {
              restartingSeen = true;
              notify("更新已校验，软件即将自动重启。");
            }
          } catch (err) {
            missed += 1;
            if (restartingSeen || missed >= 4) {
              notify("软件正在重启完成更新…");
              return;
            }
          }
        }
        throw new Error("更新等待超时，请重新打开软件后检查版本");
      }
      async function handleAppUpdateAvailability(version, { manual = false } = {}) {
        if (!version?.update_available) {
          if (manual) setFormalOpsOutput(version?.check_status === "unavailable" ? version.notes : `当前已是最新版本\n\n版本：${version?.version || "未知"}\n构建：${version?.build || "未知"}`);
          return false;
        }
        if (window.__currentUser?.is_owner !== true) return false;
        const targetBuild = String(version.latest_build || version.latest || "新版本");
        const dismissedKey = `jianglai_update_dismissed_${targetBuild}`;
        if (!manual && (appUpdatePromptedBuild === targetBuild || localStorage.getItem(dismissedKey) === "1")) return false;
        appUpdatePromptedBuild = targetBuild;
        const size = appUpdateSizeText(version.download_size);
        const message = [
          `发现新版本 ${version.latest || targetBuild}${size ? `（${size}）` : ""}。`,
          version.notes || "",
          "",
          version.automatic_install_supported
            ? "是否现在下载并安装？完成后软件会自动重启，客户资料不会被删除。"
            : "当前版本暂不支持自动安装，请联系管理员更新。"
        ].filter((item, index) => item || index === 2).join("\n");
        if (!version.automatic_install_supported) {
          if (manual) setFormalOpsOutput(message);
          return false;
        }
        if (!window.confirm(message)) {
          localStorage.setItem(dismissedKey, "1");
          if (manual) setFormalOpsOutput(`已暂缓更新到 ${version.latest || targetBuild}。软件会在后台继续检查官方更新。`);
          return false;
        }
        localStorage.removeItem(dismissedKey);
        await installAvailableAppUpdate(version);
        return true;
      }
      async function checkForAppUpdate() {
        await ensureFormalLogin();
        setFormalOpsOutput("正在连接官方更新服务…");
        try {
          const version = await api("/api/app/update/check", { method:"POST", body:"{}" });
          await handleAppUpdateAvailability(version, { manual:true });
        } catch (err) {
          setFormalOpsOutput("检查更新失败：" + err.message);
        }
      }
      window.checkForAppUpdate = checkForAppUpdate;
      function setFormalOpsHtml(html) {
        ensureFormalOpsPanel();
        const output = document.getElementById("formalOpsOutput");
        if (output) {
          output.style.whiteSpace = "normal";
          output.innerHTML = html;
        }
      }
      function planName(plan) {
        return ({ trial: "永久免费报价版", standard: "AI跟单版", pro: "团队协作版", enterprise: "企业版（原套餐）", buyout: "工厂买断版", private_buyout: "企业定制版" }[plan]) || plan || "未激活";
      }
      function syncFormalMembership(item) {
        if (!item) return;
        const key = ({ trial: "trial", standard: "standard", pro: "pro", enterprise: "enterprise", buyout: "buyout", private_buyout: "private_buyout" }[item.plan]) || "trial";
        const quotaTotal = Number(item.quota_total ?? planConfig[key]?.quota ?? 0);
        const reportedRemaining = item.points_remaining ?? item.remaining_points;
        const quotaUsed = item.quota_used != null
          ? Number(item.quota_used || 0)
          : Math.max(quotaTotal - Number(reportedRemaining ?? quotaTotal), 0);
        state.member = {
          ...state.member,
          loggedIn: true,
          planKey: key,
          plan: planConfig[key]?.name || planName(item.plan),
          expiresAt: item.expires_at_text || state.member.expiresAt,
          quotaUsed,
          quotaTotal,
          aiBalanceCents: Number(item.ai_balance_cents || 0), painPointAnalysisRemaining: Number(item.pain_point_analysis_remaining || 0),
          role: memberPlanRank({ planKey: key }) >= planConfig.standard.rank ? "工厂管理员" : "管理员"
        };
        saveState();
        renderMemberStatus();
        renderMemberSystem();
        renderVisualQuotaButton();
        syncHomeImagePointCost();
      }
      function renderMembershipUsage(items = [], walletItems = []) {
        const list = document.getElementById("memberUsageList");
        if (!list) return;
        if (!items.length && !walletItems.length) {
          list.innerHTML = `<div class="member-usage-row"><span>尚无扣点记录</span><strong>0点</strong><span>—</span></div>`;
          return;
        }
        const pointRows = items.map(item => `
          <div class="member-usage-row">
            <span>${escapeHtml(item.label || item.feature || "AI服务")} · ${escapeHtml(new Date(item.created_at).toLocaleString("zh-CN", { hour12: false }))}</span>
            <strong>${Number(item.points || 0) > 0 ? `-${Number(item.points || 0)}点` : Number(item.points || 0) < 0 ? `返还 +${Math.abs(Number(item.points || 0))}点` : "套餐变更"}</strong>
            <span>余 ${Number(item.balance_after || 0)}点</span>
          </div>
        `);
        const walletRows = walletItems.map(item => `
          <div class="member-usage-row">
            <span>${escapeHtml(item.label || "AI生图")} · ${escapeHtml(new Date(item.created_at).toLocaleString("zh-CN", { hour12: false }))}</span>
            <strong>-¥${(Number(item.amount_cents || 0) / 100).toFixed(2)}</strong>
            <span>余 ¥${(Number(item.balance_after_cents || 0) / 100).toFixed(2)}</span>
          </div>
        `);
        list.innerHTML = [...walletRows, ...pointRows].join("");
      }
      async function loadMembershipUsage() {
        if (isStaticFileMode) return;
        try {
          await ensureFormalLogin();
          const data = await api("/api/license/usage");
          if (data.membership) syncFormalMembership(data.membership);
          renderMembershipUsage(data.items || [], data.wallet_items || []);
        } catch (err) {
          renderMembershipUsage([]);
        }
      }
      async function requireServerFeatureConnected(featureName, policyId) {
        if (!consumeQuota(featureName, false)) return false;
        if (isStaticFileMode) return true;
        try {
          await ensureFormalLogin();
          const data = await api(`/api/license/access?feature=${encodeURIComponent(policyId)}`);
          if (data.membership) syncFormalMembership(data.membership);
          return true;
        } catch (err) {
          window.__memberReturnPage = document.querySelector(".page.active")?.dataset.page || "工作台";
          showPage("账号套餐");
          const output = document.getElementById("memberOutput");
          if (output) output.textContent = err.message;
          notify(err.message);
          return false;
        }
      }
      const IMAGE_GENERATION_TEST_STORAGE_KEY = "formal_image_generation_last_success_at";
      function imageGenerationVerificationStorage() {
        try {
          return window.localStorage || null;
        } catch (_) {
          return null;
        }
      }
      function lastSuccessfulImageGenerationAt() {
        return imageGenerationVerificationStorage()?.getItem(IMAGE_GENERATION_TEST_STORAGE_KEY) || "";
      }
      function markImageGenerationActuallyVerified() {
        imageGenerationVerificationStorage()?.setItem(IMAGE_GENERATION_TEST_STORAGE_KEY, new Date().toISOString());
      }
      function clearImageGenerationActualVerification() {
        imageGenerationVerificationStorage()?.removeItem(IMAGE_GENERATION_TEST_STORAGE_KEY);
      }
      function isImageGenerationSystemCheck(item = {}) {
        return String(item.name || "").includes("图片生成");
      }
      function imageGenerationActuallyVerified() {
        return Boolean(lastSuccessfulImageGenerationAt());
      }
      function friendlyCheckDetail(item) {
        if (isImageGenerationSystemCheck(item)) {
          const testedAt = lastSuccessfulImageGenerationAt();
          if (item.ok && testedAt) {
            return `实际生图已成功验证 · ${new Date(testedAt).toLocaleString("zh-CN", { hour12: false })}`;
          }
          if (item.ok) return "图片服务已连接；首张实际生图成功后会在这里记录验证时间";
          return item.detail || "图片服务暂时不可用，请稍后重试";
        }
        if (item.ok && (item.name.includes("数据库") || item.name.includes("目录"))) return "读写与校验正常";
        if (item.name.includes("自动数据备份")) return item.ok ? item.detail : "尚未生成，请点击“立即安全快照”";
        if (item.name.includes("会员状态")) {
          return String(item.detail || "")
            .replace("trial", "永久免费报价版")
            .replace("standard", "AI跟单版")
            .replace("pro", "团队协作版");
        }
        if (item.name.includes("云端同步")) {
          return item.ok ? item.detail : "云端暂不可用，当前已安全保存在本机";
        }
        if (item.detail === "not_configured") return "未接入";
        if (String(item.detail || "").includes("未配置")) return "未配置；请接入真实模型后使用";
        return item.detail || "";
      }
      function friendlyAiRouteReason(reason = "", retryAfterSeconds = 0) {
        const labels = {
          healthy:"可用",
          unauthorized:"Key 无效或已失效",
          forbidden:"账号或模型权限不足",
          model_or_endpoint_unavailable:"模型未开通或接口不匹配",
          billing_required:"余额或额度不足",
          bad_request:"请求配置需要检查",
          rate_limited:"调用频率受限",
          upstream_unavailable:"上游服务暂时繁忙",
          transient:"线路暂时波动",
          unverified:"尚未完成真实验证",
          not_configured:"未配置"
        };
        const base = labels[String(reason || "")] || "暂时不可用";
        return Number(retryAfterSeconds || 0) > 0 ? `${base} · 约 ${Number(retryAfterSeconds)} 秒后自动重试` : base;
      }
      function renderVisualQuotaButton() {
        const imageButton = document.getElementById("realVisualButton");
        if (!imageButton) return;
        if (!window.__imageGenerationConfigured) {
          imageButton.disabled = true;
          imageButton.textContent = "AI生成主图（需图片服务）";
          imageButton.title = window.__imageGenerationSetupIssue || "图片服务正在连接将莱云端";
          return;
        }
        const remaining = Math.max(Number(state.member.quotaTotal || 0) - Number(state.member.quotaUsed || 0), 0);
        const planReady = memberPlanRank() >= planConfig.pro.rank;
        const imagePointCost = selectedHomeImagePointCost();
        imageButton.disabled = false;
        imageButton.textContent = !planReady
          ? `升级专业版后生成 · 每张${imagePointCost}点`
          : remaining < imagePointCost
            ? `点数不足：剩余${remaining}点 · 本次需${imagePointCost}点`
            : `AI直接生成1张主图 · ${imagePointCost}点（剩余${remaining}点）`;
        imageButton.title = !planReady
          ? "点击查看专业版权益"
          : remaining < imagePointCost
            ? (isBuyoutPlanKey() ? "买断版可按0.05元/点充值" : "点击升级套餐或续费点数")
            : "使用已接入的图片服务生成主图，失败不扣点";
      }
      function updateIntegrationButtons(integrations = {}) {
        window.__imageGenerationConfigured = !!integrations.image_configured;
        window.__imageGenerationSetupIssue = integrations.image_setup_issue || "";
        window.__imageGenerationVerificationStatus = integrations.image_verification_status || (integrations.image_verified ? "verified" : "unknown");
        window.__imageGenerationRetryAllowed = integrations.image_retry_allowed !== false;
        const reasoningVerified = integrations.deep_reasoning_verified === true
          || integrations.deep_reasoning_configured === true
          || integrations.deepseek_reasoning_verified === true
          || integrations.deepseek_reasoning_configured === true;
        const reasoningAvailable = integrations.deep_reasoning_available !== undefined
          ? integrations.deep_reasoning_available !== false
          : integrations.deepseek_reasoning_available !== false;
        const reasoningRetryable = integrations.deep_reasoning_retryable === true
          || integrations.deepseek_reasoning_retryable === true;
        const reasoningProvider = "深度分析服务";
        // A retryable cooldown does not invalidate an already verified Key.
        // Keep the mode selectable so the backend can explicitly fall back to
        // Flash, and refresh status when the advertised cooldown expires.
        const reasoningUsable = reasoningVerified && (reasoningAvailable || reasoningRetryable);
        window.__deepseekReasoningVerified = reasoningUsable;
        window.__deepseekReasoningAvailable = reasoningVerified && reasoningAvailable;
        window.__deepseekReasoningStatus = String(integrations.deep_reasoning_status || integrations.deepseek_reasoning_status || "");
        window.clearTimeout?.(window.__deepseekReasoningRetryTimer);
        window.__deepseekReasoningRetryTimer = null;
        if (reasoningVerified && !reasoningAvailable && reasoningRetryable) {
          const retryAfterSeconds = Math.max(
            0,
            Math.min(Number(integrations.deep_reasoning_retry_after_seconds || integrations.deepseek_reasoning_retry_after_seconds || 0), 300)
          );
          window.__deepseekReasoningRetryTimer = window.setTimeout?.(() => {
            loadIntegrationStatus().catch(() => null);
          }, Math.max(1000, (retryAfterSeconds + 1) * 1000));
        }
        const deepButton = document.getElementById("homeDeepAnalysisButton");
        if (deepButton) {
          deepButton.classList.toggle("is-unavailable", !window.__deepseekReasoningVerified);
          deepButton.disabled = !window.__deepseekReasoningVerified;
          deepButton.setAttribute("aria-disabled", window.__deepseekReasoningVerified ? "false" : "true");
          deepButton.title = window.__deepseekReasoningVerified
            ? reasoningAvailable
              ? `复杂报价、主图/详情图策划和经营风险使用 ${reasoningProvider} 深度分析`
              : `${reasoningProvider} 暂时繁忙；本次会明确使用备用线路，并在冷却后自动恢复`
            : reasoningVerified
              ? window.__deepseekReasoningStatus === "action_required"
                ? `${reasoningProvider} 需要检查模型权限、余额或 Key`
                : `${reasoningProvider} 暂时繁忙，请稍后重试`
              : "请先联系管理员接入并验证深度分析服务";
        }
        if (!window.__deepseekReasoningVerified && normalizeHomeAnalysisMode(window.__homeAnalysisMode) === "deep") {
          resetHomeDeepAnalysisSelection(
            reasoningVerified
              ? `${reasoningProvider} 当前不可用，已保留输入；恢复后可重新选择深度分析。`
              : "深度分析服务尚未验证，已保留输入；配置完成后可重新选择深度分析。"
          );
        }
        window.refreshStaleImageSetupRecoveryUi?.();
        renderVisualQuotaButton();
      }
      async function loadIntegrationStatus() {
        const [data, version, license, system] = await Promise.all([
          api("/api/integrations/status"),
          api("/api/app/version"),
          api("/api/license/status").catch(() => null),
          api("/api/system/check").catch(() => null)
        ]);
        const item = data.integrations || {};
        updateIntegrationButtons(item);
        const membership = license?.membership;
        if (membership) syncFormalMembership(membership);
        const aiText = item.text_ai_available
          ? "AI已连接"
          : item.openai_configured
            ? "AI线路待恢复"
            : "AI未连接";
        const cloudCheck = system?.system?.checks?.find(check => check.name.includes("云端同步"));
        const dataText = cloudCheck?.ok ? "云端共享" : "本机保存";
        const backup = system?.system?.backup;
        if (backup?.ok) {
          state.serverBackup = backup;
          saveState();
          renderBackupStatus();
        }
        window.__baseServiceStatus = `${aiText} · ${dataText}`;
        renderFormalStatus(window.__baseServiceStatus);
        window.setTimeout(() => {
          handleAppUpdateAvailability(version).catch(err => {
            console.warn("自动更新提示未能完成", err);
          });
        }, 800);
      }
      async function activateMembershipCodeValue(code) {
        await ensureFormalLogin();
        const data = await api("/api/license/activate", { method: "POST", body: JSON.stringify({ code }) });
        const item = data.membership;
        syncFormalMembership(item);
        renderFormalStatus("AI与云端服务已连接");
        setFormalOpsOutput(`激活成功\n\n套餐：${planName(item.plan)}\n到期：${item.expires_at_text}\nAI点数：${item.quota_used}/${item.quota_total}`);
        const output = document.getElementById("memberOutput");
        if (output) output.textContent = `激活成功\n套餐：${planName(item.plan)}\n到期：${item.expires_at_text}\nAI点数：${item.quota_used}/${item.quota_total}`;
        notify("会员激活成功。" );
        return item;
      }
      async function activateLicense() {
        const code = prompt("请输入购买后获得的会员激活码");
        if (!code) return;
        try { await activateMembershipCodeValue(code); }
        catch (err) { alert(err.message); }
      }
      async function connectOpenAI() {
        await ensureFormalLogin();
        const key = prompt("请输入 AI 服务的 API Key\\n只会保存到本机配置文件，不会显示在页面上。");
        if (!key) return;
        const baseUrl = prompt("请输入接口地址\\n普赞云中转站用：https://www.puzan.cloud/v1\\n官方 OpenAI 用：https://api.openai.com/v1", "https://www.puzan.cloud/v1") || "https://www.puzan.cloud/v1";
        const model = prompt("请输入中转站控制台显示的准确模型ID\n必须选择支持图片理解的多模态模型；不要凭印象填写。", "");
        if (!model?.trim()) {
          setFormalOpsOutput("尚未保存：模型ID不能为空。请到中转站的模型列表复制准确ID后重试。");
          return;
        }
        setFormalOpsOutput("正在保存并进行真实文字测试...");
        try {
          const data = await api("/api/system/openai", {
            method: "POST",
            body: JSON.stringify({ provider: "openai_compatible", api_key: key.trim(), base_url: baseUrl.trim(), model: model.trim(), vision_model: model.trim() })
          });
          const item = data.openai;
          setFormalOpsOutput(`${item.ok ? "接入成功" : "接入失败"}\\n\\n状态：${item.message}\\n接口：${item.base_url || baseUrl}\\n模型：${item.model}\\nKey：${item.key_mask || "未保存"}\\n\\n${item.ok ? "现在首页会使用真实模型；接下来请回首页分别测试文字和图片。" : "请重新复制完整 API Key，确认模型ID存在，并检查账户余额/权限。"}`);
          await loadIntegrationStatus();
        } catch (err) {
          setFormalOpsOutput("接入失败：" + err.message);
        }
      }
      async function connectKimi() {
        await ensureFormalLogin();
        const key = prompt("请粘贴 Kimi 开放平台 API Key\n位置：Kimi 开放平台 → API Key\n请使用按量计费的开放平台 Key，不要填写 Kimi 会员或 Kimi Code 登录凭证。Key 只保存在本机。 ");
        if (!key) return;
        setFormalOpsOutput("正在连接 Kimi K3…\n\n将进行两次小额真实验证：\n1. 文字与深度推理\n2. 产品图片理解\n\n图片流程仍以火神为第一线路；K3 仅在火神异常时作为国内理解备用。");
        try {
          const data = await api("/api/system/openai", {
            method: "POST",
            body: JSON.stringify({
              provider: "kimi",
              api_key: key.trim(),
              base_url: "https://api.moonshot.cn/v1",
              model: "kimi-k3",
              vision_model: "kimi-k3"
            })
          });
          const item = data.openai || {};
          setFormalOpsOutput(`${item.ok ? "Kimi K3 接入成功" : "Kimi K3 尚未接通"}\n\n状态：${item.message || "未知"}\n主分析模型：${item.model || "kimi-k3"}\n图片理解模型：${item.vision_model || "kimi-k3"}\n接口：${item.base_url || "https://api.moonshot.cn/v1"}\nKey：${item.key_mask || "未保存"}\n\n${item.ok ? "复杂文字分析仍可使用 K3；图片理解和提示词整理会按后台线路自动切换，最终生图只使用主图片服务与备用图片服务。" : "原有模型和生图配置没有被覆盖。请检查 Kimi 开放平台余额、K3权限或重新复制完整 Key。"}`);
          await loadIntegrationStatus();
        } catch (err) {
          setFormalOpsOutput("Kimi K3 接入失败：" + err.message);
        }
      }
      async function connectBailian() {
        await ensureFormalLogin();
        const key = prompt("请粘贴阿里云百炼 API Key\\n位置：百炼控制台 → API Key\\nKey 只保存在这台电脑，不会显示在页面上。");
        if (!key) return;
        setFormalOpsOutput("正在连接阿里云百炼…\n\n将依次验证：\n1. 文字问答\n2. 图片理解\n\n已经配置的专用生图服务会保持不变。");
        try {
          const data = await api("/api/system/openai", {
            method: "POST",
            body: JSON.stringify({
              provider: "aliyun_bailian",
              api_key: key.trim(),
              base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1",
              model: "qwen3.7-plus",
              vision_model: "qwen3.7-plus"
            })
          });
          const item = data.openai || {};
          setFormalOpsOutput(`${item.ok ? "阿里云百炼接入成功" : "阿里云百炼尚未接通"}\n\n状态：${item.message || "未知"}\n文字模型：${item.model || "qwen3.7-plus"}\n看图模型：${item.vision_model || "qwen3.7-plus"}\nKey：${item.key_mask || "未保存"}\n\n${item.ok ? "首页问答和图片识别可使用千问；百炼不会参与图片生成。" : "配置没有覆盖原来的可用服务。请检查百炼余额、模型权限或重新复制 Key。"}`);
          await loadIntegrationStatus();
        } catch (err) {
          setFormalOpsOutput("阿里云百炼接入失败：" + err.message);
        }
      }
      async function connectDeepSeek() {
        await ensureFormalLogin();
        const key = prompt("请粘贴 DeepSeek 官方 API Key\n位置：DeepSeek 开放平台 → API keys\n同一个 Key 会用于 Flash 快速备用和 Pro 深度分析；不会替换现有识图和生图服务。");
        if (!key) return;
        const model = "deepseek-v4-flash";
        setFormalOpsOutput("正在连接 DeepSeek 双层文字通道…\n\nFlash 用于快速备用，DeepSeek Pro 用于报价复核、利润风险和复杂排产。DeepSeek 不接收图片，识图和生图仍使用现有服务。");
        try {
          const data = await api("/api/system/openai", {
            method: "POST",
            body: JSON.stringify({
              provider: "deepseek",
              api_key: key.trim(),
              base_url: "https://api.deepseek.com",
              model: "deepseek-v4-flash"
            })
          });
          const item = data.openai || {};
          const reasoningFresh = item.reasoning_ok === true;
          const reasoningVerified = item.reasoning_verified === true || reasoningFresh;
          const reasoningProbeUnavailable = !reasoningFresh && item.reasoning_retryable === true;
          const reasoningTemporarilyUnavailable = reasoningVerified && reasoningProbeUnavailable;
          setFormalOpsOutput(`${item.ok ? (reasoningTemporarilyUnavailable ? "DeepSeek 双层文字能力已保留，Pro 暂时繁忙" : reasoningVerified ? "DeepSeek 双层文字能力已接通" : reasoningProbeUnavailable ? "DeepSeek Flash 已接通，Pro 暂时无法验证" : "DeepSeek Flash 已接通，Pro 尚未开通") : "DeepSeek 尚未接通"}\n\n快速备用：${item.message || "未知"} · ${item.model || model}\n深度分析：${item.reasoning_message || "尚未验证"} · ${item.reasoning_model || "deepseek-v4-pro"}\n用途：普通线路异常时快速备用；复杂报价、利润、排产和合同风险使用 Pro\n图片：仍由现有识图/生图服务处理\nKey：${item.key_mask || "未保存"}\n\n${reasoningFresh ? "Flash 和 Pro 均已真实调用验证；现有主模型、识图和生图配置未改变。" : reasoningTemporarilyUnavailable ? "此前 Pro 验证仍然有效；本次只是临时线路异常，软件会先回落到 Flash，恢复后继续使用 Pro。" : reasoningProbeUnavailable ? "Flash 已保存；Pro 本次因临时线路异常尚未完成验证，请稍后直接重新保存，不代表账户未开通。" : item.ok ? "Flash 已保存，但软件不会假装 Pro 可用。请在 DeepSeek 官方控制台开通 Pro 后重新保存。" : "配置没有覆盖原来的可用服务，请检查 DeepSeek 余额、模型权限或重新复制 Key。"}`);
          await loadIntegrationStatus();
        } catch (err) {
          setFormalOpsOutput("DeepSeek 接入失败：" + err.message);
        }
      }
      function showHuoshenImageConfig() {
        hideSeedreamImageConfig();
        const form = document.getElementById("huoshenImageConfigForm");
        form?.classList.add("show");
        document.getElementById("huoshenImageApiKey")?.focus();
      }
      function hideHuoshenImageConfig() {
        document.getElementById("huoshenImageConfigForm")?.classList.remove("show");
        const keyInput = document.getElementById("huoshenImageApiKey");
        if (keyInput) keyInput.value = "";
      }
      function showSeedreamImageConfig() {
        hideHuoshenImageConfig();
        const form = document.getElementById("seedreamImageConfigForm");
        form?.classList.add("show");
        document.getElementById("seedreamImageApiKey")?.focus();
      }
      function hideSeedreamImageConfig() {
        document.getElementById("seedreamImageConfigForm")?.classList.remove("show");
        const keyInput = document.getElementById("seedreamImageApiKey");
        if (keyInput) keyInput.value = "";
      }
      async function connectHuoshenImage() {
        await ensureFormalLogin();
        const key = document.getElementById("huoshenImageApiKey")?.value.trim() || "";
        const model = document.getElementById("huoshenImageModelId")?.value.trim() || "gpt-image-2";
        const size = document.getElementById("huoshenImageSize")?.value || "auto";
        if (!key) {
          setFormalOpsOutput("请粘贴火神AI生图分组的 API Key。文字聊天 Key 不能代替它。");
          document.getElementById("huoshenImageApiKey")?.focus();
          return;
        }
        setFormalOpsOutput("正在验证火神AI生图分组…\n\n这一步只读取模型列表，不会生成图片或产生生图费用。");
        try {
          const data = await api("/api/integrations/image", {
            method: "POST",
            body: JSON.stringify({
              provider: "huoshenai_images_api",
              api_key: key,
              base_url: "https://huoshenai.com/v1",
              model,
              size
            })
          });
          const item = data.image || {};
          clearImageGenerationActualVerification();
          hideHuoshenImageConfig();
          setFormalOpsOutput(`主图片服务已保存\n\n默认尺寸：${item.size || size}\nKey：${item.key_mask || "已安全保存"}\n\n✓ 生图、改图、识图和提示词理解优先走主线路\n✓ 主线路失败时自动切换已保存的备用线路\n✓ 百炼万相不会参与图片生成\n✓ 整次生成只扣一次软件点数\n\n配置已保存。`);
          await loadIntegrationStatus();
        } catch (err) {
          setFormalOpsOutput("火神AI生图接入失败：" + err.message);
        }
      }
      async function connectSeedreamImage() {
        await ensureFormalLogin();
        const key = document.getElementById("seedreamImageApiKey")?.value.trim() || "";
        const model = document.getElementById("seedreamImageModelId")?.value.trim() || "doubao-seedream-5-0-pro-260628";
        const size = document.getElementById("seedreamImageSize")?.value || "2K";
        if (!key) {
          setFormalOpsOutput("请先粘贴火山方舟 ARK_API_KEY。现有火神AI或阿里云 Key 不能代替它。");
          document.getElementById("seedreamImageApiKey")?.focus();
          return;
        }
        setFormalOpsOutput("正在连接火山方舟…\n\n这一步只读取官方模型列表，验证 ARK_API_KEY 能否看到 Seedream 5.0 Pro，不会生成图片或产生生图费用。实际生图权限会在首次生成时确认。");
        try {
          const data = await api("/api/integrations/image", {
            method: "POST",
            body: JSON.stringify({
              provider: "volcengine_seedream",
              api_key: key,
              base_url: "https://ark.cn-beijing.volces.com/api/v3",
              model,
              size
            })
          });
          const item = data.image || {};
          clearImageGenerationActualVerification();
          hideSeedreamImageConfig();
          setFormalOpsOutput(`备用图片服务已保存\n\n默认尺寸：${item.size || size}\nKey：${item.key_mask || "已安全保存"}\n\n✓ 主图片服务正常时不会调用备用线路\n✓ 主图片服务异常时自动接替\n△ 尚需首次实际生图确认生成权限\n\n失败不会重复扣软件点数，原图和要求也会保留。`);
          await loadIntegrationStatus();
        } catch (err) {
          setFormalOpsOutput("Seedream 5.0 Pro 接入失败：" + err.message);
        }
      }
      async function connectCloud() {
        await ensureFormalLogin();
        const apiUrl = prompt("请输入云服务器接口地址\\n例如：https://api.your-domain.com\\n现在没有云服务器时可以先取消，软件继续使用本机数据。");
        if (!apiUrl) return;
        setFormalOpsOutput("正在保存云端配置...");
        try {
          const data = await api("/api/cloud/config", {
            method: "POST",
            body: JSON.stringify({ enabled: true, api_url: apiUrl.trim() })
          });
          const cloud = data.cloud || {};
          setFormalOpsOutput(`云端配置已保存\n\n当前模式：${cloud.mode === "cloud" ? "云端共享已接入" : "本机数据"}\n接口地址：${cloud.api_url || "未配置"}\n\n新上传的客户文件、AI分析、报价记录和跟单进度会自动同步到云端。后面做 App 时，直接读取同一套云端数据即可。`);
          await loadIntegrationStatus();
        } catch (err) {
          setFormalOpsOutput("云端配置失败：" + err.message);
        }
      }
      async function connectVector() {
        await ensureFormalLogin();
        const apiId = prompt("请输入 Vectorizer.AI API ID");
        if (!apiId) return;
        const apiSecret = prompt("请输入 Vectorizer.AI API Secret\n凭证只保存在本机配置文件中。");
        if (!apiSecret) return;
        setFormalOpsOutput("正在验证矢量转换接口...");
        try {
          const data = await api("/api/integrations/vector", {
            method: "POST",
            body: JSON.stringify({ api_id: apiId.trim(), api_secret: apiSecret.trim() })
          });
          const item = data.vector || {};
          setFormalOpsOutput(`矢量接口已接入\n\n服务：Vectorizer.AI\n套餐：${item.subscription || "未订阅"}\n剩余额度：${item.credits ?? 0}\n\n现在可以在“制图 > 图片转矢量/CDR”中上传图片并下载 SVG/PDF/EPS/DXF。`);
          await loadIntegrationStatus();
        } catch (err) {
          setFormalOpsOutput("矢量接口接入失败：" + err.message);
        }
      }
      async function testOpenAI() {
        await ensureFormalLogin();
        setFormalOpsOutput("正在实际发送文字和图片进行测试...");
        try {
          const data = await api("/api/system/openai/test");
          const item = data.openai;
          setFormalOpsOutput(`${item.ok ? "AI 服务可用" : "AI 服务不可用"}\\n\\n状态：${item.message}\\n接口：${item.base_url || "未设置"}\\n文字模型：${item.model || "未设置"}\\n图片模型：${item.vision_model || "未设置"}\\nKey：${item.key_mask || "未保存"}`);
          await loadIntegrationStatus();
        } catch (err) {
          setFormalOpsOutput("测试失败：" + err.message);
        }
      }
      async function runSystemCheck() {
        try {
          await ensureFormalLogin();
          const data = await api("/api/system/check");
          const system = data.system;
          const backupText = system.backup?.ok ? `自动备份：${new Date(system.backup.latest_at).toLocaleString("zh-CN", { hour12: false })} · 共 ${system.backup.count} 份 · 保留 ${system.backup.retention_days} 天` : "自动备份：尚未生成";
          const checksHtml = system.checks.map(item => {
            const checkPassed = Boolean(item.ok) && (!isImageGenerationSystemCheck(item) || imageGenerationActuallyVerified());
            const checkLabel = checkPassed ? "✓" : item.ok && isImageGenerationSystemCheck(item) ? "待实测" : "待完成";
            return `<div class="formal-check${checkPassed ? "" : " pending"}"><strong>${checkLabel} ${escapeHtml(item.name)}</strong><span>${escapeHtml(friendlyCheckDetail(item))}</span></div>`;
          }).join("");
          const actionsHtml = (system.action_items || []).length
            ? `<ul class="formal-action-list">${system.action_items.map(item => `<li>${escapeHtml(item.action || item.name)}</li>`).join("")}</ul>`
            : `<div class="muted" style="margin-top:10px">全部上线检查已经通过。</div>`;
          const aiRoutesHtml = system.integrations?.text_ai_available
            ? `<div class="muted" style="margin-top:18px">AI服务已连接，线路异常时由后台自动切换。</div>`
            : `<div class="muted" style="margin-top:18px">AI服务暂不可用，请稍后重试；持续异常可在官网账号中心提交工单。</div>`;
          const recentFailures = Array.isArray(system.recent_ai_failures) ? system.recent_ai_failures.slice(-3).reverse() : [];
          const diagnosticsHtml = recentFailures.length
            ? `<details style="margin-top:16px"><summary><strong>最近服务异常（仅显示安全诊断信息）</strong></summary><div class="formal-check-list" style="margin-top:8px">${recentFailures.map(item => `<div class="formal-check pending"><strong>${escapeHtml(item.stage || "AI服务")} · ${escapeHtml(item.error_type || "service_error")}</strong><span>${escapeHtml(item.time || "时间未知")}${item.status_code ? ` · 状态 ${escapeHtml(String(item.status_code))}` : ""} · 诊断号 ${escapeHtml(item.id || "—")}</span></div>`).join("")}</div></details>`
            : "";
          setFormalOpsHtml(`
            <div class="formal-ops-scores">
              <div class="formal-ops-score"><span>本机系统完成度</span><strong>${Number(system.local_ready_score || 0)}分</strong><em>${system.local_ready ? "数据库、备份、目录和隐私均正常" : "请先处理本机安全项"}</em></div>
              <div class="formal-ops-score"><span>正式上线完成度</span><strong>${Number(system.ready_score || 0)}分</strong><em>${system.production_ready ? "可以正式开放" : "等待外部服务部署"}</em></div>
            </div>
            <div class="muted" style="margin-bottom:10px">v${escapeHtml(system.version)} (${escapeHtml(system.build)}) · ${escapeHtml(planName(system.membership.plan))} · 文件 ${Number(system.counts.files || 0)} / 报价 ${Number(system.counts.quotes || 0)} / 跟单 ${Number(system.counts.tasks || 0)}${Number(system.counts.task_recycle_bin || 0) ? ` / 回收站 ${Number(system.counts.task_recycle_bin || 0)}` : ""}<br>${escapeHtml(backupText)}</div>
            <div class="formal-check-list">${checksHtml}</div>${aiRoutesHtml}${diagnosticsHtml}${actionsHtml}`);
          await loadIntegrationStatus();
        } catch (err) {
          setFormalOpsOutput("系统体检失败：" + err.message);
        }
      }
      async function createSafetySnapshot() {
        try {
          await ensureFormalLogin();
          setFormalOpsOutput("正在创建并校验数据库安全快照…");
          const data = await api("/api/system/backup/create", { method: "POST", body: "{}" });
          state.serverBackup = data.backup;
          saveState();
          renderBackupStatus();
          notify("安全快照已创建并通过完整性校验。" );
          await runSystemCheck();
        } catch (err) {
          setFormalOpsOutput("安全快照失败：" + err.message);
        }
      }
      async function refreshFormalStatus() {
        await runSystemCheck();
      }
      async function downloadFormalBackup() {
        try {
          await ensureFormalLogin();
          const headers = {};
          if (apiToken) headers.Authorization = "Bearer " + apiToken;
          const res = await fetch("/api/system/backup", { headers });
          if (!res.ok) throw new Error("导出失败");
          const blob = await res.blob();
          const url = URL.createObjectURL(blob);
          const a = document.createElement("a");
          a.href = url;
          a.download = `将莱数字员工数据备份-${new Date().toISOString().slice(0, 10)}.json`;
          document.body.appendChild(a);
          a.click();
          a.remove();
          URL.revokeObjectURL(url);
          setFormalOpsOutput("已导出备份文件。这个文件包含报价、跟单、AI分析、设置和文件记录，方便迁移或售后排查。");
        } catch (err) {
          setFormalOpsOutput("导出备份失败：" + err.message);
        }
      }
      function fileToBase64(file) {
        return new Promise((resolve, reject) => {
          const reader = new FileReader();
          reader.onload = () => resolve(reader.result);
          reader.onerror = reject;
          reader.readAsDataURL(file);
        });
      }
      let lastPreflightUpload = null;
      function preflightFileKey(file) {
        return [file?.name || "", Number(file?.size || 0), Number(file?.lastModified || 0)].join("|");
      }
      async function uploadPreflightFileConnected(file) {
        if (!file) throw new Error("请先选择需要体检的生产文件");
        if (Number(file.size || 0) > 18 * 1024 * 1024) throw new Error("生产文件不能超过18MB");
        const key = preflightFileKey(file);
        if (lastPreflightUpload?.key === key && Number(lastPreflightUpload.id || 0) > 0) return lastPreflightUpload.id;
        const base64 = await fileToBase64(file);
        const data = await api("/api/files", {
          method: "POST",
          body: JSON.stringify({ name:file.name, mime_type:file.type || "application/octet-stream", base64 })
        });
        if (!data.file?.id) throw new Error("生产文件上传失败");
        lastPreflightUpload = { key, id:Number(data.file.id) };
        return lastPreflightUpload.id;
      }
      function parsePreflightExpectedSize(value) {
        const match = String(value || "").trim().match(/(\d+(?:\.\d+)?)\s*[x×X*＊]\s*(\d+(?:\.\d+)?)\s*(mm|毫米|cm|厘米|m|米|in|英寸)?/i);
        if (!match) return { width:0, height:0 };
        const unit = String(match[3] || "mm").toLowerCase();
        const multiplier = ["cm", "厘米"].includes(unit) ? 10 : ["m", "米"].includes(unit) ? 1000 : ["in", "英寸"].includes(unit) ? 25.4 : 1;
        return { width:Number(match[1]) * multiplier, height:Number(match[2]) * multiplier };
      }
      async function runPreflightCheckConnected() {
        const file = document.getElementById("designFile")?.files?.[0];
        const output = document.getElementById("designOutput");
        const button = document.getElementById("preflightRunButton");
        if (!file) {
          notify("请先选择需要体检的生产文件。");
          document.getElementById("designFile")?.click();
          return;
        }
        if (output) output.innerHTML = `<div class="preflight-empty">正在读取文件真实参数…<br>依次检查结构、尺寸、出血、色彩、字体和透明效果。</div>`;
        if (button) { button.disabled = true; button.textContent = "正在体检…"; }
        try {
          await ensureFormalLogin();
          const fileId = await uploadPreflightFileConnected(file);
          const expected = parsePreflightExpectedSize(val("preflightExpectedSize"));
          const data = await api("/api/preflight/check", {
            method: "POST",
            body: JSON.stringify({
              file_id: fileId,
              customer: val("preflightCustomer") || document.getElementById("homeCustomerName")?.value?.trim() || "新客户",
              requirement_text: val("designText"),
              profile: val("preflightProfile") || "general",
              expected_width_mm: expected.width,
              expected_height_mm: expected.height,
              minimum_dpi: Number(val("preflightMinimumDpi") || 300),
              required_bleed_mm: Number(val("preflightBleed") || 0),
              require_cmyk: true
            })
          });
          renderPreflightReport(data.report || {});
          await loadPreflightReportsConnected();
          renderFormalStatus(`印前体检完成 · ${data.report?.result?.status_label || "等待人工复核"}`);
          notify("印前体检完成，请根据报告确认生产或退回修改。");
        } catch (err) {
          if (output) output.innerHTML = `<div class="preflight-empty">体检没有完成：${escapeHtml(err.message)}<br>原文件没有被修改，可以修正后重新体检。</div>`;
          notify(err.message);
        } finally {
          if (button) { button.disabled = false; button.textContent = "开始真实体检"; }
        }
      }
      async function loadPreflightReportsConnected() {
        const container = document.getElementById("preflightHistory");
        if (!container || isStaticFileMode) return [];
        try {
          await ensureFormalLogin();
          const customer = val("preflightCustomer");
          const suffix = customer && customer !== "新客户" ? `?customer=${encodeURIComponent(customer)}` : "";
          const data = await api(`/api/preflight/reports${suffix}`);
          const reports = Array.isArray(data.reports) ? data.reports : [];
          renderPreflightHistory(reports);
          return reports;
        } catch (err) {
          container.innerHTML = `<div class="muted">读取体检记录失败：${escapeHtml(err.message)}</div>`;
          return [];
        }
      }
      async function decidePreflightReportConnected(reportId, decision, automaticStatus = "") {
        let note = "";
        let confirmRisk = false;
        if (decision === "pending") {
          if (!confirm("确定撤回本次人工审核吗？撤回后报告会恢复为等待复核。")) return;
        } else if (decision === "rejected") {
          const value = prompt("请填写退回修改要求", "请按体检报告修改后重新上传，并重新确认最终生产版本。");
          if (value === null) return;
          note = value.trim();
          if (!note) { notify("退回修改必须填写原因。"); return; }
          if (!confirm("确定退回修改吗？系统会保留本次报告和退回原因。")) return;
        } else if (automaticStatus === "blocked") {
          const value = prompt("报告存在必须修改项。如确需例外放行，请填写具体原因和责任确认", "");
          if (value === null) return;
          note = value.trim();
          if (!note) { notify("阻断报告例外放行必须填写原因。"); return; }
          if (!confirm("报告仍有阻断问题。确定承担风险并例外放行吗？")) return;
          confirmRisk = true;
        } else {
          if (!confirm("确认已经人工核对画面、尺寸、安全边距和最终版本，可以进入生产吗？")) return;
          note = "已由制前人员人工核对画面、尺寸、安全边距和最终版本。";
        }
        try {
          await ensureFormalLogin();
          const data = await api(`/api/preflight/reports/${Number(reportId)}/decision`, {
            method: "POST",
            body: JSON.stringify({ decision, note, confirm_risk:confirmRisk })
          });
          renderPreflightReport(data.report || {});
          await loadPreflightReportsConnected();
          notify(decision === "approved" ? "已确认可生产。" : decision === "rejected" ? "已退回修改。" : "已撤回审核。");
        } catch (err) {
          notify(err.message);
        }
      }
      async function recordPreflightCustomerConfirmationConnected(reportId, status) {
        let confirmedBy = "";
        let note = "";
        if (status === "pending") {
          if (!confirm("确定撤回这条客户确认记录吗？稿件版本和制前审核记录不会删除。")) return;
        } else {
          const nameValue = prompt("填写实际确认人姓名或微信昵称", "");
          if (nameValue === null) return;
          confirmedBy = nameValue.trim();
          if (!confirmedBy) { notify("请填写实际确认人，避免后续无法追溯。" ); return; }
          if (status === "revision_requested") {
            const noteValue = prompt("填写客户要求修改的具体内容", "");
            if (noteValue === null) return;
            note = noteValue.trim();
            if (!note) { notify("客户要求修改时必须填写具体意见。" ); return; }
          } else {
            const noteValue = prompt("确认渠道或补充说明（可选）", "客户已通过微信/邮件确认此稿件版本。");
            if (noteValue === null) return;
            note = noteValue.trim();
          }
          const actionText = status === "confirmed" ? "客户已确认此版本" : "客户要求修改此版本";
          if (!confirm(`确定记录“${actionText}”吗？系统会保留确认人、时间和意见。`)) return;
        }
        try {
          await ensureFormalLogin();
          const data = await api(`/api/preflight/reports/${Number(reportId)}/customer-confirmation`, {
            method: "POST",
            body: JSON.stringify({ status, confirmed_by:confirmedBy, note })
          });
          renderPreflightReport(data.report || {});
          await loadPreflightReportsConnected();
          notify(status === "confirmed" ? "客户确认已留档。" : status === "revision_requested" ? "客户修改意见已留档。" : "客户确认记录已撤回。" );
        } catch (err) {
          notify(err.message);
        }
      }
      async function createPreflightOnlineProof(reportId, taskId, versionNo) {
        try {
          await ensureFormalLogin();
          const data = await api(`/api/tasks/${Number(taskId)}/confirmations`, {
            method:"POST",
            body:JSON.stringify({
              expires_days:7,
              preflight_report_id:Number(reportId),
              note:`请核对并确认 V${Number(versionNo || 1)} 校样文件`
            })
          });
          if (!data?.share_url) throw new Error("服务端没有返回校样链接");
          await navigator.clipboard?.writeText(data.share_url).catch(() => null);
          notify(`V${Number(versionNo || 1)} 在线校样链接已生成并复制。`);
        } catch (err) {
          notify("在线校样链接生成失败：" + err.message);
        }
      }
      function homeFileKey(file) {
        return [file?.name || "", Number(file?.size || 0), Number(file?.lastModified || 0)].join("|");
      }
      async function removeHomeFileConnected(index) {
        const input = document.getElementById("homeFiles");
        const files = Array.from(input?.files || []);
        if (!input || index < 0 || index >= files.length) return;
        const originSession = activeCustomerSession();
        const originSessionId = String(originSession?.id || "");
        const removedFile = files[index];
        const removedKey = homeFileKey(removedFile);
        const pendingUpload = (originSessionId && homeSessionUploadPromises.get(originSessionId)) || latestUploadPromise;
        const originFileIds = [...uploadedFileIds];
        const originFileRecords = uploadedHomeFileRecords.map(record => ({ ...record }));
        const originUploadFailures = (window.__homeUploadFailures || []).map(item => ({ ...item }));
        const transfer = new DataTransfer();
        files.forEach((file, fileIndex) => { if (fileIndex !== index) transfer.items.add(file); });
        input.files = transfer.files;
        const remainingFileCount = transfer.files.length;
        if (originSession) {
          originSession.pendingFileIds = [...originFileIds];
          originSession.pendingFileRecords = originFileRecords.map(record => ({ ...record }));
          originSession.uploadFailures = originUploadFailures.map(item => ({ ...item }));
        }
        window.__suppressHomeUploadSync = true;
        window.updateHomeFileName?.();
        lastAnalysisData = null;
        lastAnalysisSignature = "";
        latestAnalysisPromise = null;
        latestAnalysisSignature = "";
        if (pendingUpload) await pendingUpload.catch(() => []);
        const targetSession = originSessionId ? homeSessionById(originSessionId) : originSession;
        let targetRecords = Array.isArray(targetSession?.pendingFileRecords)
          ? targetSession.pendingFileRecords.map(record => ({ ...record }))
          : originFileRecords.map(record => ({ ...record }));
        let targetIds = Array.isArray(targetSession?.pendingFileIds)
          ? [...targetSession.pendingFileIds]
          : [...originFileIds];
        let targetFailures = Array.isArray(targetSession?.uploadFailures)
          ? targetSession.uploadFailures.map(item => ({ ...item }))
          : originUploadFailures.map(item => ({ ...item }));
        const recordIndex = targetRecords.findIndex(record => record.key === removedKey);
        const record = recordIndex >= 0 ? targetRecords[recordIndex] : null;
        if (recordIndex >= 0) targetRecords.splice(recordIndex, 1);
        if (record?.id) targetIds = targetIds.filter(id => Number(id) !== Number(record.id));
        targetFailures = targetFailures.filter(item => item?.key !== removedKey);
        if (!remainingFileCount && !targetRecords.length) targetIds = [];
        if (targetSession) {
          targetSession.pendingFileIds = [...targetIds];
          targetSession.pendingFileRecords = targetRecords.map(item => ({ ...item }));
          targetSession.uploadFailures = targetFailures.map(item => ({ ...item }));
          targetSession.updatedAt = new Date().toISOString();
        }
        const originIsActive = () => !originSessionId || state.activeCustomerSessionId === originSessionId;
        if (originIsActive()) {
          uploadedFileIds = [...targetIds];
          uploadedHomeFileRecords = targetRecords.map(item => ({ ...item }));
          window.__homeUploadFailures = targetFailures.map(item => ({ ...item }));
          if (latestUploadPromise === pendingUpload) latestUploadPromise = null;
        }
        if (originSessionId && homeSessionUploadPromises.get(originSessionId) === pendingUpload) {
          homeSessionUploadPromises.delete(originSessionId);
        }
        saveState();
        if (originIsActive()) window.updateHomeFileName?.();
        if (record?.id && !isStaticFileMode) {
          try {
            await api("/api/files/remove", { method: "POST", body: JSON.stringify({ file_id: record.id }) });
            if (!originSessionId || state.activeCustomerSessionId === originSessionId) {
              renderFormalStatus(`已移除误上传文件：${removedFile.name}`);
            }
          } catch (err) {
            if (!originSessionId || state.activeCustomerSessionId === originSessionId) {
              renderFormalStatus(`当前对话已移除 ${removedFile.name} · ${err.message}`);
            }
          }
        }
        if (originIsActive() && !remainingFileCount) {
          setTextIfExists("homeFileName", "上传图片、表格、文档、视频或图纸");
          const answer = document.getElementById("aiAnswer");
          if (answer) answer.textContent = "图片已移除，可以重新上传或直接输入问题。";
          hideHomeNextActions();
        }
      }
      async function removeAllHomeFilesConnected() {
        const input = document.getElementById("homeFiles");
        for (let index = Array.from(input?.files || []).length - 1; index >= 0; index -= 1) {
          await removeHomeFileConnected(index);
        }
      }
      async function importPriceTableFileConnected(file) {
        if (!file) throw new Error("没有选择价格表文件");
        if (file.size > 8 * 1024 * 1024) throw new Error("价格表不能超过 8MB");
        await ensureFormalLogin();
        const base64 = await fileToBase64(file);
        return api("/api/price-table/import", {
          method: "POST",
          body: JSON.stringify({ name: file.name, mime_type: normalizedUploadMime(file), base64 })
        });
      }
      async function uploadFilesFromInput(inputId, statusId) {
        const files = Array.from(document.getElementById(inputId)?.files || []);
        if (!files.length) return [];
        if (isStaticFileMode) {
          return [];
        }
        const targetSessionId = inputId === "homeFiles" ? activeCustomerSession()?.id || "" : "";
        const targetSession = targetSessionId ? homeSessionById(targetSessionId) : null;
        await ensureFormalLogin();
        const ids = [];
        const uploadedRecords = [];
        const failedFiles = [];
        const existingRecordsByKey = inputId === "homeFiles"
          ? new Map((Array.isArray(targetSession?.pendingFileRecords) ? targetSession.pendingFileRecords : uploadedHomeFileRecords).map(record => {
              const stableKey = String(record?.key || "").trim() || `uploaded-id:${Number(record?.id || 0)}`;
              return [stableKey, record];
            }))
          : new Map();
        if (inputId === "homeFiles") {
          existingRecordsByKey.forEach(record => {
            const id = Number(record?.id || 0);
            if (id > 0) {
              ids.push(id);
              uploadedRecords.push({ ...record, id });
            }
          });
        }
        for (const file of files) {
          const fileKey = homeFileKey(file);
          const existingRecord = existingRecordsByKey.get(fileKey);
          if (existingRecord?.id) {
            ids.push(Number(existingRecord.id));
            uploadedRecords.push({ ...existingRecord, key: fileKey, name: file.name, mimeType: normalizedUploadMime(file) || existingRecord.mimeType || "application/octet-stream" });
            continue;
          }
          if (file.size > 18 * 1024 * 1024) {
            failedFiles.push({ key: fileKey, name: file.name, reason: "超过18MB" });
            renderFormalStatus(`文件较大，已在页面显示：${file.name}`);
            continue;
          }
          try {
            const base64 = await fileToBase64(file);
            const data = await api("/api/files", {
              method: "POST",
              body: JSON.stringify({ name: file.name, mime_type: normalizedUploadMime(file), base64 })
            });
            if (data.file?.id) {
              ids.push(data.file.id);
              uploadedRecords.push({ id: data.file.id, key: fileKey, name: file.name, mimeType: normalizedUploadMime(file) });
            } else {
              failedFiles.push({ key: fileKey, name: file.name, reason: "服务没有返回文件编号" });
            }
          } catch (err) {
            failedFiles.push({ key: fileKey, name: file.name, reason: String(err?.message || "上传失败") });
          }
        }
        const uniqueUploadedRecords = [...new Map(uploadedRecords
          .filter(record => Number(record?.id || 0) > 0)
          .map(record => [Number(record.id), { ...record, id: Number(record.id) }])).values()];
        if (inputId === "homeFiles") {
          const session = homeSessionById(targetSessionId);
          if (session) {
            session.pendingFileIds = [...new Set(ids)];
            session.pendingFileRecords = uniqueUploadedRecords.map(record => ({ ...record }));
            session.uploadFailures = failedFiles.map(item => ({ ...item }));
            session.updatedAt = new Date().toISOString();
          }
          if (state.activeCustomerSessionId === targetSessionId) {
            uploadedFileIds = [...new Set(ids)];
            uploadedHomeFileRecords = uniqueUploadedRecords;
            window.__homeUploadFailures = failedFiles;
            lastAnalysisData = null;
            lastAnalysisSignature = "";
          }
          saveState();
        }
        const targetStillActive = inputId !== "homeFiles" || state.activeCustomerSessionId === targetSessionId;
        if (ids.length && targetStillActive) {
          const label = document.getElementById(statusId);
          if (label) label.textContent = `${label.textContent} · 已同步到正式版`;
          renderFormalStatus(`正式版已同步 ${ids.length} 个文件`);
          if (inputId === "homeFiles") {
            autoAnalyzeHomeFilesAfterUpload(ids);
          }
        }
        if (failedFiles.length && targetStillActive) {
          if (inputId === "homeFiles") keepHomeComposerAttachmentsForRetry();
          else renderFormalStatus(`${ids.length ? `已上传 ${ids.length} 个；` : ""}${failedFiles.length} 个附件仍保留，可重试`);
        }
        return ids;
      }
      function shouldReuseHomeRetainedFiles(text = "", availableIds = []) {
        const ids = (Array.isArray(availableIds) ? availableIds : []).map(Number).filter(id => id > 0);
        const value = String(text || "").replace(/\s+/g, " ").trim();
        if (!ids.length || !value) return false;
        // 宝贝详情是一个持续任务。用户只说“要8张”“再加2张”时，仍要保留
        // 上一轮产品/图案/风格参考，不能把这句话拆成没有图片上下文的新聊天。
        const activeDetailPackageSession = typeof activeCustomerSession === "function" ? activeCustomerSession() : null;
        if (activeDetailPackageSession?.detailPackageJob && (
          /(?:要|做|生成|制作|出|改成|调整为|一共|总共|共)?\s*(?:\d{1,2}|十二|十一|十|[一二两三四五六七八九])\s*张/i.test(value)
          || /(?:再|还要)(?:加|增加|补)\s*\d{1,2}\s*张/i.test(value)
          || /(?:少|减|去掉)\s*\d{1,2}\s*张/i.test(value)
        )) return true;
        // 明确提到图片、附件或上一张内容时，保留视觉上下文。
        const explicitImageReference = /(?:这|那)(?:张|幅|个)?(?:图|图片|照片|截图|图像|画面|效果图|附件)|(?:上一张|前一张|刚才那张|刚刚那张|之前那张|刚才的|刚刚的|之前的|上次的|原始|参考)(?:图|图片|照片|截图|图像|画面|效果图|附件)|(?:图|图片|照片|截图|图像|画面|参考图|原图|效果图|附件)(?:里|中|上|内)/i.test(value);
        if (explicitImageReference) return true;

        // 表格和文档追问也要继续携带上一轮附件，例如“表格里白卡多少钱”。
        const explicitDocumentReference = /(?:这|那|刚才|刚刚|之前|上次|当前|这个|那个).{0,6}(?:表格|excel|工作表|价格表|文件|文档)|(?:表格|excel|工作表|价格表|文件|文档)(?:里|中|上|内|的数据|的价格)/i.test(value);
        if (explicitDocumentReference) return true;

        // 改图指令即使只说“它/这只猫”，也必须继续携带参考图。
        const visualEditAction = /(?:生成|制作|做成|变成|转换|改成|换成|修改|调整|修正|修复|校正|修一下|修自然|改自然|抠图|抠掉|去背|移除背景|只留|补全|去掉|删除|添加|增加|保留|重画|重做|再来一张|重新生成)/i.test(value);
        const visualSubjectOrStyle = /(?:它|他|她|这只|那只|这个人|这个人物|这个产品|猫|狗|动物|人物|人脸|脸|面部|表情|眼神|眼睛|嘴|笑|严肃|手|脚|爪|五官|四肢|尾巴|耳朵|背景|颜色|风格|构图|光线|比例|细节|漫画|动漫|二次元|卡通|Q版|写实|油画|水彩|素描|海报|主图|产品图)/i.test(value);
        if (visualEditAction && visualSubjectOrStyle) return true;

        // “表情变了，原来比较严肃”虽然不是命令句，但已经给出了明确的
        // 改图方向。继续携带上一张效果图，不能退回成普通文字问答。
        const implicitVisualCorrection = /(?:不对|不行|不像|不一样|变了|变成|少了|多了|丢了|偏了|错了|没有保留|恢复|改回|还原|原来|本来|之前是|其他不变|其余不变)/i.test(value)
          && /(?:图|图片|画面|效果图|产品图|人物|人脸|脸|面部|五官|眼睛|眼神|嘴|表情|笑|严肃|猫|狗|动物|毛色|花纹|姿势|背景|颜色|风格|构图|光线|比例|细节|文字|logo)/i.test(value);
        if (implicitVisualCorrection) return true;

        // 看图问答常用指代，例如“这个人大概多少岁”“这只猫什么品种”。
        const visibleSubject = /(?:这只|那只|这个人|那个人|这个人物|那个人物|这件|那件|这款|那款|它|他|她|猫|狗|动物|人物|人脸)/i.test(value);
        const visibleQuestion = /(?:是什么|什么品种|几岁|多少岁|多大年龄|大概多大|什么颜色|穿着|拿着|在哪里|长什么样|描述|识别|判断|看一下|分析一下)/i.test(value);
        return visibleSubject && visibleQuestion;
      }
      window.__shouldReuseHomeRetainedFiles = shouldReuseHomeRetainedFiles;

      function currentAnalysisPayload(options = {}) {
        const targetSession = options.sessionId ? homeSessionById(options.sessionId) : activeCustomerSession();
        const targetIsActive = !targetSession || targetSession.id === state.activeCustomerSessionId;
        let retainedFileIds = Array.isArray(options.retainedFileIds)
          ? [...options.retainedFileIds]
          : targetIsActive
            ? homeRetainedFileIds()
            : Array.isArray(targetSession?.retainedFileIds) ? [...targetSession.retainedFileIds] : [];
        const sessionPendingFileIds = Array.isArray(targetSession?.pendingFileIds) ? targetSession.pendingFileIds : [];
        const hasNewComposerFiles = targetIsActive && Boolean(document.getElementById("homeFiles")?.files?.length);
        let text = String(
          options.text ?? (
            targetIsActive
              ? document.getElementById("aiQuestion")?.value || window.__activeHomePrompt || document.getElementById("customerText")?.value || ""
            : targetSession?.activePrompt || targetSession?.draft || ""
          )
        ).trim();
        if (targetIsActive && !retainedFileIds.length && shouldReuseHomeRetainedFiles(text, [1])) {
          retainedFileIds = recoverHomeRetainedFilesFromConversation();
        }
        const includePendingFiles = options.includePendingFiles ?? (targetIsActive ? hasNewComposerFiles : true);
        const selectedRetainedFileIds = shouldReuseHomeRetainedFiles(text, retainedFileIds)
          ? [...retainedFileIds]
          : [];
        const activeFileIds = includePendingFiles && sessionPendingFileIds.length
          ? [...sessionPendingFileIds]
          : includePendingFiles && targetIsActive && uploadedFileIds.length
            ? [...uploadedFileIds]
            : includePendingFiles && hasNewComposerFiles
              ? []
              : selectedRetainedFileIds;
        if (!text && activeFileIds.length) {
          text = "请先客观描述附件是什么，再根据内容判断这是普通图片问答、客户需求、订单截图、报价资料、做图需求还是跟单事项。不确定的地方直接说明，不要猜测，也不要强行套用报价模板。";
        }
        const startsNewCustomer = /^(?:新客户|换个客户|换一个客户|另一个客户|另外一个客户|新询盘|重新开始|开始新客户)/.test(text);
        const sessionAnalysis = options.previousAnalysis ?? targetSession?.businessAnalysis;
        return {
          text,
          file_ids: activeFileIds,
          retained_file_ids: retainedFileIds,
          analysis_mode: normalizeHomeAnalysisMode(
            options.analysisMode
            || targetSession?.analysisMode
            || (targetIsActive ? window.__homeAnalysisMode : "standard")
          ),
          customer: String(options.customer || targetSession?.customer || (targetIsActive ? document.getElementById("homeCustomerName")?.value || currentCustomerName() : "") || "新客户").trim(),
          use_memory: true,
          previous_analysis: startsNewCustomer ? null : (sessionAnalysis || (targetIsActive ? window.__latestAiAnalysis : null) || null)
        };
      }
      function payloadSignature(payload) {
        return JSON.stringify({
          text: String(payload?.text || "").trim(),
          analysis_mode: String(payload?.analysis_mode || "standard"),
          customer: String(payload?.customer || "").trim(),
          file_ids: [...(payload?.file_ids || [])].sort((a, b) => a - b),
          retained_file_ids: [...(payload?.retained_file_ids || [])].sort((a, b) => a - b),
          previous_analysis: payload?.previous_analysis ? {
            product: payload.previous_analysis.product,
            size: payload.previous_analysis.size,
            qty: payload.previous_analysis.qty,
            material: payload.previous_analysis.material,
            process: payload.previous_analysis.process,
            tax_shipping: payload.previous_analysis.tax_shipping,
            sample_required: payload.previous_analysis.sample_required,
            missing: payload.previous_analysis.missing
          } : null
        });
      }
      function analysisResultIsRetryableFailure(data) {
        const analysis = data?.analysis;
        return Boolean(
          analysis?.provider_error
          || ["service_unavailable", "image_unavailable"].includes(String(analysis?.intent || ""))
        );
      }
      async function requestAnalysis(payload, sessionId = "") {
        payload = {
          ...payload,
          customer: String(payload?.customer || document.getElementById("homeCustomerName")?.value || currentCustomerName() || "新客户").trim(),
          use_memory: payload?.use_memory !== false
        };
        const signature = payloadSignature(payload);
        const requestKey = sessionId || signature;
        const cacheKey = sessionId ? `${sessionId}\u0000${signature}` : signature;
        const pending = homeSessionAnalysisRequests.get(requestKey);
        if (pending?.signature === signature) return pending.promise;
        if (homeSessionAnalysisCache.has(cacheKey)) return homeSessionAnalysisCache.get(cacheKey);
        const request = api("/api/ai/analyze", {
          method: "POST",
          body: JSON.stringify(payload)
        }).then(data => {
          lastAnalysisData = data;
          lastAnalysisSignature = signature;
          if (analysisResultIsRetryableFailure(data)) {
            // A temporary model/provider failure must never satisfy an explicit
            // retry from memory. Keep only successful analysis responses.
            homeSessionAnalysisCache.delete(cacheKey);
          } else {
            homeSessionAnalysisCache.set(cacheKey, data);
            if (homeSessionAnalysisCache.size > 20) homeSessionAnalysisCache.delete(homeSessionAnalysisCache.keys().next().value);
          }
          return data;
        }).finally(() => {
          if (homeSessionAnalysisRequests.get(requestKey)?.promise === request) homeSessionAnalysisRequests.delete(requestKey);
          if (latestAnalysisPromise === request) latestAnalysisPromise = null;
        });
        homeSessionAnalysisRequests.set(requestKey, { signature, promise: request });
        if (!sessionId || sessionId === state.activeCustomerSessionId) {
          latestAnalysisPromise = request;
          latestAnalysisSignature = signature;
        }
        return request;
      }
      async function syncAnalysis(options = {}) {
        const sessionId = options.sessionId || activeCustomerSession()?.id || "";
        await ensureFormalLogin();
        const pendingUpload = homeSessionUploadPromises.get(sessionId) || (sessionId === state.activeCustomerSessionId ? latestUploadPromise : null);
        if (pendingUpload) await pendingUpload.catch(() => []);
        if (sessionId === state.activeCustomerSessionId && (!uploadedFileIds.length || window.__homeUploadFailures?.length) && document.getElementById("homeFiles")?.files?.length) {
          const upload = uploadFilesFromInput("homeFiles", "homeFileName");
          homeSessionUploadPromises.set(sessionId, upload);
          latestUploadPromise = upload;
          await upload.catch(() => []);
          if (homeSessionUploadPromises.get(sessionId) === upload) homeSessionUploadPromises.delete(sessionId);
        }
        const payload = currentAnalysisPayload({ ...options, sessionId });
        if (!payload.text && !payload.file_ids.length) return null;
        return requestAnalysis(payload, sessionId);
      }
      async function retryHomeAnalysisInPlace() {
        const input = document.getElementById("aiQuestion");
        const session = activeCustomerSession();
        const retryText = String(window.__activeHomePrompt || window.__homeVisiblePrompt || "").trim();
        const hasReference = Boolean(
          document.getElementById("homeFiles")?.files?.length || homeRetainedFileIds().length
        );
        if (!retryText && !hasReference) {
          input?.focus();
          renderFormalStatus("原问题已经没有可重试的内容 · 请补充一句后发送");
          return;
        }
        if (input && !input.value.trim() && retryText) {
          input.value = retryText;
          autoGrowHomeQuestion(input);
        }
        lastAnalysisData = null;
        lastAnalysisSignature = "";
        latestAnalysisPromise = null;
        latestAnalysisSignature = "";
        renderFormalStatus("正在使用已保留的问题原地重试…");
        return window.startHomeInquiry?.({
          analysisMode: normalizeHomeAnalysisMode(
            session?.lastRequestedAnalysisMode || "standard"
          )
        });
      }
      async function autoAnalyzeHomeFilesAfterUpload(newIds) {
        if (!newIds?.length) return;
        lastAnalysisData = null;
        lastAnalysisSignature = "";
        setTextIfExists("homeIntentHint", `附件已保存。直接发送即可，系统会自动判断图片类型和你的意图。`);
        document.getElementById("homeIntentHint")?.classList.remove("is-hidden");
        renderFormalStatus(`已保存 ${newIds.length} 个文件 · 等待发送 · 未扣点`);
      }
      function renderBackendAnalysis(analysis) {
        const answer = document.getElementById("aiAnswer");
        if (!answer || !analysis) return;
        showHomeResult();
        const reasoningFallbackNotice = analysis.analysis_mode_degraded
          ? `<div class="ai-confidence-note"><strong>深度分析降级提醒：</strong>${escapeHtml(analysis.analysis_mode_message || "深度推理线路暂时不可用，本次已由普通模型完成，请复核关键数据。")}</div>`
          : "";
        if (isStructuredBusinessAnalysis(analysis)) {
          window.__latestAiAnalysis = analysis;
          window.__latestAiSourceText = (
            window.__activeHomePrompt ||
            document.getElementById("aiQuestion")?.value ||
            ""
          ).trim();
        }
        if (analysis.intent === "casual_chat") {
          hideHomeNextActions();
          answer.innerHTML = `
            <div class="ai-answer-body">
              <p class="ai-answer-lead">${escapeHtml(analysis.customer_reply || "你好，我在。你可以直接把客户消息、截图或产品图发给我。")}</p>
              <div class="actions"><button class="btn light" type="button" onclick="document.getElementById('aiQuestion')?.focus()">继续聊</button></div>
            </div>
          `.trim();
          return;
        }
        if (analysis.intent === "direct_answer") {
          hideHomeNextActions();
          const professional = analysis.response_mode === "factory_professional";
          const risks = Array.isArray(analysis.risk) ? analysis.risk : [];
          answer.innerHTML = `
            <div class="ai-answer-body">
              <p class="ai-answer-lead">${escapeHtml(analysis.direct_answer || analysis.customer_reply || "我已经理解你的问题。")}</p>
              ${reasoningFallbackNotice}
              ${professional ? `<div class="ai-answer-section"><h3>专业处理模式</h3><p>这是厂家业务或技术问题，回答会优先考虑成本、工艺、交期和落地风险。</p></div>` : ""}
              ${risks.length ? `<div class="ai-answer-section"><h3>需要注意</h3><ul>${risks.map(item => `<li>${escapeHtml(item)}</li>`).join("")}</ul></div>` : ""}
              <div class="actions"><button class="btn light" type="button" onclick="document.getElementById('aiQuestion')?.focus()">继续问</button>${analysis.source_intent === "visual_request" ? `<button class="btn dark" type="button" onclick="generateHomeVisualInline()">直接生成效果图</button>` : ""}${analysis.source_intent === "detail_request" ? `<button class="btn dark" type="button" onclick="homeGoDetail()">进入详情制作</button>` : ""}${analysis.source_intent === "followup_request" ? `<button class="btn dark" type="button" onclick="homeCreateTask()">建立跟单</button>` : ""}</div>
            </div>
          `.trim();
          renderFormalStatus(analysis.analysis_mode_degraded
            ? "深度分析服务已降级 · 本次由普通模型完成"
            : analysis.analysis_mode_effective === "deep"
              ? "DeepSeek Pro 深度分析已完成"
              : professional ? "专业问题已回答" : "通用问题已回答");
          return;
        }
        if (analysis.intent === "service_unavailable") {
          hideHomeNextActions();
          const textNeedsConfiguration = analysis.unavailable_reason === "not_configured";
          const textServiceTemporarilyUnavailable = analysis.unavailable_reason === "temporary_failure";
          answer.innerHTML = `
            <div class="ai-answer-body">
              <p class="ai-answer-lead">消息已经接住并保留，没有丢失。</p>
              <div class="ai-answer-section"><h3>${textNeedsConfiguration ? "AI回复服务还没配置" : "回复服务暂时繁忙"}</h3><p>${escapeHtml(analysis.customer_reply || "消息已经保留，本次没有扣点。")}</p></div>
              <div class="actions">${textNeedsConfiguration ? `<button class="btn dark" type="button" onclick="openFormalOps()">配置AI服务</button>` : `<button class="btn dark" type="button" onclick="window.retryHomeAnalysisInPlace?.()">原地重试</button>`}<button class="btn light" type="button" onclick="document.getElementById('aiQuestion')?.focus()">继续补充信息</button></div>
            </div>
          `.trim();
          renderFormalStatus(textNeedsConfiguration
            ? "消息已保留 · AI服务待配置 · 未扣点"
            : textServiceTemporarilyUnavailable
              ? "消息已保留 · 服务暂时繁忙 · 可原地重试 · 未扣点"
              : "消息已保留 · 可原地重试 · 未扣点");
          return;
        }
        if (analysis.intent === "image_unavailable") {
          hideHomeNextActions();
          const imageNeedsConfiguration = analysis.unavailable_reason === "not_configured";
          const imageServiceTemporarilyUnavailable = analysis.unavailable_reason === "temporary_failure";
          const imageReferenceUnreadable = analysis.unavailable_reason === "reference_unreadable";
          const imageUnavailableTitle = imageNeedsConfiguration
            ? "图片识别服务还没配置"
            : imageReferenceUnreadable
              ? "这张图片暂时无法读取"
              : "图片识别暂时不可用";
          answer.innerHTML = `
            <div class="ai-answer-body">
              <p class="ai-answer-lead">图片已经接住并安全保留，没有丢失。</p>
              <div class="ai-answer-section"><h3>${escapeHtml(imageUnavailableTitle)}</h3><p>${escapeHtml(analysis.customer_reply || "当前不能可靠读取图片内容。本次没有扣点，原图和问题都已保留。")}</p></div>
              <div class="actions">${imageNeedsConfiguration ? `<button class="btn dark" type="button" onclick="openFormalOps()">配置视觉AI</button>` : imageReferenceUnreadable ? `<button class="btn dark" type="button" onclick="removeAllHomeFiles()">重新上传图片</button>` : `<button class="btn dark" type="button" onclick="window.retryHomeAnalysisInPlace?.()">原地重试</button>`}<button class="btn light" type="button" onclick="document.getElementById('aiQuestion')?.focus()">补充文字或继续问</button>${imageReferenceUnreadable ? "" : `<button class="btn light" type="button" onclick="removeAllHomeFiles()">换一张图片</button>`}</div>
            </div>
          `.trim();
          renderFormalStatus(imageNeedsConfiguration
            ? "图片已保留 · 图片识别待配置 · 未扣点"
            : imageServiceTemporarilyUnavailable
              ? "图片已保留 · 服务暂时繁忙 · 可原地重试 · 未扣点"
              : "图片已保留 · 请重新上传清晰图片 · 未扣点");
          return;
        }
        if (analysis.intent === "image_other") {
          hideHomeNextActions();
          answer.innerHTML = `
            <div class="ai-answer-body">
              <p class="ai-answer-lead">识别结果：${escapeHtml(analysis.image_type || "这不是可直接报价的产品需求")}</p>
              <div class="ai-answer-section"><h3>图片内容</h3><p>${escapeHtml(analysis.image_summary || analysis.customer_reply || "请告诉我希望对这张图片做什么。")}</p></div>
              <div class="ai-answer-section"><h3>你可以继续问</h3><p>系统不会把人物照片、风景或普通截图硬套成产品报价；直接在下面追问这张图即可。</p></div>
              <div class="actions"><button class="btn dark" type="button" onclick="document.getElementById('aiQuestion')?.focus()">继续问这张图</button></div>
            </div>
          `.trim();
          renderFormalStatus("图片类型已识别 · 等待选择下一步");
          return;
        }
        if (analysis.intent === "image_qa") {
          hideHomeNextActions();
          answer.innerHTML = `
            <div class="ai-answer-body">
              <p class="ai-answer-lead">${escapeHtml(analysis.direct_answer || analysis.customer_reply || "暂时无法判断，请换一张清晰图片重试。")}</p>
              ${analysis.image_summary ? `<div class="ai-answer-section"><h3>图片观察</h3><p>${escapeHtml(analysis.image_summary)}</p></div>` : ""}
              <div class="actions"><button class="btn light" type="button" onclick="document.getElementById('aiQuestion')?.focus()">继续问这张图</button><button class="btn light" type="button" onclick="removeAllHomeFiles()">换一张图片</button></div>
            </div>
          `.trim();
          renderFormalStatus("图片问答已完成");
          return;
        }
        const asArray = value => Array.isArray(value) ? value : (value ? [value] : []);
        const listText = value => asArray(value).join("、") || "待确认";
        const missing = asArray(analysis.missing);
        const risks = asArray(analysis.risk);
        const customerHabits = asArray(analysis.customer_habits);
        const confidence = analysis.confidence || {};
        const confidenceTag = field => `<span class="ai-confidence-tag${confidence[field] === "high" ? "" : " low"}">${confidence[field] === "high" ? "原文明确" : confidence[field] === "memory" ? "沿用历史·请核对" : "请核对"}</span>`;
        const uncertainLabels = [
          ["product", "产品"], ["size", "尺寸"], ["qty", "数量"], ["material", "材质"],
          ["process", "工艺"], ["tax_shipping", "税运"], ["sample", "打样"]
        ].filter(item => confidence[item[0]] !== "high").map(item => item[1]);
        const extraFacts = [
        analysis.delivery && !String(analysis.delivery).includes("待确认") ? ["交期", analysis.delivery] : null,
        analysis.tax_shipping && !String(analysis.tax_shipping).includes("待确认") ? ["税运", analysis.tax_shipping] : null,
        analysis.address && ["high", "memory"].includes(confidence.address) && !String(analysis.address).includes("待确认") ? ["收货地", analysis.address] : null,
        analysis.sample_status && !String(analysis.sample_status).includes("待确认") ? ["打样", analysis.sample_status] : null
        ].filter(Boolean);
        const missingHtml = missing.length
          ? `<ul>${missing.map(item => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`
          : `<p>资料基本齐全，可以继续报价、做图或生成跟单任务。</p>`;
        const errorNote = analysis.provider_error
          ? `<div class="ai-answer-section"><h3>识别提醒</h3><p>这次没有完全读清图片内容。补充一句客户的文字要求后再发送，结果会更准。</p></div>`
          : "";
        const localFallbackNote = analysis.analysis_notice
          ? `<div class="ai-answer-section"><h3>处理提醒</h3><p>${escapeHtml(analysis.analysis_notice)}</p></div>`
          : "";
        answer.innerHTML = `
          <div class="ai-answer-body">
            <p class="ai-answer-lead">客户要做${escapeHtml(analysis.product || "定制产品")}。</p>
            ${reasoningFallbackNotice}
            ${localFallbackNote}
            <div class="ai-answer-section">
              <h3>已整理的需求</h3>
              <dl class="ai-facts">
                <div><dt>产品 ${confidenceTag("product")}</dt><dd>${escapeHtml(analysis.product || "待确认")}</dd></div>
                <div><dt>尺寸 ${confidenceTag("size")}</dt><dd>${escapeHtml(analysis.size || "待确认")}</dd></div>
                <div><dt>数量 ${confidenceTag("qty")}</dt><dd>${escapeHtml(String(analysis.qty || "待确认"))}</dd></div>
                <div><dt>材质 ${confidenceTag("material")}</dt><dd>${escapeHtml(listText(analysis.material))}</dd></div>
                <div><dt>工艺 ${confidenceTag("process")}</dt><dd>${escapeHtml(listText(analysis.process))}</dd></div>
                ${extraFacts.map(item => `<div><dt>${escapeHtml(item[0])} ${confidenceTag(item[0] === "税运" ? "tax_shipping" : item[0] === "打样" ? "sample" : item[0] === "收货地" ? "address" : "delivery")}</dt><dd>${escapeHtml(String(item[1]))}</dd></div>`).join("")}
              </dl>
              ${uncertainLabels.length ? `<div class="ai-confidence-note">${analysis.memory_inherited ? "客户明确要求按上次/老习惯处理，系统已带入历史资料。" : "以下内容没有在客户原话中完全确认。"} 请核对：${escapeHtml(uncertainLabels.join("、"))}。展开报价工作台后仍需人工确认，避免把旧订单变化静默带入。</div>` : ""}
              <p class="muted">请核对材质和工艺是否完整。有补充或更正时直接继续发送，系统会合并到当前客户。</p>
            </div>
            ${customerHabits.length ? `<div class="ai-answer-section"><h3>已记住的客户习惯</h3><p>${customerHabits.map(item => escapeHtml(item)).join("；")}</p></div>` : ""}
            <div class="ai-answer-section">
              <h3>还要问客户</h3>
              ${missingHtml}
            </div>
            <div class="ai-answer-section">
              <h3>报价前注意</h3>
              <p>${escapeHtml(risks.join("；") || "暂无明显风险，报价前再核对交期和文件。")}</p>
            </div>
            <div class="ai-answer-section">
              <h3>可以直接发给客户</h3>
              <p>${escapeHtml(analysis.customer_reply || "已识别需求，请继续补充客户消息。")}</p>
            </div>
            ${errorNote}
          </div>
        `.trim();
        renderHomeNextActions([
          analysis.product,
          analysis.size,
          analysis.qty,
          listText(analysis.material),
          listText(analysis.process),
          analysis.customer_reply
        ].join(" "));
      }
      function applyPendingHomeSessionResult(session) {
        const pending = session?.pendingAnalysisResult;
        if (!pending?.analysis || session.id !== state.activeCustomerSessionId) return false;
        const currentTurn = String(pending.currentTurn || pending.prompt || "").trim();
        window.__activeHomePrompt = String(pending.prompt || currentTurn);
        window.__homeVisiblePrompt = String(pending.visiblePrompt || currentTurn);
        window.__homeConversationContext = String(pending.prompt || currentTurn);
        if (isStructuredBusinessAnalysis(pending.analysis)) {
          window.__latestAiAnalysis = pending.analysis;
          window.__latestAiSourceText = currentTurn;
          session.businessAnalysis = pending.analysis;
          session.businessSourceText = pending.isFollowUp && session.businessSourceText
            ? `${session.businessSourceText}\n补充：${currentTurn}`.slice(-12000)
            : currentTurn;
        }
        renderBackendAnalysis(pending.analysis);
        session.answerHtml = document.getElementById("aiAnswer")?.innerHTML || session.answerHtml || "";
        session.resultEmpty = false;
        session.hasConversation = true;
        session.pendingAnalysisResult = null;
        session.processingStatus = "";
        session.hasUnreadResult = false;
        captureHomeSessionRuntime(session);
        return true;
      }
      window.__applyPendingHomeSessionResult = applyPendingHomeSessionResult;
      function clampDetailPackageCount(value, fallback = 8) {
        const number = Number(value);
        return Number.isFinite(number) ? Math.max(1, Math.min(12, Math.round(number))) : fallback;
      }
      function detailPackageRequestedCount(sourceText = "") {
        const text = String(sourceText || "").replace(/\s+/g, "").trim();
        if (!text || /第(?:\d{1,2}|[一二三四五六七八九十两]+)张/.test(text)) return null;
        const chineseNumbers = {
          一: 1, 二: 2, 两: 2, 三: 3, 四: 4, 五: 5, 六: 6,
          七: 7, 八: 8, 九: 9, 十: 10, 十一: 11, 十二: 12
        };
        const match = text.match(/(?:我要|我想要|要|做|生成|制作|出|改成|调整为|一共|总共|共)?(\d{1,2}|十二|十一|十|[一二两三四五六七八九])张/);
        if (!match) return null;
        const number = /^\d+$/.test(match[1]) ? Number(match[1]) : chineseNumbers[match[1]];
        return Number.isFinite(number) ? clampDetailPackageCount(number) : null;
      }
      function isHomeDetailPackageRequest(analysis, sourceText = "") {
        if (analysis?.source_intent === "detail_request" || analysis?.intent === "detail_request") return true;
        const text = String(sourceText || "").replace(/\s+/g, "").trim();
        if (!text) return false;
        return /(?:做|生成|制作|整理|设计|出|一键).{0,18}(?:宝贝详情|商品详情|产品详情|详情页|详情长图|整套详情|整套图文)/i.test(text)
          || /(?:宝贝详情|商品详情|产品详情|详情页|详情长图|整套详情|整套图文).{0,12}(?:做|生成|制作|整理|设计|出|要)/i.test(text)
          || /^(?!.*(?:是什么|什么意思|在哪里|有没有|怎么用|\?|？)).{0,20}(?:产品)?(?:的)?(?:宝贝详情|商品详情|产品详情|详情页|详情长图|整套详情|整套图文)(?:图|页面)?[。！!，,]?$/i.test(text);
      }
      function homeDetailPackageProduct(sourceText = "", analysis = null) {
        const analyzedProduct = String(analysis?.product || "").trim();
        if (analyzedProduct && !["定制产品", "待确认"].includes(analyzedProduct) && !analyzedProduct.includes("待确认")) return analyzedProduct;
        const text = String(sourceText || "");
        const productMatch = text.match(/(冰箱贴|磁贴|钥匙扣|钥匙链|徽章|胸章|吧唧|亚克力立牌|立牌|摆件|杯垫|鼠标垫|手机壳|帆布袋|无纺布袋|手提袋|礼品袋|购物袋|包装盒|彩盒|礼盒|纸盒|标签贴纸|不干胶|贴纸|名片|画册|说明书|展示架|标牌|保温杯|马克杯|服装|内衣|硅胶制品|五金件|电子配件)/i);
        if (productMatch) return productMatch[1];
        const local = analyze(text);
        return local.product && local.product !== "定制产品" ? local.product : "定制产品";
      }
      function homeDetailPackageReferenceRole(sourceText = "", analysis = null, referenceFileIds = []) {
        if (!referenceFileIds.length) return "auto";
        const text = `${sourceText || ""} ${analysis?.image_summary || ""}`;
        if (/(?:图案|插画|绘画|照片|头像|肖像|logo|标志|IP形象|卡通形象|花纹).{0,18}(?:印在|放在|应用|做成|制成|变成|定制)|(?:用|拿).{0,18}(?:图|照片|插画|logo|形象).{0,18}(?:做|制作|定制|生成)/i.test(text)) return "artwork";
        if (/(?:产品实拍|实物图|样品图|成品图|产品照片|现有产品|这款产品|这个产品)/i.test(text)) return "product_reference";
        if (/(?:参考风格|风格参考|照这个风格|类似画面|同款风格)/i.test(text)) return "style_reference";
        return "auto";
      }
      function detailPackagePlanNames(product = "定制产品", count = 8) {
        const names = [
          `${product}首屏主视觉`,
          "整体外观与核心卖点",
          "材质纹理与工艺细节",
          "结构、背面与使用方式",
          "尺寸比例与规格说明",
          "真实使用场景",
          "颜色、图案与定制选项",
          "工厂生产与质检",
          "包装保护与发货",
          "定制流程",
          "适用场景与礼赠用途",
          "下单须知与咨询引导"
        ];
        return names.slice(0, clampDetailPackageCount(count));
      }
      function upsertHomeDetailPackageJob(session, sourceText = "", analysis = null, referenceFileIds = [], options = {}) {
        if (!session) return null;
        const existing = session.detailPackageJob && typeof session.detailPackageJob === "object" ? session.detailPackageJob : null;
        const explicitCount = detailPackageRequestedCount(sourceText);
        const analyzedCount = Number(analysis?.detail_page_count || 0);
        const mergedReferenceIds = [...new Set([
          ...(referenceFileIds || []),
          ...(existing?.referenceFileIds || [])
        ].map(Number).filter(id => id > 0))].slice(-16);
        const product = homeDetailPackageProduct(sourceText || existing?.sourceText || "", analysis)
          || existing?.product
          || "定制产品";
        const platform = /淘宝|天猫/i.test(sourceText)
          ? "淘宝/天猫"
          : /拼多多/i.test(sourceText)
            ? "拼多多"
            : /小红书/i.test(sourceText)
              ? "小红书"
              : existing?.platform || "阿里巴巴/1688";
        const now = new Date().toISOString();
        const job = {
          id: existing?.id || `detail-job-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
          sourceText: String(options.keepSource && existing?.sourceText ? existing.sourceText : sourceText || existing?.sourceText || "").slice(0, 20000),
          product: product === "定制产品" && existing?.product ? existing.product : product,
          platform,
          targetCount: clampDetailPackageCount(
            options.targetCount
            ?? explicitCount
            ?? (Number.isFinite(analyzedCount) && analyzedCount > 0 ? analyzedCount : null)
            ?? existing?.targetCount
            ?? 8
          ),
          referenceFileIds: mergedReferenceIds,
          referenceRole: homeDetailPackageReferenceRole(sourceText, analysis, mergedReferenceIds) !== "auto"
            ? homeDetailPackageReferenceRole(sourceText, analysis, mergedReferenceIds)
            : existing?.referenceRole || "auto",
          extraInstructions: String(options.extraInstructions ?? existing?.extraInstructions ?? "").slice(0, 4000),
          pageInstructions: options.pageInstructions && typeof options.pageInstructions === "object"
            ? { ...(existing?.pageInstructions || {}), ...options.pageInstructions }
            : { ...(existing?.pageInstructions || {}) },
          status: "awaiting_confirmation",
          generatedCount: Number(existing?.generatedCount || 0),
          detailContent: String(existing?.detailContent || "").slice(0, 30000),
          generatedImages: Array.isArray(existing?.generatedImages)
            ? existing.generatedImages.slice(0, 12)
            : [],
          failures: Array.isArray(existing?.failures) ? existing.failures.slice(0, 12) : [],
          resultHtml: String(existing?.resultHtml || "").slice(0, 50000),
          createdAt: existing?.createdAt || now,
          updatedAt: now
        };
        session.detailPackageJob = job;
        session.updatedAt = now;
        saveState();
        return job;
      }
      function homeDetailPackageReferenceLabel(job) {
        if (!job?.referenceFileIds?.length) return "当前没有参考图，将按文字需求生成产品视觉";
        if (job.referenceRole === "artwork") return "参考图作为产品图案/设计素材，目标产品结构会单独生成";
        if (job.referenceRole === "product_reference") return "参考图作为产品实物与结构依据";
        if (job.referenceRole === "style_reference") return "参考图只作为画面风格依据，不会被误当成产品";
        return "系统会先判断参考图是产品实拍、图案素材还是风格参考，再按目标产品生成";
      }
      function renderHomeDetailPackageConfirmation(session = activeCustomerSession(), message = "") {
        const job = session?.detailPackageJob;
        if (!job) return false;
        const count = clampDetailPackageCount(job.targetCount);
        const estimatedPoints = 3 + count * selectedHomeImagePointCost();
        const plan = detailPackagePlanNames(job.product, count);
        const html = `
          <div class="ai-answer-body">
            <p class="ai-answer-lead">${escapeHtml(message || `已理解为给“${job.product}”制作一整套详情图。`)}</p>
            <div class="ai-answer-section">
              <h3>本套共 ${count} 张独立详情图</h3>
              <ol>${plan.map((name, index) => `<li>第${index + 1}张：${escapeHtml(name)}${job.pageInstructions?.[index + 1] ? ` <strong>（已记录修改要求）</strong>` : ""}</li>`).join("")}</ol>
              <p>${escapeHtml(homeDetailPackageReferenceLabel(job))}。每张都可单独下载，同时自动合成一张750px宽详情长图。</p>
            </div>
            <div class="actions">
              <button class="btn dark" type="button" onclick="window.confirmHomeDetailPackage?.()">确认生成${count}张 · 最多${estimatedPoints}点</button>
              <button class="btn light" type="button" onclick="window.setHomeDetailPackageCount?.(5)">改为5张</button>
              <button class="btn light" type="button" onclick="window.setHomeDetailPackageCount?.(8)">改为8张</button>
              <button class="btn light" type="button" onclick="window.setHomeDetailPackageCount?.(10)">改为10张</button>
            </div>
            <p class="muted">也可以继续填写“要8张”“再加2张”，系统会直接修改当前这套，无需重新建立任务。</p>
          </div>
        `.trim();
        session.answerHtml = html;
        session.resultEmpty = false;
        session.hasConversation = true;
        session.chatActionStatus = `详情图已规划${count}张，等待确认生成`;
        if (session.id === state.activeCustomerSessionId) {
          showHomeResult();
          hideHomeNextActions();
          const answer = document.getElementById("aiAnswer");
          if (answer) answer.innerHTML = html;
          renderFormalStatus(`详情图已规划 · ${job.product} · ${count}张`);
          window.setTimeout(() => scrollHomeReplyAboveComposer(answer), 0);
          captureActiveCustomerSession(true);
        } else {
          saveState();
        }
        renderCustomerWorkbench();
        return true;
      }
      function homeDetailPackageFollowUp(sourceText = "", session = null) {
        const targetSession = session || (typeof activeCustomerSession === "function" ? activeCustomerSession() : null);
        const job = targetSession?.detailPackageJob;
        const text = String(sourceText || "").replace(/\s+/g, "").trim();
        if (!job || !text) return null;
        const asksWhereResultIs = /^(?:图片|图|详情图|结果)(?:呢|在哪里|在哪|出来了吗|好了吗|生成了吗|怎么还没出来|怎么没显示|为什么没显示)[？?。！!]*$|(?:进度|显示|生成).{0,10}(?:到了|完成|100%).{0,10}(?:图片|图|结果).{0,10}(?:没有|没出来|没显示|在哪|呢)/i.test(text);
        if (asksWhereResultIs) {
          if (job.status === "generating") return { statusOnly: true };
          if (["completed", "partial"].includes(job.status)) return { showResult: true };
          if (job.status === "failed") return { showFailure: true };
        }
        if (/(?:为什么|怎么|咋)(?:还)?只有(?:一|1)张|(?:说好|要|要求).{0,8}(?:\d{1,2}|[一二两三四五六七八九十两]+)张.{0,8}(?:却|但|怎么)?(?:只有|只出|只生成)(?:一|1)张|没有(?:生成|做|出)(?:完|全|够)/i.test(text)) {
          const count = clampDetailPackageCount(job.targetCount || 8);
          return {
            targetCount: count,
            incompleteComplaint: true,
            message: `你说得对，当前只得到单张预览，没有完成原定${count}张。这里恢复整套任务；点击“确认生成${count}张”后才会真正启动，并显示1/${count}到${count}/${count}的实时进度。`
          };
        }
        if (/^(?:确认|开始|继续|立即|马上)?(?:生成|制作|出|补齐|补全)(?:剩下|剩余|余下)?(?:的)?(?:\d{1,2}|[一二两三四五六七八九十两]+)?张?(?:宝贝详情|商品详情|详情图|图片|图)?[。！!，,]?$/i.test(text)) {
          const asksForRemaining = /(?:剩下|剩余|余下)/.test(text);
          return {
            targetCount: asksForRemaining
              ? clampDetailPackageCount(job.targetCount || 8)
              : detailPackageRequestedCount(text) || clampDetailPackageCount(job.targetCount || 8),
            startGeneration: true,
            message: "已确认，正在启动整套宝贝详情生成。"
          };
        }
        const add = text.match(/(?:再|还要)(?:加|增加|补)(\d{1,2})张/);
        if (add) return { targetCount: clampDetailPackageCount(Number(job.targetCount || 8) + Number(add[1])), message: `已在当前这套详情中增加${Number(add[1])}张。` };
        const reduce = text.match(/(?:少|减|去掉)(\d{1,2})张/);
        if (reduce) return { targetCount: clampDetailPackageCount(Number(job.targetCount || 8) - Number(reduce[1])), message: `已在当前这套详情中减少${Number(reduce[1])}张。` };
        const pageEdit = text.match(/(?:改|修改|重做|重新生成|调整)第?\s*(\d{1,2})\s*张(?:[，,:：]?\s*(.*))?/i);
        if (pageEdit) {
          const pageIndex = clampDetailPackageCount(Number(pageEdit[1]));
          if (pageIndex > Number(job.targetCount || 8)) return null;
          const instruction = String(pageEdit[2] || "按最新要求重新设计这一张，其他页面保持原方案").trim();
          return { pageIndex, pageInstruction: instruction, message: `明白，已把第${pageIndex}张的修改要求记在当前这套详情里。` };
        }
        const requested = detailPackageRequestedCount(text);
        if (requested != null && /^(?:我)?(?:想)?(?:要|做|生成|制作|出|改成|调整为|一共|总共|共)?(?:\d{1,2}|十二|十一|十|[一二两三四五六七八九])张(?:图|图片|详情图|宝贝详情)?[。！!，,]?$/.test(text)) {
          return { targetCount: requested, message: `明白，当前这套宝贝详情改为${requested}张。` };
        }
        return null;
      }
      function handleHomeDetailPackageFollowUp(sourceText = "", session = null) {
        const targetSession = session || (typeof activeCustomerSession === "function" ? activeCustomerSession() : null);
        const update = homeDetailPackageFollowUp(sourceText, targetSession);
        if (!update) return false;
        targetSession.lastMessage = String(sourceText || "").slice(0, 80);
        targetSession.conversationContext = String(sourceText || "");
        beginHomeConversation();
        if (update.statusOnly) {
          renderHomeDetailPackageProgress(
            targetSession.detailPackageJob?.progressPercent || 1,
            targetSession.detailPackageJob?.progressMessage || "整套宝贝详情仍在生成，完成后图片会直接显示在这里。",
            targetSession,
            targetSession.processingRequestId || ""
          );
          captureActiveCustomerSession(true);
          return true;
        }
        if (update.showResult) {
          restoreHomeDetailPackageResult(targetSession);
          captureActiveCustomerSession(true);
          return true;
        }
        if (update.showFailure) {
          renderHomeDetailPackageFailure("上一轮整套详情没有完成，原产品资料和参考图仍已保留，可以原地重试。", targetSession);
          captureActiveCustomerSession(true);
          return true;
        }
        const pageInstructions = update.pageIndex
          ? { [update.pageIndex]: update.pageInstruction }
          : null;
        upsertHomeDetailPackageJob(targetSession, "", null, [], {
          keepSource: true,
          targetCount: update.targetCount ?? targetSession.detailPackageJob.targetCount,
          pageInstructions
        });
        renderHomeDetailPackageConfirmation(targetSession, update.message);
        if (update.startGeneration) {
          window.setTimeout(() => confirmHomeDetailPackage(), 0);
        }
        return true;
      }
      window.handleHomeDetailPackageFollowUp = handleHomeDetailPackageFollowUp;
      function prepareHomeDetailPackage(sourceText = "", analysis = null, referenceFileIds = []) {
        const session = activeCustomerSession();
        if (!session) return false;
        const source = String(sourceText || homeSourceText("") || session.detailPackageJob?.sourceText || "").trim();
        const refs = referenceFileIds.length ? referenceFileIds : homeRetainedFileIds();
        if (!source && !refs.length) {
          showHomeInlineVisualMessage("还缺少产品资料。", "请上传产品图、图案素材或参考图，并写清要做什么产品；系统会先规划整套详情，再确认生成张数。", `<button class="btn light" type="button" onclick="document.getElementById('homeFiles')?.click()">上传参考图</button>`);
          return false;
        }
        const job = upsertHomeDetailPackageJob(session, source, analysis, refs);
        return renderHomeDetailPackageConfirmation(session, `已建立“${job.product}”详情图任务，后续数量和修改要求都会继续记在这一套里。`);
      }
      async function prepareHomeDetailPackageFromComposer(sourceText = "", analysis = null) {
        const session = activeCustomerSession();
        if (!session) return false;
        const localFiles = Array.from(document.getElementById("homeFiles")?.files || []);
        try {
          if (localFiles.length) {
            let upload = homeSessionUploadPromises.get(session.id) || latestUploadPromise;
            if (!upload) {
              upload = uploadFilesFromInput("homeFiles", "homeFileName");
              homeSessionUploadPromises.set(session.id, upload);
              latestUploadPromise = upload;
            }
            await upload;
            if (homeSessionUploadPromises.get(session.id) === upload) homeSessionUploadPromises.delete(session.id);
          }
        } catch (error) {
          keepHomeComposerAttachmentsForRetry("产品图暂时没有上传成功，附件和文字都已保留。请直接重试，不需要重新选择。");
          return false;
        }
        const refreshed = homeSessionById(session.id) || session;
        const references = Array.isArray(refreshed.pendingFileIds) && refreshed.pendingFileIds.length
          ? [...refreshed.pendingFileIds]
          : homeRetainedFileIds();
        return prepareHomeDetailPackage(sourceText, analysis, references);
      }
      function setHomeDetailPackageCount(count) {
        const session = activeCustomerSession();
        if (!session?.detailPackageJob) return false;
        upsertHomeDetailPackageJob(session, "", null, [], { keepSource: true, targetCount: count });
        return renderHomeDetailPackageConfirmation(session, `当前这套详情图已改为${clampDetailPackageCount(count)}张。`);
      }
      function confirmHomeDetailPackage() {
        const session = activeCustomerSession();
        const job = session?.detailPackageJob;
        if (!job) return prepareHomeDetailPackage();
        const requestId = beginHomeSessionJob(session, "detail_package", job.sourceText || `${job.product} 宝贝详情`);
        if (!requestId) return false;
        job.status = "generating";
        job.progressPercent = 1;
        job.progressMessage = `已确认生成${clampDetailPackageCount(job.targetCount)}张，正在启动真实任务…`;
        job.failures = [];
        job.updatedAt = new Date().toISOString();
        window.__detailPackageHomeSessionId = session.id;
        saveState();
        setIfExists("customerText", job.sourceText || `${job.product} 宝贝详情`);
        setIfExists("contentPlatform", job.platform || "阿里巴巴/1688");
        setIfExists("contentGoal", "做宝贝详情");
        renderHomeDetailPackageProgress(1, job.progressMessage, session, requestId);
        window.setTimeout(() => generateDetailPackage({ sessionId: session.id, requestId }), 0);
        return true;
      }
      window.prepareHomeDetailPackage = prepareHomeDetailPackage;
      window.prepareHomeDetailPackageFromComposer = prepareHomeDetailPackageFromComposer;
      window.setHomeDetailPackageCount = setHomeDetailPackageCount;
      window.confirmHomeDetailPackage = confirmHomeDetailPackage;
      function isHomeInlineVisualRequest(analysis, sourceText = "") {
        if (analysis?.source_intent === "visual_request" || analysis?.intent === "visual_request") return true;
        const text = String(sourceText || "").trim();
        return /(生成|制作|做一张|做成|变成|转换|改成).{0,12}(效果图|产品图|主图|海报|漫画|动漫|二次元|卡通|Q版|插画|油画|水彩|素描|场景图)/i.test(text)
          || /(效果图|产品图|主图|海报|漫画|动漫|二次元|卡通|Q版|插画|油画|水彩|素描|场景图).{0,12}(生成|制作|做一张|做成|变成|转换|改成)/i.test(text);
      }
      function isHomeInlineVisualFollowUpRequest(sourceText = "") {
        const text = String(sourceText || "").trim();
        if (isHomeInlineVisualRequest(null, text)) return true;
        const hasReference = homeRetainedFileIds().length
          || uploadedFileIds.length
          || Number(document.getElementById("homeFiles")?.files?.length || 0);
        if (!hasReference) return false;
        const businessOnly = /(报价|询价|订单|数量|单价|总价|交期|付款|跟单|客户资料)/i.test(text)
          && !/(图|图片|照片|画面|背景|人物|产品|主体|猫|狗|手|脚|爪|脸|五官|四肢|尾巴|耳朵)/i.test(text);
        if (businessOnly) return false;
        const explicitEdit = /(再生成|再来一张|重新生成|重新做|换成|改成|做成|修改|调整|修正|修复|校正|修一下|改自然|抠图|抠掉|去背|移除背景|只留|补全|去掉|删除|添加|增加|保留|背景|颜色|风格|构图|光线|比例|细节|表情|面部|眼神|二次元|动漫|漫画|卡通|Q版|写实|油画|水彩|素描)/i.test(text);
        const implicitCorrection = /(?:不对|不行|不像|不一样|变了|变成|少了|多了|丢了|偏了|错了|没有保留|恢复|改回|还原|原来|本来|之前是|其他不变|其余不变)/i.test(text)
          && /(?:图|图片|画面|效果图|产品图|人物|人脸|脸|面部|五官|眼睛|眼神|嘴|表情|笑|严肃|猫|狗|动物|毛色|花纹|姿势|背景|颜色|风格|构图|光线|比例|细节|文字|logo)/i.test(text);
        return explicitEdit || implicitCorrection;
      }
      function homeInlineVisualPrompt(explicitPrompt = "") {
        return String(
          explicitPrompt
          || window.__lastHomeVisualUserPrompt
          || window.__activeHomePrompt
          || window.__latestAiSourceText
          || document.getElementById("customerText")?.value
          || ""
        ).trim();
      }
      function showHomeInlineVisualMessage(title, message, actions = "", targetSession = null) {
        const html = `
          <div class="ai-answer-body">
            <p class="ai-answer-lead">${escapeHtml(title)}</p>
            <div class="ai-answer-section"><p>${escapeHtml(message)}</p></div>
            ${actions ? `<div class="actions">${actions}</div>` : ""}
          </div>
        `.trim();
        if (targetSession && targetSession.id !== state.activeCustomerSessionId) {
          targetSession.answerHtml = html;
          targetSession.resultEmpty = false;
          targetSession.hasConversation = true;
          return;
        }
        const answer = document.getElementById("aiAnswer");
        if (!answer) return;
        showHomeResult();
        hideHomeNextActions();
        answer.innerHTML = html;
        window.setTimeout(() => scrollHomeReplyAboveComposer(answer), 0);
      }
      window.showHomeInlineVisualMessage = showHomeInlineVisualMessage;
      function isTransientImageUpstreamError(message = "") {
        const detail = String(message || "");
        return /(upstream[_\s-]*error|upstream.{0,80}(forbidden|access|timeout|temporar|unavailable|refused|failed)|上游.{0,40}(拒绝|超时|暂时|不可用))/i.test(detail);
      }
      function isSeedreamAuthorizationError(message = "") {
        const detail = String(message || "");
        return /(Seedream|火山方舟).{0,100}(生图权限|ModelNotOpen|permission denied|forbidden|unauthorized|access denied|拒绝)/i.test(detail)
          || /(ModelNotOpen|permission denied|access denied).{0,100}(Seedream|生图|模型)/i.test(detail);
      }
      function isImageGroupConfigurationError(message = "") {
        const detail = String(message || "");
        if (isTransientImageUpstreamError(detail)) return false;
        return /(图片分组|图片专用\s*Key|未开通|尚未开通|被暂停|forbidden|permission|not activated|not supported|ModelNotOpen|configured account)/i.test(detail);
      }
      function isImageCredentialError(message = "") {
        return /(\b401\b|invalid.?api.?key|incorrect.?api.?key|Key.{0,30}(无效|拒绝|未通过验证)|凭证.{0,20}(无效|失败))/i.test(String(message || ""));
      }
      function isTemporaryImageGenerationError(message = "") {
        if (isTransientImageUpstreamError(message)) return true;
        if (isImageGroupConfigurationError(message)) return false;
        return /(暂时|繁忙|稍后|超时|网络|连接中断|请求过多|timeout|temporar|rate.?limit|\b429\b|\b500\b|\b502\b|\b503\b|\b504\b)/i.test(String(message || ""));
      }
      function imageGenerationFailureMessage(message = "") {
        const detail = String(message || "图片服务没有返回结果");
        if (isSeedreamAuthorizationError(detail)) {
          return `所选将莱图片模型暂未获得生成权限，请管理员在“系统状态”检查图片服务。本次不会扣AI点或生图余额，原图片和要求都已保留。`;
        }
        if (isImageGroupConfigurationError(detail)) {
          return `当前图片模型尚未完成可用性验证。这不是上传图片或要求的问题；本次不会扣AI点或生图余额，原图片和要求都已保留。`;
        }
        if (/(图片主线路和备用线路|图片备用线路超时)/.test(detail)) {
          return `系统已经自动尝试两条图片线路，但本次都没有返回结果。原图片和要求都已保留；可以在当前页面原地重试，本次失败不会扣AI点或生图余额。`;
        }
        if (isTemporaryImageGenerationError(detail)) {
          return `图片服务暂时繁忙，原图片和要求都已保留；可以在当前页面原地重试，本次失败不会扣AI点或生图余额。`;
        }
        return `图片服务暂时没有返回结果。原图片和要求都已保留，可以原地重试；本次失败不会扣AI点或生图余额。`;
      }
      function imageGenerationFailureActions(message = "") {
        if (isImageGroupConfigurationError(message)) {
          return `<button class="btn dark" type="button" onclick="retryTemporaryImageGeneration()">重新检测并重试</button><button class="btn light" type="button" onclick="document.getElementById('aiQuestion')?.focus()">修改要求</button>`;
        }
        if (isTemporaryImageGenerationError(message)) {
          return `<button class="btn dark" type="button" onclick="retryTemporaryImageGeneration()">原地重试</button><button class="btn light" type="button" onclick="document.getElementById('aiQuestion')?.focus()">修改要求</button>`;
        }
        return `<button class="btn dark" type="button" onclick="retryTemporaryImageGeneration()">原地重试</button><button class="btn light" type="button" onclick="openFormalOps()">查看服务状态</button>`;
      }
      async function retryTemporaryImageGeneration() {
        if (window.__homeVisualGenerating) return;
        showHomeInlineVisualMessage("正在重新连接图片服务…", "图片和要求仍在当前询盘中，正在重新检测并原地重试。", "");
        await loadIntegrationStatus().catch(() => null);
        return generateHomeVisualInline(window.__lastHomeVisualUserPrompt || "", { forceAvailabilityCheck: true });
      }
      async function retryStaleImageSetupFromHistory() {
        if (window.__homeVisualGenerating) return;
        showHomeInlineVisualMessage("正在读取当前图片服务状态…", "旧版失败记录不会覆盖当前配置，检测完成后会在本页继续。", "");
        await loadIntegrationStatus().catch(() => null);
        const canRetry = window.__imageGenerationConfigured === true
          && window.__imageGenerationRetryAllowed !== false
          && window.__imageGenerationVerificationStatus !== "invalid";
        if (canRetry) {
          return generateHomeVisualInline(window.__lastHomeVisualUserPrompt || "", { forceAvailabilityCheck: true });
        }
        const setupIssue = window.__imageGenerationSetupIssue || "请平台检查图片服务。";
        showHomeInlineVisualMessage(
          "当前仍需检查图片模型权限。",
          setupIssue,
          `<button class="btn dark" type="button" onclick="retryTemporaryImageGeneration()">重新检测并重试</button><button class="btn light" type="button" onclick="openFormalOps()">查看当前图片服务</button>`
        );
      }
      async function generateHomeVisualInline(explicitPrompt = "", options = {}) {
        const originSession = homeSessionById(options.sessionId || activeCustomerSession()?.id || "");
        if (!originSession) return;
        const originSessionId = originSession.id;
        const rawGenerationMode = String(options.generationMode || originSession.lastImageCreationMode || "mockup");
        const legacyGenerationMode = rawGenerationMode === "visual"
          ? "mockup"
          : rawGenerationMode === "detail" || rawGenerationMode === "detail_package" ? "detail_image" : rawGenerationMode;
        const requestedMode = ["main_image", "detail_image", "ad_image", "text_image", "mockup"].includes(legacyGenerationMode)
          ? legacyGenerationMode
          : "mockup";
        const generationMode = requestedMode === "detail_image" ? "mockup" : requestedMode;
        const isTextImage = generationMode === "text_image";
        const modePresentation = {
          main_image: {
            outputLabel: "主图",
            loading: "正在根据产品图生成电商主图…",
            stages: ["正在根据产品图生成电商主图…", "正在突出产品主体并整理背景…", "正在完成高清主图…"],
            emptyPrompt: "保持产品结构、图案和颜色不变，生成一张主体突出、背景干净的专业电商主图",
            filename: "AI主图.webp",
            alt: "AI生成的产品主图"
          },
          ad_image: {
            outputLabel: "广告图",
            loading: "正在根据产品图制作广告画面…",
            stages: ["正在根据产品图制作广告画面…", "正在安排卖点层级和视觉氛围…", "正在完成高清广告图…"],
            emptyPrompt: "保持产品本身不变，生成一张视觉醒目、适合推广投放的专业广告图",
            filename: "AI广告图.webp",
            alt: "AI生成的产品广告图"
          },
          text_image: {
            outputLabel: "图片",
            loading: "正在把文字描述生成图片…",
            stages: ["正在把文字描述生成图片…", "正在完善构图、光线和风格…", "正在完成高清图片…"],
            emptyPrompt: "",
            filename: "AI文字生图.webp",
            alt: "AI根据文字生成的图片"
          },
          mockup: {
            outputLabel: "效果图",
            loading: "正在根据参考图生成产品效果图…",
            stages: ["正在根据参考图生成产品效果图…", "正在保持产品结构、比例和设计…", "正在完成高清效果图…"],
            emptyPrompt: "保持参考产品和设计不变，生成一张专业的真实场景效果图",
            filename: "AI效果图.webp",
            alt: "AI生成的产品效果图"
          }
        }[generationMode] || null;
        const outputLabel = modePresentation?.outputLabel || "图片";
        const outputFilename = modePresentation?.filename || "AI生成图片.webp";
        const outputAlt = modePresentation?.alt || "AI生成的图片";
        if (homeSessionVisualJobs.has(originSessionId) || homeSessionIsProcessing(originSession)) {
          notify("当前已有图片任务在处理中，完成后会直接显示在这里。");
          return;
        }
        if (window.__imageGenerationConfigured === false && !options.forceAvailabilityCheck) {
          const setupIssue = window.__imageGenerationSetupIssue || "图片服务尚未完成云端验证。";
          const temporaryIssue = isTemporaryImageGenerationError(setupIssue);
          const noBillingStarted = "图片服务还没有配置完成，没有开始扣点。";
          showHomeInlineVisualMessage(
            temporaryIssue
              ? "图片服务暂时没有连上，没有开始扣点。"
              : noBillingStarted,
            temporaryIssue ? `${setupIssue}。图片和要求已经保留，可以原地重试。` : `${setupIssue} 请查看系统状态。`,
            temporaryIssue
              ? `<button class="btn dark" type="button" onclick="retryTemporaryImageGeneration()">重新检测并原地重试</button><button class="btn light" type="button" onclick="openFormalOps()">查看图片服务</button>`
              : `<button class="btn dark" type="button" onclick="retryTemporaryImageGeneration()">重新检测并重试</button><button class="btn light" type="button" onclick="document.getElementById('aiQuestion')?.focus()">继续补充要求</button>`
            , originSession
          );
          return;
        }
        const originIsActive = () => state.activeCustomerSessionId === originSessionId;
        let userPrompt = String(explicitPrompt || originSession.lastVisualUserPrompt || (originIsActive() ? homeInlineVisualPrompt("") : originSession.activePrompt || "")).trim();
        const requestId = beginHomeSessionJob(originSession, "visual", userPrompt);
        if (!requestId) return;
        const job = { requestId, promise: null };
        homeSessionVisualJobs.set(originSessionId, job);
        if (state.activeCustomerSessionId === originSessionId) window.__homeVisualGenerating = true;
        originSession.lastVisualUserPrompt = userPrompt;
        originSession.lastImageCreationMode = generationMode;
        originSession.processingProgressMessage = modePresentation.loading;
        originSession.activePrompt = userPrompt || originSession.activePrompt || "";
        if (originIsActive()) window.__lastHomeVisualUserPrompt = userPrompt;
        const loadingHtml = window.__homeVisualProcessingHtml?.(originSession) || `
          <div class="ai-answer-body">
            <div class="ai-loading" id="homeVisualThinkingState" role="status" aria-live="polite" aria-label="正在首页生成${outputLabel}">
              <span class="ai-loading-spinner" aria-hidden="true"><i></i><i></i><i></i></span>
              <span class="ai-loading-copy"><strong id="homeVisualThinkingText">${modePresentation.loading}</strong><em><span class="ai-elapsed-time" id="homeVisualThinkingElapsed">已等待 0 秒</span><span class="ai-elapsed-note">完成后图片会直接显示在这里</span></em></span>
            </div>
          </div>
        `.trim();
        originSession.answerHtml = loadingHtml;
        originSession.resultEmpty = false;
        originSession.hasConversation = true;
        if (originIsActive()) {
          const answer = document.getElementById("aiAnswer");
          showHomeResult();
          hideHomeNextActions();
          if (answer) answer.innerHTML = loadingHtml;
          scrollImageStudioToTop();
          captureActiveCustomerSession(true);
          syncHomeSessionProcessingUi(originSession);
        }
        renderCustomerWorkbench();
        const stages = modePresentation.stages;
        const thinkingTimer = window.setInterval(() => {
          const startedAt = new Date(originSession.processingStartedAt || 0).getTime();
          const elapsed = startedAt > 0 ? Math.max(0, Math.floor((Date.now() - startedAt) / 1000)) : 0;
          const stageIndex = Math.min(Math.floor(elapsed / 5), stages.length - 1);
          originSession.processingProgressMessage = stages[stageIndex];
          originSession.answerHtml = window.__homeVisualProcessingHtml?.(originSession) || originSession.answerHtml;
          if (originIsActive()) {
            setTextIfExists("homeVisualThinkingText", stages[stageIndex]);
            setTextIfExists("homeVisualThinkingElapsed", `已等待 ${elapsed} 秒`);
          }
        }, 1000);
        if (originIsActive()) window.__homeVisualThinkingTimer = thinkingTimer;
        let failed = false;
        try {
          await ensureFormalLogin();
          const pendingUpload = isTextImage ? null : homeSessionUploadPromises.get(originSessionId) || (originIsActive() ? latestUploadPromise : null);
          if (pendingUpload) await pendingUpload.catch(() => []);
          if (!isTextImage && originIsActive() && (!(originSession.pendingFileIds || []).length || originSession.uploadFailures?.length) && document.getElementById("homeFiles")?.files?.length) {
            const upload = uploadFilesFromInput("homeFiles", "homeFileName");
            homeSessionUploadPromises.set(originSessionId, upload);
            latestUploadPromise = upload;
            await upload;
            if (homeSessionUploadPromises.get(originSessionId) === upload) homeSessionUploadPromises.delete(originSessionId);
          }
          const retainedIds = Array.isArray(options.retainedFileIds)
            ? [...options.retainedFileIds]
            : Array.isArray(originSession.retainedFileIds) ? [...originSession.retainedFileIds] : [];
          const pendingFileIds = Array.isArray(originSession.pendingFileIds) ? [...originSession.pendingFileIds] : [];
          const activeIds = isTextImage ? [] : Array.isArray(options.referenceFileIds) && options.referenceFileIds.length
            ? [...options.referenceFileIds]
            : pendingFileIds.length ? pendingFileIds : retainedIds;
          const composerFileIds = pendingFileIds;
          const composerUploadRecords = Array.isArray(originSession.pendingFileRecords) ? originSession.pendingFileRecords.map(record => ({ ...record })) : [];
          const composerFileNames = Array.isArray(options.composerFileNames) && options.composerFileNames.length
            ? [...options.composerFileNames]
            : composerUploadRecords.map(record => record.name).filter(Boolean);
          const attachmentBinding = await bindCurrentTurnUserAttachments(composerUploadRecords, originSessionId);
          const referenceFileIds = activeIds.slice(-16);
          const referenceFileId = referenceFileIds[referenceFileIds.length - 1] || null;
          const meaningfulPrompt = userPrompt.replace(/(请|帮我|给我|生成|制作|做一张|效果图|图片|一下|一张)/g, "").trim();
          if (isTextImage && meaningfulPrompt.length < 6) {
            showHomeInlineVisualMessage(
              "画面描述还不够。",
              "至少写清主体、背景和风格；文字生图不会读取历史附件。",
              `<button class="btn dark" type="button" onclick="document.getElementById('aiQuestion')?.focus()">补充画面描述</button>`,
              originSession
            );
            return;
          }
          if (!isTextImage && !referenceFileId) {
            showHomeInlineVisualMessage(
              `${outputLabel}生成需要一张参考图。`,
              "请上传产品图、设计稿或图案素材。系统会以它为准生成，不会凭文字猜产品结构。",
              `<button class="btn dark" type="button" onclick="document.getElementById('homeFiles')?.click()">上传参考图</button>`,
              originSession
            );
            return;
          }
          if (!userPrompt) userPrompt = modePresentation.emptyPrompt;
          const referenceFidelityRules = [
            "参考图是本次生成的唯一视觉事实来源；先识别参考图中的产品主体、结构、材质、图案、文字和原始画面用途。",
            "参考图原有背景、截图边框、页面留白、拍摄杂物和展示界面不属于产品设计，除非用户明确要求保留，否则不要复制到成品图。",
            "若参考图是平面设计稿、图案或包装展开稿，应把设计准确应用到对应实物表面，保持元素相对位置、方向、比例和铺图范围；不得随意重排、缩小或留出用户没有要求的白边。",
            "参考图中的 Logo、品牌字、包装文字和装饰线必须保持原有内容与位置；看不清的细字不要编造或改写。",
            "主体必须完整进入画面，边缘不被裁断，并在用户所选画幅内留出合理安全边距。"
          ].join("");
          const prompt = generationMode === "text_image"
            ? `用户画面描述：${userPrompt}\n这是纯文字生图任务，不得读取或沿用历史附件、上一张图片或旧图。请严格依据本次文字描述生成一张构图完整、主体清晰、可直接查看的高质量图片。准确执行用户指定的主体、数量、场景、颜色、光线、镜头和风格；没有明确要求时不要添加文字、数字、水印或Logo。`
            : generationMode === "main_image"
              ? `用户主图要求：${userPrompt}\n${referenceFidelityRules}\n请以本次参考图中的产品为唯一主体，生成专业电商主图。保持产品结构、比例、开孔、部件、材质、图案、Logo位置和主要颜色准确不变；主体完整、清晰、醒目，背景干净，光线真实，构图适合商品列表和首屏展示。不要增加参考图中不存在的配件、文字、水印或虚构Logo。`
              : generationMode === "ad_image"
                ? `用户广告图要求：${userPrompt}\n${referenceFidelityRules}\n请以本次参考产品为核心制作专业广告画面。保持产品结构、材质、图案、Logo位置和主要颜色准确不变，围绕用户指定的活动主题、卖点和氛围设计构图，预留清晰的信息层级和文案区域。仅在用户明确提供广告文字时使用该文字；不要编造价格、折扣、品牌、Logo或产品功能。`
                : `用户效果图要求：${userPrompt}\n${referenceFidelityRules}\n请以本次参考图为依据生成一张专业、可供客户确认的真实产品效果图，而不是把原图简单贴进新背景。若参考图是产品，保持结构、比例、开孔、部件、图案、Logo位置和主要颜色；若参考图是人物，保留可辨认的五官、发型、姿态和服装特征；若参考图是动物，保留品种、个体特征、毛色花纹、脸部、表情、体型和姿态。只改变用户明确要求的材质呈现、摆放方式、背景、光线或场景。若这是对上一张效果图的修改，只改变用户明确指出的部位或属性，未提到的主体、表情、姿势、构图、背景、颜色和风格全部保持不变。不要虚构主体细节；没有明确要求时不要添加文字、数字、水印或虚构Logo。`;
          if (originIsActive()) window.__lastHomeVisualPrompt = prompt;
          originSession.lastVisualPrompt = prompt;
          lastVisualPrompts = [prompt];
          if (userPrompt && originIsActive()) setIfExists("visualText", userPrompt);
          const data = await api("/api/image/generate", {
            method: "POST",
            timeoutMs: 240000,
            body: JSON.stringify({
              prompt,
              reference_file_id: referenceFileId,
              reference_file_ids: referenceFileIds,
              source_text: userPrompt,
              customer: String(options.customer || originSession.customer || "图片制作").trim(),
              model_alias: String(document.getElementById("homeImageModel")?.value || "jianglai_tiangong"),
              resolution: String(document.getElementById("homeImageQuality")?.value || "2K"),
              generation_mode: generationMode,
              aspect_ratio: String(document.getElementById("homeImageRatio")?.value || "1:1 方图"),
              target_platform: String(document.getElementById("homeTargetPlatform")?.value || "智能匹配"),
              purpose: String(document.getElementById("homeMockupPurpose")?.value || "客户确认图")
            })
          });
          if (!homeSessionById(originSessionId) || originSession.processingRequestId !== requestId) return;
          const image = data.image || {};
          if (!image.url) throw new Error("图片接口没有返回结果");
          markImageGenerationActuallyVerified();
          if (data.membership) syncFormalMembership(data.membership);
          if (!isTextImage && composerFileIds.length) {
            if (attachmentBinding.allAttachmentsPersistent) {
              await clearHomeComposerAttachmentsAfterRecognition(composerFileIds, composerFileNames, originSessionId, composerUploadRecords);
            } else if (originIsActive()) {
              keepHomeComposerAttachmentsForRetry();
            }
          }
          if (image.file_id) {
            originSession.retainedFileIds = [Number(image.file_id)];
            originSession.retainedFileNames = [`上一张${outputLabel}`];
            if (originIsActive()) {
              latestVisualFileId = Number(image.file_id);
              window.__lastRecognizedHomeFileIds = [Number(image.file_id)];
              window.__homeRetainedFileNames = [`上一张${outputLabel}`];
              renderHomeRetainedFileContext();
            }
          }
          const visualVersion = recordVisualVersion(image, {
            customer: originSession.customer,
            product: originSession.businessAnalysis?.product || "定制产品",
            prompt,
            sourceText: userPrompt
          });
          const referenceStatus = referenceFileId
            ? (image.reference_used ? `已按顺序参考${image.reference_name ? `“${customerFacingAttachmentName(image.reference_name)}”` : `${referenceFileIds.length}张图片`}` : `未能直接使用参考图${image.reference_warning ? `：${image.reference_warning}` : ""}`)
            : "仅按本次文字描述生成，未使用附件";
          const designLearningStatus = data.design_learning?.applied_summary
            ? `已参考制作偏好：${data.design_learning.applied_summary}`
            : "";
          const factoryLearningStatus = data.factory_design_learning?.applied_summary
            ? `已参考全厂通用制图经验：${data.factory_design_learning.applied_summary}`
            : "";
          const promptEnhancementStatus = data.prompt_enhancement?.used_ai || image.prompt_refined
              ? "已由文字AI理解并优化制作要求，再交给图片模型生成"
            : "已按图片制作规则整理要求，再交给图片模型生成";
          const billingStatus = Number(image.amount_charged_cents || 0) > 0
            ? `本次成功生成已扣¥${(Number(image.amount_charged_cents || 0) / 100).toFixed(2)}，当前AI生图余额¥${(Number(data.membership?.ai_balance_cents || 0) / 100).toFixed(2)}`
            : Number(image.points_charged || 0) > 0
              ? `本次成功生成已扣${Number(image.points_charged || 0)} AI点`
              : "本次未产生生图费用";
          const resultHtml = `
            <div class="ai-answer-body">
              <p class="ai-answer-lead">${outputLabel}已经生成，直接在这里查看。</p>
              <div class="home-inline-visual">
                ${generatedImagePreviewMarkup(image.url, image.filename || outputFilename, outputAlt, image.file_id)}
                <div class="home-inline-visual-meta">${escapeHtml(referenceStatus)}<br>${escapeHtml(promptEnhancementStatus)}<br>${escapeHtml(billingStatus)}<br>${designLearningStatus ? `${escapeHtml(designLearningStatus)}<br>` : ""}${factoryLearningStatus ? `${escapeHtml(factoryLearningStatus)}<br>` : ""}下载使用前请核对主体结构、颜色、Logo和画面细节。</div>
                <div class="actions">
                  ${generatedImageDownloadButton(image.url, image.filename || outputFilename, "下载原图并完成", "btn dark", true)}
                  <button class="btn light" type="button" onclick="generateHomeVisualInline('', { generationMode: '${generationMode}' })">再生成一张</button>
                  <button class="btn light" type="button" onclick="document.getElementById('aiQuestion')?.focus()">继续修改</button>
                  <button class="btn light" type="button" onclick="clearHomeGeneratedResult({ clearInputs: true })">清除结果</button>
                </div>
              </div>
            </div>
          `.trim();
          originSession.answerHtml = resultHtml;
          originSession.resultEmpty = false;
          originSession.hasConversation = true;
          if (originIsActive()) {
            const answer = document.getElementById("aiAnswer");
            if (answer) {
              answer.innerHTML = resultHtml;
              answer.querySelector("img")?.addEventListener("load", () => scrollHomeReplyAboveComposer(answer));
            }
            renderFormalStatus(`${outputLabel}已在首页生成 · 可以下载或继续修改`);
            captureActiveCustomerSession(true);
          }
        } catch (err) {
          failed = true;
          const failureMessage = String(err?.message || "图片服务没有返回结果");
          if (/(\b401\b|invalid.?api.?key|incorrect.?api.?key|Key.{0,30}(无效|拒绝|未通过验证)|凭证.{0,20}(无效|失败))/i.test(failureMessage)) {
            clearImageGenerationActualVerification();
            window.__imageGenerationConfigured = false;
            window.__imageGenerationSetupIssue = failureMessage;
            renderVisualQuotaButton();
          } else if (isImageGroupConfigurationError(failureMessage)) {
            window.__imageGenerationSetupIssue = failureMessage;
          }
          showHomeInlineVisualMessage(
            `这次${outputLabel}没有生成成功。`,
            imageGenerationFailureMessage(failureMessage),
            imageGenerationFailureActions(failureMessage),
            originSession
          );
          if (originIsActive()) renderFormalStatus("首页生图提醒：" + failureMessage);
        } finally {
          window.clearInterval(thinkingTimer);
          if (window.__homeVisualThinkingTimer === thinkingTimer) window.__homeVisualThinkingTimer = null;
          if (homeSessionVisualJobs.get(originSessionId)?.requestId === requestId) homeSessionVisualJobs.delete(originSessionId);
          const targetSession = homeSessionById(originSessionId);
          if (targetSession && finishHomeSessionJob(targetSession, requestId, failed ? "failed" : "completed")) {
            if (homeSessionIsForeground(targetSession)) {
              window.__homeVisualGenerating = false;
              syncHomeSessionProcessingUi(targetSession);
              captureActiveCustomerSession(true);
            } else {
              notifyHomeSessionCompleted(targetSession, failed);
            }
          }
          renderCustomerWorkbench();
        }
      }
      async function generateRealVisualConnected() {
        const generateButton = document.querySelector('button[onclick="generateRealVisual()"]');
        if (generateButton?.disabled) return;
        if (!document.getElementById("visualLockStructure")?.checked) {
          notify("请先确认产品结构和比例不能改变，避免效果图误导客户。");
          document.getElementById("visualLockStructure")?.focus();
          return;
        }
        const originalButtonText = generateButton?.textContent || "AI直接生成主图";
        const visualCustomer = String(currentCustomerName() || "新客户").trim() || "新客户";
        if (generateButton) {
          generateButton.disabled = true;
          generateButton.textContent = "正在生成…";
        }
        const visualGenerationOptions = homeImageGenerationRequestOptions();
        const visualModel = HOME_IMAGE_MODEL_CATALOG[visualGenerationOptions.model_alias] || HOME_IMAGE_MODEL_CATALOG.jianglai_tiangong;
        const visualPointCost = Number(visualModel.resolutions[visualGenerationOptions.resolution]?.points || 30);
        if (!consumeQuota("AI真实生图", false, visualPointCost)) {
          if (generateButton) {
            generateButton.disabled = false;
            generateButton.textContent = originalButtonText;
          }
          return;
        }
        generateVisualPlan(false);
        const basePrompt = lastVisualPrompts[0] || "真实产品摄影，白底，产品完整居中，突出材质、工艺和定制能力。";
        const structureRule = "必须保持参考产品的结构、比例、开孔、折弯和部件位置，不得擅自改变产品造型。";
        const logoRule = document.getElementById("visualLockLogo")?.checked ? "参考图里如果有Logo，必须保持Logo位置、比例和样式，不得虚构或改写。" : "不得虚构Logo。";
        const colorRule = `颜色要求：${val("visualColorPolicy") || "保持参考图颜色"}。`;
        const prompt = `${basePrompt}\n${structureRule}\n${logoRule}\n${colorRule}\n画面中不出现任何文字、数字、尺寸线、标签、水印或虚构Logo。No text, no numbers, no labels, no dimensions, no watermark, no invented logo.`;
        const output = document.getElementById("visualOutput");
        const cards = document.getElementById("visualSetCards");
        const planCards = cards?.innerHTML || "";
        if (output) output.textContent = "AI 正在生成真实产品图，通常需要 10-60 秒，请不要重复点击。";
        try {
          await ensureFormalLogin();
          if (latestVisualUploadPromise) {
            const ids = await latestVisualUploadPromise;
            latestVisualFileId = ids[ids.length - 1] || latestVisualFileId;
          }
          if (!latestVisualFileId && document.getElementById("visualFile")?.files?.length) {
            const ids = await uploadFilesFromInput("visualFile", "visualFileName");
            latestVisualFileId = ids[ids.length - 1] || null;
          }
          const data = await api("/api/image/generate", {
            method: "POST",
            timeoutMs: 240000,
            body: JSON.stringify({
              prompt,
              reference_file_id: latestVisualFileId,
              source_text: val("visualText"),
              customer: visualCustomer,
              ...visualGenerationOptions
            })
          });
          const image = data.image || {};
          if (!image.url) throw new Error("图片接口没有返回结果");
          markImageGenerationActuallyVerified();
          if (data.membership) syncFormalMembership(data.membership);
          const visualVersion = recordVisualVersion(image, { customer:visualCustomer });
          if (cards) {
            cards.innerHTML = `
              <div class="generated-image-panel" style="grid-column:1 / -1">
                ${generatedImagePreviewMarkup(image.url, image.filename || "AI产品图.webp", "AI生成的产品图", image.file_id)}
                <div class="actions">
                  ${generatedImageDownloadButton(image.url, image.filename || "AI产品图.webp", "下载原图")}
                  <button class="btn light" onclick="generateRealVisual()">重新生成</button>
                  ${visualVersion ? `<button class="btn light" onclick="markVisualVersionSent('${escapeHtml(visualVersion.id)}')">标记已发客户</button><button class="btn dark" onclick="lockVisualVersion('${escapeHtml(visualVersion.id)}')">客户确认并锁定</button>` : ""}
                </div>
              </div>
              ${planCards}
            `;
          }
          const referenceStatus = latestVisualFileId
            ? (image.reference_used
              ? `参考图：已用于生成${image.reference_name ? `（${customerFacingAttachmentName(image.reference_name)}）` : ""}`
              : `参考图：当前模型未能直接使用，已按文字要求生成${image.reference_warning ? `（${image.reference_warning}）` : ""}`)
            : "参考图：未上传";
          const learnedStatus = data.design_learning?.applied_summary
            ? `\n已参考客户习惯：${data.design_learning.applied_summary}`
            : data.design_learning?.enabled
              ? "\n已开启客户制图偏好学习，本次确认或改图后会继续改良。"
              : "";
          const factoryLearnedStatus = data.factory_design_learning?.applied_summary
            ? `\n已参考全厂通用制图经验：${data.factory_design_learning.applied_summary}`
            : "";
          const promptEnhancedStatus = data.prompt_enhancement?.used_ai
            ? "\n已由文字AI理解并润色客户要求，再交给将莱图片引擎生成。"
            : "\n已按工厂产品规则整理客户要求，再交给将莱图片引擎生成。";
          if (output) output.textContent = `真实产品图已生成。\n${referenceStatus}${promptEnhancedStatus}${learnedStatus}${factoryLearnedStatus}\n\n请在发给客户前核对产品结构、颜色、Logo、材质和工艺，效果图仅用于确认方向。`;
          renderFormalStatus("AI产品图已生成 · 可以下载或重新生成");
        } catch (err) {
          const failureMessage = String(err?.message || "图片服务没有返回结果");
          if (isImageGroupConfigurationError(failureMessage)) {
            clearImageGenerationActualVerification();
            window.__imageGenerationConfigured = false;
            window.__imageGenerationSetupIssue = failureMessage;
            renderVisualQuotaButton();
          }
          if (output) output.textContent = `AI生图失败：${imageGenerationFailureMessage(failureMessage)}\n\n${isImageGroupConfigurationError(failureMessage) ? "请打开系统状态，检查图片服务 Key 与所选模型权限。" : "可以点击重新生成原地重试，也可以先使用上方“整套出图方案”生成文字提示词。"}`;
          renderFormalStatus("AI生图提醒：" + failureMessage);
        } finally {
          if (generateButton) {
            generateButton.disabled = false;
            generateButton.textContent = originalButtonText;
          }
        }
      }
      async function generateDetailPageConnected() {
        if (!consumeQuota("AI宝贝详情", false)) return;
        const output = document.getElementById("customerOutput");
        if (output) output.textContent = "AI 正在整理商品标题、卖点、参数、详情页结构和客服话术…";
        try {
          await ensureFormalLogin();
          if (latestUploadPromise) await latestUploadPromise.catch(() => []);
          const context = customerContext();
          const data = await api("/api/ai/detail", {
            method: "POST",
            body: JSON.stringify({
              text: val("customerText"),
              platform: context.platform,
              goal: context.goal,
              analysis: window.__latestAiAnalysis || context.info,
              file_ids: uploadedFileIds.length ? uploadedFileIds : homeRetainedFileIds()
            })
          });
          if (!data.detail?.content) throw new Error("详情页接口没有返回内容");
          if (data.membership) syncFormalMembership(data.membership);
          if (output) output.textContent = data.detail.content;
          const detailCharged = !["local_template", "local_fallback"].includes(data.detail.provider);
          renderFormalStatus(`AI宝贝详情已生成${detailCharged ? " · 已扣3点" : " · 本机生成，未扣点"}`);
        } catch (err) {
          if (output) output.textContent = "宝贝详情生成失败：" + err.message;
          renderFormalStatus("宝贝详情提醒：" + err.message);
        }
      }
      let activeDetailPackage = null;
      const detailPackageResultsBySession = new Map();
      function detailPackageHomeSession(explicitSessionId = "") {
        const sessionId = String(explicitSessionId || window.__detailPackageHomeSessionId || "");
        return sessionId ? homeSessionById(sessionId) : null;
      }
      function detailPackageResultForSession(session = activeCustomerSession()) {
        if (!session?.id) return activeDetailPackage;
        return detailPackageResultsBySession.get(session.id)
          || (activeDetailPackage?.sessionId === session.id ? activeDetailPackage : null);
      }
      function homeDetailPackageProgressHtml(percent, message, job = null) {
        const progress = Math.max(0, Math.min(100, Number(percent || 0)));
        const count = clampDetailPackageCount(job?.targetCount || 8);
        return `
          <div class="ai-answer-body" data-home-detail-package-progress>
            <p class="ai-answer-lead">正在原聊天生成整套宝贝详情</p>
            <div class="ai-answer-section">
              <h3>${escapeHtml(job?.product || "当前产品")} · ${count}张</h3>
              <div class="detail-package-progress"><i data-home-detail-package-bar style="width:${progress}%"></i></div>
              <p data-home-detail-package-status>${escapeHtml(message || "正在准备整套详情页…")}</p>
              <p class="muted">可以切换客户处理其他事情；生成结果会回到这条客户会话，不会跳到其他页面。</p>
            </div>
          </div>
        `.trim();
      }
      function renderHomeDetailPackageProgress(percent, message, targetSession = detailPackageHomeSession(), requestId = "") {
        if (!targetSession) return false;
        if (requestId && targetSession.processingRequestId && targetSession.processingRequestId !== requestId) return false;
        const progress = Math.max(0, Math.min(100, Number(percent || 0)));
        const html = homeDetailPackageProgressHtml(percent, message, targetSession.detailPackageJob);
        targetSession.answerHtml = html;
        targetSession.resultEmpty = false;
        targetSession.hasConversation = true;
        targetSession.processingKind = "detail_package";
        targetSession.processingStatus = "processing";
        if (targetSession.detailPackageJob) {
          targetSession.detailPackageJob.progressPercent = progress;
          targetSession.detailPackageJob.progressMessage = String(message || "正在生成整套宝贝详情").slice(0, 500);
          targetSession.detailPackageJob.updatedAt = new Date().toISOString();
        }
        if (targetSession.id === state.activeCustomerSessionId) {
          showHomeResult();
          hideHomeNextActions();
          const answer = document.getElementById("aiAnswer");
          const existing = answer?.querySelector("[data-home-detail-package-progress]");
          if (existing) {
            const bar = existing.querySelector("[data-home-detail-package-bar]");
            const status = existing.querySelector("[data-home-detail-package-status]");
            if (bar) bar.style.width = `${progress}%`;
            if (status) status.textContent = message || "正在准备整套详情页…";
            targetSession.answerHtml = answer.innerHTML;
          } else if (answer) {
            answer.innerHTML = html;
          }
        }
        saveState();
        return true;
      }
      function renderHomeDetailPackageResult(packageResult, failures = [], targetSession = detailPackageHomeSession()) {
        if (!targetSession) return false;
        const panels = Array.isArray(packageResult?.panels) ? packageResult.panels : [];
        if (!panels.length) return renderHomeDetailPackageFailure("图片服务没有返回任何可交付图片。", targetSession);
        const failedCount = Array.isArray(failures) ? failures.length : 0;
        const targetCount = clampDetailPackageCount(targetSession.detailPackageJob?.targetCount || panels.length || 8);
        const completion = failedCount
          ? `已完成${panels.length}/${targetCount}张，${failedCount}张失败；失败项没有伪装成完成。`
          : `已真实完成${panels.length}/${targetCount}张，并合成1张完整详情长图。`;
        const html = `
          <div class="ai-answer-body" data-home-detail-package-result>
            <p class="ai-answer-lead">整套宝贝详情已生成</p>
            <div class="ai-answer-section">
              <h3>${escapeHtml(targetSession.detailPackageJob?.product || "当前产品")} · ${escapeHtml(completion)}</h3>
              <div class="detail-package-assets">
                ${panels.map((panel, index) => {
                  const pageNumber = Number.isFinite(Number(panel?.taskIndex)) ? Number(panel.taskIndex) + 1 : index + 1;
                  return `
                    <div class="detail-package-asset">
                      ${generatedImagePreviewMarkup(panel.previewUrl || panel.url, panel.filename || `详情图-${pageNumber}.png`, panel.name || `详情图${pageNumber}`, panel.file_id)}
                      <strong>第${pageNumber}张 · ${escapeHtml(panel.name || `详情图${pageNumber}`)}</strong>
                      <div class="actions"><button class="btn light" type="button" onclick="window.downloadDetailPackageAsset?.(${index})">下载本张</button></div>
                    </div>
                  `;
                }).join("")}
              </div>
              <div class="actions">
                <button class="btn dark" type="button" onclick="window.downloadDetailPackageConnected?.()">下载完整详情长图并完成</button>
                ${failedCount ? `<button class="btn light" type="button" onclick="window.confirmHomeDetailPackage?.()">只补齐失败的${failedCount}张</button>` : ""}
                <button class="btn light" type="button" onclick="window.clearHomeGeneratedResult?.({ clearInputs: true })">清除结果</button>
              </div>
            </div>
          </div>
        `.trim();
        targetSession.answerHtml = html;
        targetSession.resultEmpty = false;
        targetSession.hasConversation = true;
        targetSession.processingKind = "";
        targetSession.processingStatus = "";
        targetSession.chatActionStatus = failedCount ? `宝贝详情已完成${panels.length}/${targetCount}张` : `宝贝详情${targetCount}张已完成`;
        targetSession.hasUnreadResult = targetSession.id !== state.activeCustomerSessionId;
        if (targetSession.detailPackageJob) {
          targetSession.detailPackageJob.status = failedCount ? "partial" : "completed";
          targetSession.detailPackageJob.generatedCount = panels.length;
          targetSession.detailPackageJob.progressPercent = 100;
          targetSession.detailPackageJob.progressMessage = completion;
          targetSession.detailPackageJob.failures = (Array.isArray(failures) ? failures : []).slice(0, 12);
          targetSession.detailPackageJob.resultHtml = html;
          targetSession.detailPackageJob.updatedAt = new Date().toISOString();
        }
        if (targetSession.id === state.activeCustomerSessionId) {
          showHomeResult();
          hideHomeNextActions();
          const answer = document.getElementById("aiAnswer");
          if (answer) {
            answer.innerHTML = html;
            hydrateGeneratedImagePreviews(answer);
            window.setTimeout(() => scrollHomeReplyAboveComposer(answer), 0);
          }
        }
        saveState();
        renderCustomerWorkbench();
        return true;
      }
      function restoreHomeDetailPackageResult(targetSession = activeCustomerSession()) {
        if (!targetSession) return false;
        const packageResult = detailPackageResultForSession(targetSession);
        if (packageResult?.panels?.length) {
          return renderHomeDetailPackageResult(packageResult, targetSession.detailPackageJob?.failures || [], targetSession);
        }
        const html = String(targetSession.detailPackageJob?.resultHtml || "");
        if (!html) {
          return renderHomeDetailPackageFailure("生成记录显示任务已完成，但本机图片缓存已经失效。请点击原地重试，图片会重新回到当前会话。", targetSession);
        }
        targetSession.answerHtml = html;
        targetSession.resultEmpty = false;
        targetSession.hasConversation = true;
        if (targetSession.id === state.activeCustomerSessionId) {
          showHomeResult();
          hideHomeNextActions();
          const answer = document.getElementById("aiAnswer");
          if (answer) {
            answer.innerHTML = html;
            hydrateGeneratedImagePreviews(answer);
            window.setTimeout(() => scrollHomeReplyAboveComposer(answer), 0);
          }
        }
        saveState();
        return true;
      }
      function renderHomeDetailPackageFailure(message, targetSession = detailPackageHomeSession()) {
        if (!targetSession) return false;
        const detail = String(message || "整套详情生成失败");
        const html = `
          <div class="ai-answer-body" data-home-detail-package-failure>
            <p class="ai-answer-lead">整套详情没有完成</p>
            <div class="ai-answer-section"><p>${escapeHtml(detail)}</p></div>
            <div class="actions"><button class="btn dark" type="button" onclick="window.confirmHomeDetailPackage?.()">原地重试</button></div>
          </div>
        `.trim();
        targetSession.answerHtml = html;
        targetSession.resultEmpty = false;
        targetSession.hasConversation = true;
        targetSession.processingKind = "";
        targetSession.processingStatus = "";
        targetSession.chatActionStatus = "宝贝详情生成失败，可原地重试";
        targetSession.hasUnreadResult = targetSession.id !== state.activeCustomerSessionId;
        if (targetSession.detailPackageJob) {
          targetSession.detailPackageJob.status = "failed";
          targetSession.detailPackageJob.progressPercent = 0;
          targetSession.detailPackageJob.progressMessage = detail;
          targetSession.detailPackageJob.updatedAt = new Date().toISOString();
        }
        if (targetSession.id === state.activeCustomerSessionId) {
          showHomeResult();
          hideHomeNextActions();
          const answer = document.getElementById("aiAnswer");
          if (answer) answer.innerHTML = html;
        }
        saveState();
        renderCustomerWorkbench();
        return true;
      }
      function setDetailPackageProgress(percent, message, targetSession = detailPackageHomeSession(), requestId = "", updateHome = true) {
        const workspace = document.getElementById("detailPackageWorkspace");
        const bar = document.getElementById("detailPackageProgressBar");
        workspace?.classList.remove("is-hidden");
        if (bar) bar.style.width = `${Math.max(0, Math.min(100, Number(percent || 0)))}%`;
        setTextIfExists("detailPackageStatus", message || "正在准备整套详情页…");
        if (updateHome) renderHomeDetailPackageProgress(percent, message, targetSession, requestId);
      }
      function detailPackageTextLines(value, limit = 12) {
        return String(value || "")
          .split(/\n+/)
          .map(line => line.replace(/^\s*(?:[-*•·]|\d+[.、)]?)\s*/, "").trim())
          .filter(Boolean)
          .slice(0, limit);
      }
      function parseDetailPackageSections(content = "") {
        const headings = ["商品标题", "核心卖点", "产品参数", "详情页图片顺序", "定制流程", "下单须知", "客服承接话术"];
        const sections = Object.fromEntries(headings.map(name => [name, []]));
        let current = "";
        String(content || "").split(/\r?\n/).forEach(rawLine => {
          const line = rawLine.replace(/^\s{0,3}#{1,6}\s*/, "").replace(/\*\*/g, "").trim();
          if (!line) return;
          const matched = headings.find(name => line === name || line === `${name}：` || line === `${name}:`);
          if (matched) {
            current = matched;
            return;
          }
          const inline = headings.find(name => line.startsWith(`${name}：`) || line.startsWith(`${name}:`));
          if (inline) {
            current = inline;
            const value = line.slice(inline.length + 1).trim();
            if (value) sections[inline].push(value);
            return;
          }
          if (current) sections[current].push(line);
        });
        return Object.fromEntries(Object.entries(sections).map(([name, lines]) => [name, lines.join("\n").trim()]));
      }
      function detailPackageReferenceFileIds(job = activeCustomerSession()?.detailPackageJob) {
        const jobIds = Array.isArray(job?.referenceFileIds)
          ? job.referenceFileIds.map(Number).filter(id => id > 0)
          : [];
        if (jobIds.length) return [...new Set(jobIds)].slice(-16);
        const uploadedImageIds = uploadedHomeFileRecords
          .filter(record => String(record?.mimeType || "").toLowerCase().startsWith("image/"))
          .map(record => Number(record?.id || 0))
          .filter(id => id > 0);
        const fallbackIds = homeRetainedFileIds().map(Number).filter(id => id > 0);
        return [...new Set(uploadedImageIds.length ? uploadedImageIds : fallbackIds)].slice(-9);
      }
      function detailPackageVisualPrompts(context, hasReference, targetCount = 8, job = null) {
        const info = context.info || {};
        const facts = `产品：${info.product || "定制产品"}；尺寸：${info.size || "待确认"}；材质：${info.material || "待确认"}；工艺：${info.finish || "待确认"}。`;
        const referenceRole = job?.referenceRole || "auto";
        const fidelity = !hasReference
          ? "根据已提供的真实产品资料制作概念效果图；不确定的结构不要虚构复杂细节。"
          : referenceRole === "artwork"
            ? `参考图是要应用在“${info.product || "目标产品"}”上的图案或设计素材，不是产品实物。完整保留参考图的主体特征、构图和识别元素，把它自然应用到目标产品表面；必须生成目标产品，不能只复刻参考图。`
            : referenceRole === "product_reference"
              ? "参考图是产品实物或样品依据。严格保持产品结构、比例、开合方式、材质、颜色、图案和Logo位置，不得擅自增减零件或改款。"
              : referenceRole === "style_reference"
                ? "参考图只用于画面风格、配色和氛围参考；产品本身必须以文字里的目标产品、尺寸、材质和结构为准。"
                : `先判断参考图属于产品实拍、图案素材还是风格参考：产品实拍要保持结构；图案素材要应用到“${info.product || "目标产品"}”表面；风格参考只借鉴视觉语言。无论哪种情况都必须生成文字指定的目标产品。`;
        const common = `${facts}${fidelity} 只生成产品摄影素材，画面内不要添加任何文字、数字、尺寸线、水印、边框或虚构Logo，四周保留安全裁切空间，适合${context.platform || "1688/淘宝"}详情页。`;
        const product = info.product || "定制产品";
        const tasks = [
          {
            name: `${product}首屏主视觉`,
            caption: "完整展示产品外观、定制方向与第一视觉卖点",
            prompt: `${common} 生成高端电商棚拍主视觉：产品完整展示并占画面约75%，三分之二正面视角，干净浅灰到米白渐变背景，柔和专业灯光，真实阴影，材质与边缘清晰，构图稳定。`
          },
          {
            name: "整体外观与核心卖点",
            caption: "多角度看清产品整体结构与主要优势",
            prompt: `${common} 生成产品整体展示素材：同一款产品以正面、侧面或轻微转角的自然组合呈现，主体完整、比例一致，突出核心结构和真实质感，不要变成复杂拼贴或信息图。`
          },
          {
            name: "材质工艺细节",
            caption: "放大呈现材料纹理、边缘与加工细节",
            prompt: `${common} 生成材质与工艺细节视觉：以一张完整产品图配合2到3个自然的局部特写区域，突出材质纹理、边缘、连接、印刷或表面工艺；不要做信息图，不要出现文字或标注。`
          },
          {
            name: "结构背面与使用方式",
            caption: "展示背面、连接、开合、安装或实际使用方法",
            prompt: `${common} 生成结构与使用方式素材：清楚展示产品背面、侧面、连接、开合、安装、磁吸、悬挂或摆放方式中真正适用于该产品的部分；不得凭空增加功能。`
          },
          {
            name: "尺寸比例与规格",
            caption: "用真实参照物表现产品大小与厚薄比例",
            prompt: `${common} 生成尺寸比例素材：不出现文字和尺寸线，通过手持、桌面、包装或常见参照物自然表现产品真实大小、厚度和比例，产品结构必须准确。`
          },
          {
            name: "真实使用场景",
            caption: "让客户直观看到产品的实际使用效果",
            prompt: `${common} 生成真实商业使用场景图：产品是绝对主体，选择符合产品用途的办公、零售、礼赠、居家、展会或工厂场景，环境简洁可信，展示真实比例和使用方式，不出现无关产品，不遮挡主体。`
          },
          {
            name: "颜色图案与定制选项",
            caption: "表现可定制的颜色、图案、形状与工艺方向",
            prompt: `${common} 生成定制选项视觉素材：在不改变基础产品结构的前提下，自然展示2到4种合理的颜色、图案、形状或表面工艺方向；参考图内容必须正确保留，不得虚构品牌文字。`
          },
          {
            name: "工厂生产与质检",
            caption: "展示真实加工、检查和批量交付能力",
            prompt: `${common} 生成真实工厂生产与质检场景：产品在合理的加工、组装、检查或批量陈列环境中，设备和工序必须符合该产品，不夸大厂房、人员或产能。`
          },
          {
            name: "包装保护与发货",
            caption: "展示包装方式、运输保护与批量发货状态",
            prompt: `${common} 生成包装发货素材：展示该产品合理的单件保护、内包装、装箱或批量发货状态，包装结构可信，产品不变形，不出现快递单或虚构文字。`
          },
          {
            name: "定制流程视觉",
            caption: "从来图沟通、打样确认到批量生产",
            prompt: `${common} 生成定制流程相关的真实视觉素材：以产品、设计确认、样品、生产和成品为主线形成干净有序的场景，不要出现流程文字或箭头，留出软件后期排版空间。`
          },
          {
            name: "适用场景与礼赠用途",
            caption: "覆盖零售、企业定制、活动与礼赠使用",
            prompt: `${common} 生成多场景用途素材：围绕该产品最真实的零售、企业、活动、收藏、礼赠或日常使用场景进行自然组合，产品保持一致且清晰。`
          },
          {
            name: "下单咨询收尾图",
            caption: "以完整产品阵列强化定制与批量采购印象",
            prompt: `${common} 生成详情页收尾视觉：同款产品以整齐、真实的成品阵列或包装组合呈现，体现可定制和批量交付，背景简洁并留出后期准确文字排版区域。`
          }
        ];
        if (job?.extraInstructions) {
          tasks.forEach(task => { task.prompt += ` 客户补充要求：${job.extraInstructions}`; });
        }
        Object.entries(job?.pageInstructions || {}).forEach(([page, instruction]) => {
          const task = tasks[Number(page) - 1];
          if (task && instruction) task.prompt += ` 本页客户修改要求：${String(instruction).slice(0, 2000)}`;
        });
        return tasks.slice(0, clampDetailPackageCount(targetCount));
      }
      function detailPackageCanvasLines(ctx, value, maxWidth, maxLines = 30) {
        const output = [];
        const paragraphs = String(value || "").split(/\n/);
        paragraphs.forEach(paragraph => {
          const text = paragraph.trim();
          if (!text || output.length >= maxLines) return;
          let line = "";
          Array.from(text).forEach(character => {
            const next = line + character;
            if (line && ctx.measureText(next).width > maxWidth) {
              output.push(line);
              line = character;
            } else {
              line = next;
            }
          });
          if (line && output.length < maxLines) output.push(line);
        });
        return output.slice(0, maxLines);
      }
      function detailPackageRoundRect(ctx, x, y, width, height, radius = 18) {
        const r = Math.max(0, Math.min(radius, width / 2, height / 2));
        ctx.beginPath();
        ctx.moveTo(x + r, y);
        ctx.lineTo(x + width - r, y);
        ctx.quadraticCurveTo(x + width, y, x + width, y + r);
        ctx.lineTo(x + width, y + height - r);
        ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
        ctx.lineTo(x + r, y + height);
        ctx.quadraticCurveTo(x, y + height, x, y + height - r);
        ctx.lineTo(x, y + r);
        ctx.quadraticCurveTo(x, y, x + r, y);
        ctx.closePath();
      }
      function detailPackageDrawWrappedText(ctx, value, x, y, maxWidth, options = {}) {
        const lineHeight = Number(options.lineHeight || 34);
        const lines = detailPackageCanvasLines(ctx, value, maxWidth, Number(options.maxLines || 30));
        lines.forEach((line, index) => ctx.fillText(line, x, y + index * lineHeight));
        return y + lines.length * lineHeight;
      }
      function detailPackageDrawImage(ctx, image, x, y, width, height) {
        detailPackageRoundRect(ctx, x, y, width, height, 22);
        ctx.save();
        ctx.clip();
        ctx.fillStyle = "#eef1f4";
        ctx.fillRect(x, y, width, height);
        if (image?.naturalWidth && image?.naturalHeight) {
          const scale = Math.max(width / image.naturalWidth, height / image.naturalHeight);
          const drawWidth = image.naturalWidth * scale;
          const drawHeight = image.naturalHeight * scale;
          ctx.drawImage(image, x + (width - drawWidth) / 2, y + (height - drawHeight) / 2, drawWidth, drawHeight);
        }
        ctx.restore();
      }
      async function loadDetailPackageImage(url) {
        const source = generatedImageNeedsAuthorization(url)
          ? await authenticatedGeneratedImageBlobUrl(url)
          : String(url || "");
        return new Promise((resolve, reject) => {
          const image = new Image();
          image.onload = () => resolve(image);
          image.onerror = () => reject(new Error("详情页素材读取失败"));
          image.src = source;
        });
      }
      async function composeDetailPackageCanvas({ context, content, images }) {
        const sections = parseDetailPackageSections(content);
        const info = context.info || {};
        const platform = context.platform || "阿里巴巴/1688";
        const theme = /淘宝|天猫/.test(platform)
          ? { primary: "#d92d20", dark: "#2b1614", accent: "#ffefe9", label: "淘宝 / 天猫" }
          : { primary: "#e85d04", dark: "#2d1b12", accent: "#fff1df", label: "1688 厂家详情" };
        const title = detailPackageTextLines(sections["商品标题"], 1)[0]
          || `${info.product || "定制产品"}厂家定制`;
        const sellingPoints = detailPackageTextLines(sections["核心卖点"], 4);
        const parameters = detailPackageTextLines(sections["产品参数"], 8);
        const process = detailPackageTextLines(sections["定制流程"], 6);
        const notes = detailPackageTextLines(sections["下单须知"], 5);
        const loadedImages = [];
        for (const item of images) {
          try { loadedImages.push(await loadDetailPackageImage(item.url)); }
          catch (_) { loadedImages.push(null); }
        }
        const canvas = document.createElement("canvas");
        canvas.width = 750;
        canvas.height = 7200;
        const ctx = canvas.getContext("2d");
        ctx.textBaseline = "top";
        ctx.fillStyle = "#f3f1ee";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        const family = '"PingFang SC", "Microsoft YaHei", Arial, sans-serif';
        let y = 0;
        ctx.fillStyle = theme.dark;
        ctx.fillRect(0, 0, 750, 355);
        ctx.fillStyle = theme.primary;
        ctx.fillRect(0, 0, 750, 12);
        ctx.font = `800 18px ${family}`;
        ctx.fillStyle = "#ffc78f";
        ctx.fillText(theme.label, 48, 50);
        ctx.font = `900 42px ${family}`;
        ctx.fillStyle = "#ffffff";
        y = detailPackageDrawWrappedText(ctx, title, 48, 92, 654, { lineHeight: 55, maxLines: 3 });
        ctx.font = `500 19px ${family}`;
        ctx.fillStyle = "#eadfd8";
        detailPackageDrawWrappedText(ctx, `${info.material || "材质可选"} · ${info.finish || "工艺可选"} · 支持来图来样定制`, 48, Math.max(y + 16, 235), 654, { lineHeight: 30, maxLines: 2 });
        y = 395;

        if (loadedImages[0]) {
          detailPackageDrawImage(ctx, loadedImages[0], 38, y, 674, 620);
          y += 655;
        }

        ctx.fillStyle = "#ffffff";
        detailPackageRoundRect(ctx, 38, y, 674, 130 + Math.max(4, sellingPoints.length) * 72, 22);
        ctx.fill();
        ctx.font = `900 29px ${family}`;
        ctx.fillStyle = theme.dark;
        ctx.fillText("为什么选择这款产品", 68, y + 38);
        const pointValues = sellingPoints.length ? sellingPoints : [
          "尺寸、材质、颜色和工艺可按需求定制",
          "支持来图来样、打样确认和批量生产",
          "生产前确认规格与工艺，减少批量误差",
          "厂家按真实数量与工艺核算价格"
        ];
        let pointY = y + 94;
        ctx.font = `600 19px ${family}`;
        pointValues.forEach((point, index) => {
          ctx.fillStyle = theme.primary;
          ctx.beginPath();
          ctx.arc(82, pointY + 13, 13, 0, Math.PI * 2);
          ctx.fill();
          ctx.font = `800 14px ${family}`;
          ctx.fillStyle = "#ffffff";
          ctx.fillText(String(index + 1), 78, pointY + 4);
          ctx.font = `600 19px ${family}`;
          ctx.fillStyle = "#28313d";
          detailPackageDrawWrappedText(ctx, point, 112, pointY, 555, { lineHeight: 28, maxLines: 2 });
          pointY += 72;
        });
        y += 160 + pointValues.length * 72;

        if (loadedImages[1]) {
          ctx.font = `900 29px ${family}`;
          ctx.fillStyle = theme.dark;
          ctx.fillText("真实场景 · 看得见的使用效果", 48, y);
          y += 56;
          detailPackageDrawImage(ctx, loadedImages[1], 38, y, 674, 520);
          y += 560;
        }

        const defaultParameters = [
          `产品：${info.product || "待确认"}`,
          `尺寸：${info.size || "待确认"}`,
          `材质：${info.material || "待确认"}`,
          `工艺：${info.finish || "待确认"}`,
          `数量：${info.qty || "待确认"}`
        ];
        const parameterValues = parameters.length ? parameters : defaultParameters;
        const parameterHeight = 112 + parameterValues.length * 57;
        ctx.fillStyle = theme.accent;
        detailPackageRoundRect(ctx, 38, y, 674, parameterHeight, 22);
        ctx.fill();
        ctx.font = `900 29px ${family}`;
        ctx.fillStyle = theme.dark;
        ctx.fillText("产品参数", 68, y + 34);
        let paramY = y + 92;
        parameterValues.forEach((value, index) => {
          if (index % 2 === 0) {
            ctx.fillStyle = "rgba(255,255,255,.62)";
            ctx.fillRect(60, paramY - 8, 630, 49);
          }
          ctx.font = `600 18px ${family}`;
          ctx.fillStyle = "#3d332d";
          detailPackageDrawWrappedText(ctx, value, 74, paramY, 600, { lineHeight: 25, maxLines: 2 });
          paramY += 57;
        });
        y += parameterHeight + 40;

        if (loadedImages[2]) {
          ctx.font = `900 29px ${family}`;
          ctx.fillStyle = theme.dark;
          ctx.fillText("材质与工艺细节", 48, y);
          y += 56;
          detailPackageDrawImage(ctx, loadedImages[2], 38, y, 674, 520);
          y += 560;
        }

        const processValues = process.length ? process : ["沟通需求", "确认报价", "设计与打样", "批量生产", "质检包装", "安排发货"];
        ctx.fillStyle = "#ffffff";
        detailPackageRoundRect(ctx, 38, y, 674, 190 + Math.ceil(processValues.length / 2) * 96, 22);
        ctx.fill();
        ctx.font = `900 29px ${family}`;
        ctx.fillStyle = theme.dark;
        ctx.fillText("厂家定制流程", 68, y + 36);
        processValues.forEach((value, index) => {
          const column = index % 2;
          const row = Math.floor(index / 2);
          const x = 68 + column * 318;
          const itemY = y + 98 + row * 96;
          ctx.fillStyle = theme.primary;
          detailPackageRoundRect(ctx, x, itemY, 42, 42, 12);
          ctx.fill();
          ctx.font = `800 15px ${family}`;
          ctx.fillStyle = "#ffffff";
          ctx.fillText(String(index + 1).padStart(2, "0"), x + 10, itemY + 11);
          ctx.font = `650 17px ${family}`;
          ctx.fillStyle = "#303944";
          detailPackageDrawWrappedText(ctx, value, x + 56, itemY + 3, 245, { lineHeight: 24, maxLines: 3 });
        });
        y += 225 + Math.ceil(processValues.length / 2) * 96;

        ctx.fillStyle = theme.dark;
        detailPackageRoundRect(ctx, 38, y, 674, 175 + Math.max(2, notes.length) * 48, 22);
        ctx.fill();
        ctx.font = `900 27px ${family}`;
        ctx.fillStyle = "#ffffff";
        ctx.fillText("下单前请确认", 68, y + 34);
        const noteValues = notes.length ? notes : ["请提供尺寸、数量、材质、工艺、设计稿和期望交期", "特殊工艺建议先打样，最终效果以确认样或实物为准"];
        let noteY = y + 88;
        ctx.font = `500 17px ${family}`;
        noteValues.forEach(note => {
          ctx.fillStyle = "#ffc78f";
          ctx.fillText("•", 72, noteY);
          ctx.fillStyle = "#f3ece7";
          detailPackageDrawWrappedText(ctx, note, 94, noteY, 575, { lineHeight: 25, maxLines: 2 });
          noteY += 48;
        });
        y += 210 + noteValues.length * 48;
        ctx.font = `600 15px ${family}`;
        ctx.fillStyle = "#7b8491";
        ctx.textAlign = "center";
        ctx.fillText("图片为效果展示，产品结构、Logo、颜色、尺寸和工艺请以下单确认稿为准", 375, y);
        ctx.textAlign = "left";
        y += 56;

        const finalCanvas = document.createElement("canvas");
        finalCanvas.width = 750;
        finalCanvas.height = Math.max(800, Math.ceil(y));
        finalCanvas.getContext("2d").drawImage(canvas, 0, 0, 750, finalCanvas.height, 0, 0, 750, finalCanvas.height);
        return finalCanvas;
      }
      async function composeDetailPackagePanelCanvas({ context, image, index, count }) {
        const info = context.info || {};
        const platform = context.platform || "阿里巴巴/1688";
        const theme = /淘宝|天猫/.test(platform)
          ? { primary: "#d92d20", dark: "#2b1614", soft: "#fff2ed", label: "淘宝 / 天猫" }
          : { primary: "#e85d04", dark: "#2d1b12", soft: "#fff5e8", label: "1688 厂家详情" };
        const canvas = document.createElement("canvas");
        canvas.width = 750;
        canvas.height = 1000;
        const ctx = canvas.getContext("2d");
        const family = '"PingFang SC", "Microsoft YaHei", Arial, sans-serif';
        ctx.textBaseline = "top";
        ctx.fillStyle = "#f6f4f1";
        ctx.fillRect(0, 0, 750, 1000);
        ctx.fillStyle = theme.dark;
        ctx.fillRect(0, 0, 750, 158);
        ctx.fillStyle = theme.primary;
        ctx.fillRect(0, 0, 750, 10);
        ctx.font = `800 16px ${family}`;
        ctx.fillStyle = "#ffc78f";
        ctx.fillText(`${theme.label} · 第 ${index + 1}/${count} 张`, 42, 34);
        ctx.font = `900 34px ${family}`;
        ctx.fillStyle = "#ffffff";
        detailPackageDrawWrappedText(ctx, image.name || `详情图 ${index + 1}`, 42, 69, 666, { lineHeight: 43, maxLines: 2 });
        let loaded = null;
        try { loaded = await loadDetailPackageImage(image.url); } catch (_) {}
        if (loaded) detailPackageDrawImage(ctx, loaded, 38, 188, 674, 610);
        else {
          ctx.fillStyle = "#e9edf2";
          detailPackageRoundRect(ctx, 38, 188, 674, 610, 22);
          ctx.fill();
          ctx.font = `700 20px ${family}`;
          ctx.fillStyle = "#707b88";
          ctx.textAlign = "center";
          ctx.fillText("图片素材暂时无法读取", 375, 475);
          ctx.textAlign = "left";
        }
        ctx.fillStyle = "#ffffff";
        detailPackageRoundRect(ctx, 38, 824, 674, 126, 20);
        ctx.fill();
        ctx.font = `800 20px ${family}`;
        ctx.fillStyle = theme.primary;
        ctx.fillText(info.product || "定制产品", 64, 850);
        ctx.font = `600 17px ${family}`;
        ctx.fillStyle = "#35404d";
        const accurateFacts = [info.size, info.material, info.finish]
          .map(item => String(item || "").trim())
          .filter(item => item && !item.includes("待确认"))
          .slice(0, 2);
        const caption = `${image.caption || "按客户要求定制，发货前请核对实物和确认稿"}${accurateFacts.length ? ` · ${accurateFacts.join(" · ")}` : ""}`;
        detailPackageDrawWrappedText(ctx, caption, 64, 884, 620, { lineHeight: 25, maxLines: 2 });
        ctx.font = `500 13px ${family}`;
        ctx.fillStyle = "#7d8793";
        ctx.textAlign = "center";
        ctx.fillText("效果图用于沟通方向，产品结构、颜色、Logo、尺寸和工艺以确认稿或实物为准", 375, 972);
        ctx.textAlign = "left";
        return canvas;
      }
      async function composeDetailPackagePanels({ context, images }) {
        const panels = [];
        for (let index = 0; index < images.length; index += 1) {
          const canvas = await composeDetailPackagePanelCanvas({ context, image: images[index], index, count: images.length });
          const blob = await detailPackageCanvasBlob(canvas);
          const pageNumber = Number.isFinite(Number(images[index]?.taskIndex))
            ? Number(images[index].taskIndex) + 1
            : index + 1;
          panels.push({
            ...images[index],
            canvas,
            blob,
            previewUrl: URL.createObjectURL(blob),
            filename: `${String(pageNumber).padStart(2, "0")}-${String(images[index].name || `详情图${pageNumber}`).replace(/[\\/:*?"<>|\s]+/g, "-").slice(0, 36)}.png`
          });
        }
        return panels;
      }
      function composeDetailPackageLongCanvas(panels = []) {
        const canvas = document.createElement("canvas");
        canvas.width = 750;
        canvas.height = Math.max(1000, panels.length * 1000);
        const ctx = canvas.getContext("2d");
        ctx.fillStyle = "#f6f4f1";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        panels.forEach((panel, index) => {
          if (panel?.canvas) ctx.drawImage(panel.canvas, 0, index * 1000, 750, 1000);
        });
        return canvas;
      }
      function detailPackageCanvasBlob(canvas) {
        return new Promise((resolve, reject) => {
          canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error("详情长图导出失败")), "image/png", .96);
        });
      }
      function renderDetailPackageResult(packageResult) {
        const result = document.getElementById("detailPackageResult");
        const preview = document.getElementById("detailPackagePreview");
        const assets = document.getElementById("detailPackageAssets");
        const copy = document.getElementById("detailPackageCopy");
        result?.classList.remove("is-hidden");
        if (preview) preview.innerHTML = `<img src="${escapeHtml(packageResult.previewUrl)}" alt="完整宝贝详情长图预览">`;
        if (copy) copy.textContent = packageResult.content;
        if (assets) {
          const panelItems = packageResult.panels?.length ? packageResult.panels : packageResult.images;
          assets.innerHTML = panelItems.map((image, index) => {
            const pageNumber = Number.isFinite(Number(image?.taskIndex)) ? Number(image.taskIndex) + 1 : index + 1;
            return `
              <div class="detail-package-asset">
                ${generatedImagePreviewMarkup(image.previewUrl || image.url, image.filename || `详情图-${pageNumber}.png`, image.name || `详情图${pageNumber}`, image.file_id)}
                <strong>第${pageNumber}张 · ${escapeHtml(image.name || `详情图${pageNumber}`)}</strong>
                <div class="actions"><button class="btn light" type="button" onclick="window.downloadDetailPackageAsset?.(${index})">下载本张</button></div>
              </div>
            `;
          }).join("");
          hydrateGeneratedImagePreviews(assets);
        }
      }

      const DETAIL_PACKAGE_IMAGE_MAX_ATTEMPTS = 3;
      function normalizedDetailPackageGeneratedImages(items = [], targetCount = 8) {
        const count = clampDetailPackageCount(targetCount);
        const normalized = new Array(count);
        (Array.isArray(items) ? items : []).forEach((item, fallbackIndex) => {
          if (!item || typeof item !== "object" || !String(item.url || "").trim()) return;
          const index = Number.isFinite(Number(item.taskIndex))
            ? Math.round(Number(item.taskIndex))
            : fallbackIndex;
          if (index < 0 || index >= count || normalized[index]) return;
          normalized[index] = {
            ...item,
            taskIndex: index,
            url: String(item.url)
          };
        });
        return normalized;
      }
      function persistedDetailPackageImages(items = []) {
        return (Array.isArray(items) ? items : []).filter(Boolean).map((item, fallbackIndex) => ({
          taskIndex: Number.isFinite(Number(item.taskIndex)) ? Number(item.taskIndex) : fallbackIndex,
          url: String(item.url || "").slice(0, 5000),
          filename: String(item.filename || "").slice(0, 500),
          name: String(item.name || "").slice(0, 500),
          caption: String(item.caption || "").slice(0, 2000),
          file_id: Number(item.file_id || 0) || 0
        })).filter(item => item.url);
      }
      function detailPackageRetryableImageError(error) {
        const message = String(error?.message || error || "");
        if (isImageGroupConfigurationError(message) || isSeedreamAuthorizationError(message) || isImageCredentialError(message)) return false;
        return isTemporaryImageGenerationError(message)
          || /(没有返回|没有生成|无有效结果|下载失败|服务没有响应|服务未连接)/i.test(message);
      }
      function detailPackageRetryDelay(attempt) {
        const delayMs = Math.min(5000, 1200 * Math.max(1, Number(attempt || 1)));
        return new Promise(resolve => window.setTimeout(resolve, delayMs));
      }
      async function generateDetailPackageImageWithRetry({
        task,
        index,
        requestBody,
        onRetry = () => {}
      }) {
        let lastError = null;
        for (let attempt = 1; attempt <= DETAIL_PACKAGE_IMAGE_MAX_ATTEMPTS; attempt += 1) {
          try {
            const data = await api("/api/image/generate", {
              method: "POST",
              timeoutMs: 240000,
              body: JSON.stringify(requestBody)
            });
            if (!data.image?.url) throw new Error("图片引擎没有返回图片");
            return { data, attempt };
          } catch (error) {
            lastError = error;
            const canRetry = attempt < DETAIL_PACKAGE_IMAGE_MAX_ATTEMPTS
              && detailPackageRetryableImageError(error);
            if (!canRetry) break;
            onRetry({
              attempt,
              nextAttempt: attempt + 1,
              error,
              index,
              task
            });
            await detailPackageRetryDelay(attempt);
          }
        }
        throw lastError || new Error("图片引擎没有返回图片");
      }

      async function generateDetailPackageConnected(options = {}) {
        const button = document.getElementById("detailPackageButton");
        const homeButton = document.getElementById("homeDetailPackageButton");
        const requestedSessionId = String(options?.sessionId || "");
        const session = (requestedSessionId ? detailPackageHomeSession(requestedSessionId) : null) || activeCustomerSession();
        const baseContext = customerContext();
        const job = session?.detailPackageJob || upsertHomeDetailPackageJob(
          session,
          baseContext.text,
          window.__latestAiAnalysis || null,
          homeRetainedFileIds()
        );
        const targetCount = clampDetailPackageCount(job?.targetCount || detailPackageRequestedCount(baseContext.text) || 8);
        const requestId = String(options?.requestId || (
          session?.processingKind === "detail_package" ? session.processingRequestId : ""
        ) || beginHomeSessionJob(session, "detail_package", job?.sourceText || baseContext.text));
        if (!requestId) return;
        const setProgress = (percent, message, updateHome = true) => setDetailPackageProgress(percent, message, session, requestId, updateHome);
        const inferredInfo = analyze(job?.sourceText || baseContext.text || "");
        const context = {
          ...baseContext,
          text: job?.sourceText || baseContext.text,
          platform: job?.platform || baseContext.platform,
          info: {
            ...inferredInfo,
            ...baseContext.info,
            product: job?.product && job.product !== "定制产品"
              ? job.product
              : baseContext.info?.product || inferredInfo.product || "定制产品"
          }
        };
        if (!String(context.text || "").trim() && !homeFileNames().length && !homeRetainedFileIds().length) {
          const message = "请先填写产品资料或上传产品图。";
          renderHomeDetailPackageFailure(message, session);
          finishHomeSessionJob(session, requestId, "failed");
          notify(message);
          return;
        }
        if (!consumeQuota("AI宝贝详情", false)) {
          renderHomeDetailPackageFailure("当前套餐或点数不足，整套详情没有启动。", session);
          finishHomeSessionJob(session, requestId, "failed");
          return;
        }
        const detailImageOptions = homeImageGenerationRequestOptions();
        const detailImageModel = HOME_IMAGE_MODEL_CATALOG[detailImageOptions.model_alias] || HOME_IMAGE_MODEL_CATALOG.jianglai_tiangong;
        const detailImagePoints = Number(detailImageModel.resolutions[detailImageOptions.resolution]?.points || 30);
        const resumedAtStart = normalizedDetailPackageGeneratedImages(job?.generatedImages, targetCount);
        const alreadyGeneratedCount = resumedAtStart.filter(Boolean).length;
        const remainingImageCount = Math.max(0, targetCount - alreadyGeneratedCount);
        const remaining = Number(state.member.quotaTotal || 0) - Number(state.member.quotaUsed || 0);
        const estimatedPoints = (job?.detailContent ? 0 : 3) + remainingImageCount * detailImagePoints;
        if (remaining > 0 && remaining < estimatedPoints) {
          const message = `补齐剩余${remainingImageCount}张详情图最多需要${estimatedPoints}点，当前剩余${remaining}点，请先续费或减少张数。`;
          renderHomeDetailPackageFailure(message, session);
          finishHomeSessionJob(session, requestId, "failed");
          notify(message);
          return;
        }
        if (window.__imageGenerationConfigured === false) {
          const message = `图片服务尚未接通：${window.__imageGenerationSetupIssue || "请到系统状态检查图片服务 Key"}`;
          setProgress(0, message);
          renderHomeDetailPackageFailure(message, session);
          finishHomeSessionJob(session, requestId, "failed");
          notify("请先在系统状态中接通图片生成服务。");
          return;
        }
        if (job) {
          job.status = "generating";
          job.targetCount = targetCount;
          job.updatedAt = new Date().toISOString();
          saveState();
        }
        setTextIfExists("detailPackageDescription", `文字AI整理内容，将莱图片引擎生成${targetCount}张产品素材，软件排成${targetCount}张独立详情图并自动合成长图。`);
        if (button) { button.disabled = true; button.textContent = `正在生成${targetCount}张详情图…`; }
        if (homeButton) homeButton.disabled = true;
        document.getElementById("detailPackageResult")?.classList.add("is-hidden");
        try {
          await ensureFormalLogin();
          setProgress(5, "正在同步产品资料和参考图…");
          if (latestUploadPromise) await latestUploadPromise.catch(() => []);
          if ((!uploadedFileIds.length || window.__homeUploadFailures?.length) && document.getElementById("homeFiles")?.files?.length) {
            latestUploadPromise = uploadFilesFromInput("homeFiles", "homeFileName");
            await latestUploadPromise;
          }
          const referenceFileIds = detailPackageReferenceFileIds(job);
          const allFileIds = [...new Set([
            ...uploadedFileIds,
            ...homeRetainedFileIds(),
            ...referenceFileIds
          ].map(Number).filter(id => id > 0))];
          if (job && allFileIds.length) job.referenceFileIds = allFileIds.slice(-16);
          consumeHomeDetailPackageAttachments(session, allFileIds);
          let detailContent = String(job?.detailContent || "").trim();
          if (!detailContent) {
            setProgress(12, "文字AI正在整理商品标题、卖点、参数和详情页结构…");
            const detailData = await api("/api/ai/detail", {
              method: "POST",
              body: JSON.stringify({
                text: context.text,
                platform: context.platform,
                goal: "生成可直接排版的宝贝详情页",
                analysis: window.__latestAiAnalysis || context.info,
                file_ids: allFileIds
              })
            });
            if (!detailData.detail?.content) throw new Error("文字AI没有返回详情页内容");
            detailContent = String(detailData.detail.content);
            if (detailData.membership) syncFormalMembership(detailData.membership);
            if (job) {
              job.detailContent = detailContent.slice(0, 30000);
              saveState();
            }
          } else {
            setProgress(12, "已保留上次整理好的详情文案，本次只补齐缺失图片…");
          }
          const tasks = detailPackageVisualPrompts(context, Boolean(referenceFileIds.length), targetCount, job);
          const previousPackage = detailPackageResultForSession(session);
          const resumeSource = job?.generatedImages?.length
            ? job.generatedImages
            : previousPackage?.images || [];
          const resumedImages = normalizedDetailPackageGeneratedImages(resumeSource, tasks.length);
          const imagesByIndex = [...resumedImages];
          const pendingTaskIndexes = tasks.map((_, index) => index).filter(index => !imagesByIndex[index]);
          const failuresByIndex = new Map();
          let nextPendingCursor = 0;
          let completedTasks = tasks.length - pendingTaskIndexes.length;
          if (completedTasks > 0) {
            setProgress(
              18 + Math.round(completedTasks / Math.max(1, tasks.length) * 62),
              `已恢复${completedTasks}/${targetCount}张成功结果，正在补齐剩余${pendingTaskIndexes.length}张…`
            );
          }
          const worker = async () => {
            while (nextPendingCursor < pendingTaskIndexes.length) {
              const index = pendingTaskIndexes[nextPendingCursor];
              nextPendingCursor += 1;
              const task = tasks[index];
              setProgress(18 + Math.round(completedTasks / Math.max(1, tasks.length) * 62), `将莱图片引擎正在生成第${index + 1}/${targetCount}张：${task.name}…`);
              try {
                const { data } = await generateDetailPackageImageWithRetry({
                  task,
                  index,
                  requestBody: {
                    prompt: task.prompt,
                    reference_file_ids: referenceFileIds,
                    reference_file_id: referenceFileIds[referenceFileIds.length - 1] || null,
                    source_text: `${context.text}\n本张用途：${task.name}`,
                    customer: session?.customer || currentCustomerName(),
                    ...detailImageOptions
                  },
                  onRetry: ({ nextAttempt }) => {
                    setProgress(
                      18 + Math.round(completedTasks / Math.max(1, tasks.length) * 62),
                      `第${index + 1}/${targetCount}张暂未返回，正在自动进行第${nextAttempt}/${DETAIL_PACKAGE_IMAGE_MAX_ATTEMPTS}次尝试…`
                    );
                  }
                });
                imagesByIndex[index] = { ...data.image, name: task.name, caption: task.caption, taskIndex: index };
                markImageGenerationActuallyVerified();
                if (data.membership) syncFormalMembership(data.membership);
                if (job) {
                  job.generatedImages = persistedDetailPackageImages(imagesByIndex);
                  job.generatedCount = job.generatedImages.length;
                  job.progressMessage = `已成功保存${job.generatedCount}/${targetCount}张，剩余页面继续生成`;
                  job.updatedAt = new Date().toISOString();
                  saveState();
                }
              } catch (error) {
                failuresByIndex.set(index, `第${index + 1}张 ${task.name}：${String(error?.message || "生成失败")}`);
              } finally {
                completedTasks += 1;
                const successCount = imagesByIndex.filter(Boolean).length;
                setProgress(
                  18 + Math.round(completedTasks / Math.max(1, tasks.length) * 62),
                  `已处理${completedTasks}/${targetCount}张，其中${successCount}张成功保存，正在继续…`
                );
              }
            }
          };
          await Promise.all(Array.from({ length: Math.min(2, pendingTaskIndexes.length) }, () => worker()));
          const failures = [...failuresByIndex.values()];
          const images = imagesByIndex.filter(Boolean);
          if (!images.length) throw new Error(failures[0] || "图片引擎没有生成可用图片，本次未完成的图片不会扣点");
          setProgress(84, `正在把准确文字排进${images.length}张独立详情图…`);
          const panels = await composeDetailPackagePanels({ context, images });
          const canvas = composeDetailPackageLongCanvas(panels);
          const blob = await detailPackageCanvasBlob(canvas);
          const previousRenderedPackage = detailPackageResultsBySession.get(session?.id || "");
          if (previousRenderedPackage?.previewUrl) URL.revokeObjectURL(previousRenderedPackage.previewUrl);
          (previousRenderedPackage?.panels || []).forEach(panel => {
            if (panel?.previewUrl) URL.revokeObjectURL(panel.previewUrl);
          });
          const previewUrl = URL.createObjectURL(blob);
          const productName = String(context.info?.product || "宝贝详情").replace(/[\\/:*?"<>|\s]+/g, "-").slice(0, 32) || "宝贝详情";
          activeDetailPackage = {
            blob,
            previewUrl,
            filename: `${productName}-${/淘宝|天猫/.test(context.platform) ? "淘宝" : "1688"}-详情长图.png`,
            content: detailContent,
            images,
            panels,
            sessionId: session?.id || ""
          };
          if (session?.id) detailPackageResultsBySession.set(session.id, activeDetailPackage);
          if (job) {
            job.status = failures.length ? "partial" : "completed";
            job.generatedCount = panels.length;
            job.generatedImages = persistedDetailPackageImages(imagesByIndex);
            job.failures = failures.slice(0, 12);
            job.updatedAt = new Date().toISOString();
            saveState();
          }
          const partial = failures.length ? `；${failures.length}张生成失败，已完成${panels.length}张` : "";
          setProgress(100, `${panels.length}张独立详情图和750px宽PNG长图已生成${partial}`, false);
          renderDetailPackageResult(activeDetailPackage);
          renderHomeDetailPackageResult(activeDetailPackage, failures, session);
          renderFormalStatus(`整套宝贝详情已生成 · ${panels.length}张独立详情图 + 1张完整长图`);
          notify(`${panels.length}张独立详情图已生成，可以逐张或整套下载。`);
        } catch (error) {
          if (job) {
            job.status = job.generatedImages?.length ? "partial" : "failed";
            job.updatedAt = new Date().toISOString();
            saveState();
          }
          const message = String(error?.message || "整套详情生成失败");
          setProgress(0, `整套详情没有完成：${message}`, false);
          renderHomeDetailPackageFailure(message, session);
          renderFormalStatus(`整套宝贝详情提醒：${message}`);
          notify(message);
        } finally {
          finishHomeSessionJob(session, requestId, job?.status === "failed" ? "failed" : "completed");
          if (button) {
            button.disabled = false;
            button.textContent = job?.status === "partial"
              ? `只补齐失败页面 · 最多${estimatedPoints}点`
              : `重新生成${targetCount}张 · 最多${estimatedPoints}点`;
          }
          if (homeButton) homeButton.disabled = false;
        }
      }
      function downloadDetailPackageAssetConnected(index) {
        const packageResult = detailPackageResultForSession();
        const panel = packageResult?.panels?.[Number(index)];
        if (!panel?.previewUrl) {
          notify("这张详情图暂时不可下载，请先生成整套详情。");
          return;
        }
        const link = document.createElement("a");
        link.href = panel.previewUrl;
        link.download = panel.filename || `详情图-${Number(index) + 1}.png`;
        document.body.appendChild(link);
        link.click();
        link.remove();
      }
      function downloadDetailPackageConnected() {
        const packageResult = detailPackageResultForSession();
        if (!packageResult?.previewUrl) {
          notify("请先生成整套图文详情。");
          return;
        }
        const link = document.createElement("a");
        link.href = packageResult.previewUrl;
        link.download = packageResult.filename || "宝贝详情长图.png";
        document.body.appendChild(link);
        link.click();
        link.remove();
        window.setTimeout(() => clearHomeGeneratedResult({ clearInputs: true, reason: "download" }), 120);
      }
      function clearHomeDetailPackageResult(targetSession = activeCustomerSession()) {
        if (!targetSession?.id) return false;
        const packageResult = detailPackageResultForSession(targetSession);
        if (packageResult?.previewUrl) URL.revokeObjectURL(packageResult.previewUrl);
        (packageResult?.panels || []).forEach(panel => {
          if (panel?.previewUrl) URL.revokeObjectURL(panel.previewUrl);
        });
        detailPackageResultsBySession.delete(targetSession.id);
        if (activeDetailPackage?.sessionId === targetSession.id) activeDetailPackage = null;
        if (targetSession.detailPackageJob) targetSession.detailPackageJob = null;
        return Boolean(packageResult);
      }
      function vectorJobTime(value) {
        if (!value) return "—";
        const date = new Date(value);
        return Number.isNaN(date.getTime()) ? value : date.toLocaleString("zh-CN", { hour12: false });
      }
      let currentVectorJobs = [];
      function renderVectorLearning(data = {}) {
        const container = document.getElementById("vectorLearningSummary");
        if (!container) return;
        const profiles = Array.isArray(data.profiles) ? data.profiles : [];
        const accepted = Number(data.counts?.accepted || 0);
        const total = Number(data.counts?.total || 0);
        if (!profiles.length) {
          container.innerHTML = `<div class="vector-empty">还没有已确认样本。当前共记录 ${total} 个样本条目，完成专业矢量结果并勾选学习后会开始形成标准。</div>`;
          return;
        }
        container.innerHTML = `
          <div class="member-note" style="margin-bottom:10px">已确认 ${accepted} 个样本${total > accepted ? ` · ${total - accepted} 个未纳入/已撤回` : ""}。不同用途和格式不会混在一起学习。</div>
          <div class="vector-job-list">
            ${profiles.map(profile => {
              const standard = profile.standard || {};
              const maturity = standard.maturity || {};
              const rules = Array.isArray(standard.rules) && standard.rules.length ? standard.rules.join("、") : "等待更多验收说明";
              const specs = standard.specifications || {};
              const specLabels = [
                specs.bleed_mm ? `四周出血 ${Number(specs.bleed_mm)}mm` : "",
                specs.safe_margin_mm ? `安全边距 ${Number(specs.safe_margin_mm)}mm` : "",
                specs.minimum_dpi ? `图片 ≥ ${Number(specs.minimum_dpi)}DPI` : "",
                specs.color_mode ? `色彩 ${String(specs.color_mode)}` : "",
                specs.minimum_line_width_mm ? `最小线宽 ${Number(specs.minimum_line_width_mm)}mm` : "",
                specs.minimum_text_pt ? `最小文字 ${Number(specs.minimum_text_pt)}pt` : "",
                specs.spot_color_required ? "保留专色" : ""
              ].filter(Boolean);
              const workflow = Array.isArray(standard.workflow) ? standard.workflow : [];
              const next = Number(maturity.next_target || 0);
              return `<div class="vector-job">
                <div class="vector-job-main">
                  <div class="vector-job-top"><strong>${escapeHtml(profile.production_use || "通用")} · ${escapeHtml(String(profile.output_format || "svg").toUpperCase())}</strong><span class="vector-job-status completed">${escapeHtml(maturity.label || "样本积累中")}</span></div>
                  <div class="vector-job-meta">已确认 ${Number(profile.sample_count || 0)} 个样本 · 主要工具：${escapeHtml(standard.primary_provider || "专业矢量工具")}${next ? ` · 距下一阶段还差 ${next} 个` : ""}</div>
                  ${specLabels.length ? `<div class="vector-job-note"><strong>量化标准：</strong>${escapeHtml(specLabels.join("、"))}</div>` : ""}
                  <div class="vector-job-note">已学习：${escapeHtml(rules)}</div>
                  ${workflow.length ? `<div class="vector-job-note"><strong>全流程：</strong>${workflow.map(stage => `${escapeHtml(stage.label || "检查")}（${escapeHtml((stage.learned_rules || []).join("、") || "按基础清单人工复核")}）`).join(" → ")}</div>` : ""}
                </div>
              </div>`;
            }).join("")}
          </div>`;
      }
      async function loadVectorLearning() {
        if (isStaticFileMode) return;
        const providerInput = document.getElementById("vectorProviderName");
        let savedProvider = "";
        try {
          savedProvider = window.localStorage?.getItem("factoryVectorProviderName") || "";
        } catch (_) {
          // 部分桌面 WebView 会禁用 localStorage；不应因此阻断矢量工作台。
        }
        if (providerInput && savedProvider && (!providerInput.value || providerInput.value === "专业矢量工具")) providerInput.value = savedProvider;
        const clearButton = document.getElementById("vectorLearningClearButton");
        if (clearButton) clearButton.style.display = window.__currentUser?.is_owner ? "" : "none";
        const data = await api("/api/vector/learning");
        renderVectorLearning(data);
        return data;
      }
      async function clearVectorLearning() {
        if (!confirm("确定清空本工厂全部矢量学习样本和标准吗？已完成的矢量文件与任务不会删除。")) return;
        try {
          await ensureFormalLogin();
          const data = await api("/api/vector/learning/clear", { method: "POST", body: "{}" });
          renderVectorLearning({ profiles: [], counts: { total: 0, accepted: 0 } });
          await loadVectorJobs();
          notify(`已清空 ${Number(data.deleted || 0)} 个矢量学习样本。`);
        } catch (err) {
          notify(err.message);
        }
      }
      function renderVectorJobs(jobs = []) {
        const list = document.getElementById("vectorJobList");
        if (!list) return;
        currentVectorJobs = jobs;
        if (!jobs.length) {
          list.innerHTML = `<div class="vector-empty">还没有转矢量任务。提交客户图片后会在这里排队，员工会收到提醒。</div>`;
          return;
        }
        const currentUser = window.__currentUser || {};
        list.innerHTML = jobs.map(job => {
          const id = Number(job.id || 0);
          const status = job.status || "pending";
          const canHandle = !job.assigned_user_id || Number(job.assigned_user_id) === Number(currentUser.user_id) || currentUser.is_owner;
          const resultInput = canHandle && ["processing", "completed"].includes(status) ? `
            <input class="vector-result-upload" id="vectorResultFile-${id}" type="file" accept=".svg,.pdf,.eps,.dxf,.ai,.cdr">
            <input id="vectorResultProvider-${id}" value="${escapeHtml(job.provider_name || "专业矢量工具")}" placeholder="本次专业工具">
            <label class="vector-consent" style="margin:0"><input id="vectorLearnResult-${id}" type="checkbox" ${job.learning_enabled !== false ? "checked" : ""}><span>确认成品并纳入工厂标准学习</span></label>
            <button class="btn ${status === "completed" ? "light" : "dark"}" type="button" onclick="completeVectorJob(${id})">${status === "completed" ? "替换结果文件" : "上传结果并完成"}</button>
          ` : "";
          let actions = "";
          if (status === "pending") {
            actions = `<button class="btn dark" type="button" onclick="claimVectorJob(${id})">领取并下载原图</button>`;
          } else if (status === "processing") {
            actions = `
              <button class="btn light" type="button" onclick="downloadVectorJobFile(${id}, 'source')">下载原图</button>
              ${resultInput}
            `;
          } else {
            actions = `
              <button class="btn dark" type="button" onclick="downloadVectorJobFile(${id}, 'result')">下载结果</button>
              <button class="btn light" type="button" onclick="downloadVectorJobFile(${id}, 'source')">查看原图</button>
              ${resultInput}
            `;
          }
          return `
            <div class="vector-job">
              <div class="vector-job-main">
                <div class="vector-job-top"><strong>#${id} · ${escapeHtml(job.customer || "新客户")}</strong><span class="vector-job-status ${escapeHtml(status)}">${escapeHtml(job.status_label || "待处理")}</span></div>
                <div class="vector-job-meta">原图：${escapeHtml(job.source_name || "—")} · 输出 ${escapeHtml(String(job.output_format || "svg").toUpperCase())} · ${escapeHtml(job.production_use || "—")}<br>提交：${escapeHtml(job.submitted_by_name || "员工")} · ${escapeHtml(vectorJobTime(job.created_at))}${job.assigned_name ? ` · 处理人：${escapeHtml(job.assigned_name)}` : " · 等待领取"}${job.completed_at ? ` · 完成：${escapeHtml(vectorJobTime(job.completed_at))}` : ""}</div>
                ${job.note ? `<div class="vector-job-note">要求：${escapeHtml(job.note)}</div>` : ""}
                ${job.learning_enabled !== false ? `<div class="vector-job-note">任务标准：${escapeHtml(job.standard_summary || "尚无已确认样本")} · 使用工具：${escapeHtml(job.provider_name || "专业矢量工具")}</div>` : `<div class="vector-job-note">本任务未启用学习</div>`}
                ${job.result_note ? `<div class="vector-job-note">交付说明：${escapeHtml(job.result_note)}</div>` : ""}
              </div>
              <div class="vector-job-actions">${actions}</div>
            </div>
          `;
        }).join("");
      }
      async function loadVectorJobs() {
        if (isStaticFileMode) return;
        const list = document.getElementById("vectorJobList");
        try {
          await ensureFormalLogin();
          const data = await api("/api/vector/jobs");
          renderVectorJobs(data.jobs || []);
          await loadVectorLearning().catch(() => null);
        } catch (err) {
          if (list) list.innerHTML = `<div class="vector-empty">读取任务失败：${escapeHtml(err.message)}</div>`;
        }
      }
      async function downloadVectorJobFile(jobId, kind) {
        try {
          await ensureFormalLogin();
          const response = await fetch(`/api/vector/jobs/${Number(jobId)}/${kind}`, {
            headers: { Authorization: "Bearer " + apiToken }
          });
          if (!response.ok) {
            let message = "文件下载失败";
            try { message = (await response.json()).error || message; } catch {}
            throw new Error(message);
          }
          const blob = await response.blob();
          const link = document.createElement("a");
          link.href = URL.createObjectURL(blob);
          const job = currentVectorJobs.find(item => Number(item.id) === Number(jobId));
          link.download = (kind === "source" ? job?.source_name : job?.result_name) || (kind === "source" ? "source-image" : "vector-result");
          document.body.appendChild(link);
          link.click();
          link.remove();
          window.setTimeout(() => URL.revokeObjectURL(link.href), 1000);
        } catch (err) {
          notify(err.message);
        }
      }
      async function uploadVectorResultFile(file) {
        if (!file) throw new Error("请先选择转换完成的矢量文件");
        if (file.size > 18 * 1024 * 1024) throw new Error("结果文件不能超过18MB");
        const allowed = /\.(svg|pdf|eps|dxf|ai|cdr)$/i;
        if (!allowed.test(file.name || "")) throw new Error("结果文件仅支持 SVG、PDF、EPS、DXF、AI 或 CDR");
        const base64 = await fileToBase64(file);
        const data = await api("/api/files", {
          method: "POST",
          body: JSON.stringify({ name: file.name, mime_type: file.type || "application/octet-stream", base64 })
        });
        if (!data.file?.id) throw new Error("结果文件上传失败");
        return data.file.id;
      }
      async function claimVectorJob(jobId) {
        try {
          await ensureFormalLogin();
          await api(`/api/vector/jobs/${Number(jobId)}/claim`, { method: "POST", body: "{}" });
          await loadVectorJobs();
          renderFormalStatus(`已领取转矢量任务 #${jobId} · 请人工转换并核对结果`);
          await downloadVectorJobFile(jobId, "source");
        } catch (err) {
          notify(err.message);
        }
      }
      async function completeVectorJob(jobId) {
        const input = document.getElementById(`vectorResultFile-${Number(jobId)}`);
        const file = input?.files?.[0];
        try {
          await ensureFormalLogin();
          const resultFileId = await uploadVectorResultFile(file);
          const resultNote = prompt("交付说明（选填）\n例如：已检查边缘和闭合路径，可用于丝印。", "已人工检查轮廓、颜色和文件完整性。") || "";
          const learnFromResult = document.getElementById(`vectorLearnResult-${Number(jobId)}`)?.checked !== false;
          const providerName = document.getElementById(`vectorResultProvider-${Number(jobId)}`)?.value?.trim() || "专业矢量工具";
          const data = await api(`/api/vector/jobs/${Number(jobId)}/complete`, {
            method: "POST",
            body: JSON.stringify({ result_file_id: resultFileId, result_note: resultNote, learn_from_result: learnFromResult, provider_name: providerName })
          });
          if (data.membership) syncFormalMembership(data.membership);
          await loadVectorJobs();
          const warnings = data.quality_check?.warnings || [];
          const output = document.getElementById("vectorOutput");
          if (output) {
            output.style.display = "block";
            const checklist = Array.isArray(data.quality_check?.manual_checklist) ? data.quality_check.manual_checklist : [];
            output.textContent = `任务 #${jobId} 已完成。${data.learning?.stored ? `\n已纳入全工厂统一标准：${data.learning.summary}` : "\n本次结果未纳入学习。"}${warnings.length ? `\n\n自动检查提醒：\n- ${warnings.join("\n- ")}` : "\n自动结构检查通过。"}${checklist.length ? `\n\n生产前人工清单：\n- ${checklist.join("\n- ")}` : ""}`;
          }
          renderFormalStatus(`转矢量任务 #${jobId} 已完成${data.learning?.stored ? ` · 已学习第 ${Number(data.learning.sample_count || 0)} 个样本` : ""}`);
          notify(warnings.length ? `结果已交付，但有 ${warnings.length} 项标准提醒，请查看。` : "矢量结果已上传、检查并完成交付。" );
        } catch (err) {
          notify(err.message);
        }
      }
      async function convertVectorConnected() {
        const output = document.getElementById("vectorOutput");
        const convertButton = document.getElementById("vectorSubmitButton");
        if (convertButton?.disabled) return;
        if (!consumeQuota("图片转矢量", false)) return;
        if (!document.getElementById("vectorConsent")?.checked) {
          notify("请先确认已获得客户授权，并勾选第三方人工处理说明。" );
          return;
        }
        const originalButtonText = convertButton?.textContent || "提交人工转换";
        if (convertButton) {
          convertButton.disabled = true;
          convertButton.textContent = "正在提交…";
        }
        try {
          await ensureFormalLogin();
          if (latestVectorUploadPromise) {
            const ids = await latestVectorUploadPromise;
            latestVectorFileId = ids[ids.length - 1] || latestVectorFileId;
          }
          if (!latestVectorFileId && document.getElementById("vectorFile")?.files?.length) {
            const ids = await uploadFilesFromInput("vectorFile", "vectorFileName");
            latestVectorFileId = ids[ids.length - 1] || null;
          }
          if (!latestVectorFileId) throw new Error("请先选择一张 JPG、PNG 或 WebP 图片");
          const format = val("vectorFormat") || "svg";
          const use = val("vectorUse") || "丝印/印刷";
          const customer = val("vectorCustomer") || "新客户";
          const note = val("vectorNote");
          const learnStandard = document.getElementById("vectorLearnEnabled")?.checked !== false;
          const providerName = val("vectorProviderName") || "专业矢量工具";
          try {
            window.localStorage?.setItem("factoryVectorProviderName", providerName);
          } catch (_) {
            // 配置无法持久化时仍允许本次任务继续提交。
          }
          const data = await api("/api/vector/jobs", {
            method: "POST",
            body: JSON.stringify({ file_id: latestVectorFileId, format, use, customer, note, consent: true, learn_standard: learnStandard, provider_name: providerName })
          });
          if (data.membership) syncFormalMembership(data.membership);
          if (output) {
            output.style.display = "block";
            output.textContent = `任务 #${data.job?.id || "—"} 已进入人工处理队列。${learnStandard ? `\n已带入工厂标准：${data.job?.standard_summary || "尚无已确认样本，将从本次成品开始学习"}` : "\n本任务未启用标准学习。"}\n提交阶段不扣点，员工上传有效结果后才扣30点。`;
          }
          document.getElementById("vectorFile").value = "";
          document.getElementById("vectorConsent").checked = false;
          document.getElementById("vectorNote").value = "";
          setTextIfExists("vectorFileName", "未选择文件");
          latestVectorFileId = null;
          latestVectorUploadPromise = null;
          await loadVectorJobs();
          renderFormalStatus("转矢量任务已提交 · 指定员工会收到提醒");
          notify("任务已提交，客户图片不会被自动转发或共享会员账号。" );
        } catch (err) {
          if (output) {
            output.style.display = "block";
            output.textContent = "提交失败：" + err.message;
          }
          renderFormalStatus("转矢量任务提醒：" + err.message);
        } finally {
          if (convertButton) {
            convertButton.disabled = false;
            convertButton.textContent = originalButtonText;
          }
        }
      }
      function stopHomeThinkingState(sessionId = activeCustomerSession()?.id || "") {
        const timer = homeSessionThinkingTimers.get(sessionId);
        if (timer) window.clearInterval(timer);
        homeSessionThinkingTimers.delete(sessionId);
        if (sessionId === state.activeCustomerSessionId) window.__homeThinkingTimer = null;
      }
      function showHomeThinkingState(fileCount = 0, sessionId = activeCustomerSession()?.id || "") {
        stopHomeThinkingState(sessionId);
        resetHomeConversationScrollFollow();
        showHomeResult();
        hideHomeNextActions();
        const answer = document.getElementById("aiAnswer");
        if (!answer) return;
        const stages = fileCount
          ? ["正在读取附件…", "正在理解附件和你的问题…", "正在组织回答…"]
          : ["正在理解你的问题…", "正在结合客户记录分析…", "正在组织回答…"];
        answer.innerHTML = `
          <div class="ai-answer-body">
            <div class="ai-loading" id="homeThinkingState" role="status" aria-live="polite" aria-label="将莱数字员工正在处理">
              <span class="ai-loading-spinner" aria-hidden="true"><i></i><i></i><i></i></span>
              <span class="ai-loading-copy"><strong id="homeThinkingText">${stages[0]}</strong><em>请稍候，完成后会自动显示结果</em></span>
            </div>
          </div>
        `;
        let stageIndex = 0;
        const timer = window.setInterval(() => {
          stageIndex = Math.min(stageIndex + 1, stages.length - 1);
          if (state.activeCustomerSessionId === sessionId) setTextIfExists("homeThinkingText", stages[stageIndex]);
        }, 2400);
        homeSessionThinkingTimers.set(sessionId, timer);
        window.__homeThinkingTimer = timer;
        window.requestAnimationFrame(() => {
          scrollHomeReplyAboveComposer(document.getElementById("homeThinkingState"));
        });
      }
      function consumeHomeDetailPackageAttachments(targetSession, usedFileIds = []) {
        if (!targetSession) return;
        const consumedIds = [...new Set((usedFileIds || []).map(Number).filter(id => id > 0))];
        if (targetSession.detailPackageJob && consumedIds.length) {
          targetSession.detailPackageJob.referenceFileIds = consumedIds.slice(-16);
        }
        targetSession.pendingFileIds = [];
        targetSession.pendingFileRecords = [];
        targetSession.uploadFailures = [];
        targetSession.retainedFileIds = [];
        targetSession.retainedFileNames = [];
        targetSession.updatedAt = new Date().toISOString();
        homeSessionUploadPromises.delete(targetSession.id);
        if (targetSession.id === state.activeCustomerSessionId) {
          const input = document.getElementById("homeFiles");
          if (input) input.value = "";
          uploadedFileIds = [];
          uploadedHomeFileRecords = [];
          window.__homeUploadFailures = [];
          window.__lastRecognizedHomeFileIds = [];
          window.__homeRetainedFileNames = [];
          latestUploadPromise = null;
          clearHomeFilePreviewUrls();
          document.getElementById("homeFilePreview")?.replaceChildren();
          setTextIfExists("homeFileName", "上传图片、表格、文档、视频或图纸");
          document.getElementById("homeIntentHint")?.classList.add("is-hidden");
          renderHomeRetainedFileContext();
          window.requestAnimationFrame(syncHomeComposerClearance);
        }
        saveState();
      }
      async function clearHomeComposerAttachmentsAfterRecognition(sentFileIds = [], sentFileNames = [], sessionId = activeCustomerSession()?.id || "", sentFileRecords = []) {
        const targetSession = homeSessionById(sessionId);
        const normalizedRecords = (Array.isArray(sentFileRecords) ? sentFileRecords : [])
          .map(record => ({ id: Number(record?.id || 0), mimeType: String(record?.mimeType || "").toLowerCase() }))
          .filter(record => record.id > 0);
        const imageIds = normalizedRecords.length
          ? normalizedRecords.filter(record => record.mimeType.startsWith("image/")).map(record => record.id)
          : sentFileIds.map(Number).filter(id => id > 0);
        if (targetSession && sessionId !== state.activeCustomerSessionId) {
          if (sentFileIds.length) {
            targetSession.retainedFileIds = [...sentFileIds];
            targetSession.retainedFileNames = sentFileNames.length ? [...sentFileNames] : [`${sentFileIds.length} 个已识别附件`];
          }
          targetSession.pendingFileIds = [];
          targetSession.pendingFileRecords = [];
          targetSession.uploadFailures = [];
          saveState();
          return;
        }
        const input = document.getElementById("homeFiles");
        const hadFiles = Boolean(input?.files?.length || sentFileIds.length || uploadedFileIds.length);
        if (!hadFiles) return;
        const preservedIds = sentFileIds.length ? sentFileIds : [...uploadedFileIds];
        if (preservedIds.length) {
          if (imageIds.length) latestVisualFileId = imageIds[imageIds.length - 1] || latestVisualFileId;
          window.__lastRecognizedHomeFileIds = [...preservedIds];
          window.__homeRetainedFileNames = sentFileNames.length
            ? [...sentFileNames]
            : [`${preservedIds.length} 个已识别附件`];
        }
        if (input) input.value = "";
        uploadedFileIds = [];
        uploadedHomeFileRecords = [];
        window.__homeUploadFailures = [];
        latestUploadPromise = null;
        clearHomeFilePreviewUrls();
        setTextIfExists("homeFileName", "上传图片、表格、文档、视频或图纸");
        document.getElementById("homeFilePreview")?.replaceChildren();
        document.getElementById("homeIntentHint")?.classList.add("is-hidden");
        renderHomeRetainedFileContext();
        window.requestAnimationFrame(syncHomeComposerClearance);
        const privacyNotice = document.querySelector("#homePrivacyNotice span");
        if (privacyNotice) privacyNotice.textContent = "隐私保护已开启：调用AI前自动隐藏联系方式与详细地址，并清理图片元数据";
        if (targetSession) captureHomeSessionRuntime(targetSession);
      }
      function keepHomeComposerAttachmentsForRetry(message = "部分图片缩略图还没有安全保存，附件已保留。请稍后再次发送，不需要重新选择图片。") {
        setTextIfExists("homeIntentHint", message);
        document.getElementById("homeIntentHint")?.classList.remove("is-hidden");
        renderFormalStatus("部分附件尚未完成处理 · 已保留在输入框 · 可直接重试");
        notify(message);
        window.requestAnimationFrame(syncHomeComposerClearance);
      }
      const originalStartHomeInquiry = window.startHomeInquiry;
      window.startHomeInquiry = async function (options = {}) {
        const originSession = activeCustomerSession();
        const originSessionId = originSession?.id || "";
        const homeText = (document.getElementById("aiQuestion")?.value || "").trim();
        const homeFiles = Array.from(document.getElementById("homeFiles")?.files || []);
        const restoredPendingRecords = Array.isArray(originSession?.pendingFileRecords)
          ? originSession.pendingFileRecords.filter(record => Number(record?.id || 0) > 0).map(record => ({ ...record }))
          : [];
        const restoredPendingFileIds = Array.isArray(originSession?.pendingFileIds)
          ? originSession.pendingFileIds.map(Number).filter(id => id > 0)
          : [];
        const hasPendingAttachments = homeFiles.length > 0 || restoredPendingFileIds.length > 0;
        const homeFileNamesSnapshot = homeFiles.length
          ? homeFiles.map(file => file.name)
          : restoredPendingRecords.map(record => record.name || "已上传附件");
        let requestedAnalysisMode = normalizeHomeAnalysisMode(
          options.analysisMode
          || originSession?.analysisMode
          || window.__homeAnalysisMode
        );
        window.clearTimeout(window.__homeCustomerMemoryTimer);
        window.__selectedHomeCustomerName = (document.getElementById("homeCustomerName")?.value || "新客户").trim() || "新客户";
        if (isStaticFileMode) {
          await leaveStaticPreview();
          return;
        }
        if (!homeText && !hasPendingAttachments) {
          originalStartHomeInquiry?.();
          return;
        }
        if (!originSession) return;
        if (requestedAnalysisMode === "deep" && window.__deepseekReasoningVerified !== true) {
          notify("DeepSeek Pro 当前不可用。消息已保留，请恢复服务后重新选择深度分析。");
          document.getElementById("aiQuestion")?.focus();
          return;
        }
        if (homeSessionIsProcessing(originSession)) {
          notify(`${originSession.customer} 正在处理中。可以先切换到其他客户，完成后左侧会提示。`);
          return;
        }
        archiveHomeTurn();
        const previousContext = String(originSession.conversationContext || window.__homeConversationContext || originSession.businessSourceText || window.__latestAiSourceText || "").trim();
        const startsNewCustomer = /^(新客户|换个客户|换一个客户|另一个客户|另外一个客户|新询盘|重新开始|开始新客户)/.test(homeText);
        const isFollowUp = !!previousContext && !startsNewCustomer;
        window.__homeIsFollowUp = isFollowUp;
        window.__homeVisiblePrompt = homeText || "请看一下这些附件";
        window.__activeHomePrompt = homeText || "请先客观描述附件是什么，再根据内容判断这是普通图片问答、客户需求、订单截图、报价资料、做图需求还是跟单事项。不确定的地方直接说明，不要猜测，也不要强行套用报价模板。";
        window.__homePromptPrepared = true;
        const requestPrompt = String(window.__activeHomePrompt);
        const visiblePrompt = String(window.__homeVisiblePrompt);
        const requestCustomer = String(originSession.customer || window.__selectedHomeCustomerName || "新客户").trim() || "新客户";
        const previousAnalysis = originSession.businessAnalysis || window.__latestAiAnalysis || null;
        if (typeof handleHomeDetailPackageFollowUp === "function" && handleHomeDetailPackageFollowUp(homeText, originSession)) {
          applyHomeAnalysisMode("standard", { session: originSession });
          originSession.activePrompt = requestPrompt;
          originSession.visiblePrompt = visiblePrompt;
          captureActiveCustomerSession(true);
          return;
        }
        let retainedFileIdsSnapshot = homeRetainedFileIds();
        if (!retainedFileIdsSnapshot.length && shouldReuseHomeRetainedFiles(requestPrompt, [1])) {
          retainedFileIdsSnapshot = recoverHomeRetainedFilesFromConversation();
        }
        const retainedFileIdsForTurn = shouldReuseHomeRetainedFiles(requestPrompt, retainedFileIdsSnapshot)
          ? [...retainedFileIdsSnapshot]
          : [];
        const requestHasImage = Boolean(
          homeFiles.some(file => String(file?.type || "").toLowerCase().startsWith("image/"))
          || restoredPendingRecords.some(record => String(record?.mimeType || "").toLowerCase().startsWith("image/"))
          || retainedHomeUserAttachmentItems(retainedFileIdsForTurn).some(item => item.imageLike)
        );
        if (requestedAnalysisMode === "deep" && requestHasImage) {
          requestedAnalysisMode = "standard";
          applyHomeAnalysisMode("standard", { session: originSession });
          notify("图片会先由视觉模型识别；识别完成后可再对文字结果使用深度分析。");
        }
        const loadingStartedAt = Date.now();
        const retainedFileCount = retainedFileIdsForTurn.length;
        // 整套宝贝详情必须优先于单张改图。否则“做成宝贝详情9张”会被
        // “做成”两个字抢先路由到单图生成，最后只得到1张。
        const directDetailPackageRequested = typeof isHomeDetailPackageRequest === "function"
          && isHomeDetailPackageRequest(null, homeText);
        const directInlineVisualRequested = !directDetailPackageRequested && isHomeInlineVisualFollowUpRequest(homeText);
        if (directInlineVisualRequested) {
          applyHomeAnalysisMode("standard", { session: originSession });
          await generateHomeVisualInline(homeText || requestPrompt, {
            sessionId: originSessionId,
            customer: requestCustomer,
            retainedFileIds: retainedFileIdsSnapshot,
            composerFileNames: homeFileNamesSnapshot
          });
          return;
        }
        const requestId = beginHomeSessionJob(originSession, "analysis", requestPrompt);
        if (!requestId) return;
        originSession.lastRequestedAnalysisMode = requestedAnalysisMode;
        applyHomeAnalysisMode("standard", { session: originSession, persist: false });
        originSession.conversationContext = requestPrompt;
        originSession.activePrompt = requestPrompt;
        originSession.visiblePrompt = visiblePrompt;
        if (homeText) originSession.lastMessage = homeText.slice(0, 80);
        showHomeThinkingState((hasPendingAttachments ? Math.max(homeFiles.length, restoredPendingFileIds.length) : 0) || retainedFileCount, originSessionId);
        captureActiveCustomerSession(true);
        syncHomeSessionProcessingUi(originSession);
        renderCustomerWorkbench();
        let transferredToVisual = false;
        let failed = false;
        let detailPackageRequested = false;
        try {
          const data = await syncAnalysis({
            sessionId: originSessionId,
            text: requestPrompt,
            customer: requestCustomer,
            previousAnalysis,
            retainedFileIds: retainedFileIdsSnapshot,
            includePendingFiles: hasPendingAttachments,
            analysisMode: requestedAnalysisMode
          });
          const targetSession = homeSessionById(originSessionId);
          if (!targetSession || targetSession.processingRequestId !== requestId) return;
          const sentFileIds = hasPendingAttachments
            ? (Array.isArray(targetSession.pendingFileIds) ? [...targetSession.pendingFileIds] : [])
            : [...retainedFileIdsForTurn];
          const sentUploadRecords = hasPendingAttachments && Array.isArray(targetSession.pendingFileRecords)
            ? targetSession.pendingFileRecords.map(record => ({ ...record }))
            : [];
          const attachmentBinding = await bindCurrentTurnUserAttachments(sentUploadRecords, originSessionId);
          if (data?.membership) syncFormalMembership(data.membership);
          const remainingLoadingMs = Math.max(0, 720 - (Date.now() - loadingStartedAt));
          if (remainingLoadingMs) await new Promise(resolve => setTimeout(resolve, remainingLoadingMs));
          if (data?.analysis) {
            stopHomeThinkingState(originSessionId);
            const currentTurn = String(homeText || visiblePrompt || requestPrompt).trim();
            detailPackageRequested = typeof isHomeDetailPackageRequest === "function"
              && isHomeDetailPackageRequest(data.analysis, currentTurn);
            const detailPackageMessage = data.analysis?.detail_output_incomplete
              ? String(data.analysis.direct_answer || data.analysis.customer_reply || "")
              : "";
            if (detailPackageRequested) {
              upsertHomeDetailPackageJob(
                targetSession,
                currentTurn,
                data.analysis,
                sentFileIds.length ? sentFileIds : retainedFileIdsSnapshot
              );
            }
            if (isStructuredBusinessAnalysis(data.analysis)) {
              targetSession.businessAnalysis = data.analysis;
              targetSession.businessSourceText = isFollowUp && targetSession.businessSourceText
                  ? `${targetSession.businessSourceText}\n补充：${currentTurn}`.slice(-12000)
                  : currentTurn;
              if (currentTurn) targetSession.lastMessage = currentTurn.slice(0, 80);
            }
            if (state.activeCustomerSessionId === originSessionId) {
              window.__latestAiAnalysis = isStructuredBusinessAnalysis(data.analysis) ? data.analysis : window.__latestAiAnalysis;
              window.__latestAiSourceText = currentTurn;
              window.__homeConversationContext = requestPrompt;
              renderBackendAnalysis(data.analysis);
              if (detailPackageRequested) renderHomeDetailPackageConfirmation(targetSession, detailPackageMessage);
              window.setTimeout(() => scrollHomeReplyAboveComposer(document.getElementById("aiAnswer")), 0);
              captureActiveCustomerSession(true);
            } else if (detailPackageRequested) {
              renderHomeDetailPackageConfirmation(targetSession, detailPackageMessage);
            } else {
              targetSession.pendingAnalysisResult = {
                analysis: data.analysis,
                currentTurn,
                prompt: requestPrompt,
                visiblePrompt,
                isFollowUp
              };
              targetSession.resultEmpty = false;
              targetSession.hasConversation = true;
            }
            const inlineVisualRequested = !detailPackageRequested && isHomeInlineVisualRequest(data.analysis, homeText || requestPrompt);
            const attachmentWarnings = Array.isArray(data.attachment_warnings)
              ? data.attachment_warnings.map(item => String(item || "").trim()).filter(Boolean)
              : [];
            const recognitionSucceeded = hasPendingAttachments && sentFileIds.length
              && !data.analysis.provider_error
              && !["image_unavailable", "service_unavailable"].includes(data.analysis.intent)
              && !attachmentWarnings.length;
            if (recognitionSucceeded) {
              if (attachmentBinding.allAttachmentsPersistent) {
                await clearHomeComposerAttachmentsAfterRecognition(sentFileIds, homeFileNamesSnapshot, originSessionId, sentUploadRecords);
              } else if (state.activeCustomerSessionId === originSessionId) {
                keepHomeComposerAttachmentsForRetry();
              }
            } else if (attachmentWarnings.length && state.activeCustomerSessionId === originSessionId) {
              keepHomeComposerAttachmentsForRetry(`部分表格没有读取成功：${attachmentWarnings.slice(0, 2).join("；")}。附件已保留，修正后可直接重试。`);
            }
            if (inlineVisualRequested) {
              targetSession.processingStatus = "";
              targetSession.processingKind = "";
              targetSession.processingRequestId = "";
              targetSession.processingStartedAt = "";
              targetSession.hasUnreadResult = false;
              saveState();
              transferredToVisual = true;
              await generateHomeVisualInline(homeText || requestPrompt, {
                sessionId: originSessionId,
                customer: requestCustomer,
                referenceFileIds: sentFileIds.length ? sentFileIds : retainedFileIdsSnapshot,
                composerFileNames: homeFileNamesSnapshot
              });
              return;
            }
            if (data.memory?.stored) {
              const memoryCustomer = {
                name: data.memory.customer,
                memory_summary: data.memory.summary,
                conversation_count: data.memory.conversation_count,
                updated_at: new Date().toISOString()
              };
              mergeCloudCustomerProfiles([memoryCustomer]);
              if (state.activeCustomerSessionId === originSessionId) renderConnectedMemoryStatus({ customer: memoryCustomer, memory: data.memory }, data.cloud?.ok === true);
            } else if (state.activeCustomerSessionId === originSessionId) {
              renderConnectedMemoryStatus(data, data.cloud?.ok === true);
            }
            if (isStructuredBusinessAnalysis(data.analysis) && !homeChatTaskForSession(targetSession) && typeof window.__saveHomeChatTaskConnected === "function") {
              targetSession.hasConversation = true;
              targetSession.chatAutoRecordPending = true;
              targetSession.chatAutoRecordFailed = false;
              targetSession.chatActionStatus = "正在自动建立生意进度，不需要重复保存";
              saveState();
              if (state.activeCustomerSessionId === originSessionId) renderHomeChatRecordActions();
              try {
                await window.__saveHomeChatTaskConnected(targetSession, { silent: true });
              } catch (taskError) {
                targetSession.chatAutoRecordPending = false;
                targetSession.chatAutoRecordFailed = true;
                targetSession.chatActionStatus = "自动记录暂时失败，可点击“记录生意进度”重试";
                saveState();
                if (state.activeCustomerSessionId === originSessionId) renderHomeChatRecordActions();
              }
            }
          }
          if (data?.analysis_id && state.activeCustomerSessionId === originSessionId && !detailPackageRequested && !isHomeInlineVisualRequest(data.analysis, homeText || requestPrompt)) {
            const memoryText = data.memory?.stored
              ? (data.cloud?.ok ? "客户资料与聊天已同步云端" : "客户资料与聊天已保存在本机")
              : "填写客户名称后可启用长期记忆";
            const privacyText = data.analysis?.privacy?.enabled ? "隐私脱敏已完成 · " : "";
            renderFormalStatus(`AI已处理完成 · ${privacyText}${memoryText}`);
          }
        } catch (err) {
          failed = true;
          const remainingLoadingMs = Math.max(0, 720 - (Date.now() - loadingStartedAt));
          if (remainingLoadingMs) await new Promise(resolve => setTimeout(resolve, remainingLoadingMs));
          stopHomeThinkingState(originSessionId);
          const targetSession = homeSessionById(originSessionId);
          if (targetSession && targetSession.processingRequestId === requestId) {
            if (state.activeCustomerSessionId === originSessionId) {
              originalStartHomeInquiry?.();
              captureActiveCustomerSession(true);
              renderFormalStatus("云端暂时繁忙 · 已切换本机处理");
            } else {
              targetSession.answerHtml = `<div class="ai-answer-body"><p class="ai-answer-lead">消息已经保留，可以切回后原地重试。</p><div class="ai-answer-section"><p>${escapeHtml(String(err?.message || "AI服务暂时繁忙"))}</p></div></div>`;
              targetSession.resultEmpty = false;
              targetSession.hasConversation = true;
            }
          }
        } finally {
          stopHomeThinkingState(originSessionId);
          if (!transferredToVisual) {
            const targetSession = homeSessionById(originSessionId);
            if (targetSession && finishHomeSessionJob(targetSession, requestId, failed ? "failed" : "completed")) {
              if (targetSession.id === state.activeCustomerSessionId) {
                syncHomeSessionProcessingUi(targetSession);
                captureActiveCustomerSession(true);
              } else {
                notifyHomeSessionCompleted(targetSession, failed);
              }
            }
          }
          renderCustomerWorkbench();
        }
      };
      const originalCalculateQuote = window.calculateQuote;
      window.calculateQuote = function () {
        const created = originalCalculateQuote?.();
        if (created === false || isStaticFileMode) return;
        const latestQuote = (state.quoteHistory || [])[0] || {};
        const analysis = {
          product: val("quoteProduct") || "定制产品",
          size: val("quoteSize") || "",
          qty: val("quoteQty") || "",
          material: [val("quoteMaterial") || ""],
          process: [val("quoteProcess") || ""],
          delivery: val("quoteDelivery") || "",
          tax_shipping: `${val("quoteTaxMode") || "待确认"}，不含运`,
          address: val("quoteRegion") || "",
          sample_mode: val("quoteSampleMode") || "待确认",
          sample_required: val("quoteSampleMode") === "待确认" ? null : val("quoteSampleMode") !== "不需要打样",
          source_text: val("quoteAiSource") || val("customerText") || window.__latestAiSourceText || "",
          product_configuration: latestQuote.productConfiguration || quoteCatalogConfigurationSnapshot(),
          industry_category: latestQuote.industryCategory || currentIndustryCategory(),
          industry_strategy_id: latestQuote.industryStrategyId || latestQuote.formulaSnapshot?.strategyId || "",
          missing: [],
          risk: []
        };
        const quote = {
          template: val("quoteTemplate") || "",
          total: Number(latestQuote.total || 0),
          unit: Number(latestQuote.unit || 0),
          min_accept_total: Number(latestQuote.minAcceptTotal || 0),
          urgent_total: Number(latestQuote.urgentTotal || 0),
          estimated_cost: Number(latestQuote.estimatedCost || 0),
          estimated_profit: Number(latestQuote.estimatedProfit || 0),
          margin_rate: Number(latestQuote.marginRate || 0),
          quote_no: latestQuote.quoteNo || "",
          version: Number(latestQuote.version || 1),
          approval_status: latestQuote.approvalStatus || "可发送",
          approval_reason: latestQuote.approvalReason || "",
          approved_at: latestQuote.approvedAt || "",
          approved_by: latestQuote.approvedBy || "",
          approval_note: latestQuote.approvalNote || "",
          material_unit_cost: Number(latestQuote.materialUnitCost || 0),
          estimated_material_cost: Number(latestQuote.estimatedMaterialCost || 0),
          estimated_process_cost: Number(latestQuote.estimatedProcessCost || 0),
          estimated_other_cost: Number(latestQuote.estimatedOtherCost || 0),
          cost_mode: String(latestQuote.costMode || ""),
          material_cost_draft: latestQuote.materialCostDraft && typeof latestQuote.materialCostDraft === "object" ? cloneQuoteSnapshot(latestQuote.materialCostDraft) : null,
          safety_details: latestQuote.safetyDetails && typeof latestQuote.safetyDetails === "object" ? latestQuote.safetyDetails : {},
          cost_version_id: String(latestQuote.costVersionId || latestQuote.costSnapshot?.versionId || ""),
          cost_snapshot: latestQuote.costSnapshot && typeof latestQuote.costSnapshot === "object" ? cloneQuoteSnapshot(latestQuote.costSnapshot) : null,
          formula_snapshot: latestQuote.formulaSnapshot && typeof latestQuote.formulaSnapshot === "object" ? cloneQuoteSnapshot(latestQuote.formulaSnapshot) : null,
          cost_line_items: { schemaVersion:1, items:cloneQuoteSnapshot(latestQuote.costLineItems || []) },
          standard_unit: Number(latestQuote.standardUnit || latestQuote.unit || 0),
          customer_price_source: String(latestQuote.customerPriceSource || "standard_price"),
          customer_price_source_label: String(latestQuote.customerPriceSourceLabel || "系统标准报价"),
          industry_category: latestQuote.industryCategory || currentIndustryCategory(),
          industry_strategy_id: latestQuote.industryStrategyId || latestQuote.formulaSnapshot?.strategyId || "",
          product_configuration: latestQuote.productConfiguration || quoteCatalogConfigurationSnapshot(),
          customer_text: document.getElementById("quoteCustomerOutput")?.textContent || "",
          internal_text: document.getElementById("quoteInternalOutput")?.textContent || ""
        };
        const businessContext = quoteBusinessContext();
        api("/api/quote", {
          method: "POST",
          body: JSON.stringify({
            analysis,
            quote,
            customer: currentCustomerName(),
            case_source: businessContext.caseSource || "",
            followup_task_id: Number(businessContext.taskServerId || latestQuote.followupTaskId || 0)
          })
        }).then(data => {
          if (data.membership) syncFormalMembership(data.membership);
          if (data?.quote_id) {
            latestQuote.serverId = Number(data.quote_id);
            latestQuote.approvalStatus = data.approval_status || latestQuote.approvalStatus;
            latestQuote.approvalReason = data.approval_reason || latestQuote.approvalReason || "";
            const customerRenamedFromQuote = syncQuoteCustomerToActiveSession(latestQuote.customer);
            saveState();
            renderQuoteApprovalPanel(latestQuote);
            renderQuoteProfessionalRecord(latestQuote);
            renderQuoteHistory();
            if (data.followup_task) {
              mergeFormalTasks([data.followup_task]);
              latestQuote.followupTaskId = Number(data.followup_task.id || 0);
              latestQuote.caseSource = data.followup_task.source || latestQuote.caseSource || businessContext.caseSource || "";
              businessContext.taskServerId = latestQuote.followupTaskId;
              businessContext.caseSource = latestQuote.caseSource;
              const session = activeCustomerSession();
              if (session && (!businessContext.sessionId || session.id === businessContext.sessionId)) {
                session.followupTaskId = latestQuote.followupTaskId;
                session.chatActionStatus = "报价已关联到同一笔生意，下一步等待客户反馈";
              }
              saveState();
            }
            if (customerRenamedFromQuote || activeCustomerSession()?.renameFrom) {
              Promise.resolve(window.__selectHomeCustomerConnected?.()).catch(() => {});
            }
            renderFormalStatus(data.cloud?.ok ? "报价与跟单已同步云端" : "报价与跟单已保存到服务端");
            notify(data.task_created ? "报价已保存，并自动生成跟单提醒。" : "报价已更新，原跟单任务已同步刷新。" );
          }
        }).catch(err => renderFormalStatus("正式版接口提醒：" + err.message));
      };
      const originalCreateOrder = window.createOrder;
      window.createOrder = function () {
        originalCreateOrder?.();
        if (isStaticFileMode) return;
        syncAnalysis().then(data => {
          if (!data?.analysis) return null;
          return api("/api/tasks", { method: "POST", body: JSON.stringify({ analysis: data.analysis, customer: currentCustomerName() }) });
        }).then(data => {
          if (data?.task_id) renderFormalStatus("跟单已保存 · 后续会在跟单页查看");
        }).catch(err => renderFormalStatus("正式版接口提醒：" + err.message));
      };
      const originalCreateOrderFromQuote = window.createOrderFromQuote;
      window.createOrderFromQuote = function () {
        originalCreateOrderFromQuote?.();
        if (isStaticFileMode) return;
        loadFormalTasks().catch(err => renderFormalStatus("跟单读取提醒：" + err.message));
      };
      const originalUpdateHomeFileName = window.updateHomeFileName;
      function queueHomeFileUpload() {
        const sessionId = activeCustomerSession()?.id || "";
        const previousUpload = homeSessionUploadPromises.get(sessionId) || null;
        const queuedUpload = (async () => {
          if (previousUpload) await previousUpload.catch(() => []);
          return uploadFilesFromInput("homeFiles", "homeFileName");
        })();
        if (sessionId) homeSessionUploadPromises.set(sessionId, queuedUpload);
        if (state.activeCustomerSessionId === sessionId) latestUploadPromise = queuedUpload;
        const clearQueue = () => {
          if (homeSessionUploadPromises.get(sessionId) === queuedUpload) homeSessionUploadPromises.delete(sessionId);
          if (state.activeCustomerSessionId === sessionId && latestUploadPromise === queuedUpload) latestUploadPromise = null;
        };
        queuedUpload.then(clearQueue, clearQueue);
        return queuedUpload;
      }
      window.updateHomeFileName = function () {
        originalUpdateHomeFileName?.();
        if (window.__suppressHomeUploadSync) {
          window.__suppressHomeUploadSync = false;
          return;
        }
        if (isStaticFileMode) return;
        renderHomeRetainedFileContext();
        queueHomeFileUpload().catch(err => renderFormalStatus("文件同步提醒：" + err.message));
      };
      const originalUpdateVisualFileName = window.updateVisualFileName;
      window.updateVisualFileName = function () {
        originalUpdateVisualFileName?.();
        if (isStaticFileMode) return;
        latestVisualFileId = null;
        latestVisualUploadPromise = uploadFilesFromInput("visualFile", "visualFileName");
        latestVisualUploadPromise.then(ids => {
          latestVisualFileId = ids[ids.length - 1] || null;
          if (latestVisualFileId) renderFormalStatus("参考图已上传 · 生图时会尝试保持产品结构和Logo");
        }).catch(err => renderFormalStatus("文件同步提醒：" + err.message));
      };
      const originalUpdateDesignFileName = window.updateDesignFileName;
      window.updateDesignFileName = function () {
        originalUpdateDesignFileName?.();
        if (isStaticFileMode) return;
        uploadFilesFromInput("designFile", "designFileName").catch(err => renderFormalStatus("文件同步提醒：" + err.message));
      };
      const originalUpdateVectorFileName = window.updateVectorFileName;
      window.updateVectorFileName = function () {
        originalUpdateVectorFileName?.();
        if (isStaticFileMode) return;
        latestVectorUploadPromise = uploadFilesFromInput("vectorFile", "vectorFileName");
        latestVectorUploadPromise.then(ids => {
          latestVectorFileId = ids[ids.length - 1] || latestVectorFileId;
        }).catch(err => renderFormalStatus("文件同步提醒：" + err.message));
      };
      function startDesktopSingleWindowHeartbeat() {
        if (isStaticFileMode || location.hostname !== "127.0.0.1") return;
        const windowId = window.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`;
        const lockKey = "jianglai-desktop-window";
        const channel = "BroadcastChannel" in window ? new BroadcastChannel("jianglai-desktop-window") : null;
        let primary = true;
        try {
          const existing = JSON.parse(localStorage.getItem(lockKey) || "null");
          if (existing?.id && existing.id !== windowId && Date.now() - Number(existing.updatedAt || 0) < 6500) {
            primary = false;
            channel?.postMessage({ type: "focus", target: existing.id });
            window.setTimeout(() => window.close(), 120);
          } else {
            localStorage.setItem(lockKey, JSON.stringify({ id: windowId, updatedAt: Date.now() }));
          }
        } catch (_) {}
        channel?.addEventListener("message", event => {
          if (event.data?.type === "focus" && event.data.target === windowId) window.focus();
        });
        if (!primary) return;
        const heartbeat = async () => {
          try {
            localStorage.setItem(lockKey, JSON.stringify({ id: windowId, updatedAt: Date.now() }));
            const response = await fetch("/api/app/window/heartbeat", {
              method: "POST",
              headers: { "Content-Type": "application/json" },
              body: "{}",
              cache: "no-store"
            });
            const state = await response.json();
            if (state.focus_requested) window.focus();
          } catch (_) {}
        };
        heartbeat();
        window.setInterval(heartbeat, 1800);
        window.addEventListener("focus", heartbeat);
        window.addEventListener("pagehide", () => {
          try {
            const current = JSON.parse(localStorage.getItem(lockKey) || "null");
            if (current?.id === windowId) localStorage.removeItem(lockKey);
          } catch (_) {}
          navigator.sendBeacon?.("/api/app/window/closed", new Blob(["{}"], { type: "application/json" }));
          channel?.close();
        }, { once: true });
      }
      startDesktopSingleWindowHeartbeat();
      if (isStaticFileMode) {
        updateIntegrationButtons({ image_configured: false });
        renderFormalStatus("开发预览页 · 正在打开桌面正式版");
        window.setTimeout(leaveStaticPreview, 120);
      } else {
        importSharedIntake();
        ensureFormalLogin()
          .then(() => requireTemporaryPasswordChange() ? null : loadSignedInWorkspace())
          .catch(err => renderFormalStatus("正式版服务未连接：" + err.message));
      }
      window.activateLicense = activateLicense;
      window.openFormalOps = openFormalOps;
      window.closeFormalOps = closeFormalOps;
      window.runSystemCheck = runSystemCheck;
      window.refreshFormalStatus = refreshFormalStatus;
      window.downloadFormalBackup = downloadFormalBackup;
      window.__loadMembershipUsage = loadMembershipUsage;
      window.__requireServerFeature = requireServerFeatureConnected;
      window.generateRealVisual = generateRealVisualConnected;
      window.generateHomeVisualInline = generateHomeVisualInline;
      window.clearHomeGeneratedResult = clearHomeGeneratedResult;
      window.retryTemporaryImageGeneration = retryTemporaryImageGeneration;
      window.retryStaleImageSetupFromHistory = retryStaleImageSetupFromHistory;
      window.__hydrateHomeUserAttachmentThumbs = hydrateHomeUserAttachmentThumbs;
      window.__revokeHomeUserAttachmentBlobUrls = revokeHomeUserAttachmentBlobUrls;
      window.__bindCurrentTurnUserAttachments = bindCurrentTurnUserAttachments;
      window.generateDetailPageConnected = generateDetailPageConnected;
      window.generateDetailPackageConnected = generateDetailPackageConnected;
      window.downloadDetailPackageConnected = downloadDetailPackageConnected;
      window.downloadDetailPackageAsset = downloadDetailPackageAssetConnected;
      window.__clearHomeDetailPackageResult = clearHomeDetailPackageResult;
      window.__chargeLocalVectorResult = async payload => {
        await ensureFormalLogin();
        const data = await api("/api/vector/local-complete", {
          method: "POST",
          body: JSON.stringify(payload || {})
        });
        if (data.membership) syncFormalMembership(data.membership);
        return data;
      };
      window.__vectorCloudBridge = async (file, format = "svg") => {
        if (!(file instanceof File)) throw new Error("没有可转换的图片文件");
        await ensureFormalLogin();
        const uploaded = await api("/api/files", {
          method:"POST",
          body:JSON.stringify({ name:file.name || "矢量源图", mime_type:normalizedUploadMime(file), base64:await fileToBase64(file) })
        });
        const fileId = Number(uploaded?.file?.id || 0);
        if (!fileId) throw new Error("源图上传失败");
        const data = await api("/api/vector/cloud-convert", {
          method:"POST",
          body:JSON.stringify({ file_id:fileId, format, use:"专业矢量重建", consent:true })
        });
        if (data.membership) syncFormalMembership(data.membership);
        return data;
      };
      window.convertVectorDemo = convertVectorConnected;
      window.loadVectorJobs = loadVectorJobs;
      window.__loadVectorJobs = loadVectorJobs;
      window.loadVectorLearning = loadVectorLearning;
      window.clearVectorLearning = clearVectorLearning;
      window.claimVectorJob = claimVectorJob;
      window.completeVectorJob = completeVectorJob;
      window.downloadVectorJobFile = downloadVectorJobFile;
      window.__runPreflightCheck = runPreflightCheckConnected;
      window.__loadPreflightReports = loadPreflightReportsConnected;
      window.__decidePreflightReport = decidePreflightReportConnected;
      window.__recordPreflightCustomerConfirmation = recordPreflightCustomerConfirmationConnected;
      window.__activateMembershipCode = activateMembershipCodeValue;
      window.__isStaticFileMode = isStaticFileMode;
      window.__quoteAiEnsureFormalLogin = ensureFormalLogin;
      window.__paymentClientType = function () {
        const nativePlatform = String(window.__jianglaiNativePlatform || "").toLowerCase();
        if (nativePlatform === "android") return "android_app";
        if (nativePlatform === "ios") return "ios_app";
        return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent || "") ? "mobile_web" : "desktop";
      };
      window.__createBillingOrder = async (plan, method = "wechat") => {
        await ensureFormalLogin();
        const client_type = window.__paymentClientType();
        return api("/api/billing/order", {
          method: "POST",
          body: JSON.stringify({ plan, method, client_type })
        });
      };
      window.__loadBillingOrders = async () => {
        await ensureFormalLogin();
        return api("/api/billing/orders");
      };
      window.__loadBillingOrderStatus = async orderNo => {
        await ensureFormalLogin();
        return api(`/api/billing/order/status?order_no=${encodeURIComponent(orderNo || "")}`);
      };
      window.__deleteCustomerCloud = async customer => {
        await ensureFormalLogin();
        return api("/api/customers/delete", {
          method: "POST",
          body: JSON.stringify({ customer, confirm: true })
        });
      };
      window.__quoteAiUploadFiles = uploadFilesFromInput;
      window.__useHomeUploadForVisual = async () => {
        if (latestUploadPromise) await latestUploadPromise.catch(() => []);
        if ((!uploadedFileIds.length || window.__homeUploadFailures?.length) && document.getElementById("homeFiles")?.files?.length) {
          latestUploadPromise = uploadFilesFromInput("homeFiles", "homeFileName");
          await latestUploadPromise.catch(() => []);
        }
        const retainedFileIds = homeRetainedFileIds();
        latestVisualFileId = uploadedFileIds[uploadedFileIds.length - 1] || retainedFileIds[retainedFileIds.length - 1] || latestVisualFileId;
        const referenceNames = homeFileNames().length ? homeFileNames() : homeRetainedFileNames();
        if (latestVisualFileId && referenceNames.length) setTextIfExists("visualFileName", `已从询盘带入：${referenceNames.join("、")}`);
        return latestVisualFileId;
      };
      window.__quoteAiRequestAnalysis = requestAnalysis;
      window.__vectorLocalOcrBridge = async fileId => api("/api/vector/ocr", {
        method:"POST",
        body:JSON.stringify({ file_id:Number(fileId || 0) })
      });
      window.__vectorOcrBridge = async file => {
        if (!(file instanceof File)) throw new Error("没有可识别的图片文件");
        await ensureFormalLogin();
        const base64 = await fileToBase64(file);
        const uploaded = await api("/api/files", {
          method:"POST",
          body:JSON.stringify({ name:file.name || "文字识别图片", mime_type:normalizedUploadMime(file), base64 })
        });
        const fileId = Number(uploaded?.file?.id || 0);
        if (!fileId) throw new Error("临时图片上传失败");
        try {
          const local = await window.__vectorLocalOcrBridge(fileId).catch(() => null);
          if (local?.ok && String(local.text || "").trim()) {
            return { text:String(local.text).trim(), provider:local.provider || "macos_vision_local", intent:"local_ocr" };
          }
          const data = await api("/api/ai/analyze", {
            method:"POST",
            body:JSON.stringify({
              text:"只抄录图片中肉眼可见的文字，严格保持阅读顺序和原有换行。不要解释图片，不要加标题、序号、引号或提示语；无法辨认的单字用□代替。",
              customer:"矢量文字识别临时任务",
              file_ids:[fileId],
              retained_file_ids:[],
              use_memory:false,
              analysis_mode:"standard"
            })
          });
          const analysis = data?.analysis || {};
          const intent = String(analysis.intent || "").trim().toLowerCase();
          const provider = String(analysis.provider || "").trim().toLowerCase();
          if (["image_unavailable", "service_unavailable"].includes(intent)
              || ["image_unavailable", "service_unavailable"].includes(provider)) {
            throw new Error("图片文字识别服务暂时不可用，请稍后原地重试");
          }
          let text = String(analysis.direct_answer || analysis.customer_reply || analysis.image_summary || "").trim();
          text = text.replace(/^```(?:text)?\s*/i, "").replace(/\s*```$/i, "").trim();
          return { text, provider:analysis.provider || "", intent };
        } finally {
          await api("/api/files/remove", { method:"POST", body:JSON.stringify({ file_id:fileId }) }).catch(() => null);
        }
      };
      window.__mergeVectorTextLayouts = (localLayout, classifiedLayout) => {
        const normalizeLine = (line, coordinateSpace, source) => {
          const scale = 1000 / Math.max(1, Number(coordinateSpace || 1000));
          const x = Math.max(0, Math.min(1000, Number(line?.x || 0) * scale));
          const y = Math.max(0, Math.min(1000, Number(line?.y || 0) * scale));
          const width = Math.max(0, Math.min(1000 - x, Number(line?.width || 0) * scale));
          const height = Math.max(0, Math.min(1000 - y, Number(line?.height || 0) * scale));
          const confidence = Number(line?.confidence);
          return {
            text:String(line?.text || "").trim(),
            x, y, width, height,
            confidence:Number.isFinite(confidence) ? Math.max(0, Math.min(1, confidence)) : 0,
            replace_mode:String(line?.replace_mode || line?.replaceMode || "").trim().toLowerCase(),
            source
          };
        };
        const classified = (Array.isArray(classifiedLayout?.lines) ? classifiedLayout.lines : [])
          .map(line => normalizeLine(line, classifiedLayout?.coordinate_space, "classified"))
          .filter(line => line.text && line.width >= 1 && line.height >= 1);
        const local = (Array.isArray(localLayout?.lines) ? localLayout.lines : [])
          .map(line => normalizeLine(line, localLayout?.coordinate_space, "local"))
          .filter(line => line.text && line.width >= 1 && line.height >= 1 && line.confidence >= .45);
        const overlaps = (first, second) => {
          const left = Math.max(first.x, second.x);
          const top = Math.max(first.y, second.y);
          const right = Math.min(first.x + first.width, second.x + second.width);
          const bottom = Math.min(first.y + first.height, second.y + second.height);
          const intersection = Math.max(0, right - left) * Math.max(0, bottom - top);
          const smaller = Math.max(1, Math.min(first.width * first.height, second.width * second.height));
          const centerDistance = Math.hypot(
            first.x + first.width / 2 - second.x - second.width / 2,
            first.y + first.height / 2 - second.y - second.height / 2
          );
          return intersection / smaller >= .32
            || centerDistance <= Math.max(first.height, second.height) * .72;
        };
        const merged = [...classified];
        for (const line of local) {
          const sameText = merged.some(existing => existing.text.replace(/\s+/g, "").toLowerCase()
            === line.text.replace(/\s+/g, "").toLowerCase());
          if (!sameText && !merged.some(existing => overlaps(existing, line))) merged.push(line);
        }
        merged.sort((first, second) => first.y - second.y || first.x - second.x);
        return {
          ...classifiedLayout,
          coordinate_space:1000,
          lines:merged.slice(0, 200),
          provider:[classifiedLayout?.provider, localLayout?.provider].filter(Boolean).join("+"),
          classified_line_count:classified.length,
          local_line_count:local.length
        };
      };
      window.__vectorTextLayoutBridge = async file => {
        if (!(file instanceof File)) throw new Error("没有可识别的图片文件");
        await ensureFormalLogin();
        const base64 = await fileToBase64(file);
        const uploaded = await api("/api/files", {
          method:"POST",
          body:JSON.stringify({ name:file.name || "自动文字替换图片", mime_type:normalizedUploadMime(file), base64 })
        });
        const fileId = Number(uploaded?.file?.id || 0);
        if (!fileId) throw new Error("临时图片上传失败");
        let localLayout = null;
        try {
          const local = await window.__vectorLocalOcrBridge(fileId).catch(() => null);
          if (local?.ok && Array.isArray(local.lines) && local.lines.length) {
            localLayout = local;
          }
          const data = await api("/api/ai/analyze", {
            method:"POST",
            body:JSON.stringify({
              text:"识别图片中全部肉眼可见的文字，并逐行给出位置，同时区分普通排版文字、Logo品牌字与其他艺术字。只返回严格 JSON，不要解释、不要 Markdown。格式必须是：{\"coordinate_space\":1000,\"lines\":[{\"text\":\"每一行原文\",\"x\":左边坐标,\"y\":顶部坐标,\"width\":宽度,\"height\":高度,\"confidence\":0到1,\"replace_mode\":\"standard、logo或artwork\"}]}。所有坐标都以整张图片左上角为原点并归一化到 0-1000；每个视觉文字行单独一项，严格保留原文，不能把多行合并。Logo和品牌名标记 logo；手写体、书法体、花体、竖排字、旋转字、弧形字、与插画融合的字标记 artwork；普通说明、尺寸、日期、批次、成分和生产标记标记 standard；不要把刀模线、图标、条码线、回收标志或装饰图形识别成文字。",
              customer:"矢量文字自动替换临时任务",
              file_ids:[fileId],
              retained_file_ids:[],
              use_memory:false,
              analysis_mode:"standard"
            })
          });
          const analysis = data?.analysis || {};
          const intent = String(analysis.intent || "").trim().toLowerCase();
          const provider = String(analysis.provider || "").trim().toLowerCase();
          if (["image_unavailable", "service_unavailable"].includes(intent)
              || ["image_unavailable", "service_unavailable"].includes(provider)) {
            throw new Error("图片文字识别服务暂时不可用，请稍后原地重试");
          }
          let raw = String(analysis.direct_answer || analysis.customer_reply || analysis.image_summary || "").trim();
          raw = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
          const first = raw.indexOf("{");
          const last = raw.lastIndexOf("}");
          if (first < 0 || last <= first) throw new Error("文字位置识别结果格式不完整，请原地重试");
          const layout = JSON.parse(raw.slice(first, last + 1));
          if (!Array.isArray(layout?.lines) || !layout.lines.length) throw new Error("图片中没有识别到可自动替换的文字");
          return window.__mergeVectorTextLayouts(localLayout, {
            ...layout,
            provider:analysis.provider || "",
            intent
          });
        } catch (error) {
          if (localLayout?.lines?.length) {
            return {
              ...localLayout,
              provider:localLayout.provider || "macos_vision_local",
              intent:"local_ocr_fallback",
              classified_line_count:0,
              local_line_count:localLayout.lines.length
            };
          }
          throw error;
        } finally {
          await api("/api/files/remove", { method:"POST", body:JSON.stringify({ file_id:fileId }) }).catch(() => null);
        }
      };
      window.__vectorFontOutlineBridge = async (text, fontFamily) => {
        await ensureFormalLogin();
        const data = await api("/api/vector/font-outline", {
          method:"POST",
          body:JSON.stringify({ text:String(text || ""), font_family:String(fontFamily || "") })
        });
        if (!data?.outline?.d || !data?.outline?.bbox) throw new Error("字体没有返回可用的矢量轮廓");
        return data.outline;
      };
      window.__vectorFontCatalogBridge = async () => {
        await ensureFormalLogin();
        const data = await api("/api/vector/fonts", { method:"POST", body:"{}" });
        return Array.isArray(data?.fonts) ? data.fonts : [];
      };
      window.retryHomeAnalysisInPlace = retryHomeAnalysisInPlace;
      window.__selectHomeCustomerConnected = selectHomeCustomerConnected;
      window.__saveCustomerProfileCloud = saveCustomerProfileCloud;
      window.__recordCustomerDesignFeedback = recordCustomerDesignFeedback;
      window.__clearCustomerDesignLearning = clearCustomerDesignLearningConnected;
      window.__mergeCloudCustomerProfiles = mergeCloudCustomerProfiles;
      window.__saveSettingsCloud = async (settings, options = {}) => {
        try {
          await ensureFormalLogin();
          return await api("/api/settings", { method:"PUT", body:JSON.stringify(settings || state.settings) });
        } catch (err) {
          renderFormalStatus(options.throwOnError
            ? `价格表未发布：${err.message}`
            : `价格表已保存在本机，云端同步提醒：${err.message}`);
          if (options.throwOnError) throw err;
          return null;
        }
      };
      window.__loadCustomerProfileMemory = loadCustomerProfileMemory;
      window.__loadCloudCustomers = loadCloudCustomers;
      window.__loadFormalQuotes = loadFormalQuotes;
      window.__loadFormalTasks = loadFormalTasks;
      window.__moveFormalTask = moveFormalTask;
      window.__trashFormalTask = trashFormalTask;
      window.__restoreFormalTask = restoreFormalTask;
      window.__updateFormalTaskStep = updateFormalTaskStep;
      window.__updateFormalTaskPurchase = updateFormalTaskPurchase;
      window.__updateFormalTaskOperations = updateFormalTaskOperations;
      window.__reverseFormalTaskPayment = reverseFormalTaskPayment;
      window.__loadTaskConfirmations = loadTaskConfirmations;
      window.__createTaskConfirmation = createTaskConfirmation;
      window.__saveFormalTask = saveFormalTask;
      window.__setFormalTaskOutcome = setFormalTaskOutcome;
      window.__saveQuoteOutcomeCloud = saveQuoteOutcomeCloud;
      window.__approveQuoteCloud = approveQuoteCloud;
      window.__importPriceTableFileConnected = importPriceTableFileConnected;
      window.removeHomeFile = removeHomeFileConnected;
      window.removeAllHomeFiles = async () => {
        await removeAllHomeFilesConnected();
        forgetHomeConversationFiles();
      };
      window.__resetConnectedAnalysisCache = () => {
        lastAnalysisData = null;
        lastAnalysisSignature = "";
        latestAnalysisPromise = null;
        latestAnalysisSignature = "";
        homeSessionAnalysisCache.clear();
        homeSessionAnalysisRequests.clear();
        uploadedFileIds = [];
        uploadedHomeFileRecords = [];
        window.__homeUploadFailures = [];
        window.__activeCustomerMemory = null;
      };
      window.__jianglaiUsabilityBridge = {
        data:() => ({ tasks:businessTasks(), customers:businessProfiles(), quotes:businessQuotes() }),
        displayTaskId,
        taskPhase,
        quoteKey:quoteRecordKey,
        openTask:openTaskDetail,
        openQuote:id => viewSavedQuote(encodeURIComponent(id)),
        openCustomer(id) {
          showPage("客户档案");
          setIfExists("customerProfileSearch", id);
          renderCustomerProfiles();
          document.getElementById("customerProfileSearch")?.focus();
        },
        openPage(id) {
          if (id === "外协协作") location.href = "supplier_collaboration.html";
          else if (id === "报价系统") openQuoteWorkspace();
          else showPage(id);
        },
        quickAction(action) {
          if (action === "inquiry") {
            showPage("图片制作");
            createCustomerSession();
          } else if (action === "quote") {
            openQuoteWorkspace();
            window.setTimeout(() => document.getElementById("quoteCustomerName")?.focus(), 40);
          } else if (action === "order") {
            showPage("跟单工单");
            window.setTimeout(() => document.getElementById("manualTaskCustomer")?.focus(), 40);
          } else if (action === "outsource") {
            location.href = "supplier_collaboration.html?create=order";
          }
        },
        openOrderView(taskId, mode) {
          if (mode === "production") openTaskInProductionCenter(taskId);
          else if (mode === "outsource") openTaskOutsourceCenter(taskId);
          else openTaskInOrderTracking(taskId);
        },
        getMode() {
          return state.settings?.usabilityMode === "professional" ? "professional" : "basic";
        },
        setMode(mode) {
          state.settings.usabilityMode = mode === "professional" ? "professional" : "basic";
          saveState();
        }
      };
      const requestedPage = new URLSearchParams(window.location.search).get("page") || "";
      const requestedPageMap = {
        home: "工作台",
        inquiries: "询盘处理",
        quotes: "报价系统",
        orders: "跟单工单",
        images: "图片制作",
        production: "生产中心",
        reports: "经营报表",
        more: "更多",
        reminders: "提醒中心"
      };
      if (requestedPageMap[requestedPage]) {
        window.setTimeout(() => showPage(requestedPageMap[requestedPage]), 300);
      }
      setTimeout(() => {
        const badge = document.getElementById("memberBadge");
        if (badge && !badge.dataset.licenseReady) {
          badge.dataset.licenseReady = "1";
          if (!badge.title) badge.title = "点击查看套餐、到期时间和剩余AI点数";
          badge.style.cursor = "pointer";
          badge.addEventListener("click", () => showPage("账号套餐"));
        }
      }, 500);
    })();
