/* ============================================================
 * AI\u7ed8\u5320 v3 - Photoshop \u9762\u677f\uff08UXP \u4e0e CEP \u5171\u7528\uff09
 * \u67b6\u6784: \u9762\u677f --HTTPS--> \u4e91\u7aef(guanli.aichajian.qiyefu.top) --\u4ee3\u7406--> \u4e2d\u8f6c\u7ad9
 * \u5bc6\u94a5\u767b\u5f55\uff1b\u6253\u5f00 PS \u5373\u7528\uff0c\u5173 PS \u5373\u9000\u3002
 * \u517c\u5bb9\u6027: ES5\uff08CEP6/Chromium41 \u53ef\u8dd1\uff09\uff0c\u4e0d\u7528\u7bad\u5934\u51fd\u6570/let/const/\u89e3\u6784\u3002
 * ============================================================ */
"use strict";

/* ---------------- \u5e38\u91cf\u4e0e\u72b6\u6001 ---------------- */
var SERVER = "https://guanli.aichajian.qiyefu.top";
var PANEL_VERSION = "3.4.8";
(function () {                       // ?server= \u8986\u76d6\uff08\u6d4b\u8bd5\u7528\uff09
  var m = /(?:\?|&)_?server=([^&]+)/.exec(location.search || "");
  if (m) { try { SERVER = decodeURIComponent(m[1]); } catch (e) {} }
})();

var S = {
  key: "",            // \u767b\u5f55\u5bc6\u94a5
  keyInfo: null,      // {note, quota_total, quota_used, remaining, expires_at}
  tools: [],          // \u4e91\u7aef\u5de5\u5177
  toolId: "",         // \u5f53\u524d\u5de5\u5177 id
  images: [],         // \u53c2\u8003\u56fe [{b64, format, bounds}]
  taskId: null,
  pollTimer: null,
  busy: false,
  view: "main",
  configVersion: -1,
  configTimer: null,
  updateInfo: null,
  updateRequested: false,
};

var RATIOS = [
  { v: "auto", label: "\u667a\u80fd" },
  { v: "21:9", label: "21:9", w: 16, h: 7 },
  { v: "16:9", label: "16:9", w: 16, h: 9 },
  { v: "3:2",  label: "3:2",  w: 15, h: 10 },
  { v: "4:3",  label: "4:3",  w: 13, h: 10 },
  { v: "1:1",  label: "1:1",  w: 11, h: 11 },
  { v: "3:4",  label: "3:4",  w: 10, h: 13 },
  { v: "2:3",  label: "2:3",  w: 9,  h: 13 },
  { v: "9:16", label: "9:16", w: 7,  h: 12 },
];

var $ = function (id) { return document.getElementById(id); };
function esc(s) {
  return String(s == null ? "" : s).replace(/&/g, "&amp;")
    .replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

function toast(msg, ms) {
  var t = $("toast");
  t.textContent = msg;
  t.classList.add("show");
  clearTimeout(t._tm);
  t._tm = setTimeout(function () { t.classList.remove("show"); }, ms || 2600);
}

function compareVersion(a, b) {
  var aa = String(a || "0").split("."), bb = String(b || "0").split(".");
  var n = Math.max(aa.length, bb.length);
  for (var i = 0; i < n; i++) {
    var av = parseInt(aa[i] || "0", 10) || 0, bv = parseInt(bb[i] || "0", 10) || 0;
    if (av > bv) return 1;
    if (av < bv) return -1;
  }
  return 0;
}

/* ---------------- \u672c\u5730\u5b58\u50a8 ---------------- */
var LS = {
  get: function (k, dv) {
    try { var v = localStorage.getItem(k); return v === null ? dv : v; }
    catch (e) { return dv; }
  },
  set: function (k, v) { try { localStorage.setItem(k, v); } catch (e) {} },
  del: function (k) { try { localStorage.removeItem(k); } catch (e) {} },
};

/* ---------------- HTTP\uff08UXP: fetch / CEP: XHR\uff09 ---------------- */
function httpPost(path, body) {
  var url = SERVER + path;
  var payload = JSON.stringify(body || {});
  if (typeof fetch === "function" && !window.__adobe_cep__) {
    return fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: payload,
    }).then(function (r) {
      return r.json().catch(function () { return { ok: false, msg: "\u54cd\u5e94\u5f02\u5e38 HTTP " + r.status }; });
    });
  }
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.open("POST", url, true);
    if (xhr.overrideMimeType) xhr.overrideMimeType("application/json; charset=utf-8");
    xhr.setRequestHeader("Content-Type", "application/json");
    xhr.setRequestHeader("Accept", "application/json");
    xhr.onload = function () {
      try { resolve(JSON.parse(xhr.responseText)); }
      catch (e) { resolve({ ok: false, msg: "\u54cd\u5e94\u5f02\u5e38 HTTP " + xhr.status }); }
    };
    xhr.onerror = function () { resolve({ ok: false, msg: "\u7f51\u7edc\u9519\u8bef\uff0c\u65e0\u6cd5\u8fde\u63a5\u670d\u52a1\u5668" }); };
    xhr.send(payload);
  });
}

/* ============================================================
 * \u5e73\u53f0\u9002\u914d\u5c42
 * ============================================================ */
var IS_UXP = (typeof require === "function") && !window.__adobe_cep__;
var PS = null, UXP_STORAGE = null, UXP_FMT = null, UXP_SHELL = null;
if (IS_UXP) {
  PS = require("photoshop");
  var uxpMod = require("uxp");
  UXP_STORAGE = uxpMod.storage.localFileSystem;
  UXP_FMT = uxpMod.storage.formats;
  UXP_SHELL = uxpMod.shell;
}

var Platform = {

  /* ---- \u4ece\u5f53\u524d\u56fe\u5c42\u6293\u56fe \u2192 {b64, format} ---- */
  captureLayer: function () {
    if (IS_UXP) return UxpBridge.capture("layer");
    return CepBridge.capture("layer");
  },

  /* ---- \u4ece\u9009\u533a\u6293\u56fe \u2192 {b64, format, bounds:[l,t,r,b]} ---- */
  captureSelection: function () {
    if (IS_UXP) return UxpBridge.capture("selection");
    return CepBridge.capture("selection");
  },

  /* ---- \u5f53\u524d\u9009\u533a bounds\uff1b\u65e0\u9009\u533a\u8fd4\u56de null ---- */
  selectionBounds: function () {
    if (IS_UXP) return UxpBridge.selectionBounds();
    return CepBridge.selectionBounds();
  },

  /* ---- \u672c\u5730\u6587\u4ef6\u591a\u9009 \u2192 [{b64, format}] ---- */
  pickLocal: function () {
    if (IS_UXP) return UxpBridge.pickLocal();
    return CepBridge.pickLocal();
  },

  /* ---- \u63d2\u5165\u751f\u6210\u56fe\uff08bounds \u5b58\u5728\u65f6\u7f29\u653e\u8d34\u5408\u5230\u9009\u533a\uff09 ---- */
  insertImage: function (b64, fmt, bounds) {
    if (IS_UXP) return UxpBridge.insert(b64, fmt, bounds);
    return CepBridge.insert(b64, fmt, bounds);
  },

  /* ---- \u542f\u52a8\u5b89\u88c5\u5668\uff1a\u5148\u4e0b\u8f7d\uff0c\u5168\u90e8 Photoshop \u9000\u51fa\u540e\u8986\u76d6 ---- */
  launchUpdater: function (version) {
    var uri = "huicraft-update://install?version=" + encodeURIComponent(version || "");
    if (IS_UXP && UXP_SHELL && UXP_SHELL.openPath && UXP_STORAGE) {
      return UXP_STORAGE.getPluginFolder().then(function (folder) {
        return folder.getEntry("launch-update.vbs");
      }).then(function (launcher) {
        var nativePath = launcher.nativePath || UXP_STORAGE.getNativePath(launcher);
        return UXP_SHELL.openPath(nativePath, "\u542f\u52a8 AI\u7ed8\u5320\u66f4\u65b0\u5668\uff1b\u66f4\u65b0\u5c06\u5728 Photoshop \u9000\u51fa\u540e\u81ea\u52a8\u5b89\u88c5\u3002");
      }).then(function (r) {
        if (r) throw new Error(String(r));
        return true;
      }).catch(function (openPathError) {
        if (!UXP_SHELL.openExternal) throw openPathError;
        return UXP_SHELL.openExternal(uri, "\u542f\u52a8 AI\u7ed8\u5320\u66f4\u65b0\u5668\uff1b\u66f4\u65b0\u5c06\u5728 Photoshop \u9000\u51fa\u540e\u81ea\u52a8\u5b89\u88c5\u3002").then(function (r) {
          if (r) throw new Error(String(r));
          return true;
        });
      });
    }
    return new Promise(function (resolve, reject) {
      try {
        if (window.cep && window.cep.process && window.cep.process.createProcess) {
          var userData = String(window.__adobe_cep__.getSystemPath("userData") || "").replace(/\\/g, "/");
          var userRoot = userData.replace(/\/AppData\/Roaming(?:\/.*)?$/i, "");
          var scriptPath = userRoot + "/AppData/Local/HuiCraft/update_agent.ps1";
          var powershellPath = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
          var started = window.cep.process.createProcess(
            powershellPath, "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden",
            "-File", scriptPath, "-Interactive"
          );
          if (started && Number(started.err || 0) === 0) { resolve(true); return; }
        }
        if (window.cep && window.cep.util && window.cep.util.openURLInDefaultBrowser) {
          var result = window.cep.util.openURLInDefaultBrowser(uri);
          if (!result || Number(result.err || 0) === 0) { resolve(true); return; }
        }
        reject(new Error("\u672c\u673a\u66f4\u65b0\u5668\u542f\u52a8\u5931\u8d25"));
      } catch (e) { reject(e); }
    });
  },

  /* ---- \u7f29\u7565\u56fe\uff08canvas \u53ef\u7528\u65f6\uff0c\u5f02\u6b65\u56de\u8c03\uff09 ---- */
  makeThumb: function (b64, fmt, cb) {
    try {
      var img = new Image();
      img.onload = function () {
        var out = null;
        try {
          var c = document.createElement("canvas");
          var w = 60, h = Math.max(1, Math.round(60 * img.height / img.width));
          c.width = w; c.height = h;
          var ctx = c.getContext("2d");
          ctx.drawImage(img, 0, 0, w, h);
          out = c.toDataURL("image/jpeg", 0.72);
        } catch (e) { out = null; }
        cb(out);
      };
      img.onerror = function () { cb(null); };
      img.src = "data:image/" + (fmt || "png") + ";base64," + b64;
    } catch (e) { cb(null); }
  },
};

/* ============================================================
 * UXP \u6865
 * ============================================================ */
var UxpBridge = {
  num: function (v) {
    if (v == null) return NaN;
    if (typeof v === "number") return v;
    if (typeof v._value === "number") return v._value;
    if (typeof v.value === "number") return v.value;
    try { if (v.as) return v.as("px"); } catch (e) {}
    return Number(v);
  },

  cleanBounds: function (b) {
    if (!b) return null;
    var out = {
      left: UxpBridge.num(b.left), top: UxpBridge.num(b.top),
      right: UxpBridge.num(b.right), bottom: UxpBridge.num(b.bottom),
    };
    if (!isFinite(out.left) || !isFinite(out.top) || !isFinite(out.right) ||
        !isFinite(out.bottom) || out.right <= out.left || out.bottom <= out.top) return null;
    return out;
  },

  selectionBounds: function () {
    var doc = PS.app.activeDocument;
    if (!doc) return Promise.resolve(null);
    try {
      var direct = UxpBridge.cleanBounds(doc.selection && doc.selection.bounds);
      if (direct) return Promise.resolve(direct);
    } catch (e) {}
    return PS.action.batchPlay([{
      _obj: "get",
      _target: [{ _property: "selection" }, { _ref: "document", _id: doc._id || doc.id }],
      _options: { dialogOptions: "dontDisplay" },
    }], {}).then(function (items) {
      var d = items && items[0] ? (items[0].selection || items[0]) : null;
      return UxpBridge.cleanBounds(d);
    }).catch(function () { return null; });
  },

  capture: function (mode) {
    if (!PS.app.activeDocument) return Promise.reject(new Error("\u8bf7\u5148\u6253\u5f00\u4e00\u4e2a\u6587\u6863"));
    if (!PS.imaging || !PS.imaging.getPixels || !PS.imaging.encodeImageData) {
      return Promise.reject(new Error("\u5f53\u524d Photoshop \u4e0d\u652f\u6301\u76f4\u63a5\u6293\u56fe\uff0c\u8bf7\u5347\u7ea7 Photoshop \u6216\u4f7f\u7528\u672c\u5730\u6dfb\u52a0"));
    }
    return PS.core.executeAsModal(function () {
      var doc = PS.app.activeDocument;
      var layer = doc.activeLayers && doc.activeLayers[0];
      var boundsPromise = mode === "selection" ? UxpBridge.selectionBounds() : Promise.resolve(null);
      return boundsPromise.then(function (bounds) {
        if (mode === "selection" && !bounds) throw new Error("\u8bf7\u5148\u7528\u9009\u6846\u5de5\u5177\u9009\u62e9\u533a\u57df");
        if (mode !== "selection" && !layer) throw new Error("\u8bf7\u9009\u4e2d\u4e00\u4e2a\u56fe\u5c42");
        var opts = {
          documentID: doc._id || doc.id,
          colorSpace: "RGB",
          componentSize: 8,
          applyAlpha: true,
        };
        if (mode === "selection") opts.sourceBounds = bounds;
        else {
          opts.layerID = layer._id || layer.id;
          var layerBounds = UxpBridge.cleanBounds(layer.boundsNoEffects || layer.bounds);
          if (layerBounds) opts.sourceBounds = layerBounds;
        }
        function encodeResult(result, resultBounds) {
          if (!result || !result.imageData) throw new Error("Photoshop \u672a\u8fd4\u56de\u56fe\u50cf\u50cf\u7d20");
          return PS.imaging.encodeImageData({ imageData: result.imageData, base64: true })
            .then(function (enc) {
              var w = result.imageData.width, h = result.imageData.height;
              try { result.imageData.dispose(); } catch (e) {}
              var data = (enc && enc.base64 !== undefined) ? enc.base64 : enc;
              data = String(data || "").replace(/^data:image\/[a-z0-9.+-]+;base64,/i, "");
              if (!data) throw new Error("Photoshop \u8fd4\u56de\u4e86\u7a7a\u56fe\u50cf");
              return {
                b64: data, format: "jpeg", bounds: resultBounds || null, width: w, height: h,
              };
            }, function (err) {
              try { result.imageData.dispose(); } catch (e) {}
              throw err;
            });
        }
        function readPixels(readOpts, resultBounds) {
          return PS.imaging.getPixels(readOpts).then(function (result) {
            return encodeResult(result, resultBounds);
          });
        }
        if (mode === "selection") return readPixels(opts, bounds);

        /* Adjustment layers, some smart objects and groups may have no direct
         * pixel buffer. In that case capture the visible composite within the
         * selected layer bounds (or the whole canvas) so the command still works. */
        return readPixels(opts, null).catch(function (layerError) {
          var composite = {
            documentID: doc._id || doc.id,
            colorSpace: "RGB",
            componentSize: 8,
            applyAlpha: true,
          };
          var fallbackBounds = UxpBridge.cleanBounds(layer.boundsNoEffects || layer.bounds);
          if (fallbackBounds) composite.sourceBounds = fallbackBounds;
          return readPixels(composite, null).catch(function () { throw layerError; });
        });
      });
    }, { commandName: mode === "selection" ? "AI\u7ed8\u5320\u8bfb\u53d6\u9009\u533a" : "AI\u7ed8\u5320\u8bfb\u53d6\u56fe\u5c42" })
      .catch(function (e) { throw new Error((mode === "selection" ? "\u9009\u533a\u6293\u53d6\u5931\u8d25\uff1a" : "\u56fe\u5c42\u6293\u53d6\u5931\u8d25\uff1a") + (e.message || "\u672a\u77e5\u9519\u8bef")); });
  },

  pickLocal: function () {
    return UXP_STORAGE.getFileForOpening({
      allowMultiple: true,
      types: ["png", "jpg", "jpeg", "webp"],
    }).then(function (entries) {
      if (!entries) return [];
      if (!Array.isArray(entries)) entries = [entries];
      var ps = entries.slice(0, 5).map(function (en) {
        return en.read({ format: UXP_FMT.binary }).then(function (buf) {
          var n = String(en.name || "").toLowerCase();
          var fmt = /\.jpe?g$/.test(n) ? "jpeg" : /\.webp$/.test(n) ? "webp" : "png";
          return { b64: bytesToB64(new Uint8Array(buf)), format: fmt };
        });
      });
      return Promise.all(ps);
    });
  },

  insert: function (b64, fmt, bounds) {
    var bytes = b64ToBytes(b64);
    var ext = fmt === "jpeg" || fmt === "jpg" ? "jpg" : fmt === "webp" ? "webp" : "png";
    return UXP_STORAGE.getTemporaryFolder().then(function (folder) {
      return folder.createFile("huijiang-result-" + Date.now() + "." + ext, { overwrite: true });
    }).then(function (file) {
      return file.write(bytes.buffer, { format: UXP_FMT.binary }).then(function () { return file; });
    }).then(function (file) {
      return PS.core.executeAsModal(function () {
        if (!PS.app.activeDocument) return PS.app.open(file);
        var token = UXP_STORAGE.createSessionToken(file);
        return PS.action.batchPlay([{
          _obj: "placeEvent",
          null: { _path: token, _kind: "local" },
          freeTransformCenterState: { _enum: "quadCenterState", _value: "QCSAverage" },
          _options: { dialogOptions: "dontDisplay" },
        }], {}).then(function () { return UxpBridge.fitPlacedLayer(bounds); });
      }, { commandName: "AI\u7ed8\u5320\u63d2\u5165\u751f\u6210\u56fe" });
    });
  },

  fitPlacedLayer: function (bounds) {
    var doc = PS.app.activeDocument, layer = doc.activeLayers[0];
    try { layer.name = "AI\u7ed8\u5320 " + new Date().toLocaleTimeString(); } catch (e) {}
    if (!bounds) return Promise.resolve(null);
    var b = UxpBridge.cleanBounds(layer.boundsNoEffects || layer.bounds);
    if (!b) return Promise.reject(new Error("\u65e0\u6cd5\u8bfb\u53d6\u751f\u6210\u56fe\u5c3a\u5bf8"));
    var w = b.right - b.left, h = b.bottom - b.top;
    var tw = bounds.right - bounds.left, th = bounds.bottom - bounds.top;
    var scale = Math.max(tw / w, th / h) * 100;
    return layer.scale(scale, scale).then(function () {
      var nb = UxpBridge.cleanBounds(layer.boundsNoEffects || layer.bounds);
      var dx = (bounds.left + bounds.right - nb.left - nb.right) / 2;
      var dy = (bounds.top + bounds.bottom - nb.top - nb.bottom) / 2;
      return layer.translate(dx, dy);
    }).then(function () {
      return PS.action.batchPlay([{
        _obj: "set",
        _target: [{ _ref: "channel", _property: "selection" }],
        to: { _obj: "rectangle",
          top: { _unit: "pixelsUnit", _value: bounds.top },
          left: { _unit: "pixelsUnit", _value: bounds.left },
          bottom: { _unit: "pixelsUnit", _value: bounds.bottom },
          right: { _unit: "pixelsUnit", _value: bounds.right } },
        _options: { dialogOptions: "dontDisplay" },
      }, {
        _obj: "make", new: { _class: "channel" },
        at: { _ref: "channel", _enum: "channel", _value: "mask" },
        using: { _enum: "userMaskEnabled", _value: "revealSelection" },
        _options: { dialogOptions: "dontDisplay" },
      }], {});
    });
  },
};

/* ============================================================
 * CEP \u6865\uff08ExtendScript evalScript\uff09
 * ============================================================ */
var CepBridge = {
  eval: function (code) {
    return new Promise(function (resolve) {
      try {
        window.__adobe_cep__.evalScript(code, function (r) {
          resolve(r);
        });
      } catch (e) { resolve("ERR:" + e); }
    });
  },

  capture: function (mode) {
    var self = this;
    return this.eval("hjCapture('" + mode + "')").then(function (raw) {
      if (!raw || raw.indexOf("ERR:") === 0) {
        throw new Error((raw || "\u6293\u53d6\u5931\u8d25").replace(/^ERR:/, ""));
      }
      var info = null;
      try { info = JSON.parse(raw); } catch (e) { info = { path: raw }; }
      var readResult = null;
      try {
        var encoding = window.cep && window.cep.encoding
          ? window.cep.encoding.Base64 : "Base64";
        readResult = window.cep.fs.readFile(info.path, encoding);
      } finally {
        try { window.cep.fs.unlink(info.path); } catch (e2) {}
      }
      var data = readResult && readResult.data;
      if (!readResult || Number(readResult.err || 0) !== 0 || !data) {
        var code = readResult && readResult.err !== undefined ? "\uff08\u9519\u8bef\u7801 " + readResult.err + "\uff09" : "";
        throw new Error("\u8bfb\u53d6\u622a\u56fe\u5931\u8d25" + code);
      }
      var bounds = info.bounds && info.bounds.length === 4
        ? { left: info.bounds[0], top: info.bounds[1], right: info.bounds[2], bottom: info.bounds[3] }
        : null;
      return { b64: data, format: "png", bounds: bounds,
        width: Number(info.width || 0), height: Number(info.height || 0) };
    });
  },

  selectionBounds: function () {
    return this.eval("hjSelectionBounds()").then(function (raw) {
      if (!raw || raw.indexOf("ERR:") === 0) return null;
      try {
        var b = JSON.parse(raw);
        return b && b.length === 4 ? { left:b[0], top:b[1], right:b[2], bottom:b[3] } : null;
      } catch (e) { return null; }
    });
  },

  pickLocal: function () {
    return new Promise(function (resolve) {
      var inp = document.createElement("input");
      inp.type = "file";
      inp.accept = "image/png,image/jpeg,image/webp";
      inp.multiple = true;
      inp.style.display = "none";
      inp.onchange = function () {
        var files = Array.prototype.slice.call(inp.files || [], 0, 5);
        if (!files.length) { resolve([]); return; }
        var out = [], left = files.length;
        files.forEach(function (f) {
          var rd = new FileReader();
          rd.onload = function () {
            var s = String(rd.result || "");
            var i = s.indexOf("base64,");
            var fmt = "png";
            if (s.indexOf("image/jpeg") >= 0) fmt = "jpeg";
            else if (s.indexOf("image/webp") >= 0) fmt = "webp";
            out.push({ b64: i >= 0 ? s.slice(i + 7) : s, format: fmt });
          };
          rd.onloadend = function () { if (--left === 0) resolve(out); };
          rd.onerror = function () { if (--left === 0) resolve(out); };
          rd.readAsDataURL(f);
        });
        document.body.removeChild(inp);
      };
      document.body.appendChild(inp);
      inp.click();
    });
  },

  insert: function (b64, fmt, bounds) {
    // \u6309\u5b9e\u9645\u683c\u5f0f\u5199\u5230 CEP \u53ef\u5199\u76ee\u5f55\uff0c\u518d\u4ea4\u7ed9 ExtendScript place
    var base = "";
    try {
      base = (window.__adobe_cep__.getSystemPath("userData") || "").replace(/\\/g, "/");
    } catch (e) {}
    if (!base) base = "C:/Users/Public";
    var ext = fmt === "jpeg" || fmt === "jpg" ? "jpg" : fmt === "webp" ? "webp" : "png";
    var fp = base + "/hj_ins_" + Date.now() + "." + ext;
    var encoding = window.cep && window.cep.encoding
      ? window.cep.encoding.Base64 : "Base64";
    var clean = String(b64 || "").replace(/^data:image\/[a-z0-9.+-]+;base64,/i, "")
      .replace(/\s/g, "");
    if (!clean) return Promise.reject(new Error("\u751f\u6210\u56fe\u6570\u636e\u4e3a\u7a7a"));
    var w = window.cep.fs.writeFile(fp, clean, encoding);
    if (!w || Number(w.err || 0) !== 0) {
      return Promise.reject(new Error("\u5199\u5165\u4e34\u65f6\u56fe\u7247\u5931\u8d25\uff08\u9519\u8bef\u7801 " +
        (w && w.err !== undefined ? w.err : "unknown") + "\uff09"));
    }
    function jsxQuote(value) {
      return "'" + String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'")
        .replace(/\r/g, "\\r").replace(/\n/g, "\\n") + "'";
    }
    var code = "hjInsert(" + jsxQuote(fp) +
      (bounds ? "," + jsxQuote(JSON.stringify(bounds)) : "") + ")";
    function cleanup() { try { window.cep.fs.unlink(fp); } catch (e) {} }
    return this.eval(code).then(function (r) {
      cleanup();
      if (!r || r.indexOf("ERR:") === 0) {
        throw new Error((r || "Photoshop \u672a\u8fd4\u56de\u63d2\u56fe\u7ed3\u679c").replace(/^ERR:/, ""));
      }
      return r;
    }, function (e) {
      cleanup();
      throw e;
    });
  },
};

/* ArrayBuffer \u2192 base64\uff08UXP \u672c\u5730\u6587\u4ef6\u7528\uff09 */
var B64C = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function bytesToB64(bytes) {
  var out = [], i;
  for (i = 0; i < bytes.length; i += 3) {
    var b1 = bytes[i], b2 = i + 1 < bytes.length ? bytes[i + 1] : 0,
        b3 = i + 2 < bytes.length ? bytes[i + 2] : 0;
    out.push(B64C[b1 >> 2]);
    out.push(B64C[((b1 & 3) << 4) | (b2 >> 4)]);
    out.push(i + 1 < bytes.length ? B64C[((b2 & 15) << 2) | (b3 >> 6)] : "=");
    out.push(i + 2 < bytes.length ? B64C[b3 & 63] : "=");
  }
  return out.join("");
}

function b64ToBytes(text) {
  var clean = String(text || "").replace(/[^A-Za-z0-9+/=]/g, "");
  var table = {}, i, out = [], buffer = 0, bits = 0;
  for (i = 0; i < B64C.length; i++) table[B64C.charAt(i)] = i;
  for (i = 0; i < clean.length; i++) {
    var ch = clean.charAt(i);
    if (ch === "=") break;
    if (table[ch] === undefined) continue;
    buffer = (buffer << 6) | table[ch]; bits += 6;
    if (bits >= 8) { bits -= 8; out.push((buffer >> bits) & 255); }
  }
  return new Uint8Array(out);
}

/* ============================================================
 * \u767b\u5f55 / \u914d\u7f6e
 * ============================================================ */
function doLogin() {
  var key = $("key-input").value.trim();
  if (!key) { toast("\u8bf7\u8f93\u5165\u5bc6\u94a5"); return; }
  $("btn-login").disabled = true;
  $("btn-login").textContent = "\u767b\u5f55\u4e2d\u2026";
  httpPost("/api/panel/login", { key: key, client_version: PANEL_VERSION }).then(function (d) {
    $("btn-login").disabled = false;
    $("btn-login").textContent = "\u767b \u5f55";
    if (!d.ok) { toast(d.msg || "\u767b\u5f55\u5931\u8d25"); return; }
    S.key = key;
    LS.set("hj_key", key);
    applyConfig(d);
    enterMain();
  });
}

function applyConfig(d) {
  S.keyInfo = d.key || S.keyInfo;
  if (d.tools) S.tools = d.tools.filter(function (t) { return t.enabled !== false; });
  if (d.config_version !== undefined) S.configVersion = Number(d.config_version);
  if (d.update) S.updateInfo = d.update;
  if (!S.toolId && S.tools.length) S.toolId = S.tools[0].id;
  renderAll();
}

function refreshConfig() {           // \u751f\u6210\u540e\u5237\u65b0\u989d\u5ea6
  return httpPost("/api/panel/login", { key: S.key, client_version: PANEL_VERSION }).then(function (d) {
    if (d.ok) applyConfig(d);
  });
}

function checkRemoteConfig() {
  if (!S.key) return;
  httpPost("/api/panel/config", {
    key: S.key, config_version: S.configVersion, client_version: PANEL_VERSION,
  }).then(function (d) {
    if (!d.ok) return;
    if (d.changed) applyConfig(d);
    else {
      if (d.config_version !== undefined) S.configVersion = Number(d.config_version);
      if (d.update) { S.updateInfo = d.update; renderUpdateStatus(); }
    }
  });
}

function startConfigTimer() {
  if (S.configTimer) clearInterval(S.configTimer);
  S.configTimer = setInterval(checkRemoteConfig, 300000);
}

function logout() {
  LS.del("hj_key");
  S.key = "";
  if (S.configTimer) { clearInterval(S.configTimer); S.configTimer = null; }
  $("main-view").classList.add("hidden");
  $("login-view").classList.remove("hidden");
  $("key-input").value = "";
}

function enterMain() {
  $("login-view").classList.add("hidden");
  $("main-view").classList.remove("hidden");
  switchView("main");
  renderHistory();
  startConfigTimer();
}

/* ============================================================
 * \u4e3b\u754c\u9762\u6e32\u67d3
 * ============================================================ */
function currentTool() {
  for (var i = 0; i < S.tools.length; i++)
    if (S.tools[i].id === S.toolId) return S.tools[i];
  return S.tools[0] || null;
}

function renderAll() {
  renderHead();
  renderToolSwitch();
  renderUpload();
  renderFoot();
  renderUpdateStatus();
}

function renderUpdateStatus() {
  var btn = $("update-status");
  if (!btn) return;
  var latest = S.updateInfo && S.updateInfo.latest_version;
  var available = latest && compareVersion(latest, PANEL_VERSION) > 0;
  btn.classList.toggle("has-update", !!available && !S.updateRequested);
  btn.classList.toggle("updating", !!S.updateRequested);
  btn.disabled = !available || S.updateRequested;
  if (S.updateRequested) {
    btn.textContent = "\u9000\u51faPS\u540e\u5b89\u88c5";
    btn.title = "\u66f4\u65b0\u6b63\u5728\u540e\u53f0\u4e0b\u8f7d\uff1b\u5173\u95ed\u5168\u90e8 Photoshop \u540e\u4f1a\u81ea\u52a8\u8986\u76d6\u65e7\u7248\u672c";
  } else if (available) {
    btn.textContent = "\u6709\u66f4\u65b0 v" + latest;
    btn.title = "\u53d1\u73b0 v" + latest + "\uff0c\u70b9\u51fb\u4e0b\u8f7d\u5e76\u66f4\u65b0";
  } else {
    btn.textContent = "v" + PANEL_VERSION.replace(/\.0$/, "");
    btn.title = "\u5f53\u524d\u5df2\u662f\u6700\u65b0\u7248 v" + PANEL_VERSION;
  }
}

function requestUpdate() {
  var latest = S.updateInfo && S.updateInfo.latest_version;
  if (!latest || compareVersion(latest, PANEL_VERSION) <= 0 || S.updateRequested) return;
  S.updateRequested = true;
  renderUpdateStatus();
  toast("\u6b63\u5728\u542f\u52a8\u66f4\u65b0\u5668\u2026", 3000);
  Platform.launchUpdater(latest).then(function () {
    toast("\u66f4\u65b0\u6b63\u5728\u540e\u53f0\u4e0b\u8f7d\uff1b\u5173\u95ed\u5168\u90e8 Photoshop \u540e\u4f1a\u81ea\u52a8\u8986\u76d6\u65e7\u7248\u672c\u3002", 6000);
  }).catch(function (e) {
    S.updateRequested = false;
    renderUpdateStatus();
    toast("\u542f\u52a8\u66f4\u65b0\u5931\u8d25\uff0c\u8bf7\u91cd\u65b0\u8fd0\u884c\u6700\u65b0\u7248\u5b89\u88c5\u5668\uff1a" + (e.message || e), 5000);
  });
}

function quotaText() {
  if (!S.keyInfo) return "-";
  if (!S.keyInfo.quota_total) return "\u4e0d\u9650\u989d\u5ea6";
  return "\u5269\u4f59 " + (S.keyInfo.quota_total - S.keyInfo.quota_used) + " \u5f20";
}

function renderHead() {
  $("uhead-name").textContent = "\u666e\u901a\u7528\u6237";
  $("uhead-quota").textContent = quotaText();
  $("acc-note").textContent = "\u5df2\u767b\u5f55";
  $("acc-quota").textContent = S.keyInfo
    ? (S.keyInfo.quota_total ? (S.keyInfo.quota_used + " / " + S.keyInfo.quota_total + " \u5f20")
       : "\u4e0d\u9650\u989d\u5ea6") : "-";
  $("acc-expires").textContent = S.keyInfo
    ? (S.keyInfo.expires_at ? new Date(S.keyInfo.expires_at * 1000).toLocaleDateString() : "\u6c38\u4e45")
    : "-";
}

function renderToolSwitch() {
  var box = $("tool-switch");
  box.innerHTML = "";
  S.tools.forEach(function (t) {
    var b = document.createElement("button");
    b.className = "tool-tab" + (t.id === S.toolId ? " on" : "");
    var ico = t.id === "banana_retouch"
      ? '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 17.5V20h2.5L18.8 7.7l-2.5-2.5L4 17.5zm16.7-12.6-1.6-1.6a1 1 0 0 0-1.4 0l-1.2 1.2L19 7l1.7-1.7a.5.5 0 0 0 0-.4z"/></svg>'
      : '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l1.7 5.3L19 9l-5.3 1.7L12 16l-1.7-5.3L5 9l5.3-1.7L12 2zm6 12 .9 2.6 2.6.9-2.6.9L18 21l-.9-2.6-2.6-.9 2.6-.9L18 14z"/></svg>';
    b.innerHTML = '<span class="t-ico">' + ico + '</span><span class="tool-name">' + esc(t.name) + "</span>";
    b.onclick = function () {
      if (!box.classList.contains("tool-menu-open")) { box.classList.add("tool-menu-open"); return; }
      S.toolId = t.id;
      box.classList.remove("tool-menu-open");
      renderAll();
    };
    box.appendChild(b);
  });
}

function toolNeedsImages() {
  var t = currentTool();
  return t && t.need_image && t.need_image !== "none" && t.need_image !== "optional";
}

function renderUpload() {
  var t = currentTool();
  var banana = t && t.id === "banana_retouch";
  $("prompt-title").textContent = banana ? "\u4fee\u56fe\u9700\u6c42" : "\u751f\u56fe\u9700\u6c42";
  $("row-quality").classList.toggle("hidden", banana);
  $("prompt-input").placeholder = banana
    ? "\u8bf7\u8f93\u5165\u60a8\u7684\u4fee\u56fe\u9700\u6c42\uff0c\u4f8b\u5982\uff1a\u628a\u80cc\u666f\u6362\u6210\u96ea\u5c71\u65e5\u843d"
    : "\u63cf\u8ff0\u60f3\u8981\u751f\u6210\u7684\u753b\u9762\uff1b\u4e5f\u53ef\u4ee5\u6dfb\u52a0\u53c2\u8003\u56fe";

  var row = $("img-row");
  row.innerHTML = "";
  S.images.forEach(function (im, idx) {
    var cell = document.createElement("div");
    cell.className = "img-cell" + (im.bounds ? " stick" : "");
    cell.innerHTML = '<img src="data:image/' + (im.format || "png") + ";base64," + im.b64 + '">'
      + '<span class="del" title="\u79fb\u9664" aria-label="\u79fb\u9664"></span>';
    cell.onclick = function (ev) {
      if (ev.target && ev.target.className === "del") return;
      openImgPop(im.b64, im.format);
    };
    cell.querySelector(".del").onclick = function (ev) {
      ev.stopPropagation();
      S.images.splice(idx, 1);
      renderUpload();
    };
    row.appendChild(cell);
  });
  if (S.images.length < 5) {
    var add = document.createElement("button");
    add.className = "img-add";
    add.innerHTML = '<span class="plus-mark" aria-hidden="true"></span>';
    add.title = "\u6dfb\u52a0\u539f\u56fe";
    add.onclick = openUploadMenu;
    row.appendChild(add);
  }
}

function selectedRatio() {
  return $("p-ratio").value || "auto";
}

/* \u667a\u80fd\u6bd4\u4f8b\u5728\u6709\u9009\u533a\u65f6\u5339\u914d\u6700\u63a5\u8fd1\u7684\u670d\u52a1\u7aef\u6863\u4f4d\uff0c\u51cf\u5c11\u8d34\u56de\u540e\u7684\u88c1\u5207\u3002 */
function ratioForBounds(bounds) {
  var selected = selectedRatio();
  if (selected !== "auto" || !bounds) return selected;
  var w = bounds.right - bounds.left, h = bounds.bottom - bounds.top;
  if (w <= 0 || h <= 0) return selected;
  var target = w / h;
  var choices = [
    { name:"21:9", value:21/9 }, { name:"16:9", value:16/9 },
    { name:"3:2", value:3/2 }, { name:"4:3", value:4/3 },
    { name:"1:1", value:1 }, { name:"3:4", value:3/4 },
    { name:"2:3", value:2/3 }, { name:"9:16", value:9/16 },
    { name:"9:21", value:9/21 },
  ];
  var best = choices[0], score = Math.abs(Math.log(target / best.value));
  for (var i = 1; i < choices.length; i++) {
    var next = Math.abs(Math.log(target / choices[i].value));
    if (next < score) { best = choices[i]; score = next; }
  }
  return best.name;
}

function renderFoot() {
  $("foot-quota").textContent = quotaText()
    + " \u00b7 \u672c\u6b21 " + (parseInt($("p-count").value, 10) || 1) + " \u5f20";
}

/* ============================================================
 * \u4e0a\u4f20\uff1a\u4e09\u79cd\u65b9\u5f0f
 * ============================================================ */
function closePops() {
  document.body.classList.remove("pop-open");
  $("pop-mask").classList.add("hidden");
  $("menu-pop").classList.add("hidden");
  $("menu-pop").style.left = "8px";
  $("menu-pop").style.top = "8px";
  $("img-pop").classList.add("hidden");
  $("prompt-pop").classList.add("hidden");
}

function openUploadMenu() {
  var menu = $("menu-pop");
  var add = document.querySelector(".img-add");
  var rect = add && add.getBoundingClientRect ? add.getBoundingClientRect() : null;
  var menuW = 150, menuH = 127, gap = 6;
  var left = rect ? rect.left : 8;
  var top = rect ? rect.bottom + gap : 8;
  var maxLeft = Math.max(8, window.innerWidth - menuW - 8);
  if (left > maxLeft) left = maxLeft;
  if (left < 8) left = 8;
  if (top + menuH > window.innerHeight - 70 && rect) top = Math.max(8, rect.top - menuH - gap);
  menu.style.left = Math.round(left) + "px";
  menu.style.top = Math.round(top) + "px";
  document.body.classList.add("pop-open");
  menu.classList.remove("hidden");
  $("pop-mask").classList.remove("hidden");
}

function addImage(im) {
  if (S.images.length >= 5) { toast("\u6700\u591a 5 \u5f20\u53c2\u8003\u56fe"); return false; }
  S.images.push(im);
  if (!im.width || !im.height) {
    try {
      var probe = new Image();
      probe.onload = function () { im.width = probe.naturalWidth || probe.width; im.height = probe.naturalHeight || probe.height; };
      probe.src = "data:image/" + (im.format || "png") + ";base64," + im.b64;
    } catch (e) {}
  }
  renderUpload();
  return true;
}

function addDroppedFiles(files) {
  var list = Array.prototype.slice.call(files || [], 0, Math.max(0, 5 - S.images.length));
  if (!list.length) return;
  var added = 0;
  list.forEach(function (file) {
    if (!/^image\//i.test(file.type || "") && !/\.(png|jpe?g|webp)$/i.test(file.name || "")) return;
    var rd = new FileReader();
    rd.onload = function () {
      var value = String(rd.result || ""), comma = value.indexOf(",");
      var fmt = /image\/jpe?g/i.test(value) ? "jpeg" : /image\/webp/i.test(value) ? "webp" : "png";
      if (addImage({ b64: comma >= 0 ? value.slice(comma + 1) : value, format: fmt })) {
        added++;
        toast("\u5df2\u6dfb\u52a0 " + added + " \u5f20\u53c2\u8003\u56fe");
      }
    };
    rd.readAsDataURL(file);
  });
}

function onUploadPick(src) {
  closePops();
  if (src === "local") {
    Platform.pickLocal().then(function (list) {
      var added = 0;
      (list || []).forEach(function (im) {
        if (!im || !im.b64) return;
        if (addImage({ b64: im.b64, format: im.format })) added++;
      });
      if (added) toast("\u5df2\u6dfb\u52a0 " + added + " \u5f20");
    }).catch(function (e) { toast(e.message || "\u6dfb\u52a0\u5931\u8d25"); });
  } else if (src === "layer") {
    toast("\u6b63\u5728\u6293\u53d6\u5f53\u524d\u56fe\u5c42\u2026");
    Platform.captureLayer().then(function (im) {
      addImage(im);
      toast("\u5df2\u4ece\u56fe\u5c42\u83b7\u53d6 \u2713");
    }).catch(function (e) { toast(e.message || "\u6293\u53d6\u5931\u8d25"); });
  } else if (src === "selection") {
    toast("\u6b63\u5728\u6293\u53d6\u9009\u533a\u2026");
    Platform.captureSelection().then(function (im) {
      im.bounds = im.bounds || null;
      addImage(im);
      if (im.bounds) setSwitch("sw-stick", true);   // \u6709\u9009\u533a\u65f6\u81ea\u52a8\u5f00\u8d34\u56de
      toast("\u5df2\u4ece\u9009\u533a\u83b7\u53d6 \u2713");
    }).catch(function (e) { toast(e.message || "\u6293\u53d6\u5931\u8d25"); });
  }
}

function setSwitch(id, on) {
  var el = $(id);
  el.classList.toggle("on", !!on);
  el.setAttribute("data-on", on ? "1" : "0");
}

/* ============================================================
 * \u751f\u6210
 * ============================================================ */
function onGenerate() {
  if (S.busy) return;
  var t = currentTool();
  if (!t) { toast("\u6682\u65e0\u53ef\u7528\u5de5\u5177"); return; }
  var prompt = $("prompt-input").value.trim();
  if (!prompt) { toast("\u8bf7\u8f93\u5165\u63d0\u793a\u8bcd"); return; }
  var needImg = toolNeedsImages();
  if (needImg && !S.images.length) { toast("\u8bf7\u5148\u6dfb\u52a0\u81f3\u5c11 1 \u5f20\u539f\u56fe"); return; }

  S.busy = true;
  var wantsSelection = $("sw-stick").getAttribute("data-on") === "1";
  if (!wantsSelection) {
    submitGenerate(t, prompt, null);
    return;
  }
  Platform.selectionBounds().then(function (bounds) {
    if (!bounds) {
      S.busy = false;
      toast("\u5df2\u5f00\u542f\u63d2\u56de\u9009\u533a\uff0c\u8bf7\u5148\u5728 Photoshop \u91cc\u6846\u9009\u533a\u57df", 4200);
      return;
    }
    submitGenerate(t, prompt, bounds);
  }).catch(function (e) {
    S.busy = false;
    toast("\u8bfb\u53d6\u9009\u533a\u5931\u8d25\uff1a" + (e.message || e || "\u672a\u77e5\u9519\u8bef"), 4200);
  });
}

function submitGenerate(t, prompt, bounds) {
  var params = {
    prompt: prompt,
    ratio: ratioForBounds(bounds),
    count: parseInt($("p-count").value, 10) || 1,
    resolution: $("p-resolution").value || "1K",
    quality: $("p-quality").value || "auto",
    stick_selection: !!bounds,
  };
  var btn = $("btn-gen");
  btn.disabled = true; btn.textContent = "\u63d0\u4ea4\u4e2d\u2026";
  $("result-empty").classList.add("hidden");
  $("result-grid").innerHTML = "";
  $("progress-box").classList.remove("hidden");
  setProgress(4, "\u6b63\u5728\u63d0\u4ea4\u670d\u52a1\u5668\u2026");

  var body = { key: S.key, tool_id: t.id, params: params };
  if (S.images.length) body.images = S.images.map(function (im) {
    return { b64: im.b64, format: im.format || "png", width: im.width || 0, height: im.height || 0 };
  });

  httpPost("/api/panel/generate", body).then(function (d) {
    if (!d.ok) {
      failGenerate(d.msg || "\u63d0\u4ea4\u5931\u8d25");
      return;
    }
    S.taskId = d.task_id;
    S._gen = { tool: t, prompt: prompt, bounds: bounds, autoInsert: !!bounds, t0: Date.now() };
    btn.textContent = "\u751f\u6210\u4e2d\u2026";
    pollTask();
  }).catch(function () { failGenerate("\u7f51\u7edc\u9519\u8bef"); });
}

function setProgress(pct, stage) {
  $("progress-fill").style.width = Math.max(2, Math.min(100, pct)) + "%";
  if (stage) $("progress-stage").textContent = stage;
}

function failGenerate(msg) {
  S.busy = false;
  S.taskId = null;
  clearTimeout(S.pollTimer);
  var btn = $("btn-gen");
  btn.disabled = false; btn.textContent = "\u7acb\u5373\u751f\u6210";
  $("progress-box").classList.add("hidden");
  $("result-empty").classList.remove("hidden");
  toast(msg, 3600);
  refreshConfig();
}

function pollTask() {
  if (!S.taskId) return;
  // \u5047\u8fdb\u5ea6\uff082s \u4e00\u8df3\uff0c\u6700\u9ad8 93%\uff09
  var el = (Date.now() - (S._gen ? S._gen.t0 : Date.now())) / 1000;
  setProgress(Math.min(93, 5 + el * 1.2), "\u751f\u6210\u4e2d\u2026 \u8bf7\u7a0d\u5019");
  httpPost("/api/panel/task/" + S.taskId, { key: S.key }).then(function (d) {
    if (!S.taskId) return;
    if (!d.ok) { failGenerate(d.msg || "\u67e5\u8be2\u5931\u8d25"); return; }
    var st = d.status;
    if (st === "done") { onTaskDone(d); return; }
    if (st === "error") { failGenerate(d.error || "\u751f\u6210\u5931\u8d25"); return; }
    if (st === "canceled") { failGenerate("\u5df2\u53d6\u6d88"); return; }
    S.pollTimer = setTimeout(pollTask, 2200);
  }).catch(function () {
    S.pollTimer = setTimeout(pollTask, 3000);
  });
}

function onTaskDone(d) {
  var taskId = S.taskId;
  S.busy = false;
  S.taskId = null;
  clearTimeout(S.pollTimer);
  setProgress(100, "\u751f\u6210\u5b8c\u6210 \u2713");
  var btn = $("btn-gen");
  btn.disabled = false; btn.textContent = "\u7acb\u5373\u751f\u6210";

  var images = d.images || [];
  $("result-count").textContent = images.length ? ("\u5171 " + images.length + " \u5f20") : "";
  var bounds = S._gen ? S._gen.bounds : null;
  setTimeout(function () { $("progress-box").classList.add("hidden"); }, 600);

  var grid = $("result-grid");
  grid.innerHTML = "";
  if (!images.length) {
    $("result-empty").classList.remove("hidden");
    toast("\u751f\u6210\u5b8c\u6210\uff08\u65e0\u8fd4\u56de\u56fe\uff09");
    return;
  }
  images.forEach(function (im) {
    var cell = document.createElement("div");
    cell.className = "res-cell";
    cell.innerHTML = '<img src="data:image/' + (im.format || "png") + ";base64," + im.b64 + '">'
      + '<button class="ins">\u63d2\u5165\u56fe\u5c42</button>';
    cell.onclick = function () { openImgPop(im.b64, im.format, bounds); };
    cell.querySelector(".ins").onclick = function (ev) {
      ev.stopPropagation();
      insertToPS(im, bounds);
    };
    grid.appendChild(cell);
  });

  if (bounds && S._gen && S._gen.autoInsert) autoInsertResults(images, bounds);

  // \u5386\u53f2\uff08\u7f29\u7565\u56fe\u5f02\u6b65\u8865\uff09
  pushHistory({
    t: Date.now(),
    tool: S._gen ? S._gen.tool.name : "",
    prompt: S._gen ? S._gen.prompt : "",
    thumb: "",
    task_id: taskId || "",
  });
  Platform.makeThumb(images[0].b64, images[0].format, function (thumb) {
    if (!thumb) return;
    var list = [];
    try { list = JSON.parse(LS.get(histKey(), "[]")) || []; } catch (e) { list = []; }
    for (var i = 0; i < list.length; i++) {
      if (list[i].task_id === taskId) { list[i].thumb = thumb; break; }
    }
    LS.set(histKey(), JSON.stringify(list));
    renderHistory();
  });
  refreshConfig();
  renderFoot();
}

function autoInsertResults(images, bounds) {
  var chain = Promise.resolve();
  images.forEach(function (im) {
    chain = chain.then(function () { return Platform.insertImage(im.b64, im.format, bounds); });
  });
  toast("\u751f\u6210\u5b8c\u6210\uff0c\u6b63\u5728\u81ea\u52a8\u63d2\u5165\u9009\u533a\u2026");
  chain.then(function () {
    toast("\u5df2\u81ea\u52a8\u63d2\u5165\u9009\u533a\uff0c\u5171 " + images.length + " \u4e2a\u65b0\u56fe\u5c42 \u2713");
  }).catch(function (e) {
    toast("\u81ea\u52a8\u63d2\u5165\u5931\u8d25\uff0c\u53ef\u70b9\u51fb\u7ed3\u679c\u91cd\u8bd5\uff1a" + (e.message || e), 4200);
  });
}

function insertToPS(im, bounds) {
  toast("\u6b63\u5728\u63d2\u5165\u56fe\u5c42\u2026");
  Platform.insertImage(im.b64, im.format, bounds || null).then(function () {
    toast("\u5df2\u63d2\u5165\u4e3a\u65b0\u56fe\u5c42 \u2713");
  }).catch(function (e) {
    toast("\u63d2\u5165\u5931\u8d25: " + (e.message || e), 3600);
  });
}

function openImgPop(b64, fmt, bounds) {
  $("img-pop-img").src = "data:image/" + (fmt || "png") + ";base64," + b64;
  $("img-pop").classList.remove("hidden");
  $("pop-mask").classList.remove("hidden");
  $("img-pop")._img = { b64: b64, format: fmt };
  $("img-pop")._bounds = bounds || null;
}

/* ============================================================
 * \u5386\u53f2\uff08\u672c\u5730\uff09
 * ============================================================ */
function histKey() { return "hj_hist3_" + (S.key || "").slice(0, 14); }

function pushHistory(h) {
  var list = [];
  try { list = JSON.parse(LS.get(histKey(), "[]")) || []; } catch (e) { list = []; }
  list.unshift(h);
  if (list.length > 30) list = list.slice(0, 30);
  LS.set(histKey(), JSON.stringify(list));
  renderHistory();
}

function renderHistory() {
  var box = $("history-list");
  if (!box) return;
  var list = [];
  try { list = JSON.parse(LS.get(histKey(), "[]")) || []; } catch (e) { list = []; }
  box.innerHTML = "";
  if (!list.length) {
    box.innerHTML = '<div class="hist-empty">\u6682\u65e0\u5386\u53f2\u8bb0\u5f55</div>';
    return;
  }
  list.forEach(function (h, idx) {
    var it = document.createElement("div");
    it.className = "hist-item";
    var d = new Date(h.t || 0);
    var mm = (d.getMonth() + 1) + "-" + d.getDate() + " " +
      ("0" + d.getHours()).slice(-2) + ":" + ("0" + d.getMinutes()).slice(-2);
    it.innerHTML = (h.thumb
        ? '<img src="' + h.thumb + '">'
        : '<img style="opacity:.25" src="data:image/png;base64,">')
      + '<div class="h-info"><div class="h-tool">' + esc(h.tool) + " \u00b7 " + mm + "</div>"
      + '<div class="h-prompt">' + esc(h.prompt) + "</div></div>";
    it.onclick = function () {
      if (!h.task_id) { toast("\u8be5\u8bb0\u5f55\u65e0\u56fe\u53ef\u53d6"); return; }
      toast("\u53d6\u56de\u4e2d\u2026");
      httpPost("/api/panel/task/" + h.task_id, { key: S.key }).then(function (d) {
        if (d.ok && d.status === "done" && d.images && d.images.length) {
          insertToPS(d.images[0], null);
        } else {
          toast("\u8be5\u8bb0\u5f55\u7684\u56fe\u7247\u5df2\u88ab\u670d\u52a1\u5668\u6e05\u7406");
        }
      });
    };
    box.appendChild(it);
  });
}

/* ============================================================
 * \u89c6\u56fe\u5207\u6362
 * ============================================================ */
function switchView(v) {
  S.view = v;
  ["main", "history", "account"].forEach(function (n) {
    $("view-" + n).classList.toggle("on", n === v);
  });
  ["main", "history", "account"].forEach(function (n) {
    var b = $("nav-" + n);
    if (b) b.classList.toggle("on", n === v);
  });
}

/* ============================================================
 * \u7ed1\u5b9a
 * ============================================================ */
function bind() {
  $("btn-login").onclick = doLogin;
  $("key-input").onkeydown = function (e) {
    if (e.key === "Enter" || e.keyCode === 13) doLogin();
  };

  $("nav-main").onclick = function () { switchView("main"); };
  $("nav-history").onclick = function () { switchView("history"); };
  $("nav-account").onclick = function () { switchView("account"); };
  $("btn-logout").onclick = logout;

  // \u4e0a\u4f20\u83dc\u5355
  var items = $("menu-pop").querySelectorAll(".pop-item");
  for (var i = 0; i < items.length; i++) {
    items[i].onclick = function () { onUploadPick(this.getAttribute("data-src")); };
  }
  $("pop-mask").onclick = closePops;

  // \u63d0\u793a\u8bcd
  $("prompt-input").oninput = function () { $("prompt-count").textContent = this.value.length + " / 3000"; };
  $("btn-clear").onclick = function () { $("prompt-input").value = ""; $("prompt-input").oninput(); };
  $("btn-prompt-full").onclick = function () {
    $("prompt-pop-input").value = $("prompt-input").value;
    $("prompt-pop").classList.remove("hidden"); $("pop-mask").classList.remove("hidden");
  };
  $("btn-prompt-close").onclick = closePops;
  $("btn-prompt-save").onclick = function () {
    $("prompt-input").value = $("prompt-pop-input").value;
    $("prompt-input").oninput(); closePops();
  };

  // \u6587\u4ef6\u5939/\u684c\u9762\u62d6\u56fe
  var drop = $("img-drop");
  drop.ondragover = function (e) { e.preventDefault(); drop.classList.add("drag"); };
  drop.ondragleave = function () { drop.classList.remove("drag"); };
  drop.ondrop = function (e) {
    e.preventDefault(); drop.classList.remove("drag");
    addDroppedFiles(e.dataTransfer && e.dataTransfer.files);
  };

  // \u5f00\u5173
  $("sw-stick").onclick = function () {
    setSwitch("sw-stick", this.getAttribute("data-on") !== "1");
  };
  $("p-count").onchange = renderFoot;

  // \u751f\u6210
  $("btn-gen").onclick = onGenerate;
  $("btn-cancel").onclick = function () {
    if (S.taskId) {
      httpPost("/api/panel/cancel", { key: S.key, task_id: S.taskId });
      failGenerate("\u6b63\u5728\u53d6\u6d88\u2026");
    }
  };

  // \u5386\u53f2\u6e05\u7a7a
  $("btn-hist-clear").onclick = function () {
    LS.del(histKey());
    renderHistory();
    toast("\u5df2\u6e05\u7a7a");
  };

  // \u5927\u56fe\u5f39\u5c42
  $("btn-pop-close").onclick = closePops;
  $("btn-pop-insert").onclick = function () {
    var im = $("img-pop")._img, b = $("img-pop")._bounds;
    closePops();
    if (im) insertToPS(im, b);
  };
}

/* ============================================================
 * \u542f\u52a8
 * ============================================================ */
document.addEventListener("DOMContentLoaded", function () {
  bind();
  $("update-status").onclick = requestUpdate;
  var saved = LS.get("hj_key", "");
  if (saved) {
    $("key-input").value = saved;
    S.key = saved;
    httpPost("/api/panel/login", { key: saved, client_version: PANEL_VERSION }).then(function (d) {
      if (d.ok) { applyConfig(d); enterMain(); }
      else { toast(d.msg || "\u5bc6\u94a5\u5df2\u5931\u6548\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55"); }
    });
  }
});
