"use strict";

let photoshop = null;
let uxp = null;
let app = null;
let core = null;
let action = null;
let localFileSystem = null;
let formats = { binary: "binary" };

if (typeof require === "function") {
  photoshop = require("photoshop");
  uxp = require("uxp");
  app = photoshop.app;
  core = photoshop.core;
  action = photoshop.action;
  localFileSystem = uxp.storage.localFileSystem;
  formats = uxp.storage.formats;
}

const VERSION = "0.3.9";
const MAIN_WORKFLOW_FILE = "xihuandujia.json";
const CUSTOM_CATEGORY = "我的常用";
const MEMBER_LOGIN_HINT = "请先登录启动器，在使用插件！";
const STORAGE_KEYS = {
  bridgeUrl: "xhn.bridgeUrl",
  checkpoint: "xhn.checkpoint",
  customFeatures: "xhn.customFeatures",
  referenceName: "xhn.referenceName",
  coverVideo: "xhn.settings.coverVideo",
  autoSend: "xhn.settings.autoSend",
  layerNameMode: "xhn.settings.layerNameMode",
  searchQuery: "xhn.searchQuery"
};

const DEFAULT_CUSTOM_FEATURES = [
  { id: "skin", name: "修皮肤", prompt: "professional skin retouching, natural pores, clean face, realistic skin texture" },
  { id: "smooth", name: "自动磨皮", prompt: "beauty retouching, soft skin, preserve facial details, natural light" },
  { id: "photo", name: "自动修图", prompt: "professional photo retouching, high quality, realistic details" },
  { id: "grass", name: "修复草地", prompt: "restore grass field, natural green grass, realistic texture" }
];

const MAJORS = [
  { name: "功能模块", ready: true, icon: "home", note: "回到功能主页" },
  { name: "AI打光", ready: true, icon: "light", note: "夕阳逆光、镜头光晕、室内窗口光等。" },
  { name: "人像精修", ready: true, icon: "portrait", note: "磨皮、网红感。" },
  { name: "AI生成", ready: true, icon: "generate", note: "创成填充、物品移除、打字改图等。" },
  { name: "胶片模拟", ready: true, icon: "film", note: "预留入口，等待更新。" },
  { name: "色彩处理", ready: true, icon: "color", note: "追色、追光追色。" },
  { name: "系统设置", ready: true, icon: "settings", note: "插件设置与版本信息。" }
];

const CATEGORIES = [
  {
    "name": "人像精修",
    "mode": "portrait",
    "features": [
      "磨皮",
      "网红感"
    ]
  },
  {
    "name": "本地创成",
    "mode": "local",
    "features": [
      "局部重绘",
      "创成填充",
      "物体移除",
      "高质感重绘",
      "高质量放大",
      "换衣服",
      "抠图",
      "高级改图"
    ]
  },
  {
    "name": "色彩处理",
    "mode": "color",
    "features": [
      "追色1.0",
      "追光追色1.0"
    ]
  },
  {
    "name": "创意云",
    "mode": "typing",
    "features": [
      "打字改图1.0",
      "场景生成",
      "场景融合",
      "一键下雪",
      "一键下雨",
      "一键秋天",
      "水面波光",
      "圣诞氛围",
      "草地修复",
      "一路生花",
      "一键阴天",
      "Qwen改图"
    ]
  },
  {
    "name": "AI打光",
    "mode": "oneClick",
    "features": [
      "夕阳逆光",
      "镜头光晕",
      "室内窗口光",
      "草坪阴影光",
      "人物轮廓光",
      "伦勃朗光"
    ]
  }
];

const FILM_CATEGORIES = [
  {
    "name": "胶片人像",
    "mode": "film",
    "features": [
      "清透胶片",
      "复古暖调",
      "富士人像",
      "柯达婚礼"
    ]
  },
  {
    "name": "婚礼色调",
    "mode": "film",
    "features": [
      "森系胶片",
      "草坪婚礼",
      "室内柔光",
      "黑白纪实"
    ]
  }
];

const LIGHT_CATEGORIES = [
  {
    "name": "图像合成",
    "mode": "lightPro",
    "features": [
      "背景提取",
      "全景融合",
      "重绘融合",
      "自由光线"
    ]
  },
  {
    "name": "常用光效",
    "mode": "light",
    "features": [
      "逆光增强",
      "柔光补光",
      "轮廓光",
      "日落暖光"
    ]
  },
  {
    "name": "环境光影",
    "mode": "light",
    "features": [
      "天空柔光",
      "窗边光",
      "黄昏氛围",
      "阴天提亮"
    ]
  }
];

const EXCLUSIVE_CATEGORIES = [
  {
    "name": "独家",
    "mode": "exclusive",
    "features": []
  }
];

const HOME_CARDS = [
  {
    major: "人像精修",
    title: "人像精修",
    tag: "磨皮",
    subtitle: "磨皮 / 网红感",
    cover: "assets/thumb-portrait.png",
    description: "完美人像修图入口，主打磨皮、肤质和网红质感。"
  },
  {
    major: "AI生成",
    title: "AI生成",
    tag: "开始修图",
    subtitle: "创成填充 / 物品移除 / 打字改图",
    cover: "assets/thumb-generate.png",
    description: "文字改图、局部生成和重绘入口，后续可继续接入云端工作流。"
  },
  {
    major: "AI打光",
    title: "AI打光",
    tag: "光线",
    subtitle: "夕阳逆光 / 轮廓光 / 室内窗口光",
    cover: "assets/thumb-light.png",
    description: "为人像快速重塑光影，当前使用本机专用桥接。"
  },
  {
    major: "色彩处理",
    title: "色彩处理",
    tag: "追色",
    subtitle: "追色1.0 / 追光追色1.0",
    cover: "assets/thumb-color.png",
    description: "参考图追色和追光，保持人物与构图稳定。"
  },
  {
    major: "胶片模拟",
    title: "胶片模拟",
    tag: "更新中",
    subtitle: "胶片质感栏目预留",
    cover: "assets/thumb-film.png",
    description: "栏目暂时空着，右侧显示更新中，后续接入你的胶片模型。"
  },
  {
    major: "系统设置",
    title: "系统设置",
    tag: "设置",
    subtitle: "账号 / 缓存 / 版本",
    cover: "assets/thumb-settings.png",
    description: "查看用户码、版本、安全状态和清理临时缓存。"
  }
];

const AI_GENERATE_CATEGORY = {
  name: "AI生成",
  mode: "typing",
  features: ["创成填充", "物品移除", "打字改图", "一键花瓣", "头纱飘逸", "重绘"]
};

const FILM_PENDING_CATEGORY = {
  name: "胶片模拟",
  mode: "filmPending",
  features: []
};

const HOME_CATEGORY = {
  name: "功能模块",
  mode: "home",
  features: []
};

const SETTINGS_CATEGORY = {
  name: "系统设置",
  mode: "settings",
  features: []
};

const SETTINGS_CARDS = [
  { title: "账号状态", tag: "用户", subtitle: "启动器登录 / 用户码 / 到期时间", cover: "assets/thumb-settings.png", action: "account" },
  { title: "自动回写", tag: "PS", subtitle: "控制结果是否自动发送至 Photoshop", cover: "assets/thumb-auto-send.png", action: "autoSend" },
  { title: "图层命名", tag: "命名", subtitle: "功能名、时间或原图名", cover: "assets/thumb-layer-name.png", action: "layerName" },
  { title: "缓存清理", tag: "清理", subtitle: "清除参考图、参数草稿和结果缓存", cover: "assets/thumb-cache.png", action: "clearCache" },
  { title: "安全策略", tag: "本机", subtitle: "只允许连接本机 XNH 桥接服务", cover: "assets/thumb-security.png", action: "security" },
  { title: "版本信息", tag: "版本", subtitle: "插件版本、更新时间和联系方式", cover: "assets/thumb-version.png", action: "version" }
];

const FEATURE_THUMBNAILS = {
  "磨皮": "assets/thumb-skin.png",
  "网红感": "assets/thumb-social.png",
  "修皮肤": "assets/thumb-skin.png",
  "自动磨皮": "assets/thumb-skin.png",
  "自动修图": "assets/thumb-social.png",
  "自动精修": "assets/thumb-social.png",
  "创成填充": "assets/thumb-fill.png",
  "创成扩图": "assets/thumb-fill.png",
  "物品移除": "assets/thumb-remove.png",
  "物体移除": "assets/thumb-remove.png",
  "打字改图": "assets/thumb-type.png",
  "打字改图1.0": "assets/thumb-type.png",
  "一键花瓣": "assets/thumb-petals.png",
  "一路生花": "assets/thumb-petals.png",
  "头纱飘逸": "assets/thumb-veil.png",
  "重绘": "assets/thumb-redraw.png",
  "局部重绘": "assets/thumb-redraw.png",
  "高质感重绘": "assets/thumb-redraw.png",
  "夕阳逆光": "assets/thumb-sunset.png",
  "日落暖光": "assets/thumb-sunset.png",
  "黄昏氛围": "assets/thumb-sunset.png",
  "镜头光晕": "assets/thumb-flare.png",
  "室内窗口光": "assets/thumb-window.png",
  "窗边光": "assets/thumb-window.png",
  "草坪阴影光": "assets/thumb-lawn.png",
  "人物轮廓光": "assets/thumb-rim.png",
  "轮廓光": "assets/thumb-rim.png",
  "伦勃朗光": "assets/thumb-rembrandt.png",
  "追色1.0": "assets/thumb-color-match.png",
  "追光追色1.0": "assets/thumb-color-light.png",
  "追色0.5版本": "assets/thumb-color-match.png",
  "追色追光0.5版本": "assets/thumb-color-light.png",
  "背景提取": "assets/thumb-cutout.png",
  "抠图": "assets/thumb-cutout.png",
  "全景融合": "assets/thumb-scene.png",
  "场景生成": "assets/thumb-scene.png",
  "场景融合": "assets/thumb-scene.png",
  "重绘融合": "assets/thumb-redraw.png",
  "自由光线": "assets/thumb-light.png",
  "逆光增强": "assets/thumb-sunset.png",
  "柔光补光": "assets/thumb-window.png",
  "天空柔光": "assets/thumb-light.png",
  "清透胶片": "assets/thumb-film.png",
  "复古暖调": "assets/thumb-film.png",
  "富士人像": "assets/thumb-film.png",
  "柯达婚礼": "assets/thumb-film.png",
  "森系胶片": "assets/thumb-film.png",
  "草坪婚礼": "assets/thumb-lawn.png",
  "室内柔光": "assets/thumb-window.png",
  "黑白纪实": "assets/thumb-film.png"
};

const WORKFLOW_BY_CATEGORY = {
  "人像精修": "XHN-Portrait-Pro.json",
  "本地创成": "XHN-Local-Create-Pro.json",
  "色彩处理": "XHN-Color-Light-Pro.json",
  "创意云": "XHN-Qwen-Edit-Pro.json",
  "AI打光": "XHN-AI-Lighting-Pro.json",
  "胶片人像": "XHN-Color-Light-Pro.json",
  "婚礼色调": "XHN-Color-Light-Pro.json",
  "图像合成": "XHN-Transparent-Cutout-Pro.json",
  "常用光效": "XHN-AI-Lighting-Pro.json",
  "环境光影": "XHN-AI-Lighting-Pro.json"
};

const WORKFLOW_BY_FEATURE = {
  "修皮肤": "XHN-Portrait-Pro.json",
  "自动精修": "XHN-Portrait-Pro.json",
  "商业质感": "XHN-Portrait-Pro.json",
  "去双下巴": "XHN-Portrait-Pro.json",
  "老照片修复": "XHN-Upscale-Pro.json",
  "照片放大": "XHN-Upscale-Pro.json",
  "修复裙摆": "XHN-Local-Create-Pro.json",
  "瘦身": "XHN-Local-Create-Pro.json",
  "光影优化": "XHN-AI-Lighting-Pro.json",
  "面部增强": "XHN-Portrait-Pro.json",
  "面部迁移": "XHN-Portrait-Pro.json",
  "局部重绘": "XHN-Local-Create-Pro.json",
  "创成填充": "XHN-Qwen-Edit-Pro.json",
  "物体移除": "XHN-Local-Create-Pro.json",
  "高质感重绘": "XHN-Local-Create-Pro.json",
  "高质量放大": "XHN-Upscale-Pro.json",
  "换衣服": "XHN-Qwen-Edit-Pro.json",
  "抠图": "XHN-Transparent-Cutout-Pro.json",
  "高级改图": "XHN-Qwen-Edit-Pro.json",
  "追色0.5版本": "XHN-Color-Light-Pro.json",
  "追色追光0.5版本": "XHN-Color-Light-Pro.json",
  "打字改图1.0": "XHN-Qwen-Edit-Pro.json",
  "场景生成": "XHN-Creative-Cloud-Pro.json",
  "场景融合": "XHN-Creative-Cloud-Pro.json",
  "一键下雪": "XHN-Creative-Cloud-Pro.json",
  "一键下雨": "XHN-Creative-Cloud-Pro.json",
  "一键秋天": "XHN-Creative-Cloud-Pro.json",
  "水面波光": "XHN-Creative-Cloud-Pro.json",
  "圣诞氛围": "XHN-Creative-Cloud-Pro.json",
  "草地修复": "XHN-Creative-Cloud-Pro.json",
  "一路生花": "XHN-Creative-Cloud-Pro.json",
  "一键阴天": "XHN-Creative-Cloud-Pro.json",
  "Qwen改图": "XHN-Qwen-Edit-Pro.json",
  "人物光影优化": "XHN-AI-Lighting-Pro.json",
  "逆光效果": "XHN-AI-Lighting-Pro.json",
  "室内补光": "XHN-AI-Lighting-Pro.json",
  "光影融合": "XHN-AI-Lighting-Pro.json",
  "增加轮廓光": "XHN-AI-Lighting-Pro.json",
  "自然光": "XHN-AI-Lighting-Pro.json",
  "闪光灯效果": "XHN-AI-Lighting-Pro.json",
  "黄昏逆光": "XHN-AI-Lighting-Pro.json",
  "夕阳光": "XHN-AI-Lighting-Pro.json",
  "清透胶片": "XHN-Color-Light-Pro.json",
  "复古暖调": "XHN-Color-Light-Pro.json",
  "富士人像": "XHN-Color-Light-Pro.json",
  "柯达婚礼": "XHN-Color-Light-Pro.json",
  "森系胶片": "XHN-Color-Light-Pro.json",
  "草坪婚礼": "XHN-Creative-Cloud-Pro.json",
  "室内柔光": "XHN-AI-Lighting-Pro.json",
  "黑白纪实": "XHN-Color-Light-Pro.json",
  "背景提取": "XHN-Transparent-Cutout-Pro.json",
  "全景融合": "XHN-Creative-Cloud-Pro.json",
  "重绘融合": "XHN-Local-Create-Pro.json",
  "自由光线": "XHN-AI-Lighting-Pro.json",
  "逆光增强": "XHN-AI-Lighting-Pro.json",
  "柔光补光": "XHN-AI-Lighting-Pro.json",
  "轮廓光": "XHN-AI-Lighting-Pro.json",
  "日落暖光": "XHN-AI-Lighting-Pro.json",
  "天空柔光": "XHN-AI-Lighting-Pro.json",
  "窗边光": "XHN-AI-Lighting-Pro.json",
  "黄昏氛围": "XHN-AI-Lighting-Pro.json",
  "阴天提亮": "XHN-AI-Lighting-Pro.json"
};

const PROMPTS = {
  "修皮肤": "专业修皮肤，保留真实毛孔和肤质，清理瑕疵，保持本人五官和构图不变。",
  "自动精修": "自动人像精修，优化肤色、眼神和面部细节，保持人物身份、姿势和构图不变。",
  "商业质感": "商业级人像质感，增强皮肤、服装和画面细节，光影干净高级，保持真实自然。",
  "去双下巴": "自然去双下巴，微调下颌线和颈部阴影，保持本人脸型和整体比例不变。",
  "老照片修复": "老照片修复，去除划痕、污渍和模糊，恢复真实面部细节，保留原照片年代感。",
  "照片放大": "高质量照片放大，增强清晰度和细节，保留原图颜色、肤质和构图。",
  "修复裙摆": "修复裙摆和布料褶皱，保持原本材质、纹理、光影和婚纱质感。",
  "瘦身": "自然瘦身，微调腰身和轮廓，保持姿势、服装纹理、背景和人物比例真实。",
  "光影优化": "优化人物光影，提亮面部，平衡高光和阴影，保留真实肤质和人物身份。",
  "面部增强": "增强面部细节，优化眼睛、眉毛、嘴唇和肤质，保持本人五官和脸型不变。",
  "面部迁移": "参考图辅助面部迁移，融合面部细节，保持目标图姿势、光影和真实质感。",
  "局部重绘": "局部重绘，保持原图风格、光影和周围纹理一致，生成自然真实的修改结果。",
  "创成填充": "创成填充，按参考图或提示词补全画面，匹配周围材质、透视、光影和画质。",
  "物体移除": "移除选中物体，重建干净背景，保持周围纹理、透视和原始光影一致。",
  "高质感重绘": "高质感重绘，增强材质和细节，保持原始构图、人物和真实摄影质感。",
  "高质量放大": "高质量放大，恢复细节和边缘，避免过锐化，保持颜色、构图和人物不变。",
  "换衣服": "根据提示词或参考图换衣服，保持脸、姿势、身形比例、布料褶皱和光影自然。",
  "抠图": "干净抠图，保留主体边缘和发丝细节，输出透明背景效果。",
  "高级改图": "高级改图，严格按照提示词修改，只改变需要修改的区域，未说明的部分保持不变。",
  "追色0.5版本": "轻量追色，参考图只用于色彩和氛围，保留原图肤质、结构和自然对比。",
  "追色追光0.5版本": "参考图追色追光，匹配色调和光影氛围，保持人物身份、肤色真实和构图不变。",
  "打字改图1.0": "按输入文字改图，保持未指定区域、人物、构图和真实光影不变。",
  "场景生成": "围绕主体生成新场景，保持人物、姿势和镜头角度，画面融合自然专业。",
  "场景融合": "把人物自然融合进参考场景，匹配透视、景深、色彩和光影。",
  "一键下雪": "真实冬季下雪效果，天空和前景有自然雪花，地面出现轻微积雪和冷色环境光，人物保持不变，不能生成花瓣、羽毛、纸片或白色碎片。",
  "一键下雨": "添加真实雨天氛围、湿润地面和柔和阴天光，保留主体和构图。",
  "一键秋天": "转换为秋天氛围，加入暖色树叶和自然季节色彩，保持人物与构图不变。",
  "水面波光": "添加水面波光和真实反射，高光自然柔和，保持主体和透视一致。",
  "圣诞氛围": "添加圣诞氛围、暖色灯光和节日装饰，保持人物身份和原始构图。",
  "草地修复": "修复草地和草坪质感，去除脏乱破损区域，保持户外自然光。",
  "一路生花": "人物保持不变，一条蜿蜒的粉色 + 白色 + 浅紫色花路，裙摆与花路无缝衔接,从前景延伸至背景，花瓣散落，形成 “步步生花” 的视觉效果,裙子四周增加花瓣,细节拉满，人物保持不变，一条蜿蜒的粉色 + 白色 + 浅紫色花路，裙摆与花路无缝衔接,从前景延伸至背景，花瓣散落，形成 “步步生花” 的视觉效果,裙子四周增加花瓣,细节拉满",
  "一键阴天": "转换为自然阴天效果，柔化光线和对比，保持主体和场景结构不变。",
  "Qwen改图": "Qwen 智能改图，严格按照提示词和参考图修改，未说明区域保持不变。",
  "人物光影优化": "优化人物光影，自然提亮面部，保留真实阴影、肤质和人物身份。",
  "逆光效果": "添加电影感逆光和边缘高光，控制眩光，保留面部细节和构图。",
  "室内补光": "室内自然补光，面部干净明亮，保留环境色彩和真实阴影。",
  "光影融合": "自然融合光影和阴影，增强空间层次，保留人物和材质细节。",
  "增加轮廓光": "给主体增加轮廓光，增强边缘分离感，保持脸部、姿势和质感不变。",
  "自然光": "自然日光效果，光线柔和真实，平衡对比，保持原场景和肤质。",
  "闪光灯效果": "专业闪光灯效果，增加眼神光和主体清晰度，阴影控制自然。",
  "黄昏逆光": "黄昏暖色逆光，金色氛围和自然光晕，保持人物真实肤色和身份。",
  "夕阳光": "夕阳暖光效果，柔和金色高光和自然户外氛围，保持主体和构图不变。",
  "清透胶片": "清透胶片色调，肤色自然，高光柔和，带轻微胶片颗粒。",
  "复古暖调": "复古暖色胶片风格，阴影柔和，色彩怀旧自然。",
  "富士人像": "富士人像色彩，绿色清新，肤色干净，胶片对比柔和。",
  "柯达婚礼": "柯达婚礼胶片色，暖色高光，肤色优雅，画面高级自然。",
  "森系胶片": "森系婚礼胶片色调，绿色干净，光线柔和，氛围浪漫。",
  "草坪婚礼": "草坪婚礼色调，草地自然干净，婚纱洁白，户外光真实。",
  "室内柔光": "室内柔光色调，高光奶油感，肤色干净，高级人像质感。",
  "黑白纪实": "黑白纪实婚礼照片，灰阶丰富，胶片颗粒自然，对比真实。",
  "背景提取": "提取主体并清理背景，保留人物边缘、发丝和透明背景细节。",
  "全景融合": "全景融合，保持主体身份，透视自然，光影和场景衔接干净。",
  "重绘融合": "局部重绘融合，保留脸和头发，自然重塑光影和纹理。",
  "自由光线": "自由创意光线，控制光源方向、阴影和氛围，形成高级人像光影。",
  "逆光增强": "增强逆光，增加干净轮廓光和自然高光，保留面部细节。",
  "柔光补光": "柔光补光，自然提亮面部，保留真实肤质和阴影层次。",
  "轮廓光": "电影感轮廓光，让主体从背景中分离，边缘高光自然。",
  "日落暖光": "日落暖光，金色氛围，肤色真实，高光柔和。",
  "天空柔光": "天空柔光，自然户外补光，阴影干净，色彩真实。",
  "窗边光": "窗边侧光，柔和高光与阴影，形成优雅人像光线。",
  "黄昏氛围": "黄昏氛围，暖色环境光，电影感自然色彩。",
  "阴天提亮": "阴天提亮，柔和对比，光线自然干净，保留细节。"
};

Object.assign(PROMPTS, {
  "磨皮": "完美人像磨皮，保留真实毛孔和肤质，清理瑕疵，肤色自然通透，保持本人五官、脸型和构图不变。",
  "网红感": "网红感人像精修，皮肤干净通透，眼神明亮，五官立体但不过度液化，保持人物身份和真实质感。",
  "夕阳逆光": "夕阳逆光打光，暖色边缘光和自然高光，控制眩光，保留面部细节和原始构图。",
  "镜头光晕": "镜头光晕效果，添加高级摄影光晕和轻微空气感，人物面部清晰自然，画面不脏不糊。",
  "室内窗口光": "室内窗口光，模拟窗边柔和侧光，提亮面部暗部，保留环境真实阴影和肤色。",
  "草坪阴影光": "草坪阴影光，修正户外草坪阴影和肤色偏差，增强自然轮廓光，保持背景真实。",
  "人物轮廓光": "人物轮廓光，为头发、肩部和服装边缘增加干净分离光，保持脸部和姿势不变。",
  "伦勃朗光": "伦勃朗光人像，形成专业三角光影，增强面部立体感，肤质自然高级。",
  "创成填充": "创成填充，按提示词自然补全画面，匹配周围材质、透视、光影和清晰度。",
  "物品移除": "物品移除，移除选中或指定物品并重建干净背景，保持周围纹理、透视和原始光影一致。",
  "打字改图": "按输入文字改图，只改变描述的部分，人物、构图、背景和未指定区域保持不变。",
  "一键花瓣": "一键花瓣，添加自然飘落花瓣和柔和氛围，人物保持不变，花瓣与景深、光影协调。",
  "头纱飘逸": "头纱飘逸，增加自然轻盈的头纱动态和柔和风感，保持人物五官、姿势、服装和场景不变。",
  "重绘": "高质量重绘，提升画面质感和细节，保持人物身份、构图、服装纹理和真实摄影感。",
  "追色1.0": "追色1.0，参考图只用于色彩、明暗和氛围，保留原图人物、肤质、构图和自然对比。",
  "追光追色1.0": "追光追色1.0，匹配参考图色调和光影氛围，保持人物身份、肤色真实和构图不变。"
});

const BACKEND_PROMPTS = {
  "修皮肤": "professional skin retouching, natural pores, clean skin, realistic skin texture, preserve face identity, commercial portrait quality",
  "自动精修": "automatic professional portrait retouching, clean skin tone, detailed eyes, natural facial texture, preserve identity and composition",
  "商业质感": "commercial portrait finishing, premium skin texture, crisp fabric detail, clean highlights, magazine retouching, preserve real facial features",
  "去双下巴": "subtle double chin correction, natural jawline refinement, preserve same person, realistic neck shadow, no face shape distortion",
  "老照片修复": "old photo restoration, remove scratches and stains, recover realistic facial detail, keep original era style, natural contrast",
  "照片放大": "high quality photo enlargement, sharpen fine details, preserve natural skin texture, clean edges, realistic high resolution",
  "修复裙摆": "repair dress hem and fabric folds, preserve original dress material, natural drape, realistic wedding photo texture",
  "瘦身": "natural body slimming, subtle waist and silhouette refinement, preserve pose, clothing texture and background geometry",
  "光影优化": "optimize portrait lighting, natural facial illumination, soft highlight and shadow balance, preserve skin texture and identity",
  "面部增强": "face detail enhancement, clear eyes, natural lips and eyebrows, realistic pores, preserve exact identity and face shape",
  "面部迁移": "reference guided face detail transfer, keep target pose and lighting, natural identity blend, realistic portrait texture",
  "局部重绘": "localized redraw, preserve original style, clean realistic result, maintain lighting and surrounding texture",
  "创成填充": "fill the selected or missing area naturally, match surrounding texture, lighting, perspective and camera quality",
  "物体移除": "remove selected object, reconstruct clean background, preserve realistic texture, perspective and original light",
  "高质感重绘": "high texture redraw, premium realistic material detail, keep original composition, clean high-end photo finish",
  "高质量放大": "professional high resolution upscale, realistic detail recovery, clean edges, no over-sharpening, preserve color and composition",
  "换衣服": "change clothing according to the prompt or reference, preserve face, pose, body proportion, natural fabric folds and lighting",
  "抠图": "clean subject cutout, preserve hair edge, transparent background, natural fine boundary",
  "高级改图": "advanced local image editing, follow the instruction precisely, preserve unchanged areas, realistic professional result",
  "追色0.5版本": "subtle reference color match, preserve original skin texture, natural contrast, professional color grading",
  "追色追光0.5版本": "reference color and lighting match, cinematic but natural highlight, preserve subject identity, realistic skin tone and contrast",
  "打字改图1.0": "edit the image according to the typed instruction, preserve unchanged subject, composition and realistic lighting",
  "场景生成": "generate a new realistic scene around the subject, preserve person, pose and camera angle, professional compositing",
  "场景融合": "blend subject into the reference scene, match perspective, lighting, depth and color, realistic professional composite",
  "一键下雪": "realistic winter snowfall, real snowflakes in foreground and background, light snow accumulation on grass and ground, cold winter atmosphere, preserve people and composition, not petals, not feathers, not paper flakes, not white leaves",
  "一键下雨": "realistic rain atmosphere, wet ground reflection, soft overcast lighting, preserve subject and composition",
  "一键秋天": "autumn atmosphere, warm leaves, natural seasonal color, realistic outdoor photo, preserve people and composition",
  "水面波光": "sparkling water surface light, realistic reflection, soft natural highlight, preserve main subject and perspective",
  "圣诞氛围": "christmas atmosphere, warm festive lights, realistic decorations, preserve person identity and original composition",
  "草地修复": "repair grass field, natural green lawn texture, remove damaged patches, realistic outdoor light",
  "一路生花": "人物保持不变，一条蜿蜒的粉色 + 白色 + 浅紫色花路，裙摆与花路无缝衔接,从前景延伸至背景，花瓣散落，形成 “步步生花” 的视觉效果,裙子四周增加花瓣,细节拉满，人物保持不变，一条蜿蜒的粉色 + 白色 + 浅紫色花路，裙摆与花路无缝衔接,从前景延伸至背景，花瓣散落，形成 “步步生花” 的视觉效果,裙子四周增加花瓣,细节拉满",
  "一键阴天": "natural cloudy day atmosphere, soft diffused light, controlled contrast, preserve subject and original scene structure",
  "Qwen改图": "Qwen image edit, follow the user instruction precisely, use reference image when provided, preserve unchanged areas",
  "人物光影优化": "visible professional portrait relighting, brighten face and eyes naturally, recover clean shadow detail, add soft directional highlights, preserve skin texture and identity",
  "逆光效果": "visible cinematic backlight, strong natural rim light around hair and shoulders, controlled warm flare, preserve face detail and original composition",
  "室内补光": "visible indoor fill lighting, clean softbox illumination on face, lift dark shadows naturally, preserve room color and realistic shadow direction",
  "光影融合": "visible light and shadow fusion, stronger dimensional depth, natural highlight and shadow transition, premium portrait relighting, preserve identity and material detail",
  "增加轮廓光": "visible cinematic rim light around the subject, clear edge separation on hair shoulders and clothing, realistic highlight, preserve face and pose",
  "自然光": "visible natural daylight correction, soft realistic illumination, brighter clean subject, balanced contrast, preserve original scene and skin texture",
  "闪光灯效果": "visible professional flash lighting effect, clean catchlight in eyes, crisp subject detail, controlled studio shadows, realistic photo finish",
  "黄昏逆光": "visible warm dusk backlight, golden atmosphere, low-angle rim glow, natural flare, preserve portrait identity and realistic skin tone",
  "夕阳光": "visible warm sunset sunlight, golden directional light from low angle, warm highlights on grass and subject edges, stronger outdoor golden glow, preserve subject, clothing and composition",
  "清透胶片": "clean transparent film look, natural skin tone, soft highlight, subtle grain",
  "复古暖调": "warm vintage film tone, soft shadow, nostalgic portrait color, natural skin",
  "富士人像": "fuji style portrait color, fresh green, clean skin, soft film contrast",
  "柯达婚礼": "kodak wedding film color, warm highlight, elegant skin tone, premium wedding photography",
  "森系胶片": "forest wedding film tone, clean green, gentle light, romantic mood",
  "草坪婚礼": "outdoor grass wedding color, natural green grass, clean white dress, realistic light",
  "室内柔光": "indoor soft light, creamy highlight, clean skin, high-end portrait retouching",
  "黑白纪实": "black and white documentary wedding photo, rich grayscale, film grain, realistic contrast",
  "背景提取": "clean background extraction, preserve subject edges, transparent-ready mask, realistic hair details",
  "全景融合": "wide scene fusion, preserve subject identity, natural perspective, clean light integration",
  "重绘融合": "localized light fusion, preserve face and hair, natural relighting, realistic texture",
  "自由光线": "creative studio light, controlled direction, soft cinematic shadow, premium portrait lighting",
  "逆光增强": "backlight enhancement, clean rim light, natural highlight, preserve face details",
  "柔光补光": "soft fill light, brighten face naturally, preserve realistic skin texture",
  "轮廓光": "cinematic rim light, separate subject from background, natural edge highlight",
  "日落暖光": "golden sunset light, warm atmosphere, realistic skin tone, soft highlight",
  "天空柔光": "soft skylight, natural outdoor fill light, clean shadow, realistic color",
  "窗边光": "window side light, gentle highlight and shadow, elegant portrait lighting",
  "黄昏氛围": "dusk atmosphere, warm ambient light, cinematic natural color",
  "阴天提亮": "cloudy day brightening, soft contrast, clean natural light, preserve detail"
};

Object.assign(BACKEND_PROMPTS, {
  "磨皮": "professional portrait skin smoothing, natural pores, clean blemish removal, realistic skin texture, preserve exact identity face shape and composition",
  "网红感": "social media style portrait retouching, clean translucent skin, bright eyes, subtle facial dimension, natural realistic face, preserve identity",
  "夕阳逆光": "warm sunset backlight, natural rim light, controlled flare, preserve facial detail and original composition",
  "镜头光晕": "premium camera lens flare, soft atmospheric glow, clean face detail, realistic portrait lighting",
  "室内窗口光": "soft indoor window side light, natural face fill, preserve room color and realistic shadows",
  "草坪阴影光": "outdoor lawn shadow correction, natural grass light, clean skin tone, subtle rim light, preserve realistic background",
  "人物轮廓光": "clean portrait rim light around hair shoulders and clothing, subject separation, preserve face pose and identity",
  "伦勃朗光": "professional Rembrandt portrait lighting, triangular cheek light, dimensional face shadows, realistic skin texture",
  "创成填充": "generative fill, complete the image naturally, match surrounding texture perspective lighting and quality",
  "物品移除": "remove selected object and reconstruct clean background, preserve surrounding texture perspective and original light",
  "打字改图": "edit according to the typed instruction only, preserve person composition background and unchanged areas",
  "一键花瓣": "natural falling flower petals, soft romantic atmosphere, realistic depth of field and lighting, preserve people and composition",
  "头纱飘逸": "flowing bridal veil motion, light airy fabric, natural wind, preserve face pose clothes and scene",
  "重绘": "high quality redraw and detail enhancement, preserve identity composition clothing texture and realistic photography",
  "追色1.0": "reference color match, transfer color mood and luminance only, preserve person skin texture composition and natural contrast",
  "追光追色1.0": "reference color and lighting match, transfer light mood naturally, preserve subject identity skin tone and composition"
});

const FEATURE_LORAS = {
  "修皮肤": "写真极速磨皮",
  "自动精修": "自动修图美颜",
  "商业质感": "提升质感XL",
  "去双下巴": "双下巴",
  "老照片修复": "模糊变清晰 2.2万步",
  "照片放大": "模糊变清晰 2.2万步",
  "修复裙摆": "裙摆",
  "瘦身": "万能修图",
  "光影优化": "优化光影",
  "面部增强": "万能修图",
  "面部迁移": "参考",
  "局部重绘": "万能修图",
  "创成填充": "Qwen-Image-Lightning-8steps",
  "物体移除": "removal_timestep_alpha",
  "高质感重绘": "add-detail-xl",
  "高质量放大": "add-detail-xl",
  "换衣服": "婚纱修图",
  "抠图": "",
  "高级改图": "Qwen-Image-Lightning-8steps",
  "追色0.5版本": "",
  "追色追光0.5版本": "优化光影",
  "打字改图1.0": "Qwen-Image-Lightning-8steps",
  "场景生成": "Qwen_角色置景_RoleScene Blend",
  "场景融合": "婚纱照融图",
  "一键下雪": "xuejing_encrypted.safetensors",
  "一键下雨": "wuqi",
  "一键秋天": "秋天优化",
  "水面波光": "XHN波光粼粼",
  "圣诞氛围": "圣诞",
  "草地修复": "草地01",
  "一路生花": "万能修图",
  "一键阴天": "wuqi",
  "Qwen改图": "Qwen-Image-Lightning-8steps",
  "人物光影优化": "优化光影",
  "逆光效果": "逆光1.1",
  "室内补光": "室内打光",
  "光影融合": "影子打光",
  "增加轮廓光": "轮廓光",
  "自然光": "优化光影",
  "闪光灯效果": "室内打光",
  "黄昏逆光": "黄昏逆光",
  "夕阳光": "黄昏逆光",
  "清透胶片": "提升质感XL",
  "复古暖调": "优化光影",
  "富士人像": "超真实日常",
  "柯达婚礼": "婚纱照融图",
  "森系胶片": "婚纱照融图",
  "草坪婚礼": "飞-草坪修复V3",
  "室内柔光": "室内打光",
  "黑白纪实": "提升质感XL",
  "背景提取": "",
  "全景融合": "婚纱照融图",
  "重绘融合": "优化光影",
  "自由光线": "室内打光",
  "逆光增强": "逆光1.1",
  "柔光补光": "影子打光",
  "轮廓光": "轮廓光",
  "日落暖光": "黄昏逆光",
  "天空柔光": "优化光影",
  "窗边光": "室内打光",
  "黄昏氛围": "黄昏逆光",
  "阴天提亮": "优化光影"
};

const FEATURE_LORA_CHOICES = {
  "修皮肤": [
    {
      "label": "写真磨皮",
      "value": "写真极速磨皮"
    },
    {
      "label": "自动美颜",
      "value": "自动修图美颜"
    },
    {
      "label": "万能精修",
      "value": "万能修图"
    }
  ],
  "自动精修": [
    {
      "label": "自动美颜",
      "value": "自动修图美颜"
    },
    {
      "label": "万能精修",
      "value": "万能修图"
    }
  ],
  "商业质感": [
    {
      "label": "提升质感",
      "value": "提升质感XL"
    },
    {
      "label": "细节增强",
      "value": "add-detail-xl"
    }
  ],
  "去双下巴": [
    {
      "label": "双下巴",
      "value": "双下巴"
    },
    {
      "label": "万能精修",
      "value": "万能修图"
    }
  ],
  "老照片修复": [
    {
      "label": "模糊变清晰",
      "value": "模糊变清晰 2.2万步"
    },
    {
      "label": "细节增强",
      "value": "add-detail-xl"
    }
  ],
  "照片放大": [
    {
      "label": "模糊变清晰",
      "value": "模糊变清晰 2.2万步"
    },
    {
      "label": "细节增强",
      "value": "add-detail-xl"
    }
  ],
  "修复裙摆": [
    {
      "label": "裙摆",
      "value": "裙摆"
    },
    {
      "label": "万能精修",
      "value": "万能修图"
    }
  ],
  "瘦身": [
    {
      "label": "万能修图",
      "value": "万能修图"
    },
    {
      "label": "细节增强",
      "value": "add-detail-xl"
    }
  ],
  "光影优化": [
    {
      "label": "优化光影",
      "value": "优化光影"
    },
    {
      "label": "逆光",
      "value": "逆光1.1"
    }
  ],
  "面部增强": [
    {
      "label": "万能修图",
      "value": "万能修图"
    },
    {
      "label": "自动美颜",
      "value": "自动修图美颜"
    }
  ],
  "面部迁移": [
    {
      "label": "参考",
      "value": "参考"
    },
    {
      "label": "万能精修",
      "value": "万能修图"
    }
  ],
  "局部重绘": [
    {
      "label": "万能修图",
      "value": "万能修图"
    },
    {
      "label": "细节增强",
      "value": "add-detail-xl"
    }
  ],
  "物体移除": [
    {
      "label": "移除",
      "value": "removal_timestep_alpha"
    },
    {
      "label": "万能修图",
      "value": "万能修图"
    }
  ],
  "高质感重绘": [
    {
      "label": "细节增强",
      "value": "add-detail-xl"
    },
    {
      "label": "提升质感",
      "value": "提升质感XL"
    }
  ],
  "高质量放大": [
    {
      "label": "细节增强",
      "value": "add-detail-xl"
    },
    {
      "label": "模糊变清晰",
      "value": "模糊变清晰 2.2万步"
    }
  ],
  "换衣服": [
    {
      "label": "婚纱修图",
      "value": "婚纱修图"
    },
    {
      "label": "Qwen编辑",
      "value": "Qwen-Image-Lightning-8steps"
    }
  ],
  "追色追光0.5版本": [
    {
      "label": "优化光影",
      "value": "优化光影"
    },
    {
      "label": "参考追色",
      "value": "参考"
    },
    {
      "label": "逆光",
      "value": "逆光1.1"
    }
  ],
  "打字改图1.0": [
    {
      "label": "Qwen编辑",
      "value": "Qwen-Image-Lightning-8steps"
    }
  ],
  "场景生成": [
    {
      "label": "角色置景",
      "value": "Qwen_角色置景_RoleScene Blend"
    },
    {
      "label": "婚纱融图",
      "value": "婚纱照融图"
    }
  ],
  "场景融合": [
    {
      "label": "婚纱融图",
      "value": "婚纱照融图"
    },
    {
      "label": "角色置景",
      "value": "Qwen_角色置景_RoleScene Blend"
    }
  ],
  "一键下雪": [
    {
      "label": "大雪 xuejing",
      "value": "xuejing_encrypted.safetensors"
    },
    {
      "label": "小雪 落雪留痕",
      "value": "Kontext_落雪留痕_encrypted.safetensors"
    }
  ],
  "一键下雨": [
    {
      "label": "阴雨雾气",
      "value": "wuqi"
    },
    {
      "label": "优化光影",
      "value": "优化光影"
    }
  ],
  "一键秋天": [
    {
      "label": "秋天优化",
      "value": "秋天优化"
    },
    {
      "label": "优化光影",
      "value": "优化光影"
    }
  ],
  "水面波光": [
    {
      "label": "波光",
      "value": "XHN波光粼粼"
    },
    {
      "label": "光影优化",
      "value": "优化光影"
    }
  ],
  "圣诞氛围": [
    {
      "label": "圣诞",
      "value": "圣诞"
    },
    {
      "label": "暖光",
      "value": "黄昏逆光"
    }
  ],
  "草地修复": [
    {
      "label": "草地01",
      "value": "草地01"
    },
    {
      "label": "高尔夫草地",
      "value": "高尔夫草地"
    },
    {
      "label": "草坪修复",
      "value": "飞-草坪修复V3"
    }
  ],
  "一路生花": [
    {
      "label": "万能修图",
      "value": "万能修图"
    },
    {
      "label": "优化光影",
      "value": "优化光影"
    }
  ],
  "一键阴天": [
    {
      "label": "雾气",
      "value": "wuqi"
    },
    {
      "label": "优化光影",
      "value": "优化光影"
    }
  ],
  "Qwen改图": [
    {
      "label": "Qwen编辑",
      "value": "Qwen-Image-Lightning-8steps"
    }
  ],
  "人物光影优化": [
    {
      "label": "优化光影",
      "value": "优化光影"
    },
    {
      "label": "影子打光",
      "value": "影子打光"
    }
  ],
  "逆光效果": [
    {
      "label": "逆光1.1",
      "value": "逆光1.1"
    },
    {
      "label": "逆光",
      "value": "逆光"
    }
  ],
  "室内补光": [
    {
      "label": "室内打光",
      "value": "室内打光"
    },
    {
      "label": "影子打光",
      "value": "影子打光"
    }
  ],
  "光影融合": [
    {
      "label": "影子打光",
      "value": "影子打光"
    },
    {
      "label": "优化光影",
      "value": "优化光影"
    }
  ],
  "增加轮廓光": [
    {
      "label": "轮廓光",
      "value": "轮廓光"
    },
    {
      "label": "逆光",
      "value": "逆光1.1"
    }
  ],
  "自然光": [
    {
      "label": "优化光影",
      "value": "优化光影"
    },
    {
      "label": "室内打光",
      "value": "室内打光"
    }
  ],
  "闪光灯效果": [
    {
      "label": "室内打光",
      "value": "室内打光"
    },
    {
      "label": "影子打光",
      "value": "影子打光"
    }
  ],
  "黄昏逆光": [
    {
      "label": "黄昏逆光",
      "value": "黄昏逆光"
    },
    {
      "label": "逆光1.1",
      "value": "逆光1.1"
    }
  ],
  "夕阳光": [
    {
      "label": "黄昏逆光",
      "value": "黄昏逆光"
    },
    {
      "label": "优化光影",
      "value": "优化光影"
    }
  ]
};

const LORA_NAME_ALIASES = {
  "写真极速磨皮": "写真极速磨皮.safetensors",
  "自动修图美颜": "自动修图美颜_encrypted.safetensors",
  "万能修图": "万能修图_encrypted.safetensors",
  "提升质感XL": "超真实日常.safetensors",
  "add-detail-xl": "模糊变清晰 2.2万步.safetensors",
  "双下巴": "双下巴.safetensors",
  "模糊变清晰 2.2万步": "模糊变清晰 2.2万步.safetensors",
  "裙摆": "裙摆.safetensors",
  "婚纱修图": "婚纱修图_encrypted.safetensors",
  "婚纱照融图": "婚纱照融图_encrypted.safetensors",
  "优化光影": "优化光影_encrypted.safetensors",
  "影子打光": "影子打光_encrypted.safetensors",
  "轮廓光": "轮廓光_encrypted.safetensors",
  "室内打光": "室内打光_encrypted.safetensors",
  "逆光": "逆光_encrypted.safetensors",
  "逆光1.1": "逆光1.1.safetensors",
  "黄昏逆光": "黄昏逆光_encrypted.safetensors",
  "参考": "参考.safetensors",
  "Qwen-Image-Lightning-8steps": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
  "Qwen_角色置景_RoleScene Blend": "Qwen_角色置景_RoleScene Blend(1).safetensors",
  "removal_timestep_alpha": "removal_timestep_alpha-2-1740.safetensors",
  "xuejing": "xuejing_encrypted.safetensors",
  "Kontext_落雪留痕": "Kontext_落雪留痕_encrypted.safetensors",
  "wuqi": "wuqi_encrypted.safetensors",
  "秋天优化": "秋天优化_encrypted.safetensors",
  "XHN波光粼粼": "XHN波光粼粼_encrypted.safetensors",
  "圣诞": "圣诞.safetensors",
  "草地01": "草地01.safetensors",
  "高尔夫草地": "高尔夫草地.safetensors",
  "飞-草坪修复V3": "飞-草坪修复V3.safetensors",
  "超真实日常": "超真实日常.safetensors",
  "古风修图_1.0": "古风修图_1.0_encrypted.safetensors",
  "床单": "床单_encrypted.safetensors",
  "guangbolinlin": "guangbolinlin_encrypted.safetensors",
  "jin_000002250": "jin_000002250_encrypted.safetensors",
  "一键竹林前景": "一键竹林前景_encrypted.safetensors",
  "云顶白底": "云顶白底.safetensors",
  "FULX极致真实": "FULX极致真实.safetensors",
  "flux2放大": "flux2放大.safetensors",
  "FLUX.1-Turbo-Alpha": "FLUX.1-Turbo-Alpha.safetensors"
};

function canonicalLoraName(name) {
  const raw = String(name || "").trim();
  if (!raw) return "";
  return LORA_NAME_ALIASES[raw] || raw;
}

const ORIGINAL_FEATURE_MAP = {
  "修皮肤": {
    "key": "portrait.skin-pro",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Portrait-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "自动精修": {
    "key": "portrait.auto-retouch-pro",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Portrait-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "商业质感": {
    "key": "portrait.commercial-texture",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Portrait-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "去双下巴": {
    "key": "portrait.double-chin",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Portrait-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "老照片修复": {
    "key": "quality.old-photo-repair",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Upscale-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "照片放大": {
    "key": "quality.photo-upscale",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Upscale-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "修复裙摆": {
    "key": "local.dress-hem-repair",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Local-Create-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "瘦身": {
    "key": "portrait.body-slim-pro",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Local-Create-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "光影优化": {
    "key": "portrait.light-optimize",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "面部增强": {
    "key": "portrait.face-enhance",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Portrait-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "面部迁移": {
    "key": "portrait.face-transfer",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Portrait-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "局部重绘": {
    "key": "local.redraw",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Local-Create-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "创成填充": {
    "key": "local.generative-fill",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Qwen-Edit-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "物体移除": {
    "key": "local.object-remove",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Local-Create-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "高质感重绘": {
    "key": "local.high-texture-redraw",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Local-Create-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "高质量放大": {
    "key": "quality.high-upscale",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Upscale-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "换衣服": {
    "key": "local.change-clothes",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Qwen-Edit-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "抠图": {
    "key": "background.extract",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Transparent-Cutout-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "高级改图": {
    "key": "edit.qwen-advanced",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Qwen-Edit-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "追色0.5版本": {
    "key": "color.match-soft",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Color-Light-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "追色追光0.5版本": {
    "key": "color.match-light-pro",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Color-Light-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "打字改图1.0": {
    "key": "creative.text-edit",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Qwen-Edit-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "场景生成": {
    "key": "creative.scene-generate",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Creative-Cloud-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "场景融合": {
    "key": "creative.scene-fusion",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Creative-Cloud-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "一键下雪": {
    "key": "creative.heavy-snow",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Creative-Cloud-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "一键下雨": {
    "key": "creative.rain",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Creative-Cloud-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "一键秋天": {
    "key": "creative.autumn",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Creative-Cloud-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "水面波光": {
    "key": "creative.water-shimmer",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Creative-Cloud-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "圣诞氛围": {
    "key": "creative.christmas",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Creative-Cloud-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "草地修复": {
    "key": "creative.grass-repair",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Creative-Cloud-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "一路生花": {
    "key": "creative.flower-road",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Creative-Cloud-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "一键阴天": {
    "key": "creative.cloudy",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Creative-Cloud-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "Qwen改图": {
    "key": "creative.qwen-edit",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Qwen-Edit-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "人物光影优化": {
    "key": "lighting.portrait-optimize",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "逆光效果": {
    "key": "lighting.backlight",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "室内补光": {
    "key": "lighting.indoor-fill",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "光影融合": {
    "key": "lighting.shadow-fusion",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "增加轮廓光": {
    "key": "lighting.rim",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "自然光": {
    "key": "lighting.natural",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "闪光灯效果": {
    "key": "lighting.flash",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "黄昏逆光": {
    "key": "lighting.dusk-backlight",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "夕阳光": {
    "key": "lighting.sunset",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "清透胶片": {
    "key": "film.clear",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Color-Light-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "复古暖调": {
    "key": "film.warm-vintage",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Color-Light-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "富士人像": {
    "key": "film.fuji-portrait",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Color-Light-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "柯达婚礼": {
    "key": "film.kodak-wedding",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Color-Light-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "森系胶片": {
    "key": "film.forest-wedding",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Color-Light-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "草坪婚礼": {
    "key": "film.grass-wedding",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Creative-Cloud-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "室内柔光": {
    "key": "film.indoor-soft",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "黑白纪实": {
    "key": "film.blackwhite-doc",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Color-Light-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "背景提取": {
    "key": "background.extract",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Transparent-Cutout-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "全景融合": {
    "key": "scene.panorama-fusion",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Creative-Cloud-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "重绘融合": {
    "key": "scene.redraw-fusion",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-Local-Create-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "自由光线": {
    "key": "light.free",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "逆光增强": {
    "key": "light.backlight",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "柔光补光": {
    "key": "light.fill",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "轮廓光": {
    "key": "light.rim",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "日落暖光": {
    "key": "light.sunset-warm",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "天空柔光": {
    "key": "light.sky-soft",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "窗边光": {
    "key": "light.window",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "黄昏氛围": {
    "key": "light.dusk",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  },
  "阴天提亮": {
    "key": "light.cloudy-bright",
    "component": "xhnProfessionalWorkflow",
    "workflowId": "XHN-AI-Lighting-Pro",
    "route": "local-custom",
    "evidence": "XHN rebuilt professional local workflow, no legacy cloud route",
    "params": [
      "prompt",
      "checkpoint",
      "steps",
      "cfg",
      "denoise",
      "referenceStrength",
      "lightStrength",
      "blendStrength",
      "identityLock"
    ]
  }
};

const OPERATION_PROFILES = {
  "修皮肤": {
    "workflow": "XHN-Portrait-Pro.json",
    "operation": "portrait.skin-pro",
    "lora": "写真极速磨皮",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4.2,
      "denoise": 0.18,
      "maxDenoise": 0.3,
      "blendStrength": 0.82,
      "lowFrequency": 0.36,
      "sourceKeep": 0.36,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "自动精修": {
    "workflow": "XHN-Portrait-Pro.json",
    "operation": "portrait.auto-retouch-pro",
    "lora": "自动修图美颜",
    "preserveIdentity": true,
    "defaults": {
      "steps": 12,
      "cfg": 4.4,
      "denoise": 0.22,
      "maxDenoise": 0.34,
      "blendStrength": 0.8,
      "lowFrequency": 0.42,
      "sourceKeep": 0.32,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "商业质感": {
    "workflow": "XHN-Portrait-Pro.json",
    "operation": "portrait.commercial-texture",
    "lora": "提升质感XL",
    "preserveIdentity": true,
    "defaults": {
      "steps": 14,
      "cfg": 4.8,
      "denoise": 0.24,
      "maxDenoise": 0.36,
      "blendStrength": 0.78,
      "lowFrequency": 0.55,
      "lightStrength": 0.42,
      "sourceKeep": 0.3,
      "width": 1344,
      "sampler": "dpmpp_2m"
    }
  },
  "去双下巴": {
    "workflow": "XHN-Portrait-Pro.json",
    "operation": "portrait.double-chin",
    "lora": "双下巴",
    "preserveIdentity": true,
    "defaults": {
      "steps": 12,
      "cfg": 4.2,
      "denoise": 0.24,
      "maxDenoise": 0.34,
      "blendStrength": 0.82,
      "sourceKeep": 0.36,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "老照片修复": {
    "workflow": "XHN-Upscale-Pro.json",
    "operation": "quality.old-photo-repair",
    "lora": "模糊变清晰 2.2万步",
    "preserveIdentity": true,
    "defaults": {
      "steps": 12,
      "cfg": 4,
      "denoise": 0.28,
      "maxDenoise": 0.38,
      "blendStrength": 0.76,
      "lowFrequency": 0.5,
      "sourceKeep": 0.3,
      "width": 1536,
      "sampler": "dpmpp_2m"
    }
  },
  "照片放大": {
    "workflow": "XHN-Upscale-Pro.json",
    "operation": "quality.photo-upscale",
    "lora": "模糊变清晰 2.2万步",
    "preserveIdentity": true,
    "localProcessor": "upscale",
    "disableLora": true,
    "defaults": {
      "steps": 10,
      "cfg": 3.8,
      "denoise": 0.16,
      "maxDenoise": 0.24,
      "blendStrength": 0.86,
      "lowFrequency": 0.35,
      "sourceKeep": 0.45,
      "upscaleScale": 1.6,
      "maxUpscaleSide": 8192,
      "disableSourceBlend": true,
      "width": 1536,
      "sampler": "dpmpp_2m"
    }
  },
  "修复裙摆": {
    "workflow": "XHN-Local-Create-Pro.json",
    "operation": "local.dress-hem-repair",
    "lora": "裙摆",
    "preserveIdentity": true,
    "defaults": {
      "steps": 14,
      "cfg": 4.5,
      "denoise": 0.34,
      "maxDenoise": 0.46,
      "blendStrength": 0.78,
      "sourceKeep": 0.28,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "瘦身": {
    "workflow": "XHN-Local-Create-Pro.json",
    "operation": "portrait.body-slim-pro",
    "lora": "万能修图",
    "preserveIdentity": true,
    "defaults": {
      "steps": 12,
      "cfg": 4,
      "denoise": 0.26,
      "maxDenoise": 0.38,
      "blendStrength": 0.84,
      "sourceKeep": 0.38,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "光影优化": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "portrait.light-optimize",
    "lora": "优化光影",
    "preserveIdentity": true,
    "defaults": {
      "steps": 12,
      "cfg": 4.2,
      "denoise": 0.22,
      "maxDenoise": 0.34,
      "lightStrength": 0.58,
      "blendStrength": 0.82,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "面部增强": {
    "workflow": "XHN-Portrait-Pro.json",
    "operation": "portrait.face-enhance",
    "lora": "万能修图",
    "preserveIdentity": true,
    "defaults": {
      "steps": 12,
      "cfg": 4.3,
      "denoise": 0.2,
      "maxDenoise": 0.3,
      "blendStrength": 0.84,
      "lowFrequency": 0.46,
      "sourceKeep": 0.38,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "面部迁移": {
    "workflow": "XHN-Portrait-Pro.json",
    "operation": "portrait.face-transfer",
    "lora": "参考",
    "preserveIdentity": true,
    "requiresReference": true,
    "referenceMode": "style-reference",
    "defaults": {
      "steps": 14,
      "cfg": 4.4,
      "denoise": 0.28,
      "maxDenoise": 0.4,
      "referenceStrength": 0.58,
      "blendStrength": 0.74,
      "sourceKeep": 0.28,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "局部重绘": {
    "workflow": "XHN-Local-Create-Pro.json",
    "operation": "local.redraw",
    "lora": "万能修图",
    "preserveIdentity": true,
    "requiresSelection": true,
    "selectionMode": "crop-return",
    "defaults": {
      "steps": 12,
      "cfg": 4.6,
      "denoise": 0.34,
      "maxDenoise": 0.48,
      "blendStrength": 0.78,
      "sourceKeep": 0.26,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "创成填充": {
    "workflow": "XHN-Qwen-Edit-Pro.json",
    "operation": "local.generative-fill",
    "lora": "Qwen-Image-Lightning-8steps",
    "preserveIdentity": true,
    "requiresReference": true,
    "referenceMode": "style-reference",
    "defaults": {
      "steps": 8,
      "cfg": 1,
      "denoise": 0.92,
      "maxDenoise": 1,
      "referenceStrength": 0.6,
      "blendStrength": 0.78,
      "width": 1024,
      "sampler": "euler",
      "qwenLoraStrength": 1,
      "disableSourceBlend": true
    }
  },
  "物体移除": {
    "workflow": "XHN-Local-Create-Pro.json",
    "operation": "local.object-remove",
    "lora": "removal_timestep_alpha",
    "preserveIdentity": true,
    "requiresSelection": true,
    "selectionMode": "crop-return",
    "localProcessor": "remove",
    "disableSourceBlend": true,
    "negative": "remaining object, leftover petals, white fragments, duplicated debris, blur, smear, broken background texture, low quality, artifacts",
    "defaults": {
      "steps": 14,
      "cfg": 4.2,
      "denoise": 0.48,
      "maxDenoise": 0.62,
      "blendStrength": 1,
      "sourceKeep": 0,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "高质感重绘": {
    "workflow": "XHN-Local-Create-Pro.json",
    "operation": "local.high-texture-redraw",
    "lora": "add-detail-xl",
    "preserveIdentity": true,
    "defaults": {
      "steps": 14,
      "cfg": 4.8,
      "denoise": 0.3,
      "maxDenoise": 0.44,
      "blendStrength": 0.8,
      "lowFrequency": 0.52,
      "sourceKeep": 0.3,
      "width": 1344,
      "sampler": "dpmpp_2m"
    }
  },
  "高质量放大": {
    "workflow": "XHN-Upscale-Pro.json",
    "operation": "quality.high-upscale",
    "lora": "add-detail-xl",
    "preserveIdentity": true,
    "localProcessor": "upscale",
    "disableLora": true,
    "defaults": {
      "steps": 10,
      "cfg": 3.8,
      "denoise": 0.16,
      "maxDenoise": 0.24,
      "blendStrength": 0.86,
      "lowFrequency": 0.42,
      "sourceKeep": 0.45,
      "upscaleScale": 2,
      "maxUpscaleSide": 8192,
      "disableSourceBlend": true,
      "width": 1536,
      "sampler": "dpmpp_2m"
    }
  },
  "换衣服": {
    "workflow": "XHN-Qwen-Edit-Pro.json",
    "operation": "local.change-clothes",
    "lora": "婚纱修图",
    "preserveIdentity": true,
    "requiresReference": true,
    "referenceMode": "style-reference",
    "defaults": {
      "steps": 8,
      "cfg": 1,
      "denoise": 0.88,
      "maxDenoise": 1,
      "referenceStrength": 0.65,
      "blendStrength": 0.78,
      "width": 1024,
      "sampler": "euler",
      "qwenLoraStrength": 1,
      "disableSourceBlend": true
    }
  },
  "抠图": {
    "workflow": "XHN-Transparent-Cutout-Pro.json",
    "operation": "background.extract",
    "lora": "",
    "preserveIdentity": true,
    "localProcessor": "background",
    "disableLora": true,
    "defaults": {
      "steps": 1,
      "cfg": 1,
      "denoise": 0.18,
      "maxDenoise": 0.24,
      "blendStrength": 0.86,
      "width": 1024,
      "disableSourceBlend": true
    }
  },
  "高级改图": {
    "workflow": "XHN-Qwen-Edit-Pro.json",
    "operation": "edit.qwen-advanced",
    "lora": "Qwen-Image-Lightning-8steps",
    "preserveIdentity": true,
    "requiresReference": true,
    "referenceMode": "style-reference",
    "defaults": {
      "steps": 8,
      "cfg": 1,
      "denoise": 0.86,
      "maxDenoise": 1,
      "referenceStrength": 0.62,
      "blendStrength": 0.78,
      "width": 1024,
      "sampler": "euler",
      "qwenLoraStrength": 1,
      "disableSourceBlend": true
    }
  },
  "追色0.5版本": {
    "workflow": "XHN-Color-Light-Pro.json",
    "operation": "color.match-soft",
    "lora": "",
    "preserveIdentity": true,
    "requiresReference": true,
    "referenceMode": "color-match",
    "colorOnly": true,
    "disableLora": true,
    "defaults": {
      "steps": 1,
      "cfg": 1,
      "denoise": 0.12,
      "maxDenoise": 0.18,
      "referenceStrength": 0.55,
      "originalStrength": 0.88,
      "blendStrength": 0.92,
      "width": 1280,
      "disableSourceBlend": true
    }
  },
  "追色追光0.5版本": {
    "workflow": "XHN-Color-Light-Pro.json",
    "operation": "color.match-light",
    "lora": "",
    "preserveIdentity": true,
    "requiresReference": true,
    "referenceMode": "color-match",
    "colorOnly": true,
    "disableLora": true,
    "defaults": {
      "steps": 1,
      "cfg": 1,
      "denoise": 0.12,
      "maxDenoise": 0.18,
      "referenceStrength": 0.62,
      "originalStrength": 0.78,
      "lightStrength": 0.55,
      "blendStrength": 0.92,
      "sourceKeep": 0.5,
      "disableSourceBlend": true,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "打字改图1.0": {
    "workflow": "XHN-Qwen-Edit-Pro.json",
    "operation": "creative.text-edit",
    "lora": "Qwen-Image-Lightning-8steps",
    "preserveIdentity": true,
    "referenceMode": "style-reference",
    "defaults": {
      "steps": 8,
      "cfg": 1,
      "denoise": 0.88,
      "maxDenoise": 1,
      "referenceStrength": 0.58,
      "blendStrength": 0.78,
      "width": 1024,
      "sampler": "euler",
      "qwenLoraStrength": 1,
      "disableSourceBlend": true
    }
  },
  "场景生成": {
    "workflow": "XHN-Creative-Cloud-Pro.json",
    "operation": "creative.scene-generate",
    "lora": "Qwen_角色置景_RoleScene Blend",
    "preserveIdentity": true,
    "referenceMode": "none",
    "defaults": {
      "steps": 14,
      "cfg": 4.6,
      "denoise": 0.46,
      "maxDenoise": 0.58,
      "blendStrength": 0.72,
      "lightStrength": 0.48,
      "sourceKeep": 0.22,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "场景融合": {
    "workflow": "XHN-Creative-Cloud-Pro.json",
    "operation": "creative.scene-fusion",
    "lora": "婚纱照融图",
    "preserveIdentity": true,
    "referenceMode": "none",
    "defaults": {
      "steps": 14,
      "cfg": 4.4,
      "denoise": 0.42,
      "maxDenoise": 0.54,
      "blendStrength": 0.76,
      "lightStrength": 0.5,
      "sourceKeep": 0.26,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "一键下雪": {
    "workflow": "XHN-Creative-Cloud-Pro.json",
    "operation": "creative.heavy-snow",
    "lora": "xuejing_encrypted.safetensors",
    "preserveIdentity": true,
    "referenceMode": "none",
    "negative": "flower petals, feathers, paper flakes, confetti, white leaves, cotton pieces, fake snow sticker, low quality, blurry, distorted, artifacts",
    "defaults": {
      "steps": 14,
      "cfg": 4.7,
      "denoise": 0.52,
      "maxDenoise": 0.64,
      "blendStrength": 0.76,
      "originalStrength": 0.72,
      "lightStrength": 0.58,
      "sourceKeep": 0.24,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "一键下雨": {
    "workflow": "XHN-Creative-Cloud-Pro.json",
    "operation": "creative.rain",
    "lora": "wuqi",
    "preserveIdentity": true,
    "referenceMode": "none",
    "defaults": {
      "steps": 12,
      "cfg": 4.2,
      "denoise": 0.36,
      "maxDenoise": 0.48,
      "blendStrength": 0.82,
      "lightStrength": 0.42,
      "sourceKeep": 0.3,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "一键秋天": {
    "workflow": "XHN-Creative-Cloud-Pro.json",
    "operation": "creative.autumn",
    "lora": "秋天优化",
    "preserveIdentity": true,
    "referenceMode": "none",
    "defaults": {
      "steps": 12,
      "cfg": 4.2,
      "denoise": 0.3,
      "maxDenoise": 0.42,
      "blendStrength": 0.84,
      "lightStrength": 0.48,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "水面波光": {
    "workflow": "XHN-Creative-Cloud-Pro.json",
    "operation": "creative.water-shimmer",
    "lora": "XHN波光粼粼",
    "preserveIdentity": true,
    "referenceMode": "none",
    "defaults": {
      "steps": 12,
      "cfg": 4.2,
      "denoise": 0.32,
      "maxDenoise": 0.44,
      "blendStrength": 0.82,
      "lightStrength": 0.66,
      "sourceKeep": 0.32,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "圣诞氛围": {
    "workflow": "XHN-Creative-Cloud-Pro.json",
    "operation": "creative.christmas",
    "lora": "圣诞",
    "preserveIdentity": true,
    "referenceMode": "none",
    "defaults": {
      "steps": 12,
      "cfg": 4.3,
      "denoise": 0.34,
      "maxDenoise": 0.46,
      "blendStrength": 0.82,
      "lightStrength": 0.56,
      "sourceKeep": 0.3,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "草地修复": {
    "workflow": "XHN-Creative-Cloud-Pro.json",
    "operation": "creative.grass-repair",
    "lora": "草地01",
    "preserveIdentity": true,
    "referenceMode": "none",
    "defaults": {
      "steps": 12,
      "cfg": 4.2,
      "denoise": 0.38,
      "maxDenoise": 0.5,
      "blendStrength": 0.82,
      "lightStrength": 0.4,
      "sourceKeep": 0.28,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "一路生花": {
    "workflow": "XHN-Creative-Cloud-Pro.json",
    "operation": "creative.flower-road",
    "lora": "万能修图",
    "preserveIdentity": true,
    "referenceMode": "none",
    "defaults": {
      "steps": 14,
      "cfg": 4.5,
      "denoise": 0.38,
      "maxDenoise": 0.5,
      "blendStrength": 0.8,
      "lightStrength": 0.52,
      "sourceKeep": 0.28,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "一键阴天": {
    "workflow": "XHN-Creative-Cloud-Pro.json",
    "operation": "creative.cloudy",
    "lora": "wuqi",
    "preserveIdentity": true,
    "referenceMode": "none",
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.26,
      "maxDenoise": 0.38,
      "blendStrength": 0.86,
      "lightStrength": 0.38,
      "sourceKeep": 0.38,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "Qwen改图": {
    "workflow": "XHN-Qwen-Edit-Pro.json",
    "operation": "creative.qwen-edit",
    "lora": "Qwen-Image-Lightning-8steps",
    "preserveIdentity": true,
    "referenceMode": "style-reference",
    "defaults": {
      "steps": 8,
      "cfg": 1,
      "denoise": 0.9,
      "maxDenoise": 1,
      "referenceStrength": 0.62,
      "blendStrength": 0.78,
      "width": 1024,
      "sampler": "euler",
      "qwenLoraStrength": 1,
      "disableSourceBlend": true
    }
  },
  "人物光影优化": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "lighting.portrait-optimize",
    "lora": "优化光影",
    "preserveIdentity": true,
    "defaults": {
      "steps": 14,
      "cfg": 4.6,
      "denoise": 0.34,
      "maxDenoise": 0.52,
      "lightStrength": 0.72,
      "blendStrength": 0.74,
      "lowFrequency": 0.2,
      "sourceKeep": 0.18,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "逆光效果": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "lighting.backlight",
    "lora": "逆光1.1",
    "preserveIdentity": true,
    "defaults": {
      "steps": 14,
      "cfg": 4.8,
      "denoise": 0.4,
      "maxDenoise": 0.58,
      "lightStrength": 0.8,
      "blendStrength": 0.7,
      "lowFrequency": 0.26,
      "sourceKeep": 0.12,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "室内补光": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "lighting.indoor-fill",
    "lora": "室内打光",
    "preserveIdentity": true,
    "defaults": {
      "steps": 14,
      "cfg": 4.6,
      "denoise": 0.36,
      "maxDenoise": 0.54,
      "lightStrength": 0.74,
      "blendStrength": 0.72,
      "lowFrequency": 0.2,
      "sourceKeep": 0.16,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "光影融合": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "lighting.shadow-fusion",
    "lora": "影子打光",
    "preserveIdentity": true,
    "defaults": {
      "steps": 14,
      "cfg": 4.7,
      "denoise": 0.38,
      "maxDenoise": 0.56,
      "lightStrength": 0.76,
      "blendStrength": 0.7,
      "lowFrequency": 0.24,
      "sourceKeep": 0.14,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "增加轮廓光": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "lighting.rim",
    "lora": "轮廓光",
    "preserveIdentity": true,
    "defaults": {
      "steps": 14,
      "cfg": 4.7,
      "denoise": 0.38,
      "maxDenoise": 0.56,
      "lightStrength": 0.78,
      "blendStrength": 0.7,
      "lowFrequency": 0.24,
      "sourceKeep": 0.14,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "自然光": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "lighting.natural",
    "lora": "优化光影",
    "preserveIdentity": true,
    "defaults": {
      "steps": 12,
      "cfg": 4.3,
      "denoise": 0.32,
      "maxDenoise": 0.48,
      "lightStrength": 0.6,
      "blendStrength": 0.76,
      "lowFrequency": 0.18,
      "sourceKeep": 0.22,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "闪光灯效果": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "lighting.flash",
    "lora": "室内打光",
    "preserveIdentity": true,
    "defaults": {
      "steps": 14,
      "cfg": 4.7,
      "denoise": 0.38,
      "maxDenoise": 0.56,
      "lightStrength": 0.76,
      "blendStrength": 0.7,
      "lowFrequency": 0.26,
      "sourceKeep": 0.14,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "黄昏逆光": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "lighting.dusk-backlight",
    "lora": "黄昏逆光",
    "preserveIdentity": true,
    "defaults": {
      "steps": 14,
      "cfg": 4.8,
      "denoise": 0.48,
      "maxDenoise": 0.6,
      "lightStrength": 0.94,
      "blendStrength": 0.74,
      "lowFrequency": 0.26,
      "sourceKeep": 0.06,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "夕阳光": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "lighting.sunset",
    "lora": "黄昏逆光",
    "preserveIdentity": true,
    "defaults": {
      "steps": 14,
      "cfg": 4.8,
      "denoise": 0.48,
      "maxDenoise": 0.6,
      "lightStrength": 0.96,
      "blendStrength": 0.74,
      "lowFrequency": 0.24,
      "sourceKeep": 0.06,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "清透胶片": {
    "workflow": "XHN-Color-Light-Pro.json",
    "operation": "film.clear",
    "lora": "提升质感XL",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.22,
      "maxDenoise": 0.34,
      "lightStrength": 0.45,
      "blendStrength": 0.88,
      "sourceKeep": 0.38,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "复古暖调": {
    "workflow": "XHN-Color-Light-Pro.json",
    "operation": "film.warm-vintage",
    "lora": "优化光影",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.22,
      "maxDenoise": 0.34,
      "lightStrength": 0.45,
      "blendStrength": 0.88,
      "sourceKeep": 0.38,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "富士人像": {
    "workflow": "XHN-Color-Light-Pro.json",
    "operation": "film.fuji-portrait",
    "lora": "超真实日常",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.22,
      "maxDenoise": 0.34,
      "lightStrength": 0.45,
      "blendStrength": 0.88,
      "sourceKeep": 0.38,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "柯达婚礼": {
    "workflow": "XHN-Color-Light-Pro.json",
    "operation": "film.kodak-wedding",
    "lora": "婚纱照融图",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.22,
      "maxDenoise": 0.34,
      "lightStrength": 0.45,
      "blendStrength": 0.88,
      "sourceKeep": 0.38,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "森系胶片": {
    "workflow": "XHN-Color-Light-Pro.json",
    "operation": "film.forest-wedding",
    "lora": "婚纱照融图",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.22,
      "maxDenoise": 0.34,
      "lightStrength": 0.45,
      "blendStrength": 0.88,
      "sourceKeep": 0.38,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "草坪婚礼": {
    "workflow": "XHN-Creative-Cloud-Pro.json",
    "operation": "film.grass-wedding",
    "lora": "飞-草坪修复V3",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.22,
      "maxDenoise": 0.34,
      "lightStrength": 0.45,
      "blendStrength": 0.88,
      "sourceKeep": 0.38,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "室内柔光": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "film.indoor-soft",
    "lora": "室内打光",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.22,
      "maxDenoise": 0.34,
      "lightStrength": 0.45,
      "blendStrength": 0.88,
      "sourceKeep": 0.38,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "黑白纪实": {
    "workflow": "XHN-Color-Light-Pro.json",
    "operation": "film.blackwhite-doc",
    "lora": "提升质感XL",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.22,
      "maxDenoise": 0.34,
      "lightStrength": 0.45,
      "blendStrength": 0.88,
      "sourceKeep": 0.38,
      "width": 1280,
      "sampler": "dpmpp_2m"
    }
  },
  "背景提取": {
    "workflow": "XHN-Transparent-Cutout-Pro.json",
    "operation": "background.extract",
    "lora": "",
    "preserveIdentity": true,
    "localProcessor": "background",
    "disableLora": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.18,
      "maxDenoise": 0.24,
      "lightStrength": 0.58,
      "blendStrength": 0.84,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m",
      "disableSourceBlend": true
    }
  },
  "全景融合": {
    "workflow": "XHN-Creative-Cloud-Pro.json",
    "operation": "scene.panorama-fusion",
    "lora": "婚纱照融图",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.24,
      "maxDenoise": 0.36,
      "lightStrength": 0.58,
      "blendStrength": 0.84,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m",
      "disableSourceBlend": false
    }
  },
  "重绘融合": {
    "workflow": "XHN-Local-Create-Pro.json",
    "operation": "scene.redraw-fusion",
    "lora": "优化光影",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.24,
      "maxDenoise": 0.36,
      "lightStrength": 0.58,
      "blendStrength": 0.84,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m",
      "disableSourceBlend": false
    }
  },
  "自由光线": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "light.free",
    "lora": "室内打光",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.24,
      "maxDenoise": 0.36,
      "lightStrength": 0.58,
      "blendStrength": 0.84,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m",
      "disableSourceBlend": false
    }
  },
  "逆光增强": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "light.backlight",
    "lora": "逆光1.1",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.24,
      "maxDenoise": 0.36,
      "lightStrength": 0.58,
      "blendStrength": 0.84,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m",
      "disableSourceBlend": false
    }
  },
  "柔光补光": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "light.fill",
    "lora": "影子打光",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.24,
      "maxDenoise": 0.36,
      "lightStrength": 0.58,
      "blendStrength": 0.84,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m",
      "disableSourceBlend": false
    }
  },
  "轮廓光": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "light.rim",
    "lora": "轮廓光",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.24,
      "maxDenoise": 0.36,
      "lightStrength": 0.58,
      "blendStrength": 0.84,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m",
      "disableSourceBlend": false
    }
  },
  "日落暖光": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "light.sunset-warm",
    "lora": "黄昏逆光",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.24,
      "maxDenoise": 0.36,
      "lightStrength": 0.58,
      "blendStrength": 0.84,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m",
      "disableSourceBlend": false
    }
  },
  "天空柔光": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "light.sky-soft",
    "lora": "优化光影",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.24,
      "maxDenoise": 0.36,
      "lightStrength": 0.58,
      "blendStrength": 0.84,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m",
      "disableSourceBlend": false
    }
  },
  "窗边光": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "light.window",
    "lora": "室内打光",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.24,
      "maxDenoise": 0.36,
      "lightStrength": 0.58,
      "blendStrength": 0.84,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m",
      "disableSourceBlend": false
    }
  },
  "黄昏氛围": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "light.dusk",
    "lora": "黄昏逆光",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.24,
      "maxDenoise": 0.36,
      "lightStrength": 0.58,
      "blendStrength": 0.84,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m",
      "disableSourceBlend": false
    }
  },
  "阴天提亮": {
    "workflow": "XHN-AI-Lighting-Pro.json",
    "operation": "light.cloudy-bright",
    "lora": "优化光影",
    "preserveIdentity": true,
    "defaults": {
      "steps": 10,
      "cfg": 4,
      "denoise": 0.24,
      "maxDenoise": 0.36,
      "lightStrength": 0.58,
      "blendStrength": 0.84,
      "sourceKeep": 0.34,
      "width": 1280,
      "sampler": "dpmpp_2m",
      "disableSourceBlend": false
    }
  }
};

const DEDICATED_WORKFLOW_BY_FEATURE = {
  "修皮肤": "XHN-Pro-Skin-Retouch.json",
  "自动精修": "XHN-Pro-Auto-Retouch.json",
  "去双下巴": "XHN-Pro-Double-Chin.json",
  "照片放大": "XHN-Pro-Photo-Upscale.json",
  "瘦身": "XHN-Pro-Body-Slim.json",
  "物体移除": "XHN-Pro-Object-Remove.json",
  "高质量放大": "XHN-Pro-High-Upscale.json",
  "抠图": "XHN-Pro-Cutout.json",
  "追色0.5版本": "XHN-Pro-Color-Match.json",
  "追色追光0.5版本": "XHN-Pro-Color-Light-Match.json",
  "逆光效果": "XHN-Pro-Backlight.json",
  "增加轮廓光": "XHN-Pro-Rim-Light.json",
  "黄昏逆光": "XHN-Pro-Dusk-Backlight.json"
};

Object.keys(DEDICATED_WORKFLOW_BY_FEATURE).forEach(function (feature) {
  WORKFLOW_BY_FEATURE[feature] = DEDICATED_WORKFLOW_BY_FEATURE[feature];
  if (OPERATION_PROFILES[feature]) {
    OPERATION_PROFILES[feature].workflow = DEDICATED_WORKFLOW_BY_FEATURE[feature];
    OPERATION_PROFILES[feature].localProcessor = "";
    OPERATION_PROFILES[feature].disableWorkflowLoras = false;
  }
});


Object.keys(WORKFLOW_BY_CATEGORY).forEach(function (category) {
  WORKFLOW_BY_CATEGORY[category] = MAIN_WORKFLOW_FILE;
});
Object.keys(WORKFLOW_BY_FEATURE).forEach(function (feature) {
  WORKFLOW_BY_FEATURE[feature] = MAIN_WORKFLOW_FILE;
});
Object.keys(OPERATION_PROFILES).forEach(function (feature) {
  OPERATION_PROFILES[feature].workflow = MAIN_WORKFLOW_FILE;
});
Object.assign(OPERATION_PROFILES, {
  "磨皮": { workflow: MAIN_WORKFLOW_FILE, operation: "portrait.skin", originalRoute: "local-comfy-replacement", denoise: 0.22, lowFrequency: 0.42, blendStrength: 0.82, defaults: { steps: 12, cfg: 4.2, denoise: 0.22, blendStrength: 0.82 } },
  "网红感": { workflow: MAIN_WORKFLOW_FILE, operation: "portrait.social", originalRoute: "local-comfy-replacement", denoise: 0.24, lowFrequency: 0.46, blendStrength: 0.8, defaults: { steps: 12, cfg: 4.4, denoise: 0.24, blendStrength: 0.8 } },
  "追色1.0": { workflow: MAIN_WORKFLOW_FILE, operation: "color.match", referenceMode: "color-match", colorOnly: true, originalRoute: "local-color-only", defaults: { referenceStrength: 0.78, originalStrength: 0.86, denoise: 0.18 } },
  "追光追色1.0": { workflow: MAIN_WORKFLOW_FILE, operation: "color.match-light", referenceMode: "color-match", colorOnly: true, originalRoute: "local-color-only", defaults: { referenceStrength: 0.84, originalStrength: 0.78, lightStrength: 0.45, denoise: 0.22 } },
  "物品移除": { workflow: MAIN_WORKFLOW_FILE, operation: "local.remove", requiresSelection: true, selectionMode: "crop-return", originalRoute: "local-comfy-replacement", defaults: { denoise: 0.42, blendStrength: 0.72 } },
  "创成填充": { workflow: MAIN_WORKFLOW_FILE, operation: "local.outpaint", originalRoute: "local-comfy-replacement", defaults: { denoise: 0.52, blendStrength: 0.72 } },
  "打字改图": { workflow: MAIN_WORKFLOW_FILE, operation: "edit.typed", referenceMode: "style-reference", originalRoute: "local-qwen-replacement", defaults: { denoise: 0.42, referenceStrength: 0.56, blendStrength: 0.8 } },
  "一键花瓣": { workflow: MAIN_WORKFLOW_FILE, operation: "imagination.petals", originalRoute: "local-comfy-replacement", defaults: { denoise: 0.32, lightStrength: 0.5, blendStrength: 0.78 } },
  "头纱飘逸": { workflow: MAIN_WORKFLOW_FILE, operation: "imagination.veil", originalRoute: "local-comfy-replacement", defaults: { denoise: 0.34, blendStrength: 0.78 } },
  "重绘": { workflow: MAIN_WORKFLOW_FILE, operation: "local.redraw", originalRoute: "local-comfy-replacement", defaults: { denoise: 0.36, blendStrength: 0.78 } },
  "夕阳逆光": { workflow: MAIN_WORKFLOW_FILE, operation: "light.sunset-backlight", originalRoute: "local-comfy-replacement", defaults: { denoise: 0.28, lightStrength: 0.68, blendStrength: 0.82 } },
  "镜头光晕": { workflow: MAIN_WORKFLOW_FILE, operation: "light.lens-flare", originalRoute: "local-comfy-replacement", defaults: { denoise: 0.24, lightStrength: 0.58, blendStrength: 0.84 } },
  "室内窗口光": { workflow: MAIN_WORKFLOW_FILE, operation: "light.window", originalRoute: "local-comfy-replacement", defaults: { denoise: 0.22, lightStrength: 0.56, blendStrength: 0.84 } },
  "草坪阴影光": { workflow: MAIN_WORKFLOW_FILE, operation: "light.lawn-shadow", originalRoute: "local-comfy-replacement", defaults: { denoise: 0.24, lightStrength: 0.5, blendStrength: 0.84 } },
  "人物轮廓光": { workflow: MAIN_WORKFLOW_FILE, operation: "light.rim", originalRoute: "local-comfy-replacement", defaults: { denoise: 0.26, lightStrength: 0.62, blendStrength: 0.84 } },
  "伦勃朗光": { workflow: MAIN_WORKFLOW_FILE, operation: "light.rembrandt", originalRoute: "local-comfy-replacement", defaults: { denoise: 0.26, lightStrength: 0.6, blendStrength: 0.82 } }
});
WORKFLOW_BY_CATEGORY["AI生成"] = MAIN_WORKFLOW_FILE;
WORKFLOW_BY_CATEGORY["胶片模拟"] = MAIN_WORKFLOW_FILE;
HOME_CARDS.forEach(function (card) {
  WORKFLOW_BY_FEATURE[card.title] = MAIN_WORKFLOW_FILE;
});

const WORKFLOW_HELP = {
  "xihuandujia.json": "XNH 完美人像磨皮资源：保持人物身份和构图稳定，适合肤质优化、柔和光影和商业人像精修。",
  "XHN-Portrait-Pro.json": "XHN 人像精修专业工作流：批量磨皮XL/人像XL + 人像修图 LoRA，主打肤质、面部、裙摆和商业质感。",
  "XHN-Local-Create-Pro.json": "XHN 本地创成专业工作流：XHN合成/RealVis + 局部重绘、移除、质感重绘和局部生成。",
  "XHN-Color-Light-Pro.json": "XHN 色彩追光专业工作流：参考图追色、追光、色彩和光影混合。",
  "XHN-Creative-Cloud-Pro.json": "XHN 创意云专业工作流：场景生成、融合、雪雨秋天、波光、圣诞、草地修复等。",
  "XHN-AI-Lighting-Pro.json": "XHN AI 打光专业工作流：逆光、补光、轮廓光、自然光、黄昏夕阳等光影增强。",
  "XHN-Qwen-Edit-Pro.json": "XHN Qwen 改图工作流：Qwen Image Edit 多图参考编辑，适合打字改图、换衣服、高级改图。",
  "XHN-Transparent-Cutout-Pro.json": "XHN 抠图工作流：本地透明背景/主体提取，保留发丝边缘。",
  "XHN-Upscale-Pro.json": "XHN 放大修复工作流：清晰化、老照片修复、高质量放大和细节恢复。"
};

const LIGHT_CARD_META = {
  "背景提取": {
    tag: "主体分离",
    detail: "从当前画布提取人物或主体，保留边缘细节，适合后续换景和合成。",
    tone: "extract"
  },
  "全景融合": {
    tag: "大场景合成",
    detail: "将人物与参考场景做整体融合，统一透视、光影和色彩。",
    tone: "fusion"
  },
  "重绘融合": {
    tag: "局部重绘",
    detail: "针对局部区域重绘光影，尽量保留人物脸、发型和服装细节。",
    tone: "redraw"
  },
  "自由光线": {
    tag: "光线设计",
    detail: "用提示词控制主光、轮廓光、阴影方向和摄影棚质感。",
    tone: "free"
  },
  "逆光增强": { tag: "逆光", detail: "增强人物边缘光和背景高光，适合外景婚纱。", tone: "rim" },
  "柔光补光": { tag: "补光", detail: "柔和提亮人物暗部，保留真实肤色。", tone: "soft" },
  "轮廓光": { tag: "分离主体", detail: "加强主体边缘层次，让人物从背景中立起来。", tone: "rim" },
  "日落暖光": { tag: "黄昏", detail: "加入暖色日落氛围，统一草地和肤色。", tone: "sunset" },
  "天空柔光": { tag: "天空光", detail: "自然扩散光，适合户外阴影偏硬的照片。", tone: "soft" },
  "窗边光": { tag: "室内光", detail: "模拟窗边侧光，增加脸部立体感。", tone: "window" },
  "黄昏氛围": { tag: "氛围", detail: "增强傍晚暖色和整体电影感。", tone: "sunset" },
  "阴天提亮": { tag: "提亮", detail: "修正灰暗天气，让画面干净通透。", tone: "soft" }
};

const state = {
  bridgeUrl: "http://127.0.0.1:7868",
  workflows: [],
  models: [],
  modelCatalog: {},
  launcherSession: {},
  onlineConfig: {},
  activeMajor: "功能模块",
  activeCategory: "功能模块",
  activeFeature: "",
  detailOpen: false,
  searchOpen: false,
  searchQuery: localStorage.getItem(STORAGE_KEYS.searchQuery) || "",
  settings: {
    coverVideo: localStorage.getItem(STORAGE_KEYS.coverVideo) !== "0",
    autoSend: localStorage.getItem(STORAGE_KEYS.autoSend) !== "0",
    layerNameMode: localStorage.getItem(STORAGE_KEYS.layerNameMode) || "功能名称 + 时间"
  },
  inputImage: "",
  inputPreviewUrl: "",
  inputDocument: null,
  sourceSelection: null,
  referenceImage: "",
  referenceName: localStorage.getItem(STORAGE_KEYS.referenceName) || "",
  referencePreviewUrl: "",
  optionSelections: {},
  paramDrafts: {},
  results: [],
  latestResult: null,
  bridgeOk: false,
  bridgeReachable: false,
  bridgePollTimer: null,
  lastMemberReady: false,
  memberFocusBound: false
};

function byId(id) {
  return document.getElementById(id);
}

function eachNode(nodes, callback) {
  if (!nodes) return;
  for (let index = 0; index < nodes.length; index += 1) callback(nodes[index], index);
}

function setClass(node, className, enabled) {
  if (!node) return;
  if (enabled) node.classList.add(className);
  else node.classList.remove(className);
}

function setControlLabel(node, label) {
  if (!node) return;
  if (node.tagName === "BUTTON") node.textContent = label;
  else if ("value" in node) node.value = label;
  else node.textContent = label;
}

function setControlBusy(node, busy) {
  if (!node) return;
  if ("disabled" in node) node.disabled = !!busy;
  setClass(node, "is-disabled", !!busy);
  node.setAttribute("aria-disabled", busy ? "true" : "false");
}

function launcherMemberReady() {
  const session = state.launcherSession || {};
  return !!session.loggedIn;
}

function memberGateText() {
  return MEMBER_LOGIN_HINT;
}

function updateMemberGateUi() {
  const ready = launcherMemberReady();
  const shell = document.querySelector(".plugin-shell");
  if (shell) setClass(shell, "member-locked", !ready);
  const gate = byId("memberGate");
  if (gate) {
    gate.textContent = ready
      ? `启动器已登录：${state.launcherSession.userCode || state.launcherSession.username || "已登录"}`
      : memberGateText();
    gate.className = ready ? "member-gate member-gate-ok" : "member-gate member-gate-bad";
  }
  state.lastMemberReady = ready;
  return ready;
}

function guardMemberInteraction() {
  if (launcherMemberReady()) return true;
  updateMemberGateUi();
  setLog(memberGateText(), true);
  return false;
}

function safeTrim(value) {
  return String(value == null ? "" : value).trim();
}

function paramDraftKey() {
  return `${state.activeCategory || ""}:${currentFeature() || state.activeFeature || ""}`;
}

function currentParamDraft(create) {
  const key = paramDraftKey();
  if (!key) return {};
  if (!state.paramDrafts[key] && create) state.paramDrafts[key] = {};
  return state.paramDrafts[key] || {};
}

function draftValue(id, fallback) {
  const draft = currentParamDraft(false);
  return Object.prototype.hasOwnProperty.call(draft, id) ? draft[id] : fallback;
}

function draftNumber(id, fallback) {
  const value = Number(draftValue(id, fallback));
  return Number.isFinite(value) ? value : fallback;
}

function draftChecked(id, fallback) {
  const draft = currentParamDraft(false);
  return Object.prototype.hasOwnProperty.call(draft, id) ? !!draft[id] : !!fallback;
}

function saveDraftControl(control) {
  if (!control || !control.id) return;
  const draft = currentParamDraft(true);
  if (control.type === "checkbox") draft[control.id] = !!control.checked;
  else if ("value" in control) draft[control.id] = control.value;
  else draft[control.id] = control.textContent || "";
}

function persistCurrentParamDraft() {
  const panel = byId("parameterPanel");
  if (!panel) return;
  eachNode(panel.querySelectorAll("input[id], textarea[id], select[id]"), saveDraftControl);
}

function controlValue(id, fallback) {
  const node = byId(id);
  if (!node) return fallback || "";
  const value = "value" in node ? node.value : node.textContent;
  const cleaned = safeTrim(value);
  return cleaned || fallback || "";
}

function controlChecked(id, fallback) {
  const node = byId(id);
  if (!node || !("checked" in node)) return !!fallback;
  return !!node.checked;
}

function enableKeyboardClick(node) {
  if (!node) return node;
  node.setAttribute("role", "button");
  node.setAttribute("tabindex", "0");
  node.onkeydown = function (event) {
    if (event.key !== "Enter" && event.key !== " ") return;
    event.preventDefault();
    node.click();
  };
  return node;
}

function makePanelButton(id, label, className) {
  const button = document.createElement("button");
  button.type = "button";
  button.id = id;
  button.textContent = label;
  button.className = `panel-action ${className || ""}`.trim();
  return enableKeyboardClick(button);
}

function escapeText(value) {
  return String(value || "").replace(/[&<>]/g, function (char) {
    return { "&": "&amp;", "<": "&lt;", ">": "&gt;" }[char];
  });
}

function escapeAttr(value) {
  return escapeText(value).replace(/"/g, "&quot;");
}

function selectedAttr(value, selected) {
  return value === selected ? " selected" : "";
}

function nowText() {
  const date = new Date();
  return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}`;
}

function setLog(message, bad) {
  const line = byId("logLine");
  if (!line) return;
  line.innerHTML = `时间：${nowText()}<br />${escapeText(message)}`;
  line.style.color = bad ? "#ff6c61" : "";
}

function setProgress(value) {
  const safe = Math.max(0, Math.min(100, Math.round(value)));
  const text = byId("progressText");
  const bar = byId("progressBar");
  if (text) text.textContent = `${safe}%`;
  if (bar) bar.style.width = `${safe}%`;
}

function saveSettings() {
  localStorage.setItem(STORAGE_KEYS.coverVideo, state.settings.coverVideo ? "1" : "0");
  localStorage.setItem(STORAGE_KEYS.autoSend, state.settings.autoSend ? "1" : "0");
  localStorage.setItem(STORAGE_KEYS.layerNameMode, state.settings.layerNameMode || "功能名称 + 时间");
}

function normalizeSearchText(value) {
  return String(value || "")
    .toLowerCase()
    .replace(/\s+/g, "")
    .replace(/[\/\\_\-.,，。:：()（）[\]【】]+/g, "");
}

function activeSearchQuery() {
  return normalizeSearchText(state.searchQuery);
}

function fuzzyMatchText(text, query) {
  const haystack = normalizeSearchText(text);
  const needle = normalizeSearchText(query);
  if (!needle) return true;
  if (haystack.indexOf(needle) >= 0) return true;
  let cursor = 0;
  for (let index = 0; index < haystack.length && cursor < needle.length; index += 1) {
    if (haystack.charAt(index) === needle.charAt(cursor)) cursor += 1;
  }
  return cursor === needle.length;
}

function renderSearchEmpty(grid, label) {
  const empty = document.createElement("div");
  empty.className = "empty-custom search-empty";
  empty.textContent = `没有找到“${state.searchQuery}”相关的${label || "功能"}`;
  grid.appendChild(empty);
}

function loadCustomFeatures() {
  try {
    const raw = localStorage.getItem(STORAGE_KEYS.customFeatures);
    if (!raw) return DEFAULT_CUSTOM_FEATURES.slice();
    const parsed = JSON.parse(raw);
    if (!Array.isArray(parsed)) return DEFAULT_CUSTOM_FEATURES.slice();
    const seenClean = {};
    const cleaned = parsed
      .filter(function (item) { return item && item.name; })
      .map(function (item, index) {
        return {
          id: item.id || `custom-${Date.now()}-${index}`,
          name: safeTrim(item.name),
          prompt: safeTrim(item.prompt || "professional photo retouching, high quality, realistic details")
        };
      })
      .filter(function (item) {
        if (!item.name || /^我的常用\d+$/.test(item.name)) return false;
        if (seenClean[item.name]) return false;
        seenClean[item.name] = true;
        return true;
      });
    const seen = {};
    const merged = DEFAULT_CUSTOM_FEATURES.map(function (item) {
      seen[item.name] = true;
      return { id: item.id, name: item.name, prompt: item.prompt };
    });
    cleaned.forEach(function (item) {
      if (!seen[item.name]) {
        seen[item.name] = true;
        merged.push(item);
      }
    });
    return merged.length ? merged : DEFAULT_CUSTOM_FEATURES.slice();
  } catch (error) {
    return DEFAULT_CUSTOM_FEATURES.slice();
  }
}

state.customFeatures = loadCustomFeatures();

function categoriesForMajor(major) {
  if (major === "功能模块") return [HOME_CATEGORY];
  if (major === "系统设置") return [SETTINGS_CATEGORY];
  if (major === "AI打光") return [CATEGORIES.filter(function (item) { return item.name === "AI打光"; })[0]];
  if (major === "人像精修") return [CATEGORIES.filter(function (item) { return item.name === "人像精修"; })[0]];
  if (major === "AI生成") return [AI_GENERATE_CATEGORY];
  if (major === "胶片模拟") return [FILM_PENDING_CATEGORY];
  if (major === "色彩处理") return [CATEGORIES.filter(function (item) { return item.name === "色彩处理"; })[0]];
  if (major === "胶片仿制") return FILM_CATEGORIES;
  if (major === "光线处理") return LIGHT_CATEGORIES;
  if (major === "独家") return EXCLUSIVE_CATEGORIES;
  return CATEGORIES;
}

function saveCustomFeatures() {
  localStorage.setItem(STORAGE_KEYS.customFeatures, JSON.stringify(state.customFeatures));
}

function nextCustomFeatureName() {
  let index = state.customFeatures.length + 1;
  let name = `我的常用${index}`;
  while (customFeatureByName(name)) {
    index += 1;
    name = `我的常用${index}`;
  }
  return name;
}

function getCategoryFeatures(category) {
  if (category.name === CUSTOM_CATEGORY) return state.customFeatures.map(function (item) { return item.name; });
  return category.features || [];
}

function currentCategory() {
  const categories = categoriesForMajor(state.activeMajor);
  for (let index = 0; index < categories.length; index += 1) {
    if (categories[index].name === state.activeCategory) return categories[index];
  }
  return categories[0] || CATEGORIES[0];
}

function currentFeature() {
  const features = getCategoryFeatures(currentCategory());
  if (state.activeFeature && features.indexOf(state.activeFeature) >= 0) return state.activeFeature;
  return "";
}

function customFeatureByName(name) {
  for (let index = 0; index < state.customFeatures.length; index += 1) {
    if (state.customFeatures[index].name === name) return state.customFeatures[index];
  }
  return null;
}

function promptForFeature(feature, category) {
  if (category && category.name === CUSTOM_CATEGORY) {
    const custom = customFeatureByName(feature);
    if (custom && custom.prompt) return custom.prompt;
  }
  return PROMPTS[feature] || "专业修图，高质量，真实自然细节";
}

function promptForRequest(feature, category) {
  const visiblePrompt = getPrompt();
  const backendPrompt = BACKEND_PROMPTS[feature] || "";
  if (!backendPrompt) return visiblePrompt;
  const defaultPrompt = promptForFeature(feature, category);
  if (safeTrim(visiblePrompt) === safeTrim(defaultPrompt)) {
    return backendPrompt;
  }
  return [visiblePrompt, backendPrompt].filter(Boolean).join(", ");
}

function originalFeatureInfo(feature, category) {
  if (category && category.name === CUSTOM_CATEGORY) {
    return { key: "customFeature", component: CUSTOM_CATEGORY, workflowId: "", route: "local-custom", evidence: "user custom feature" };
  }
  return ORIGINAL_FEATURE_MAP[feature] || { key: "", component: "", workflowId: "", route: "local-custom", evidence: "XHN professional workflow" };
}

function originalWorkflowLabel(original) {
  if (!original) return "";
  return original.workflowId || original.component || original.key || "";
}

function operationProfileForFeature(feature, category) {
  const profile = Object.assign({}, OPERATION_PROFILES[feature] || {});
  profile.defaults = Object.assign({}, profile.defaults || {});
  const original = originalFeatureInfo(feature, category);
  if (original.defaults) profile.defaults = Object.assign({}, original.defaults, profile.defaults);
  profile.originalKey = original.key || "";
  profile.originalComponent = original.component || "";
  profile.originalWorkflowId = original.workflowId || "";
  profile.originalRoute = original.route || "local-custom";
  profile.originalEvidence = original.evidence || "";
  profile.originalParams = Array.isArray(original.params) ? original.params.slice() : [];
  profile.localReplacementFor = originalWorkflowLabel(original);
  if (!profile.operation && category && category.mode) profile.operation = `${category.mode}.generic`;
  return profile;
}

function operationProfileForCurrent() {
  return operationProfileForFeature(currentFeature(), currentCategory());
}

function workflowFileForFeature(feature, category) {
  const profile = operationProfileForFeature(feature, category);
  if (profile.workflow) return profile.workflow;
  if (category && category.name === CUSTOM_CATEGORY) return "XHN-Portrait-Pro.json";
  if (category && WORKFLOW_BY_CATEGORY[category.name]) return WORKFLOW_BY_CATEGORY[category.name];
  return "XHN-Portrait-Pro.json";
}

function workflowFileForCurrent() {
  return workflowFileForFeature(currentFeature(), currentCategory()) || "XHN-Portrait-Pro.json";
}

function isReferenceColorFeature(feature, category) {
  const profile = operationProfileForFeature(feature, category);
  return profile.referenceMode === "color-match" || profile.colorOnly === true;
}

function referenceModeForFeature(feature, category) {
  const profile = operationProfileForFeature(feature, category);
  if (profile.referenceMode === "none") return "";
  if (profile.referenceMode) return profile.referenceMode;
  if (isReferenceColorFeature(feature, category)) return "color-match";
  if (category && (category.mode === "typing" || category.mode === "film" || category.mode === "lightPro" || category.mode === "color")) return "style-reference";
  return "";
}

function isColorOnlyFeature(feature, category) {
  const profile = operationProfileForFeature(feature, category);
  return profile.colorOnly === true;
}

function workflowExists(file) {
  for (let index = 0; index < state.workflows.length; index += 1) {
    if (state.workflows[index].file === file) return true;
  }
  return !state.workflows.length;
}

function workflowDescription(file) {
  for (let index = 0; index < state.workflows.length; index += 1) {
    if (state.workflows[index].file === file && state.workflows[index].description) return state.workflows[index].description;
  }
  return WORKFLOW_HELP[file] || "当前功能资源由 XNH 启动器统一管理，后续可通过插件更新自动接入。";
}

function currentWorkflowDefaults() {
  const file = workflowFileForCurrent();
  const profile = operationProfileForCurrent();
  for (let index = 0; index < state.workflows.length; index += 1) {
    if (state.workflows[index].file === file) return Object.assign({}, state.workflows[index].defaults || {}, profile.defaults || {});
  }
  return Object.assign({}, profile.defaults || {});
}

function modelDescription(name) {
  const text = String(name || "").toLowerCase();
  if (!text || text === "默认模型") return "默认模型：桥接服务会按当前功能自动选择本机模型。";
  if (text.indexOf("realvis") >= 0) return "RealVis 写实模型：适合人像、商业图和真实摄影风格。";
  if (text.indexOf("dream") >= 0) return "DreamShaper：通用写实模型，兼容性好，适合快速测试。";
  if (text.indexOf("turbo") >= 0 || text.indexOf("lightning") >= 0) return "加速模型：少步数快速出图，适合极速修图。";
  if (text.indexOf("grass") >= 0 || text.indexOf("草") >= 0) return "草地/户外模型：适合草坪修复、婚礼外景和高尔夫场景。";
  return "本机模型：鼠标停留查看说明，建议先小图测试速度和稳定性。";
}

function injectRuntimeStyles() {
  if (document.getElementById("xnhRuntimeStyles")) return;
  const style = document.createElement("style");
  style.id = "xnhRuntimeStyles";
  style.textContent = `
html,body,#container{width:100%;height:100%;margin:0;padding:0;overflow:hidden;background:#eef3f6;color:#121821;font-family:"Microsoft YaHei","PingFang SC",Arial,sans-serif;font-size:14px;letter-spacing:0}
*{box-sizing:border-box}button,input,select,textarea{font:inherit;letter-spacing:0}
#container{overflow-y:auto;overflow-x:hidden}
.plugin-shell{position:relative!important;display:block!important;width:100%!important;min-height:100%;padding:12px 16px 28px!important;overflow:visible!important;background:linear-gradient(145deg,#f8fbfd 0%,#e9f0f4 100%)!important;color:#111821!important}
.plugin-shell.cover-motion-off *{transition:none!important;animation:none!important}
.xnh-titlebar{position:relative;display:block;min-height:44px;padding:4px 84px 10px;text-align:center}
.xnh-title{display:block;color:#05090e;font-size:23px;font-weight:900;line-height:30px;text-align:center;letter-spacing:0}
.xnh-version{position:absolute;right:4px;top:8px;color:#707780;font-size:12px;font-weight:800;white-space:nowrap}
.xnh-connect-card{display:flex!important;align-items:center!important;gap:10px!important;width:100%!important;min-height:62px;margin:0 0 16px!important;padding:10px 12px!important;border:1px solid rgba(255,255,255,.92)!important;border-radius:8px!important;background:rgba(248,251,253,.88)!important;box-shadow:10px 14px 28px rgba(92,105,118,.18),-8px -8px 20px rgba(255,255,255,.9)!important;visibility:visible!important;opacity:1!important}
.url-row{display:flex!important;align-items:center!important;flex:1 1 320px!important;min-width:220px;height:42px;overflow:hidden;border-radius:8px;background:#202734;box-shadow:inset 3px 3px 9px rgba(82,95,110,.18)}
.url-row .url-label{display:block!important;flex:0 0 86px;height:42px;line-height:42px;text-align:center;background:#fff!important;color:#111821!important;font-weight:900;border:0!important}
.url-row input{display:block!important;flex:1 1 auto!important;min-width:0;height:42px;margin:0!important;padding:0 12px!important;border:0!important;border-radius:0!important;background:#e8edf0!important;color:#121821!important;outline:0!important;font-weight:700}
.url-row .connect-action{display:block!important;flex:0 0 72px;height:42px;line-height:42px;text-align:center;border:0!important;border-radius:0 8px 8px 0!important;background:#202734!important;color:#fff!important;font-weight:900;cursor:pointer;transition:transform 140ms ease,background 140ms ease}
.connection-pill{display:block!important;flex:0 0 70px;text-align:center;color:#6b4b28;font-size:13px;font-weight:800;white-space:nowrap}
.state-ok{color:#121821!important}.state-bad{color:#8a4a20!important}
.user-code-box{display:flex!important;align-items:center;justify-content:center;flex:0 0 190px;min-width:132px;height:42px;padding:0 10px;border-radius:8px;background:rgba(255,255,255,.78);box-shadow:inset 4px 4px 12px rgba(130,142,153,.12),inset -4px -4px 12px rgba(255,255,255,.84);color:#111821;font-size:15px;font-weight:900;white-space:nowrap;overflow:hidden}
.user-code-box span{flex:0 0 auto}.user-code-box strong{display:block;min-width:0;max-width:108px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#111821;font-size:13px;font-weight:900}
.member-gate{display:block!important;width:100%;min-height:34px;margin:-8px 0 14px!important;padding:7px 12px;border-radius:8px;background:rgba(255,255,255,.72);font-size:13px!important;font-weight:900!important;line-height:20px!important;text-align:center;box-shadow:inset 3px 3px 9px rgba(130,142,153,.1),inset -3px -3px 9px rgba(255,255,255,.78)}
.member-gate-bad{color:#e12424!important;border:1px solid rgba(225,36,36,.24)!important;background:rgba(255,245,245,.92)!important}.member-gate-ok{color:#1f8d4c!important;border:1px solid rgba(31,141,76,.22)!important;background:rgba(242,255,247,.9)!important}
.member-locked .major-tab,.member-locked .category-tab,.member-locked .home-card,.member-locked .feature-card,.member-locked .panel-action,.member-locked .head-tool,.member-locked .search-wrap,.member-locked .option-chip,.member-locked .strength-chip,.member-locked .check-item,.member-locked .xhn-slider,.member-locked #parameterPanel input,.member-locked #parameterPanel textarea,.member-locked #parameterPanel select{opacity:.42!important;pointer-events:none!important;filter:grayscale(.24)!important}
.member-locked #bridgeUrl,.member-locked #connectBtn,.member-locked .xnh-connect-card,.member-locked .member-gate{opacity:1!important;pointer-events:auto!important;filter:none!important}
.xnh-workspace{display:flex!important;align-items:flex-start!important;gap:16px!important;width:100%!important;visibility:visible!important;opacity:1!important}
.xnh-sidebar{display:block!important;flex:0 0 112px!important;min-height:560px;padding:18px 9px;border:1px solid rgba(255,255,255,.88);border-radius:8px;background:rgba(248,251,253,.86);box-shadow:10px 14px 28px rgba(92,105,118,.18),-8px -8px 20px rgba(255,255,255,.86);visibility:visible!important;opacity:1!important}
.xnh-main{display:block!important;flex:1 1 auto!important;min-width:0;min-height:560px;padding:24px;border:1px solid rgba(255,255,255,.88);border-radius:8px;background:rgba(248,251,253,.86);box-shadow:10px 14px 28px rgba(92,105,118,.18),-8px -8px 20px rgba(255,255,255,.86);visibility:visible!important;opacity:1!important}
.major-tabs{display:flex!important;flex-direction:column!important;gap:13px!important;width:100%;height:auto!important;visibility:visible!important;opacity:1!important}
.major-tab{display:flex!important;align-items:center!important;justify-content:center!important;flex-direction:column!important;width:82px!important;min-height:72px!important;margin:0 auto!important;padding:7px 4px!important;border-radius:8px!important;background:linear-gradient(145deg,#fff,#edf2f5)!important;color:#111821!important;box-shadow:7px 9px 18px rgba(94,107,120,.2),-6px -6px 16px rgba(255,255,255,.9);cursor:pointer;transition:transform 140ms ease,background 140ms ease,color 140ms ease}
.major-tab.active{background:linear-gradient(145deg,#69717e,#4e5662)!important;color:#fff!important;box-shadow:8px 12px 18px rgba(70,80,94,.34),inset 3px 3px 8px rgba(255,255,255,.1)}
.major-tab:hover,.home-card:hover,.feature-card:hover,.panel-action:hover,.head-tool:hover{transform:translateY(-1px)}.major-tab:active,.home-card:active,.feature-card:active,.panel-action:active,.head-tool:active{transform:translateY(1px) scale(.99)}
.major-icon{display:flex!important;align-items:center!important;justify-content:center!important;width:28px!important;height:28px!important;margin:0 auto 6px!important;border-radius:7px!important;background:#fff!important;color:#111821!important;border:1px solid rgba(18,24,32,.08)!important;box-shadow:inset 2px 2px 6px rgba(108,120,132,.12),inset -2px -2px 6px rgba(255,255,255,.9);font-size:18px!important;font-weight:900!important;line-height:28px!important;text-align:center}
.major-tab.active .major-icon{background:rgba(255,255,255,.92)!important;color:#313844!important}
.major-label{display:block;width:100%;color:inherit;font-size:13px;font-weight:900;line-height:16px;text-align:center;white-space:normal}
.xnh-main-head{display:flex!important;align-items:center;justify-content:space-between;gap:14px;margin:0 0 14px;padding:0 0 13px;border-bottom:1px solid rgba(120,132,145,.28)}
.workbench-title{color:#05090e;font-size:24px;font-weight:900;line-height:31px}.workbench-hint{margin-top:2px;color:#65707d;font-size:12px;font-weight:700}
.head-tools{display:flex;align-items:center;gap:10px;min-width:0}.head-tool{display:flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:8px;background:rgba(255,255,255,.86);color:#172030;box-shadow:8px 10px 20px rgba(92,105,118,.15),-5px -5px 14px rgba(255,255,255,.92);font-size:20px;font-weight:900;cursor:pointer;transition:transform 140ms ease}
.search-wrap{display:flex!important;align-items:center;width:0;height:44px;overflow:hidden;opacity:0;transition:width 160ms ease,opacity 160ms ease}
.search-wrap.open{width:230px;opacity:1}.search-wrap input{display:block!important;flex:1 1 auto;min-width:0;height:38px;margin:0!important;padding:0 10px!important;border:0!important;border-radius:8px 0 0 8px!important;background:#fff!important;color:#e12424!important;font-weight:900!important;box-shadow:inset 3px 3px 8px rgba(120,132,145,.18)!important;outline:0!important}.search-wrap input::placeholder{color:rgba(225,36,36,.65)!important}.search-clear{display:none;flex:0 0 30px;height:38px;line-height:36px;text-align:center;border-radius:0 8px 8px 0;background:#252c38;color:#fff;font-size:18px;font-weight:900;cursor:pointer}.search-clear.visible{display:block}
.category-tabs.hidden,.feature-grid.hidden,.parameter-section.hidden,.progress-card.hidden,.result-section.hidden{display:none!important}.category-tabs{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 14px}.category-tab{height:34px;padding:0 14px;border:1px solid rgba(135,146,158,.34);border-radius:8px;background:#fff;color:#121821;font-weight:900}.category-tab.active{background:#252c38;color:#fff}
.home-grid,.feature-directory-grid{display:flex!important;flex-wrap:wrap!important;gap:14px!important;width:100%;margin:0 0 14px;padding:0!important;background:transparent!important;border:0!important;box-shadow:none!important;visibility:visible!important;opacity:1!important}.home-card,.feature-card{display:block!important;flex:1 1 270px;min-width:240px;max-width:calc(50% - 7px);border:1px solid rgba(255,255,255,.86)!important;border-radius:8px!important;overflow:hidden!important;background:rgba(255,255,255,.84)!important;color:#111821!important;box-shadow:10px 14px 24px rgba(75,88,102,.16),-6px -6px 18px rgba(255,255,255,.84)!important;cursor:pointer;transition:transform 140ms ease}.home-cover{position:relative;height:150px;background-size:cover;background-position:center}.home-cover::after{content:"";position:absolute;left:0;right:0;bottom:0;height:58%;background:linear-gradient(180deg,transparent,rgba(0,0,0,.55))}.home-cover span{position:absolute;left:14px;bottom:12px;z-index:1;color:#fff;font-size:22px;font-weight:900;text-shadow:0 2px 8px rgba(0,0,0,.48)}.home-card-body{padding:14px 16px 16px;background:rgba(255,255,255,.9)}.home-card-body strong{display:block;color:#07101c;font-size:18px;font-weight:900;line-height:24px}.home-card-body small{display:block;margin-top:5px;color:#566170;font-size:13px;font-weight:700;line-height:19px}
.parameter-section{display:block;margin-top:14px;padding:18px;border:1px solid rgba(218,174,82,.64);border-radius:8px;background:rgba(255,255,255,.52)}.param-heading{display:flex;align-items:center;justify-content:space-between;margin:0 0 14px}.minor-title{display:block;color:#07101c;font-size:17px;font-weight:900}.workflow-badge{color:#a06900;font-size:12px;font-weight:900}.param-panel{display:block}.param-card,.progress-card,.result-section{display:block;width:100%;margin:0 0 12px;padding:14px;border:1px solid rgba(116,126,139,.18);border-radius:8px;background:rgba(255,255,255,.76);box-shadow:inset 0 1px rgba(255,255,255,.7)}
.param-card label,.panel-label{display:block;margin:0 0 8px;color:#07101c;font-weight:900;line-height:20px}.reference-preview{display:flex!important;align-items:center!important;justify-content:center!important;width:100%;height:210px;margin:0 0 10px;border:1px solid rgba(218,174,82,.48);border-radius:8px;background:linear-gradient(145deg,#e9eef2,#cfd8df);overflow:hidden;text-align:center}.reference-preview img{width:100%;height:100%;object-fit:contain}.reference-preview span{max-width:86%;padding:7px 10px;border-radius:6px;background:rgba(255,255,255,.62);color:#e12424!important;font-size:13px!important;font-weight:900!important;line-height:18px!important;text-shadow:none!important;white-space:normal!important}.reference-controls,.button-row{display:flex;align-items:stretch;gap:10px;width:100%;min-height:42px}.panel-action{display:flex;align-items:center;justify-content:center;min-height:42px;padding:0 12px;border:1px solid rgba(116,126,139,.24);border-radius:8px;background:#fff;color:#111821;font-weight:900;text-align:center;cursor:pointer}.solid-button{background:linear-gradient(180deg,#ff9d22,#ff8414)!important;color:#fff!important;border:0!important}.outline-button{background:#fff!important;color:#111821!important}.danger-button{background:#596272!important;color:#fff!important;border:0!important}.compact-button{min-height:36px;padding:0 14px}.is-disabled{opacity:.48;pointer-events:none}
textarea,input,select{border:1px solid rgba(116,126,139,.25);border-radius:8px;background:#fff;color:#111821;outline:0}.prompt-card textarea,.custom-editor textarea{width:100%;min-height:86px;padding:12px;resize:vertical;background:#0f1724;color:#fff}.select-grid,.chips,.checkbox-grid{display:flex;flex-wrap:wrap;gap:8px}.select-grid select,.chips .option-chip,.chips .strength-chip{min-height:36px;padding:0 10px;border-radius:8px;background:#fff;color:#111821;font-weight:900}.option-chip.active,.strength-chip.active{background:#252c38;color:#fff}.check-item{display:inline-flex;align-items:center;gap:6px;min-height:28px;margin-right:12px;color:#111821;font-weight:800}
.slider-row{display:flex!important;align-items:center!important;gap:12px!important;width:100%;min-height:48px}.slider-row label{flex:0 0 86px;margin:0;color:#07101c;font-weight:900;white-space:nowrap}.slider-value{flex:0 0 58px!important;width:58px!important;height:32px!important;text-align:center!important;background:#151d2c!important;color:#fff!important;border:0!important;border-radius:6px!important;font-weight:900!important;box-shadow:0 6px 14px rgba(21,29,44,.22)!important}.xhn-slider{position:relative!important;display:block!important;flex:1 1 auto!important;height:34px!important;min-width:100px!important;cursor:pointer!important}.xhn-slider::before{content:"";position:absolute;left:0;right:0;top:15px;height:6px;border-radius:99px;background:linear-gradient(90deg,#f1cf67,#82ef9c)}.xhn-slider-fill{position:absolute;left:0;top:15px;height:6px;border-radius:99px;background:linear-gradient(90deg,#f4c956,#78eca0)}.xhn-slider-thumb{position:absolute;top:8px;width:20px;height:20px;margin-left:-10px;border:3px solid #f2d253;border-radius:50%;background:#1a2130;box-shadow:0 0 0 6px rgba(242,210,83,.15)}
.settings-parameter{margin-top:0!important;padding:0!important;border:0!important;background:transparent!important}.settings-parameter .param-heading{display:none!important}.settings-panel{display:block;max-width:760px;margin:0 auto;padding:22px;border-radius:8px;background:#2d2d32;color:#f2f4f6}.settings-title-row{display:block;margin:0 0 18px}.settings-title-row strong{color:#fff;font-size:22px;font-weight:900}.settings-group-title{margin:18px 0 10px;color:#cfd3da;font-weight:900}.setting-row,.about-box{border:1px solid rgba(255,255,255,.08);border-radius:8px;background:rgba(255,255,255,.04)}.setting-row{display:flex;align-items:center;justify-content:space-between;gap:14px;min-height:56px;margin-bottom:10px;padding:10px 14px;color:#f2f4f6;font-weight:900}.setting-row select{width:150px;height:36px;background:#4a4a50!important;color:#fff!important}.switch{position:relative;display:inline-block;width:54px;height:28px}.switch input{display:none}.switch i{position:absolute;inset:0;border-radius:99px;background:#777;cursor:pointer}.switch i::after{content:"";position:absolute;top:3px;left:3px;width:22px;height:22px;border-radius:50%;background:#fff;transition:left 120ms ease}.switch input:checked+i{background:#1e98ff}.switch input:checked+i::after{left:29px}.about-box{padding:14px}.about-box div{display:flex;justify-content:space-between;gap:14px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.06)}.about-box div:last-child{border-bottom:0}.about-box span{color:#c7cbd2}.about-box strong{color:#fff;text-align:right}.contact-box{margin-top:18px;padding:18px;border-radius:8px;background:#f4f8fb;color:#101821;text-align:center}.contact-box span,.contact-box strong{display:block}.contact-box strong{margin-top:12px;font-size:22px;font-weight:900}
.progress-head{display:flex;align-items:center;justify-content:space-between;font-weight:900}.progress-track{height:7px;margin:8px 0 14px;border-radius:99px;background:#d9e0e5;overflow:hidden}#progressBar{width:0;height:100%;border-radius:99px;background:linear-gradient(90deg,#f5cf61,#80ee9b)}.log-line{min-height:56px;color:#3c4b5e;line-height:20px;text-align:center}.result-list.empty{display:none}.result-item{display:flex;align-items:center;gap:10px;margin-top:10px;padding:8px;border-radius:8px;background:#fff}.result-item img{width:58px;height:58px;object-fit:cover;border-radius:6px}.empty-custom{display:block;width:100%;min-height:86px;padding:22px;border-radius:8px;background:#fff;color:#111821;font-size:16px;font-weight:900;text-align:center}
.xnh-boot-error{margin-top:18px;padding:18px;border-radius:8px;background:#fff;color:#111821;box-shadow:10px 14px 24px rgba(75,88,102,.16)}.xnh-boot-error strong,.xnh-boot-error span,.xnh-boot-error small{display:block}.xnh-boot-error strong{font-size:18px;margin-bottom:8px}.xnh-boot-error span{color:#d51f1f;font-weight:900;line-height:22px}.xnh-boot-error small{margin-top:10px;color:#596372}
.plugin-shell{height:100%!important;overflow:hidden!important;padding-bottom:14px!important}.xnh-workspace{height:calc(100% - 132px)!important;min-height:420px!important;overflow:hidden!important}.xnh-sidebar{height:100%!important;min-height:0!important;overflow-y:auto!important;overflow-x:hidden!important}.xnh-main{height:100%!important;min-height:0!important;overflow-y:auto!important;overflow-x:hidden!important;padding:20px!important}.xnh-main::-webkit-scrollbar,.xnh-sidebar::-webkit-scrollbar{width:8px}.xnh-main::-webkit-scrollbar-thumb,.xnh-sidebar::-webkit-scrollbar-thumb{border-radius:99px;background:#c5ced6}.search-active #parameterSection,.search-active #progressCard,.search-active #resultSection{display:none!important}.parameter-section{margin-top:10px!important;padding:14px!important}.param-heading{margin-bottom:10px!important}.param-card,.progress-card,.result-section{margin-bottom:10px!important;padding:12px!important;overflow:visible!important}.reference-preview{height:170px!important;margin-bottom:8px!important}.prompt-card textarea,.custom-editor textarea{min-height:74px!important;padding:10px!important}.param-grid{display:flex!important;flex-wrap:wrap!important;gap:10px!important;align-items:flex-end!important}.param-grid label{display:block!important;flex:1 1 150px!important;min-width:132px!important;margin:0!important;color:#07101c!important}.param-grid input,.param-card input[type="number"],.param-card select{display:block!important;width:100%!important;min-width:0!important;height:34px!important;min-height:34px!important;margin-top:6px!important;padding:0 10px!important;border:1px solid rgba(120,132,145,.28)!important;border-radius:7px!important;background:#fff!important;color:#111821!important;box-shadow:inset 2px 2px 6px rgba(120,132,145,.12)!important;font-weight:800!important;line-height:32px!important;opacity:1!important;visibility:visible!important}.param-card input[type="number"]{-webkit-appearance:none!important;appearance:textfield!important}.select-grid{display:flex!important;flex-wrap:wrap!important;align-items:center!important;gap:10px!important;width:100%!important;min-height:38px!important}.select-grid select{display:block!important;flex:1 1 180px!important;min-width:150px!important;width:auto!important;height:36px!important;min-height:36px!important;margin:0!important;padding:0 10px!important;background:#fff!important;color:#111821!important;line-height:34px!important;opacity:1!important;visibility:visible!important;-webkit-appearance:menulist!important;appearance:auto!important}#refreshModelsInlineBtn{flex:0 0 72px!important}.slider-row{min-height:40px!important}.slider-row label{flex-basis:96px!important}.xhn-slider{height:28px!important}.xhn-slider::before,.xhn-slider-fill{top:12px!important}.xhn-slider-thumb{top:5px!important}.button-row{min-height:38px!important}.panel-action{min-height:38px!important}.action-row .panel-action{flex:1 1 0!important}.settings-panel{max-width:none!important}
@media (max-width:760px){.plugin-shell{padding:10px 8px 22px!important}.xnh-titlebar{padding-left:4px;padding-right:74px}.xnh-title{text-align:left;font-size:20px}.xnh-connect-card{flex-wrap:wrap}.url-row{flex-basis:100%}.connection-pill{flex:0 0 auto}.user-code-box{flex:1 1 180px;justify-content:flex-start}.xnh-workspace{gap:10px}.xnh-sidebar{flex-basis:94px;padding:12px 6px}.major-tab{width:74px!important}.xnh-main{padding:16px}.home-card,.feature-card{min-width:220px;max-width:100%}.search-wrap.open{width:190px}}
@media (max-width:520px){.xnh-workspace{display:block!important}.xnh-sidebar{min-height:0;margin-bottom:12px}.major-tabs{flex-direction:row!important;flex-wrap:wrap!important}.major-tab{width:72px!important}.home-cover{height:132px}.slider-row{flex-wrap:wrap}.slider-row label{flex:0 0 100%}.slider-value{flex:0 0 58px!important}.xhn-slider{flex:1 1 160px!important}}
`;
  const target = document.head || document.documentElement || document;
  target.appendChild(style);
}

function renderShell() {
  injectRuntimeStyles();
  const container = byId("container");
  if (!container || byId("bridgeUrl")) return;
  container.innerHTML = `
    <div class="plugin-shell">
      <div class="xnh-titlebar">
        <div id="xnhBrandTitle" class="xnh-title">${escapeText(onlineBrandTitle())}</div>
        <div class="xnh-version">XNH v${escapeText(VERSION)}</div>
      </div>
      <div class="xnh-connect-card">
        <div class="url-row">
          <div class="url-label">桥接地址</div>
          <input id="bridgeUrl" type="text" value="${escapeAttr(state.bridgeUrl)}" />
          <div id="connectBtn" class="connect-action" role="button" tabindex="0">连接</div>
        </div>
        <div id="connectionState" class="connection-pill state-bad">未连接</div>
        <div class="user-code-box">
          <span>用户码：</span>
          <strong id="userCode">未登录</strong>
        </div>
      </div>
      <div id="memberGate" class="member-gate member-gate-bad">${escapeText(memberGateText())}</div>
      <div class="xnh-workspace">
        <div class="xnh-sidebar">
          <div id="majorTabs" class="major-tabs"></div>
        </div>
        <div class="xnh-main">
          <div class="xnh-main-head">
            <div>
              <div id="workbenchTitle" class="workbench-title">功能模块</div>
              <div id="workbenchHint" class="workbench-hint">选择一个栏目开始修图</div>
            </div>
            <div class="head-tools">
              <div id="moreToolBtn" class="head-tool" title="更多操作">...</div>
              <div id="searchWrap" class="search-wrap">
                <input id="featureSearchInput" type="text" value="${escapeAttr(state.searchQuery)}" placeholder="搜索全部功能" />
                <div id="searchClearBtn" class="search-clear" role="button" tabindex="0">×</div>
              </div>
              <div id="searchToggleBtn" class="head-tool search-tool" title="搜索全部功能">⌕</div>
            </div>
          </div>
          <div id="categoryTabs" class="category-tabs"></div>
          <div id="featureGrid" class="feature-grid"></div>
          <div id="parameterSection" class="parameter-section hidden">
            <div class="param-heading">
              <span class="minor-title">参数设置</span>
              <span id="workflowBadge" class="workflow-badge">XNH</span>
            </div>
            <div id="parameterPanel" class="param-panel"></div>
          </div>
          <div id="progressCard" class="progress-card hidden">
            <div class="progress-head">
              <span>任务状态</span>
              <strong id="progressText">0%</strong>
            </div>
            <div class="progress-track">
              <div id="progressBar"></div>
            </div>
            <div id="logLine" class="log-line">等待连接本地桥接服务。</div>
          </div>
          <div id="resultSection" class="result-section hidden">
            <span class="minor-title">最近结果</span>
            <div id="resultList" class="result-list empty">暂无结果</div>
          </div>
        </div>
      </div>
    </div>
  `;
}

function renderBootError(message) {
  const container = byId("container");
  if (!container) return;
  const text = message || "插件初始化失败，请重启 Photoshop 或重新安装 XNH 插件。";
  container.innerHTML = `
    <div class="plugin-shell">
      <div class="xnh-titlebar">
        <div class="xnh-title">方涛的AI修图工作台</div>
        <div class="xnh-version">XNH v${escapeText(VERSION)}</div>
      </div>
      <div class="xnh-boot-error">
        <strong>插件初始化失败</strong>
        <span>${escapeText(text)}</span>
        <small>请先关闭 Photoshop，回到 XNH 启动器点击“插件安装”，再重新打开 Photoshop。</small>
      </div>
    </div>
  `;
}

function assertBootDomReady() {
  const required = ["bridgeUrl", "connectBtn", "majorTabs", "featureGrid", "parameterPanel"];
  const missing = required.filter(function (id) { return !byId(id); });
  if (missing.length) throw new Error("面板骨架未完整加载：" + missing.join(", "));
}

function assertBridgeUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch (error) {
    throw new Error("桥接地址格式不正确，请输入 http://IP:端口 或 https://域名。");
  }
  const protocolOk = parsed.protocol === "http:" || parsed.protocol === "https:";
  if (!protocolOk) {
    throw new Error("桥接地址只支持 http/https。");
  }
}

function normalizeBridgeUrl() {
  const node = byId("bridgeUrl");
  const raw = safeTrim(node && node.value ? node.value : state.bridgeUrl) || "http://127.0.0.1:7868";
  state.bridgeUrl = raw.replace(/\/+$/, "");
  assertBridgeUrl(state.bridgeUrl);
  if (node) node.value = state.bridgeUrl;
  localStorage.setItem(STORAGE_KEYS.bridgeUrl, state.bridgeUrl);
  return state.bridgeUrl;
}

async function bridgeFetch(path, options) {
  const opts = options || {};
  const headers = Object.assign({}, opts.headers || {});
  if (opts.json) {
    headers["Content-Type"] = "application/json";
    opts.body = JSON.stringify(opts.json);
    delete opts.json;
  }
  let response;
  try {
    const baseUrl = normalizeBridgeUrl();
    const requestOptions = Object.assign({}, opts, { headers: headers });
    try {
      response = await fetch(`${baseUrl}${path}`, requestOptions);
    } catch (firstError) {
      if (baseUrl.indexOf("http://127.0.0.1:") === 0) {
        response = await fetch(`${baseUrl.replace("http://127.0.0.1:", "http://localhost:")}${path}`, requestOptions);
      } else {
        throw firstError;
      }
    }
  } catch (error) {
    throw new Error("无法连接桥接服务。请确认 XNH 启动器已启动，或检查桥接地址。");
  }
  const contentType = response.headers.get("content-type") || "";
  const rawBody = await response.text();
  let body = rawBody;
  if (contentType.indexOf("application/json") >= 0) {
    try {
      body = JSON.parse(rawBody.replace(/^\uFEFF/, ""));
    } catch (error) {
      throw new Error(`桥接服务返回了无效 JSON：${rawBody.slice(0, 180)}`);
    }
  }
  if (!response.ok) throw new Error(body && body.error ? body.error : `请求失败：${response.status}`);
  return body;
}

function majorIconGlyph(icon) {
  const glyphs = {
    home: "▤",
    light: "✶",
    portrait: "▢",
    generate: "AI",
    film: "◇",
    color: "◌",
    settings: "⚙",
    flame: "◆",
    crown: "♛",
    plus: "+"
  };
  return `<span class="major-icon" aria-hidden="true">${glyphs[icon] || glyphs.home}</span>`;
}

function featureIconGlyph(feature, category) {
  const modeGlyphs = {
    portrait: "●",
    local: "+",
    color: "▣",
    typing: "T",
    oneClick: "✦",
    imagination: "✣",
    film: "◼",
    light: "✹",
    exclusive: "+"
  };
  return modeGlyphs[(category && category.mode) || ""] || "◆";
}

function majorTabHtml(group) {
  return `${majorIconGlyph(group.icon)}<span class="major-label">${escapeText(group.name)}</span>`;
}

function jumpToMajor(major) {
  if (!guardMemberInteraction()) return;
  persistCurrentParamDraft();
  state.activeMajor = major;
  const categories = categoriesForMajor(major);
  state.activeCategory = categories[0] ? categories[0].name : "功能模块";
  state.activeFeature = "";
  state.detailOpen = false;
  renderAll();
}

function renderMajorTabs() {
  const tabs = byId("majorTabs");
  if (!tabs) return;
  tabs.innerHTML = "";
  MAJORS.forEach(function (group) {
    const name = group.name;
    const button = document.createElement("div");
    button.innerHTML = majorTabHtml(group);
    button.setAttribute("data-major", name);
    button.title = group.ready ? `${name}：当前可用` : (group.note || name);
    button.className = state.activeMajor === name ? "major-tab active" : "major-tab";
    button.onclick = function () {
      jumpToMajor(name);
    };
    enableKeyboardClick(button);
    tabs.appendChild(button);
  });
}

function renderCategories() {
  const tabs = byId("categoryTabs");
  tabs.innerHTML = "";
  const category = currentCategory();
  if (category.mode === "home" || category.mode === "settings" || category.mode === "filmPending") {
    tabs.className = "category-tabs hidden";
    return;
  }
  const categories = categoriesForMajor(state.activeMajor);
  if (categories.length <= 1) {
    tabs.className = "category-tabs hidden";
    return;
  }
  tabs.className = "category-tabs";
  categories.forEach(function (category) {
    const button = document.createElement("button");
    button.type = "button";
    button.textContent = categoryDisplayTitle(category);
    button.className = category.name === state.activeCategory ? "category-tab active" : "category-tab";
    button.addEventListener("click", function () {
      if (!guardMemberInteraction()) return;
      persistCurrentParamDraft();
      state.activeCategory = category.name;
      state.activeFeature = "";
      state.detailOpen = false;
      renderAll();
    });
    enableKeyboardClick(button);
    tabs.appendChild(button);
  });
}

function parseMaybeJson(value) {
  if (!value) return {};
  if (typeof value === "object") return value;
  try {
    return JSON.parse(String(value));
  } catch (error) {
    return {};
  }
}

function onlineClientConfig() {
  const root = parseMaybeJson(state.onlineConfig);
  return parseMaybeJson(root.client_config || root.clientConfig || root);
}

function onlineUiConfig() {
  const client = onlineClientConfig();
  return parseMaybeJson(client.ui || client.interface || {});
}

function onlineBrandTitle() {
  const client = onlineClientConfig();
  const branding = parseMaybeJson(client.branding);
  return safeTrim(branding.title || client.title || "方涛的AI修图工作台");
}

function uiGroup(kind) {
  const ui = onlineUiConfig();
  return parseMaybeJson(ui[kind]);
}

function validCoverUrl(value, fallback) {
  const text = safeTrim(value);
  if (!text) return fallback;
  if (/^(https?:\/\/|data:image\/|assets\/|\.\/assets\/)/i.test(text)) return text;
  return fallback;
}

function uiCardMeta(kind, key, fallback) {
  const base = Object.assign({}, fallback || {});
  const raw = uiGroup(kind)[key];
  const item = typeof raw === "string" ? { title: raw } : parseMaybeJson(raw);
  if (item.title) base.title = safeTrim(item.title);
  if (item.tag) base.tag = safeTrim(item.tag);
  if (item.subtitle) base.subtitle = safeTrim(item.subtitle);
  if (item.description) base.description = safeTrim(item.description);
  base.cover = validCoverUrl(item.cover || item.image || item.thumbnail, base.cover || "");
  return base;
}

function featureDisplayTitle(feature) {
  return uiCardMeta("featureCards", feature, { title: feature }).title || feature;
}

function categoryDisplayTitle(category) {
  if (!category) return "功能模块";
  return uiCardMeta("categoryTabs", category.name, { title: category.name }).title || category.name;
}

function renderHomeCards(grid) {
  grid.className = "home-grid";
  const query = activeSearchQuery();
  const cards = HOME_CARDS.filter(function (card) {
    return fuzzyMatchText([card.title, card.tag, card.subtitle, card.description].join(" "), query);
  });
  if (!cards.length) {
    renderSearchEmpty(grid, "栏目");
    return;
  }
  cards.forEach(function (card) {
    const meta = uiCardMeta("homeCards", card.title, card);
    const button = document.createElement("div");
    button.setAttribute("role", "button");
    button.setAttribute("tabindex", "0");
    button.className = "home-card";
    button.title = meta.description || card.description;
    button.innerHTML = `
      <div class="home-cover" style="background-image:url('${escapeAttr(meta.cover)}')">
        <span>${escapeText(meta.tag)}</span>
      </div>
      <div class="home-card-body">
        <strong>${escapeText(meta.title)}</strong>
        <small>${escapeText(meta.subtitle)}</small>
      </div>
    `;
    button.addEventListener("click", function () { jumpToMajor(card.major); });
    enableKeyboardClick(button);
    grid.appendChild(button);
  });
}

function featureCardSubtitle(feature, category) {
  if (!feature) return "";
  if (category && category.mode === "oneClick") return "点击进入打光细节调整";
  if (category && category.mode === "portrait") return "点击进入人像精修参数";
  if (category && category.mode === "color") return "点击进入追色处理参数";
  if (category && category.name === "AI生成") return "点击进入生成修图参数";
  if (category && category.mode === "filmPending") return "功能预留，右侧写：更新中";
  return "点击进入详细调整模块";
}

function featureCardImage(feature, category) {
  const featureOverride = feature ? uiCardMeta("featureCards", feature, { cover: "" }).cover : "";
  if (featureOverride) return featureOverride;
  const categoryOverride = category && category.name ? uiCardMeta("categoryCards", category.name, { cover: "" }).cover : "";
  if (categoryOverride) return categoryOverride;
  if (FEATURE_THUMBNAILS[feature]) return FEATURE_THUMBNAILS[feature];
  if (category && category.mode === "portrait") return "assets/thumb-portrait.png";
  if (category && category.mode === "color") return "assets/thumb-color.png";
  if (category && category.mode === "typing") return "assets/thumb-generate.png";
  if (category && category.mode === "oneClick") return "assets/thumb-light.png";
  if (category && category.mode === "filmPending") return "assets/thumb-film.png";
  if (category && category.mode === "settings") return "assets/thumb-settings.png";
  return "assets/xnh-cover.png";
}

function categorySearchMajor(category) {
  if (!category) return "功能模块";
  if (category.name === "人像精修") return "人像精修";
  if (category.name === "AI打光") return "AI打光";
  if (category.name === "AI生成") return "AI生成";
  if (category.name === "色彩处理") return "色彩处理";
  if (category.name === "胶片模拟") return "胶片模拟";
  if (category.mode === "local" || category.mode === "typing" || category.mode === "imagination") return "AI生成";
  if (category.mode === "film") return "胶片仿制";
  if (category.mode === "light" || category.mode === "lightPro") return "光线处理";
  if (category.mode === "settings") return "系统设置";
  return category.name || "功能模块";
}

function allSearchEntries() {
  const entries = [];
  const seen = {};
  HOME_CARDS.forEach(function (card) {
    const key = `major:${card.major}`;
    if (seen[key]) return;
    seen[key] = true;
    const meta = uiCardMeta("homeCards", card.title, card);
    entries.push({
      type: "major",
      major: card.major,
      title: card.title,
      displayTitle: meta.title,
      tag: meta.tag,
      subtitle: meta.subtitle,
      description: meta.description,
      cover: meta.cover
    });
  });
  [CATEGORIES, FILM_CATEGORIES, LIGHT_CATEGORIES, [AI_GENERATE_CATEGORY], [FILM_PENDING_CATEGORY], [SETTINGS_CATEGORY]].forEach(function (group) {
    group.forEach(function (category) {
      const major = categorySearchMajor(category);
      getCategoryFeatures(category).forEach(function (feature) {
        const key = `feature:${major}:${category.name}:${feature}`;
        if (seen[key]) return;
        seen[key] = true;
        const meta = uiCardMeta("featureCards", feature, {
          title: feature,
          tag: feature,
          subtitle: `${categoryDisplayTitle(category)} / ${featureCardSubtitle(feature, category)}`,
          description: promptForFeature(feature, category),
          cover: featureCardImage(feature, category)
        });
        entries.push({
          type: "feature",
          major: major,
          category: category,
          title: feature,
          displayTitle: meta.title,
          tag: meta.tag,
          subtitle: meta.subtitle,
          description: meta.description,
          cover: meta.cover
        });
      });
    });
  });
  SETTINGS_CARDS.forEach(function (card) {
    const key = `setting:${card.action}`;
    if (seen[key]) return;
    seen[key] = true;
    const meta = uiCardMeta("settingCards", card.title, card);
    entries.push({
      type: "setting",
      major: "系统设置",
      title: card.title,
      displayTitle: meta.title,
      tag: meta.tag,
      subtitle: meta.subtitle,
      description: meta.description || meta.subtitle,
      cover: meta.cover,
      action: card.action
    });
  });
  return entries;
}

function clearSearchState() {
  state.searchQuery = "";
  state.searchOpen = false;
  localStorage.removeItem(STORAGE_KEYS.searchQuery);
}

function openSearchEntry(entry) {
  if (!guardMemberInteraction()) return;
  clearSearchState();
  if (entry.type === "major") {
    jumpToMajor(entry.major);
    return;
  }
  if (entry.type === "setting") {
    state.activeMajor = "系统设置";
    state.activeCategory = "系统设置";
    state.activeFeature = "";
    state.detailOpen = false;
    renderAll();
    runSettingCardAction(entry.action);
    return;
  }
  state.activeMajor = entry.major || categorySearchMajor(entry.category);
  state.activeCategory = entry.category ? entry.category.name : state.activeMajor;
  state.activeFeature = entry.title;
  state.detailOpen = true;
  renderAll();
}

function renderGlobalSearchCards(grid, query) {
  grid.className = "home-grid feature-directory-grid global-search-grid";
  const entries = allSearchEntries().filter(function (entry) {
    return fuzzyMatchText([entry.title, entry.displayTitle, entry.tag, entry.subtitle, entry.description, entry.major].join(" "), query);
  });
  if (!entries.length) {
    renderSearchEmpty(grid, "全局功能");
    return;
  }
  entries.forEach(function (entry) {
    const button = document.createElement("div");
    button.setAttribute("role", "button");
    button.setAttribute("tabindex", "0");
    button.className = "home-card feature-card";
    button.title = `${entry.displayTitle || entry.title}\n${entry.subtitle}`;
    button.innerHTML = `
      <div class="home-cover" style="background-image:url('${escapeAttr(entry.cover)}')">
        <span>${escapeText(entry.tag || entry.title)}</span>
      </div>
      <div class="home-card-body">
        <strong>${escapeText(entry.displayTitle || entry.title)}</strong>
        <small>${escapeText(entry.subtitle || entry.major)}</small>
      </div>
    `;
    button.addEventListener("click", function () { openSearchEntry(entry); });
    enableKeyboardClick(button);
    grid.appendChild(button);
  });
}

function openFeatureDetail(feature) {
  if (!guardMemberInteraction()) return;
  persistCurrentParamDraft();
  state.activeFeature = feature;
  state.detailOpen = true;
  renderAll();
}

function renderFeatureDirectoryCards(grid, category) {
  grid.className = "home-grid feature-directory-grid";
  const query = activeSearchQuery();
  const features = getCategoryFeatures(category).filter(function (feature) {
    const meta = uiCardMeta("featureCards", feature, { title: feature, subtitle: featureCardSubtitle(feature, category) });
    return fuzzyMatchText([feature, meta.title, meta.tag, meta.subtitle, promptForFeature(feature, category), category.name, categoryDisplayTitle(category)].join(" "), query);
  });
  if (!features.length) {
    if (query) {
      renderSearchEmpty(grid, "功能");
      return;
    }
    const empty = document.createElement("div");
    empty.className = "home-card feature-card pending-feature-card";
    empty.innerHTML = `
      <div class="home-cover" style="background-image:url('${escapeAttr(featureCardImage("", category))}')">
        <span>更新中</span>
      </div>
      <div class="home-card-body">
        <strong>${escapeText(categoryDisplayTitle(category) || "功能")}</strong>
        <small>功能正在接入，后续服务器和工作流准备好后自动更新。</small>
      </div>
    `;
    grid.appendChild(empty);
    return;
  }
  features.forEach(function (feature) {
    const meta = uiCardMeta("featureCards", feature, {
      title: feature,
      tag: feature,
      subtitle: featureCardSubtitle(feature, category),
      cover: featureCardImage(feature, category)
    });
    const button = document.createElement("div");
    button.setAttribute("role", "button");
    button.setAttribute("tabindex", "0");
    button.className = "home-card feature-card";
    button.title = `${meta.title}\n${meta.subtitle}`;
    button.innerHTML = `
      <div class="home-cover" style="background-image:url('${escapeAttr(meta.cover)}')">
        <span>${escapeText(meta.tag || meta.title)}</span>
      </div>
      <div class="home-card-body">
        <strong>${escapeText(meta.title)}</strong>
        <small>${escapeText(meta.subtitle)}</small>
      </div>
    `;
    button.addEventListener("click", function () { openFeatureDetail(feature); });
    enableKeyboardClick(button);
    grid.appendChild(button);
  });
}

function renderSettingsCards(grid) {
  grid.className = "home-grid feature-directory-grid settings-directory-grid";
  const query = activeSearchQuery();
  const cards = SETTINGS_CARDS.filter(function (card) {
    return fuzzyMatchText([card.title, card.tag, card.subtitle].join(" "), query);
  });
  if (!cards.length) {
    renderSearchEmpty(grid, "设置");
    return;
  }
  cards.forEach(function (card) {
    const meta = uiCardMeta("settingCards", card.title, card);
    const button = document.createElement("div");
    button.setAttribute("role", "button");
    button.setAttribute("tabindex", "0");
    button.className = "home-card feature-card setting-card";
    button.title = `${meta.title}\n${meta.subtitle}`;
    button.innerHTML = `
      <div class="home-cover" style="background-image:url('${escapeAttr(meta.cover)}')">
        <span>${escapeText(meta.tag)}</span>
      </div>
      <div class="home-card-body">
        <strong>${escapeText(meta.title)}</strong>
        <small>${escapeText(meta.subtitle)}</small>
      </div>
    `;
    button.addEventListener("click", function () { runSettingCardAction(card.action); });
    enableKeyboardClick(button);
    grid.appendChild(button);
  });
}

function renderFeatures() {
  const grid = byId("featureGrid");
  grid.innerHTML = "";
  const category = currentCategory();
  const query = activeSearchQuery();
  if (query) {
    renderGlobalSearchCards(grid, query);
    return;
  }
  if (category.mode === "home") {
    renderHomeCards(grid);
    return;
  }
  if (category.mode === "settings") {
    grid.className = "feature-grid hidden";
    return;
  }
  if (state.detailOpen && currentFeature()) {
    grid.className = "feature-grid hidden";
    return;
  }
  if (category.mode === "filmPending") {
    renderFeatureDirectoryCards(grid, category);
    return;
  }
  renderFeatureDirectoryCards(grid, category);
  return;
  grid.className = "feature-grid";
  const features = getCategoryFeatures(category);
  const lightDirectory = state.activeMajor === "AI打光" || state.activeMajor === "光线处理";
  setClass(grid, "light-directory-grid", lightDirectory);
  if (!features.length) {
    grid.innerHTML = category.mode === "exclusive"
      ? `<div class="empty-custom exclusive-empty">独家模型待接入</div>`
      : `<div class="empty-custom">暂无功能</div>`;
    return;
  }
  features.forEach(function (feature) {
    const button = document.createElement("div");
    button.setAttribute("role", "button");
    button.setAttribute("tabindex", "0");
    button.innerHTML = `
      <span class="feature-mini-icon" aria-hidden="true">${escapeText(featureIconGlyph(feature, category))}</span>
      <span class="feature-label">${escapeText(feature)}</span>
    `;
    const profile = operationProfileForFeature(feature, category);
    const originalLabel = profile.localReplacementFor ? "\n兼容资源已匹配" : "";
    button.title = `${feature}\n用途：${promptForFeature(feature, category)}${originalLabel}`;
    button.className = feature === currentFeature() ? "feature-button active" : "feature-button";
    if (lightDirectory) button.className += " light-feature-card";
    button.addEventListener("click", function () {
      persistCurrentParamDraft();
      state.activeFeature = feature;
      renderFeatures();
      renderParams();
    });
    enableKeyboardClick(button);
    grid.appendChild(button);
  });
}

function optionHtml(items, selected) {
  return items.map(function (item) {
    return `<option value="${escapeAttr(item)}"${selectedAttr(item, selected)}>${escapeText(item)}</option>`;
  }).join("");
}


function workflowMetaForCurrent() {
  const file = workflowFileForCurrent();
  for (let index = 0; index < state.workflows.length; index += 1) {
    if (state.workflows[index].file === file) return state.workflows[index];
  }
  return {};
}

function checkpointKeywordsForFeature(feature, category, defaults) {
  const map = {
    "磨皮": ["写真极速磨皮", "批量磨皮XL", "人像XL", "真境写真XL"],
    "网红感": ["自动修图美颜", "写真极速磨皮", "人像XL", "真境写真XL"],
    "修皮肤": ["批量磨皮XL", "人像XL", "真境写真XL"],
    "自动精修": ["批量磨皮XL", "人像XL"],
    "商业质感": ["真境写真XL", "人像XL", "realvisxl"],
    "去双下巴": ["人像XL", "批量磨皮XL"],
    "老照片修复": ["realvisxl", "XHN合成", "人像XL"],
    "照片放大": ["realvisxl", "XHN合成", "人像XL"],
    "修复裙摆": ["XHN合成", "realvisxl", "人像XL"],
    "瘦身": ["XHN合成", "人像XL", "realvisxl"],
    "光影优化": ["realvisxl", "人像XL", "真境写真XL", "XHN合成"],
    "面部增强": ["人像XL", "批量磨皮XL"],
    "面部迁移": ["人像XL", "realvisxl"],
    "追色0.5版本": ["XHN合成", "realvisxl", "人像XL"],
    "追色追光0.5版本": ["realvisxl", "人像XL", "真境写真XL", "XHN合成"]
  };
  if (map[feature]) return map[feature];
  if (category && category.name === "本地创成") return ["XHN合成", "realvisxl", "dreamshaperXL", "人像XL"];
  if (category && (category.name === "创意云" || category.name === "AI生成")) return ["XHN合成", "dreamshaperXL", "realvisxl", "人像XL"];
  if (category && category.name === "AI打光") return ["realvisxl", "人像XL", "真境写真XL", "dreamshaperXL", "XHN合成"];
  if (category && category.mode === "film") return ["realvisxl", "XHN合成", "人像XL"];
  if (category && (category.mode === "light" || category.mode === "lightPro")) return ["realvisxl", "人像XL", "真境写真XL", "XHN合成"];
  const meta = workflowMetaForCurrent();
  return (meta.preferredCheckpointKeywords || defaults.preferredCheckpointKeywords || []).slice();
}

function autoCheckpointForCurrent(defaults) {
  const models = state.models || [];
  if (!models.length) return "默认模型";
  const keywords = checkpointKeywordsForFeature(currentFeature(), currentCategory(), defaults || {});
  for (let index = 0; index < keywords.length; index += 1) {
    const matched = localMatch(models, keywords[index]);
    if (matched) return matched;
  }
  return models[0];
}

function modelSelectHtml() {
  const models = state.models.length ? state.models : ["默认模型"];
  const autoSelected = autoCheckpointForCurrent(currentWorkflowDefaults());
  const selected = draftValue("checkpointName", autoSelected);
  return `<select id="checkpointName" title="${escapeAttr(modelDescription(selected))}">${optionHtml(models, selected)}</select>`;
}

function sourcePreviewCard() {
  return `
    <div class="param-card field-stack">
      <label>当前画布</label>
      <div class="reference-preview">
        ${state.inputPreviewUrl ? `<img src="${escapeAttr(state.inputPreviewUrl)}" alt="当前画布缩略图" />` : "<span>点击生成时会自动导出当前 Photoshop 画布</span>"}
      </div>
    </div>
  `;
}

function promptBox(prompt, label) {
  const text = draftValue("promptText", prompt);
  return `
    <div class="param-card prompt-card">
      <label>${escapeText(label || "正向提示词：")}</label>
      <textarea id="promptText" placeholder="请输入提示词">${escapeText(text)}</textarea>
    </div>
  `;
}

function sliderCard(id, label, value, min, max, step) {
  return `
    <div class="param-card">
      ${sliderControl(id, label, value, min, max, step)}
    </div>
  `;
}

function sliderControl(id, label, value, min, max, step) {
  const drafted = draftValue(id, value);
  const safe = Number.isFinite(Number(drafted)) ? Number(drafted) : 0;
  const safeMin = Number.isFinite(Number(min)) ? Number(min) : 0;
  const safeMax = Number.isFinite(Number(max)) ? Number(max) : 1;
  const safeStep = Number.isFinite(Number(step)) ? Number(step) : 0.01;
  const decimals = safeStep >= 1 ? 0 : 2;
  const shown = safeStep >= 1 ? String(Math.round(safe)) : safe.toFixed(decimals);
  return `
    <div class="slider-row">
      <label for="${id}">${escapeText(label)}</label>
      <input id="${id}" class="slider-value" type="text" data-min="${safeMin}" data-max="${safeMax}" data-step="${safeStep}" value="${shown}" />
      <div class="xhn-slider" data-slider-for="${id}" data-min="${safeMin}" data-max="${safeMax}" data-step="${safeStep}">
        <div class="xhn-slider-fill"></div>
        <div class="xhn-slider-thumb"></div>
      </div>
    </div>
  `;
}

function compactSliderRows(rows) {
  return `
    <div class="param-card compact-controls">
      ${rows.map(function (row) {
        return sliderControl(row.id, row.label, row.value, row.min, row.max, row.step);
      }).join("")}
    </div>
  `;
}

function checkboxCard(title, items) {
  return `
    <div class="param-card">
      <label>${escapeText(title)}</label>
      <div class="checkbox-grid">
        ${items.map(function (item, index) {
          const id = `part-${index}`;
          return `<label class="check-item"><input class="part-check" type="checkbox" value="${escapeAttr(item)}"${index === 0 ? " checked" : ""} />${escapeText(item)}</label>`;
        }).join("")}
      </div>
    </div>
  `;
}

function referenceCard(label, hint, showClear) {
  const hasReference = !!state.referenceImage || !!state.referencePreviewUrl || !!state.referenceName;
  return `
    <div class="param-card field-stack">
      <label>${escapeText(label || "选择参考图")}</label>
      <div class="reference-preview">
        ${state.referencePreviewUrl ? `<img src="${escapeAttr(state.referencePreviewUrl)}" alt="参考图缩略图" />` : `<span>${escapeText(state.referenceName || hint || "未选择参考图")}</span>`}
      </div>
      <div class="reference-controls">
        <div id="pickReferenceBtn" class="panel-action outline-button" role="button" tabindex="0">选择参考图</div>
        <div id="currentReferenceBtn" class="panel-action outline-button" role="button" tabindex="0">当前图参考</div>
        <div id="clearReferenceBtn" class="panel-action outline-button${showClear && hasReference ? "" : " is-disabled"}" role="button" tabindex="0" aria-disabled="${showClear && hasReference ? "false" : "true"}">清除</div>
      </div>
      <div class="hint compact-hint">${escapeText(state.referenceName || hint || "参考图会显示缩略图，并随当前任务一起提交。")}</div>
    </div>
  `;
}

function modelGrid(defaults) {
  const sampler = draftValue("samplerName", defaults.sampler || "euler");
  return `
    <div class="param-card">
      <div class="select-grid">
        ${modelSelectHtml()}
        <select id="samplerName">
          <option value="euler"${selectedAttr("euler", sampler)}>默认采样器</option>
          <option value="dpmpp_2m"${selectedAttr("dpmpp_2m", sampler)}>DPM++ 2M</option>
        </select>
        <div id="refreshModelsInlineBtn" class="panel-action outline-button" role="button" tabindex="0">刷新</div>
      </div>
    </div>
  `;
}

function standardPanel(prompt, defaults) {
  return `
    ${sourcePreviewCard()}
    ${promptBox(prompt, "正向提示词：")}
    ${modelGrid(defaults)}
    <div class="param-card param-grid">
      <label>迭代步数<input id="stepsValue" type="number" value="${Number(draftNumber("stepsValue", defaults.steps || 30))}" /></label>
      <label>CFG<input id="cfgValue" type="number" value="${Number(draftNumber("cfgValue", defaults.cfg || 5))}" /></label>
      <label>像素大小<input id="sizeValue" type="number" value="${Number(draftNumber("sizeValue", defaults.width || defaults.height || 1280))}" /></label>
    </div>
    ${sliderCard("denoiseValue", "重绘幅度", defaults.denoise || 0.1)}
  `;
}

function identityLockCard(colorOnly) {
  const checked = draftChecked("identityLock", true) ? " checked" : "";
  return `
    <div class="param-card identity-lock-card">
      <label class="check-item identity-check-row">
        <input id="identityLock" class="identity-check" type="checkbox"${checked} />
        ${colorOnly ? "只迁移色彩，不改人物" : "人物不变 / 构图不变"}
      </label>
    </div>
  `;
}

function exclusivePanel() {
  return `
    <div class="param-card feature-note-card exclusive-panel">
      <strong>独家</strong>
      <span>这个入口已经预留，后续接入你的独家模型和工作流后会在这里显示真实功能按钮。</span>
    </div>
  `;
}

function customPanel(feature, prompt) {
  const text = draftValue("customFeaturePrompt", prompt);
  return `
    <div class="param-card custom-editor compact-editor">
      <label>提示词 / 功能说明</label>
      <textarea id="customFeaturePrompt" placeholder="写这个按钮要执行的效果">${escapeText(text)}</textarea>
    </div>
  `;
}

function customActionButtonsHtml() {
  return `
    <div id="customActionSection" class="param-card action-card">
      <div class="button-row compact-actions">
        <div id="newCustomFeatureBtn" class="panel-action outline-button action-button" role="button" tabindex="0">新增常用</div>
        <div id="saveCustomFeatureBtn" class="panel-action solid-button action-button" role="button" tabindex="0">保存常用</div>
        <div id="deleteCustomFeatureBtn" class="panel-action danger-button action-button" role="button" tabindex="0">删除当前</div>
      </div>
    </div>
  `;
}

function actionButtonsHtml() {
  return `
    <div id="actionSection" class="param-card action-card action-section">
      <div class="button-row action-row">
        <div id="generateBtn" class="panel-action solid-button action-button" role="button" tabindex="0">开始绘制</div>
        <div id="cancelBtn" class="panel-action danger-button action-button" role="button" tabindex="0">中断任务</div>
        <div id="batchBtn" class="panel-action outline-button action-button" role="button" tabindex="0">批量处理</div>
      </div>
    </div>
  `;
}

function appendCustomActions(panel) {
  if (!panel) return;
  const section = document.createElement("div");
  section.id = "customActionSection";
  section.className = "param-card action-card custom-action-section";
  const host = document.createElement("div");
  host.className = "button-row compact-actions";
  host.appendChild(makePanelButton("newCustomFeatureBtn", "新增常用", "outline-button"));
  host.appendChild(makePanelButton("saveCustomFeatureBtn", "保存常用", "solid-button"));
  host.appendChild(makePanelButton("deleteCustomFeatureBtn", "删除当前", "danger-button"));
  section.appendChild(host);
  panel.appendChild(section);
}

function appendActionButtons(panel) {
  const section = document.createElement("div");
  section.id = "actionSection";
  section.className = "param-card action-card action-section";
  const row = document.createElement("div");
  row.className = "button-row action-row";
  row.appendChild(makePanelButton("generateBtn", "开始绘制", "solid-button"));
  row.appendChild(makePanelButton("cancelBtn", "中断任务", "danger-button"));
  row.appendChild(makePanelButton("batchBtn", "批量处理", "outline-button"));
  section.appendChild(row);
  panel.appendChild(section);
}


function featureLoraChoicePanel(feature, label) {
  const choices = FEATURE_LORA_CHOICES[feature] || [];
  if (!choices.length) return "";
  return loraChoicePanel(label || "模型选择", choices);
}

function colorPanel(feature, prompt, defaults) {
  const isLight = feature === "追色追光0.5版本" || feature === "追光追色1.0";
  const profile = operationProfileForFeature(feature, currentCategory());
  return `
    ${referenceCard("选择参考图", "必须选择参考图，用于追色或追光。", true)}
    ${isLight && !profile.disableLora ? featureLoraChoicePanel(feature, "追光模型") : ""}
    ${promptBox(prompt, isLight ? "追色追光提示词：" : "追色提示词：")}
    ${compactSliderRows([
      { id: "referenceStrength", label: "参考图强度", value: defaults.referenceStrength == null ? (isLight ? 0.58 : 0.55) : defaults.referenceStrength },
      { id: "originalStrength", label: "原图保留", value: defaults.originalStrength == null ? (isLight ? 0.80 : 0.88) : defaults.originalStrength }
    ])}
    ${isLight ? compactSliderRows([
      { id: "lightStrength", label: "光影强度", value: defaults.lightStrength == null ? 0.45 : defaults.lightStrength },
      { id: "denoiseValue", label: "重绘幅度", value: defaults.denoise == null ? 0.22 : defaults.denoise }
    ]) : ""}
  `;
}


function portraitPanel(feature, prompt, defaults) {
  const needsReference = operationProfileForFeature(feature, currentCategory()).requiresReference;
  const denoiseLabel = feature === "去双下巴" ? "轮廓修复" : feature === "瘦身" ? "瘦身强度" : feature === "修复裙摆" ? "修复幅度" : feature === "照片放大" || feature === "老照片修复" ? "修复幅度" : "重绘幅度";
  const detailLabel = feature === "修皮肤" || feature === "自动精修" ? "肤质细节" : feature === "商业质感" ? "质感细节" : "细节强度";
  return `
    ${sourcePreviewCard()}
    ${needsReference ? referenceCard("选择参考图", "当前功能需要参考图辅助匹配。", true) : ""}
    ${promptBox(prompt, "正向提示词：")}
    ${modelGrid(defaults)}
    ${featureLoraChoicePanel(feature, feature === "一键下雪" ? "雪景 LoRA 模型" : "LoRA 模型")}
    <div class="param-card param-grid">
      <label>迭代步数<input id="stepsValue" type="number" value="${Number(draftNumber("stepsValue", defaults.steps || 12))}" /></label>
      <label>CFG<input id="cfgValue" type="number" value="${Number(draftNumber("cfgValue", defaults.cfg || 4.2))}" /></label>
      <label>像素大小<input id="sizeValue" type="number" value="${Number(draftNumber("sizeValue", defaults.width || defaults.height || 1280))}" /></label>
    </div>
    ${compactSliderRows([
      { id: "denoiseValue", label: denoiseLabel, value: defaults.denoise == null ? 0.22 : defaults.denoise },
      { id: "lowFrequencyValue", label: detailLabel, value: defaults.lowFrequency == null ? 0.42 : defaults.lowFrequency },
      { id: "blendStrength", label: "人物细节保留", value: defaults.blendStrength == null ? 0.82 : defaults.blendStrength }
    ])}
    ${defaults.lightStrength != null ? sliderCard("lightStrength", "光影强度", defaults.lightStrength) : ""}
  `;
}


function localPanel(feature, prompt, defaults) {
  if (feature === "抠图") {
    return `
      ${sourcePreviewCard()}
      ${modelGrid(defaults)}
      ${compactSliderRows([
        { id: "denoiseValue", label: "边缘清理", value: defaults.denoise == null ? 0.18 : defaults.denoise },
        { id: "blendStrength", label: "主体保留", value: defaults.blendStrength == null ? 0.86 : defaults.blendStrength }
      ])}
    `;
  }
  const needsReference = operationProfileForFeature(feature, currentCategory()).requiresReference;
  const requiresSelection = operationProfileForFeature(feature, currentCategory()).requiresSelection;
  return `
    ${sourcePreviewCard()}
    ${needsReference ? referenceCard("选择参考图", "用于匹配材质、衣服、风格或构图。", true) : ""}
    ${promptBox(prompt, "正向提示词：")}
    ${modelGrid(defaults)}
    ${featureLoraChoicePanel(feature, feature === "一键下雪" ? "雪景 LoRA 模型" : "LoRA 模型")}
    <div class="param-card param-grid">
      <label>像素大小<input id="sizeValue" type="number" value="${Number(draftNumber("sizeValue", defaults.width || defaults.height || 1280))}" /></label>
      <label>步数<input id="stepsValue" type="number" value="${Number(draftNumber("stepsValue", defaults.steps || 12))}" /></label>
      <label>CFG<input id="cfgValue" type="number" value="${Number(draftNumber("cfgValue", defaults.cfg || 4.2))}" /></label>
    </div>
    ${compactSliderRows([
      { id: "denoiseValue", label: requiresSelection ? "局部重绘强度" : "重绘强度", value: defaults.denoise == null ? 0.34 : defaults.denoise },
      { id: "lowFrequencyValue", label: "细节强度", value: defaults.lowFrequency == null ? 0.42 : defaults.lowFrequency },
      { id: "blendStrength", label: "原图保留", value: defaults.blendStrength == null ? 0.78 : defaults.blendStrength }
    ])}
  `;
}



function typingPanel(feature, prompt, defaults) {
  const profile = operationProfileForFeature(feature, currentCategory());
  const optionalReference = feature === "打字改图1.0" || feature === "打字改图" || feature === "Qwen改图";
  const firstControls = [
    { id: "denoiseValue", label: "生成强度", value: defaults.denoise == null ? 0.42 : defaults.denoise },
    { id: "lightStrength", label: "优化强度", value: defaults.lightStrength == null ? 0.45 : defaults.lightStrength }
  ];
  if (optionalReference) firstControls.splice(1, 0, { id: "referenceStrength", label: "参考强度", value: defaults.referenceStrength == null ? 0.56 : defaults.referenceStrength });
  return `
    ${optionalReference ? referenceCard("可选参考图", "可不选；选择后会作为风格、材质或构图参考。", true) : ""}
    ${promptBox(prompt, "提示词：")}
    ${sourcePreviewCard()}
    ${modelGrid(defaults)}
    ${featureLoraChoicePanel(feature, feature === "一键下雪" ? "雪景 LoRA 模型" : "LoRA 模型")}
    <div class="param-card param-grid">
      <label>像素大小<input id="sizeValue" type="number" value="${Number(draftNumber("sizeValue", defaults.width || defaults.height || 1280))}" /></label>
      <label>步数<input id="stepsValue" type="number" value="${Number(draftNumber("stepsValue", defaults.steps || 12))}" /></label>
      <label>CFG<input id="cfgValue" type="number" value="${Number(draftNumber("cfgValue", defaults.cfg || 4.2))}" /></label>
    </div>
    ${compactSliderRows(firstControls)}
    ${compactSliderRows([
      { id: "blendStrength", label: "人物保留", value: defaults.blendStrength == null ? 0.80 : defaults.blendStrength },
      { id: "originalStrength", label: "原图保留", value: defaults.originalStrength == null ? 0.76 : defaults.originalStrength }
    ])}
  `;
}

function optionChip(label, value, type) {
  const key = selectionGroupKey();
  const selected = state.optionSelections[key];
  const active = selected && selected.label === label ? " active" : "";
  return `<div class="option-chip${active}" role="button" tabindex="0" data-select-group="${escapeAttr(key)}" data-select-label="${escapeAttr(label)}" data-select-value="${escapeAttr(value || label)}" data-select-type="${escapeAttr(type || "preset")}">${escapeText(label)}</div>`;
}

function loraChip(label, value) {
  return optionChip(label, value || label, "lora");
}

function ensureLoraSelection(label, value) {
  const key = selectionGroupKey();
  if (!state.optionSelections[key]) {
    state.optionSelections[key] = { label: label, value: value || label, type: "lora" };
  }
}

function loraChoicePanel(label, items) {
  if (items && items.length) ensureLoraSelection(items[0].label, items[0].value);
  return choicePanel(label, items.map(function (item) {
    return loraChip(item.label, item.value);
  }).join(""));
}

function choicePanel(label, chipsHtml) {
  return `
    <div class="param-card field-stack">
      <label>${escapeText(label)}</label>
      <div class="chips model-choices">${chipsHtml}</div>
    </div>
  `;
}


function oneClickPanel(feature, prompt, defaults) {
  return `
    ${sourcePreviewCard()}
    ${promptBox(prompt, "打光提示词：")}
    ${modelGrid(defaults)}
    ${featureLoraChoicePanel(feature, "打光 LoRA 模型")}
    <div class="param-card param-grid">
      <label>像素大小<input id="sizeValue" type="number" value="${Number(draftNumber("sizeValue", defaults.width || defaults.height || 1280))}" /></label>
      <label>步数<input id="stepsValue" type="number" value="${Number(draftNumber("stepsValue", defaults.steps || 12))}" /></label>
      <label>CFG<input id="cfgValue" type="number" value="${Number(draftNumber("cfgValue", defaults.cfg || 4.2))}" /></label>
    </div>
    ${compactSliderRows([
      { id: "lightStrength", label: "光影强度", value: defaults.lightStrength == null ? 0.58 : defaults.lightStrength },
      { id: "denoiseValue", label: "优化强度", value: defaults.denoise == null ? 0.24 : defaults.denoise },
      { id: "lowFrequencyValue", label: "颗粒 / 细节", value: defaults.lowFrequency == null ? 0.20 : defaults.lowFrequency }
    ])}
    ${sliderCard("blendStrength", "人物细节强度", defaults.blendStrength == null ? 0.84 : defaults.blendStrength)}
  `;
}


function imaginationPanel(feature, prompt) {
  return standardPanel(prompt, currentWorkflowDefaults());
}

function filmPanel(feature, prompt, defaults) {
  const useReference = feature === "富士人像" || feature === "柯达婚礼" || feature === "森系胶片" || feature === "草坪婚礼";
  return `
    ${useReference ? referenceCard("选择参考图", "可选：选择一张胶片色彩参考图，缩略图会显示在这里。", true) : ""}
    ${promptBox(prompt, "胶片提示词：")}
    ${compactSliderRows([
      { id: "denoiseValue", label: "胶片强度", value: defaults.denoise || 0.24 },
      { id: "blendStrength", label: "保留原图", value: 0.72 }
    ])}
  `;
}

function lightProPanel(feature, prompt, defaults) {
  const meta = LIGHT_CARD_META[feature] || {};
  const heading = `
    <div class="param-card feature-note-card">
      <strong>${escapeText(feature)}</strong>
      <span>${escapeText(meta.detail || "专业光线处理，使用当前 Photoshop 画布作为输入。")}</span>
    </div>
  `;
  if (feature === "背景提取") {
    return `
      ${heading}
      ${sourcePreviewCard()}
      ${choicePanel("去人算法", optionChip("Klein", "klein", "preset") + optionChip("Qwen", "qwen", "preset"))}
      ${compactSliderRows([
        { id: "denoiseValue", label: "边缘清理", value: defaults.denoise || 0.24 },
        { id: "blendStrength", label: "主体保留", value: 0.86 }
      ])}
      ${promptBox(prompt, "中文提示词：")}
    `;
  }
  if (feature === "全景融合") {
    return `
      ${heading}
      ${referenceCard("加载场景参考", "选择场景或风格参考图，也可以点当前图参考。", true)}
      ${promptBox(prompt, "中文提示词：")}
      ${modelGrid(defaults)}
      ${compactSliderRows([
        { id: "referenceStrength", label: "参考融合", value: 0.78 },
        { id: "blendStrength", label: "光影统一", value: 0.72 }
      ])}
    `;
  }
  if (feature === "重绘融合") {
    return `
      ${heading}
      ${referenceCard("加载重绘参考", "可选：用参考图控制局部质感和光影方向。", true)}
      ${checkboxCard("保留细节", ["脸部", "头发", "服装", "饰品"])}
      ${promptBox(prompt, "中文提示词：")}
      ${compactSliderRows([
        { id: "denoiseValue", label: "重绘幅度", value: defaults.denoise || 0.36 },
        { id: "lightStrength", label: "光线强度", value: 0.68 }
      ])}
    `;
  }
  if (feature === "自由光线") {
    return `
      ${heading}
      ${promptBox(prompt, "中文提示词：")}
      ${choicePanel("生成模式", optionChip("稳定", "stable", "preset") + optionChip("适中", "balanced", "preset") + optionChip("创意", "creative", "preset"))}
      ${compactSliderRows([
        { id: "lightStrength", label: "主光强度", value: 0.72 },
        { id: "denoiseValue", label: "改动幅度", value: defaults.denoise || 0.28 },
        { id: "blendStrength", label: "原图保留", value: 0.74 }
      ])}
    `;
  }
  return lightPanel(feature, prompt, defaults);
}

function lightPanel(feature, prompt, defaults) {
  return `
    ${promptBox(prompt, "光线提示词：")}
    ${compactSliderRows([
      { id: "lightStrength", label: "光线强度", value: 0.65 },
      { id: "denoiseValue", label: "重绘幅度", value: defaults.denoise || 0.26 }
    ])}
  `;
}

function homePanel() {
  return `
    <div class="param-card home-intro-card">
      <strong>选择左侧栏目或上方功能卡片开始</strong>
      <span>插件端不需要登录；请先在 XNH 启动器登录并一键启动，本面板会自动读取启动器用户码。</span>
    </div>
  `;
}

function directoryPanel(category) {
  const name = category && category.name ? category.name : "功能模块";
  const isPending = category && category.mode === "filmPending";
  return `
    <div class="param-card home-intro-card directory-intro-card">
      <strong>${escapeText(isPending ? "胶片模拟正在更新中" : "选择上方功能卡片开始")}</strong>
      <span>${escapeText(isPending ? "胶片模拟栏目暂时空着，后续接入后这里会显示具体功能。" : `当前栏目：${name}。点击上方任意功能卡片，会进入加载图片、参数设置和执行页面。`)}</span>
    </div>
  `;
}

function clearPluginCache() {
  state.referenceImage = "";
  state.referenceName = "";
  if (state.referencePreviewUrl) URL.revokeObjectURL(state.referencePreviewUrl);
  state.referencePreviewUrl = "";
  if (state.inputPreviewUrl) URL.revokeObjectURL(state.inputPreviewUrl);
  state.inputPreviewUrl = "";
  state.inputImage = "";
  state.inputDocument = null;
  state.sourceSelection = null;
  state.paramDrafts = {};
  state.results = [];
  state.latestResult = null;
  localStorage.removeItem(STORAGE_KEYS.referenceName);
  setProgress(0);
}

function runSettingCardAction(actionName) {
  if (actionName === "clearCache") {
    clearPluginCache();
    renderAll();
    setLog("插件临时缓存已清理。");
    return;
  }
  if (actionName === "autoSend") {
    state.settings.autoSend = !state.settings.autoSend;
    saveSettings();
    renderAll();
    setLog(`任务完成后自动发送至 Photoshop：${state.settings.autoSend ? "开启" : "关闭"}`);
    return;
  }
  if (actionName === "layerName") {
    const modes = ["功能名称", "功能名称 + 时间", "原图名称"];
    const current = modes.indexOf(state.settings.layerNameMode);
    state.settings.layerNameMode = modes[(current + 1) % modes.length];
    saveSettings();
    renderAll();
    setLog(`图层命名方式：${state.settings.layerNameMode}`);
    return;
  }
  if (actionName === "security") {
    setLog("会员同步策略已启用：插件同步启动器会员登录，未登录不能生成。");
    return;
  }
  if (actionName === "account") {
    const session = state.launcherSession || {};
    setLog(session.loggedIn ? `当前启动器用户码：${session.userCode || session.username || "已登录"}` : "未检测到启动器登录，请先在 XNH 启动器登录。", !session.loggedIn);
    return;
  }
  if (actionName === "version") {
    setLog(`方涛的AI修图平台 XNH v${VERSION}，更新日期 2026-06-19。`);
  }
}

function settingsPanel() {
  const session = state.launcherSession || {};
  const userCode = session.userCode || session.username || "未登录";
  const loginState = session.loggedIn ? "已登录启动器" : "未检测到启动器登录";
  const expiry = session.expiresAt ? String(session.expiresAt).replace("T", " ").replace(/\.\d+Z?$/, "") : "启动器登录后自动生成";
  const coverChecked = state.settings.coverVideo ? " checked" : "";
  const autoChecked = state.settings.autoSend ? " checked" : "";
  const selectedMode = state.settings.layerNameMode || "功能名称 + 时间";
  return `
      <div class="settings-panel">
      <div class="settings-title-row">
        <strong>系统设置</strong>
      </div>
      <div class="settings-group-title">常规设置</div>
      <div class="setting-row">
        <span>启用封面视频</span>
        <label class="switch"><input id="coverVideoToggle" type="checkbox"${coverChecked} /><i></i></label>
      </div>
      <div class="setting-row">
        <span>任务完成后自动发送至 Photoshop</span>
        <label class="switch"><input id="autoSendToggle" type="checkbox"${autoChecked} /><i></i></label>
      </div>
      <div class="setting-row">
        <span>图层命名方式</span>
        <select id="layerNameMode">
          <option${selectedAttr("功能名称", selectedMode)}>功能名称</option>
          <option${selectedAttr("功能名称 + 时间", selectedMode)}>功能名称 + 时间</option>
          <option${selectedAttr("原图名称", selectedMode)}>原图名称</option>
        </select>
      </div>
      <div class="settings-group-title">存储管理</div>
      <div class="setting-row">
        <span>清理插件产生的临时缓存文件</span>
        <div id="clearCacheBtn" class="panel-action outline-button compact-button" role="button" tabindex="0">立即清理</div>
      </div>
      <div class="settings-group-title">关于</div>
      <div class="about-box">
        <div><span>应用版本</span><strong>v${escapeText(VERSION)}</strong></div>
        <div><span>更新日期</span><strong>2026-06-19</strong></div>
        <div><span>用户码</span><strong>${escapeText(userCode)}</strong></div>
        <div><span>登录状态</span><strong>${escapeText(loginState)}</strong></div>
        <div><span>到期时间</span><strong>${escapeText(expiry)}</strong></div>
        <div><span>连接策略</span><strong>会员同步，桥接地址可自定义</strong></div>
      </div>
      <div class="contact-box">
        <span>邮箱：YUANYEPHOTO@FOXMAIL.COM</span>
        <strong>有想要的功能可以联系我！</strong>
      </div>
    </div>
  `;
}

function selectionGroupKey() {
  return `${state.activeCategory}:${currentFeature()}`;
}

function shouldShowParameterArea(category) {
  if (!category) return false;
  if (activeSearchQuery()) return false;
  if (category.mode === "settings") return true;
  if (category.mode === "home" || category.mode === "filmPending" || category.mode === "exclusive") return false;
  return !!(state.detailOpen && currentFeature());
}

function shouldShowTaskArea(category) {
  if (!category) return false;
  if (activeSearchQuery()) return false;
  if (category.mode === "home" || category.mode === "settings" || category.mode === "filmPending" || category.mode === "exclusive") return false;
  return !!(state.detailOpen && currentFeature());
}


function renderParams() {
  const panel = byId("parameterPanel");
  const category = currentCategory();
  const feature = currentFeature();
  const prompt = promptForFeature(feature, category);
  const defaults = currentWorkflowDefaults();
  let html = "";
  if (!shouldShowParameterArea(category)) html = "";
  else if (category.mode === "home") html = homePanel();
  else if (category.mode === "settings") html = settingsPanel();
  else if (category.mode === "filmPending") html = directoryPanel(category);
  else if (!state.detailOpen || !feature) html = directoryPanel(category);
  else if (category.name === CUSTOM_CATEGORY) html = customPanel(feature, prompt);
  else if (category.mode === "portrait") html = portraitPanel(feature, prompt, defaults);
  else if (category.mode === "color") html = colorPanel(feature, prompt, defaults);
  else if (category.mode === "local") html = localPanel(feature, prompt, defaults);
  else if (category.mode === "typing") html = typingPanel(feature, prompt, defaults);
  else if (category.mode === "oneClick") html = oneClickPanel(feature, prompt, defaults);
  else if (category.mode === "film") html = filmPanel(feature, prompt, defaults);
  else if (category.mode === "lightPro") html = lightProPanel(feature, prompt, defaults);
  else if (category.mode === "light") html = lightPanel(feature, prompt, defaults);
  else if (category.mode === "exclusive") html = exclusivePanel();
  else html = standardPanel(prompt, defaults);
  if (state.detailOpen && category.mode !== "exclusive" && category.mode !== "home" && category.mode !== "settings" && category.mode !== "filmPending" && feature) html += identityLockCard(isColorOnlyFeature(feature, category));
  panel.innerHTML = html;
  if (category.name === CUSTOM_CATEGORY) appendCustomActions(panel);
  if (state.detailOpen && category.mode !== "home" && category.mode !== "settings" && category.mode !== "filmPending") appendActionButtons(panel);
  updateStaticPanels(category);
  bindDynamicParams();
  updateActionLabels();
}

function updateStaticPanels(category) {
  const showParameter = shouldShowParameterArea(category);
  const showTask = shouldShowTaskArea(category);
  setClass(byId("customActionSection"), "hidden", category.name !== CUSTOM_CATEGORY);
  setClass(byId("actionSection"), "hidden", !!activeSearchQuery() || !state.detailOpen || category.mode === "exclusive" || category.mode === "home" || category.mode === "settings" || category.mode === "filmPending" || !currentFeature());
  setClass(byId("parameterSection"), "hidden", !showParameter);
  setClass(byId("progressCard"), "hidden", !showTask);
  setClass(byId("resultSection"), "hidden", !showTask);
  setClass(byId("parameterSection"), "home-parameter", category.mode === "home");
  setClass(byId("parameterSection"), "settings-parameter", category.mode === "settings");
  setClass(byId("parameterSection"), "directory-parameter", !state.detailOpen && category.mode !== "home" && category.mode !== "settings");
  const badge = byId("workflowBadge");
  if (badge) badge.textContent = category.mode === "home" || category.mode === "settings" || category.mode === "filmPending" || !state.detailOpen ? "XNH" : (featureDisplayTitle(currentFeature()) || "XNH");
}


function updateActionLabels() {
  const feature = currentFeature();
  const category = currentCategory();
  const cancel = byId("cancelBtn");
  const batch = byId("batchBtn");
  const generate = byId("generateBtn");
  setControlLabel(cancel, "中断任务");
  setControlLabel(batch, "批量处理");
  let text = "开始生成";
  if (category.name === CUSTOM_CATEGORY || category.name === "人像精修") text = feature.indexOf("修") >= 0 || feature.indexOf("精修") >= 0 ? "开始修复" : "开始生成";
  if (category.name === "本地创成") {
    if (feature === "局部重绘") text = "开始局部重绘";
    else if (feature === "物体移除") text = "开始移除";
    else if (feature === "抠图") text = "开始抠图";
    else text = "开始本地创成";
  }
  if (category.name === "色彩处理") text = feature === "追色追光0.5版本" ? "开始追色追光" : "开始追色";
  if (category.name === "创意云" || category.name === "AI生成") text = "开始修图";
  if (category.name === "AI打光") text = "开始打光";
  if (category.mode === "film") text = "开始仿色";
  if (category.mode === "lightPro") text = feature === "背景提取" ? "开始提取" : "开始融合";
  if (category.mode === "light") text = "开始补光";
  setControlLabel(generate, text);
}

function syncSliderVisual(input) {
  if (!input) return;
  const min = Number(input.getAttribute("min") || input.getAttribute("data-min") || 0);
  const max = Number(input.getAttribute("max") || input.getAttribute("data-max") || 1);
  const step = Number(input.getAttribute("step") || input.getAttribute("data-step") || 0.01);
  let value = Number(input.value);
  if (!Number.isFinite(value)) value = min;
  value = Math.max(min, Math.min(max, value));
  input.value = step >= 1 ? String(Math.round(value)) : value.toFixed(2);
  const label = byId(`${input.id}Label`);
  if (label) label.textContent = step >= 1 ? String(Math.round(value)) : value.toFixed(2);
  const slider = document.querySelector(`[data-slider-for="${input.id}"]`);
  if (!slider) return;
  const percent = max === min ? 0 : ((value - min) / (max - min)) * 100;
  const fill = slider.querySelector(".xhn-slider-fill");
  const thumb = slider.querySelector(".xhn-slider-thumb");
  if (fill) fill.style.width = `${percent}%`;
  if (thumb) thumb.style.left = `${percent}%`;
}

function pointerClientX(event) {
  if (event && event.touches && event.touches.length) return event.touches[0].clientX;
  if (event && event.changedTouches && event.changedTouches.length) return event.changedTouches[0].clientX;
  return event ? event.clientX : 0;
}

function setSliderFromPointer(slider, event) {
  const id = slider.getAttribute("data-slider-for");
  const input = byId(id);
  if (!input) return;
  const rect = slider.getBoundingClientRect();
  const min = Number(slider.getAttribute("data-min") || 0);
  const max = Number(slider.getAttribute("data-max") || 1);
  const step = Number(slider.getAttribute("data-step") || 0.01);
  const raw = min + Math.max(0, Math.min(1, (pointerClientX(event) - rect.left) / Math.max(rect.width, 1))) * (max - min);
  const snapped = Math.round(raw / step) * step;
  input.value = snapped;
  syncSliderVisual(input);
  saveDraftControl(input);
}

let activeSliderDrag = null;

function endSliderDrag() {
  activeSliderDrag = null;
  document.removeEventListener("mousemove", handleSliderDrag);
  document.removeEventListener("mouseup", endSliderDrag);
  document.removeEventListener("touchmove", handleSliderDrag);
  document.removeEventListener("touchend", endSliderDrag);
}

function handleSliderDrag(event) {
  if (!activeSliderDrag) return;
  if (event && event.preventDefault) event.preventDefault();
  setSliderFromPointer(activeSliderDrag, event);
}

function beginSliderDrag(slider, event) {
  if (event && event.preventDefault) event.preventDefault();
  activeSliderDrag = slider;
  setSliderFromPointer(slider, event);
  document.addEventListener("mousemove", handleSliderDrag);
  document.addEventListener("mouseup", endSliderDrag);
  document.addEventListener("touchmove", handleSliderDrag);
  document.addEventListener("touchend", endSliderDrag);
}

function syncNumericValue(input) {
  if (!input) return;
  const min = Number(input.getAttribute("data-min") || 0);
  const max = Number(input.getAttribute("data-max") || 1);
  const step = Number(input.getAttribute("data-step") || 0.01);
  let value = Number(input.value);
  if (!Number.isFinite(value)) value = min;
  value = Math.max(min, Math.min(max, value));
  input.value = step >= 1 ? String(Math.round(value)) : value.toFixed(2);
  const label = byId(`${input.id}Label`);
  if (label) label.textContent = `范围 ${min}-${max}`;
}

function bindDynamicParams() {
  eachNode(document.querySelectorAll("input[type='range']"), function (range) {
    const label = byId(`${range.id}Label`);
    const sync = function () {
      if (!label) return;
      const step = Number(range.step || 0.01);
      label.textContent = Number(range.value).toFixed(step >= 1 ? 0 : 2);
    };
    range.addEventListener("input", sync);
    sync();
  });
  eachNode(document.querySelectorAll(".slider-value"), function (input) {
    input.addEventListener("input", function () {
      syncSliderVisual(input);
      saveDraftControl(input);
    });
    input.addEventListener("change", function () {
      syncSliderVisual(input);
      saveDraftControl(input);
    });
    syncSliderVisual(input);
  });
  eachNode(document.querySelectorAll(".number-value"), function (input) {
    input.addEventListener("input", function () {
      syncNumericValue(input);
      saveDraftControl(input);
    });
    syncNumericValue(input);
  });
  eachNode(document.querySelectorAll(".xhn-slider"), function (slider) {
    slider.addEventListener("mousedown", function (event) { beginSliderDrag(slider, event); });
    slider.addEventListener("touchstart", function (event) { beginSliderDrag(slider, event); });
    slider.addEventListener("click", function (event) { setSliderFromPointer(slider, event); });
  });
  eachNode(document.querySelectorAll(".option-chip"), function (button) {
    button.addEventListener("click", function () {
      const group = button.getAttribute("data-select-group") || selectionGroupKey();
      state.optionSelections[group] = {
        label: button.getAttribute("data-select-label") || safeTrim(button.textContent),
        value: button.getAttribute("data-select-value") || safeTrim(button.textContent),
        type: button.getAttribute("data-select-type") || "preset"
      };
      eachNode(document.querySelectorAll(".option-chip"), function (item) {
        if ((item.getAttribute("data-select-group") || selectionGroupKey()) === group) setClass(item, "active", item === button);
      });
      setLog(`已选择：${state.optionSelections[group].label}`);
    });
    enableKeyboardClick(button);
  });
  const pick = byId("pickReferenceBtn");
  if (pick) {
    pick.addEventListener("click", pickReferenceImage);
    enableKeyboardClick(pick);
  }
  const currentReference = byId("currentReferenceBtn");
  if (currentReference) {
    currentReference.addEventListener("click", useCurrentCanvasAsReference);
    enableKeyboardClick(currentReference);
  }
  const clear = byId("clearReferenceBtn");
  if (clear) {
    clear.addEventListener("click", function () {
      if (clear.classList.contains("is-disabled")) return;
      clearReferenceImage();
    });
    enableKeyboardClick(clear);
  }
  const refresh = byId("refreshModelsInlineBtn");
  if (refresh) {
    refresh.addEventListener("click", refreshModels);
    enableKeyboardClick(refresh);
  }
  const clearCache = byId("clearCacheBtn");
  if (clearCache) {
    clearCache.addEventListener("click", function () {
      clearPluginCache();
      setLog("插件临时缓存已清理。");
      renderAll();
    });
    enableKeyboardClick(clearCache);
  }
  const coverToggle = byId("coverVideoToggle");
  if (coverToggle) {
    coverToggle.addEventListener("change", function () {
      state.settings.coverVideo = !!coverToggle.checked;
      saveSettings();
      syncShellControls();
      setLog(`封面动态效果：${state.settings.coverVideo ? "开启" : "关闭"}`);
    });
  }
  const autoToggle = byId("autoSendToggle");
  if (autoToggle) {
    autoToggle.addEventListener("change", function () {
      state.settings.autoSend = !!autoToggle.checked;
      saveSettings();
      setLog(`任务完成后自动发送至 Photoshop：${state.settings.autoSend ? "开启" : "关闭"}`);
    });
  }
  const layerName = byId("layerNameMode");
  if (layerName) {
    layerName.addEventListener("change", function () {
      state.settings.layerNameMode = layerName.value || "功能名称 + 时间";
      saveSettings();
      setLog(`图层命名方式：${state.settings.layerNameMode}`);
    });
  }
  const model = byId("checkpointName");
  if (model) {
    model.addEventListener("change", function () {
      saveDraftControl(model);
      localStorage.setItem(STORAGE_KEYS.checkpoint, model.value);
      model.title = modelDescription(model.value);
    });
  }
  eachNode(document.querySelectorAll("#parameterPanel input[id], #parameterPanel textarea[id], #parameterPanel select[id]"), function (control) {
    control.addEventListener("input", function () { saveDraftControl(control); });
    control.addEventListener("change", function () { saveDraftControl(control); });
  });
}

function syncShellControls() {
  const shell = document.querySelector(".plugin-shell");
  if (shell) {
    setClass(shell, "cover-motion-off", !state.settings.coverVideo);
    setClass(shell, "search-active", !!state.searchQuery);
  }
  const wrap = byId("searchWrap");
  if (wrap) setClass(wrap, "open", state.searchOpen || !!state.searchQuery);
  const input = byId("featureSearchInput");
  if (input && input.value !== state.searchQuery) input.value = state.searchQuery;
  const clear = byId("searchClearBtn");
  if (clear) setClass(clear, "visible", !!state.searchQuery);
  const brand = byId("xnhBrandTitle");
  if (brand) brand.textContent = onlineBrandTitle();
  updateMemberGateUi();
}

function renderAll() {
  renderShell();
  syncShellControls();
  renderMajorTabs();
  renderCategories();
  renderFeatures();
  renderParams();
  updateWorkbenchHeader();
}

function updateWorkbenchHeader() {
  const category = currentCategory();
  const feature = currentFeature();
  const categoryTitle = categoryDisplayTitle(category);
  const featureTitle = featureDisplayTitle(feature);
  const title = byId("workbenchTitle");
  const hint = byId("workbenchHint");
  const query = activeSearchQuery();
  if (title) {
    if (query) title.textContent = "全局搜索";
    else if (category.mode === "home") title.textContent = "功能模块";
    else if (category.mode === "settings") title.textContent = "系统设置";
    else title.textContent = state.detailOpen && feature ? "详细调整模块" : categoryTitle;
  }
  if (hint) {
    if (query) hint.textContent = `正在搜索全插件功能：${state.searchQuery}`;
    else if (category.mode === "home") hint.textContent = "点击功能卡片自动进入对应栏目";
    else if (category.mode === "settings") hint.textContent = "账号、安全策略、缓存和版本";
    else if (category.mode === "filmPending") hint.textContent = "胶片模拟栏目暂时更新中";
    else if (state.detailOpen && feature) hint.textContent = `${categoryTitle} / ${featureTitle}`;
    else hint.textContent = "点击上方功能卡片进入详细调整页面";
  }
}

async function checkBridge() {
  byId("connectionState").textContent = "连接中";
  byId("connectionState").className = "connection-pill state-bad";
  const health = await bridgeFetch("/health");
  const beforeReady = state.lastMemberReady;
  const ok = health && health.comfy && health.comfy.ok;
  state.bridgeOk = !!ok;
  state.bridgeReachable = true;
  state.launcherSession = health && health.session ? health.session : {};
  byId("connectionState").textContent = ok ? "已连接" : "桥接正常";
  byId("connectionState").className = ok ? "connection-pill state-bad state-ok" : "connection-pill state-bad";
  const code = byId("userCode");
  if (code) code.textContent = state.launcherSession.userCode || state.launcherSession.username || "未登录";
  const ready = updateMemberGateUi();
  if (ready) setLog(beforeReady ? (ok ? "ComfyUI 已连接" : "桥接服务正常，ComfyUI 未启动") : "已同步启动器会员登录，插件可以使用。", !ok);
  else setLog(memberGateText(), true);
  return health;
}

async function refreshOnlineConfig() {
  try {
    const data = await bridgeFetch("/online-config");
    state.onlineConfig = data && data.config ? data.config : {};
    renderAll();
    if (data && data.warning) setLog(data.warning, true);
  } catch (error) {
    state.onlineConfig = {};
  }
}

async function pollBridgeStatus() {
  const stateNode = byId("connectionState");
  if (stateNode && !state.bridgeOk) {
    stateNode.textContent = "连接中";
    stateNode.className = "connection-pill state-bad";
  }
  try {
    const health = await bridgeFetch("/health");
    const beforeReady = state.lastMemberReady;
    const ok = health && health.comfy && health.comfy.ok;
    state.bridgeOk = !!ok;
    state.bridgeReachable = true;
    state.launcherSession = health && health.session ? health.session : {};
    if (stateNode) {
      stateNode.textContent = ok ? "已连接" : "等待Comfy";
      stateNode.className = ok ? "connection-pill state-bad state-ok" : "connection-pill state-bad";
    }
    const code = byId("userCode");
    if (code) code.textContent = state.launcherSession.userCode || state.launcherSession.username || "未登录";
    const ready = updateMemberGateUi();
    if (ready && !beforeReady) setLog("已同步启动器会员登录，插件可以使用。");
    if (!ready && beforeReady) setLog(memberGateText(), true);
  } catch (error) {
    const beforeReady = state.lastMemberReady;
    state.bridgeOk = false;
    state.bridgeReachable = false;
    state.launcherSession = {};
    if (stateNode) {
      stateNode.textContent = "未连接";
      stateNode.className = "connection-pill state-bad";
    }
    const code = byId("userCode");
    if (code) code.textContent = "未登录";
    updateMemberGateUi();
    if (beforeReady) setLog(memberGateText(), true);
  }
}

async function ensureLauncherMemberAccess() {
  await pollBridgeStatus();
  const session = state.launcherSession || {};
  if (session.loggedIn) return true;
  setProgress(0);
  setLog("未检测到启动器会员登录，请先在 XNH 启动器会员中心登录后再使用插件。", true);
  return false;
}

function startBridgePolling() {
  if (state.bridgePollTimer) return;
  state.bridgePollTimer = window.setInterval(pollBridgeStatus, 1000);
  if (!state.memberFocusBound) {
    state.memberFocusBound = true;
    if (window && window.addEventListener) window.addEventListener("focus", pollBridgeStatus);
    if (document && document.addEventListener) {
      document.addEventListener("visibilitychange", function () {
        if (!document.hidden) pollBridgeStatus();
      });
    }
  }
}

async function refreshWorkflows() {
  try {
    const data = await bridgeFetch("/workflows");
    state.workflows = data.workflows || [];
  } catch (error) {
    setLog(error.message, true);
  }
}

async function refreshModels() {
  try {
    persistCurrentParamDraft();
    const data = await bridgeFetch("/models");
    state.modelCatalog = data;
    state.models = data.checkpoints || [];
    if (!activeSearchQuery()) renderParams();
    setLog(`模型库：Checkpoint ${state.models.length} / LoRA ${(data.loras || []).length} / ControlNet ${(data.controlnet || []).length} / UNET ${(data.unet || []).length}`);
  } catch (error) {
    setLog(error.message, true);
  }
}

function getNumber(id, fallback) {
  const node = byId(id);
  if (!node) return fallback;
  const value = Number(node.value);
  return Number.isFinite(value) ? value : fallback;
}

function clampNumber(value, min, max, fallback) {
  const number = Number(value);
  const safe = Number.isFinite(number) ? number : fallback;
  return Math.max(min, Math.min(max, safe));
}


function loraStrengthForFeature(feature, category, loraName) {
  const denoise = clampNumber(getNumber("denoiseValue", 0.45), 0, 1, 0.45);
  const light = clampNumber(getNumber("lightStrength", NaN), 0, 1, NaN);
  const reference = clampNumber(getNumber("referenceStrength", NaN), 0, 1.3, NaN);
  const selectedLoraName = String(loraName || "");
  if (feature === "一键下雪") {
    if (selectedLoraName.indexOf("Kontext_落雪留痕") !== -1) {
      return clampNumber(0.42 + denoise * 0.55, 0.55, 0.78, 0.65);
    }
    return clampNumber(0.58 + denoise * 0.72, 0.72, 1, 0.88);
  }
  if (feature === "一键下雨" || feature === "一键阴天") {
    return clampNumber(0.42 + denoise * 0.58, 0.45, 0.82, 0.62);
  }
  if (feature === "一键秋天" || feature === "圣诞氛围" || feature === "草地修复") {
    return clampNumber(0.44 + denoise * 0.55, 0.48, 0.86, 0.62);
  }
  if (feature === "水面波光") {
    return clampNumber(Number.isFinite(light) ? light : 0.65, 0.45, 0.92, 0.65);
  }
  if (category && category.name === "AI打光") {
    return clampNumber(Number.isFinite(light) ? 0.58 + light * 0.48 : 0.68 + denoise * 0.42, 0.62, 1.05, 0.86);
  }
  if (category && (category.mode === "light" || category.mode === "lightPro")) {
    return clampNumber(Number.isFinite(light) ? light : 0.35 + denoise, 0.2, 0.95, 0.55);
  }
  if (category && (category.mode === "color" || category.mode === "film")) {
    return clampNumber(Number.isFinite(reference) ? reference * 0.72 : 0.35 + denoise, 0.2, 0.9, 0.5);
  }
  if (category && category.name === "创意云") {
    return clampNumber(Math.max(denoise, Number.isFinite(reference) ? reference * 0.75 : 0.45), 0.22, 0.9, 0.55);
  }
  return clampNumber(0.32 + denoise * 0.65, 0.22, 0.9, 0.5);
}

function getPrompt() {
  const node = byId("promptText");
  if (node) return controlValue("promptText", promptForFeature(currentFeature(), currentCategory()));
  const custom = byId("customFeaturePrompt");
  if (custom) return controlValue("customFeaturePrompt", promptForFeature(currentFeature(), currentCategory()));
  return promptForFeature(currentFeature(), currentCategory());
}

function originalParamDictForProfile(profile, defaults, feature) {
  const outpaintScale = clampNumber(getNumber("outpaintScale", defaults.outpaintScale || 1.25), 1.05, 1.8, 1.25);
  return {
    fileName: state.inputImage || "",
    positivePrompt: getPrompt(),
    seed: -1,
    denoise: getNumber("denoiseValue", defaults.denoise == null ? 0.45 : defaults.denoise),
    cfg: getNumber("cfgValue", defaults.cfg || 5),
    steps: getNumber("stepsValue", defaults.steps || 28),
    scheduler: "normal",
    sampler_name: byId("samplerName") ? byId("samplerName").value : "euler",
    checkpoint: controlValue("checkpointName", ""),
    referenceImage: state.referenceImage || "",
    referencePhotoStrength: getNumber("referenceStrength", defaults.referenceStrength == null ? 0.85 : defaults.referenceStrength),
    mixTransparency: getNumber("blendStrength", defaults.blendStrength == null ? 0.8 : defaults.blendStrength),
    max_side: getNumber("sizeValue", defaults.width || defaults.height || 1280),
    maxWidth: getNumber("maxWidthValue", defaults.maxWidth || 0),
    maxHeight: getNumber("maxHeightValue", defaults.maxHeight || 0),
    scaleBy: outpaintScale,
    feature: feature || "",
    originalKey: profile.originalKey || "",
    component: profile.originalComponent || "",
    workflowId: profile.originalWorkflowId || ""
  };
}

function collectPayload() {
  const category = currentCategory();
  const feature = currentFeature();
  const profile = operationProfileForFeature(feature, category);
  const defaults = currentWorkflowDefaults();
  const selected = state.optionSelections[selectionGroupKey()] || null;
  const checkpoint = controlValue("checkpointName", "");
  const disableLora = profile.disableLora || profile.colorOnly;
  const selectedLora = !disableLora && selected && selected.type === "lora" ? canonicalLoraName(selected.value) : "";
  const mappedLora = !disableLora ? canonicalLoraName(profile.lora || FEATURE_LORAS[feature] || "") : "";
  const preserveParts = Array.prototype.slice.call(document.querySelectorAll(".part-check:checked")).map(function (node) { return node.value; });
  const loraStrength = loraStrengthForFeature(feature, category, selectedLora || mappedLora);
  const identityLock = controlChecked("identityLock", profile.preserveIdentity !== false);
  const sourceSelection = state.sourceSelection || state.inputDocument && state.inputDocument.selection || null;
  const loraSpecs = [];
  if (selectedLora) {
    loraSpecs.push({ name: selectedLora, strengthModel: loraStrength, strengthClip: loraStrength, allowEncrypted: true });
  } else if (mappedLora) {
    loraSpecs.push({ name: mappedLora, strengthModel: loraStrength, strengthClip: loraStrength, allowEncrypted: true });
  }
  return {
    workflowName: workflowFileForCurrent(),
    inputImage: state.inputImage,
    params: {
      prompt: promptForRequest(feature, category),
      negative: profile.negative || "",
      checkpoint: checkpoint,
      width: getNumber("sizeValue", defaults.width || 1280),
      height: getNumber("sizeValue", defaults.height || defaults.width || 1280),
      steps: getNumber("stepsValue", defaults.steps || 28),
      cfg: getNumber("cfgValue", defaults.cfg || 5),
      seed: -1,
      denoise: getNumber("denoiseValue", defaults.denoise == null ? 0.45 : defaults.denoise),
      sampler: byId("samplerName") ? byId("samplerName").value : "euler",
      scheduler: "normal",
      preserveParts: preserveParts,
      protectParts: preserveParts,
      referenceImage: state.referenceImage,
      referenceMode: referenceModeForFeature(feature, category),
      referenceStrength: getNumber("referenceStrength", defaults.referenceStrength == null ? 0.85 : defaults.referenceStrength),
      originalStrength: getNumber("originalStrength", defaults.originalStrength == null ? 0.65 : defaults.originalStrength),
      lightStrength: getNumber("lightStrength", defaults.lightStrength == null ? 0.72 : defaults.lightStrength),
      blendStrength: getNumber("blendStrength", defaults.blendStrength == null ? 0.8 : defaults.blendStrength),
      sourceKeep: defaults.sourceKeep == null ? undefined : defaults.sourceKeep,
      outpaintScale: clampNumber(getNumber("outpaintScale", defaults.outpaintScale || 1.25), 1.05, 1.8, 1.25),
      scaleBy: clampNumber(getNumber("outpaintScale", defaults.outpaintScale || 1.25), 1.05, 1.8, 1.25),
      maxWidth: getNumber("maxWidthValue", defaults.maxWidth || 0),
      maxHeight: getNumber("maxHeightValue", defaults.maxHeight || 0),
      upscaleScale: defaults.upscaleScale == null ? undefined : defaults.upscaleScale,
      maxUpscaleSide: defaults.maxUpscaleSide == null ? undefined : defaults.maxUpscaleSide,
      lowFrequency: getNumber("lowFrequencyValue", 0),
      identityLock: identityLock,
      preserveIdentity: profile.preserveIdentity !== false && identityLock,
      structureLock: identityLock,
      colorOnly: isColorOnlyFeature(feature, category),
      operation: profile.operation || "",
      localProcessor: profile.localProcessor || "",
      disableSourceBlend: !!profile.disableSourceBlend || defaults.disableSourceBlend === true,
      disableWorkflowLoras: !!(selectedLora || mappedLora) || !!profile.disableWorkflowLoras || defaults.disableWorkflowLoras === true,
      legacyPrompt: profile.legacyPrompt || defaults.legacyPrompt || "",
      legacySampler: profile.legacySampler || defaults.legacySampler || "",
      legacyScheduler: profile.legacyScheduler || defaults.legacyScheduler || "",
      useInputMask: profile.useInputMask,
      originalKey: profile.originalKey || "",
      originalComponent: profile.originalComponent || "",
      originalWorkflowId: profile.originalWorkflowId || "",
      originalRoute: profile.originalRoute || "",
      originalEvidence: profile.originalEvidence || "",
      originalParams: profile.originalParams || [],
      originalParamDict: originalParamDictForProfile(profile, defaults, feature),
      localReplacementFor: profile.localReplacementFor || "",
      requiresReference: !!profile.requiresReference,
      requiresSelection: !!profile.requiresSelection,
      selectionMode: profile.selectionMode || "",
      maxDenoise: defaults.maxDenoise == null ? undefined : defaults.maxDenoise,
      selection: sourceSelection,
      effectStrength: loraStrength,
      lora: selectedLora || mappedLora,
      loras: loraSpecs.length ? loraSpecs : undefined,
      preset: selected ? selected.label : "",
      feature: feature,
      category: state.activeCategory,
      version: VERSION
    }
  };
}

function normalizeModelAliasText(value) {
  return String(value || "")
    .toLowerCase()
    .replace(/\.(safetensors|ckpt|pt|pth)$/i, "")
    .replace(/^[a-z]+(?=[\u4e00-\u9fff])/i, "")
    .replace(/[_\-\s]+/g, "");
}

function localMatch(items, name) {
  if (!name || !Array.isArray(items) || !items.length) return "";
  const wanted = String(name).toLowerCase();
  const wantedAlias = normalizeModelAliasText(name);
  return items.find(function (item) { return String(item).toLowerCase() === wanted; })
    || items.find(function (item) { return String(item).toLowerCase().indexOf(wanted) >= 0; })
    || items.find(function (item) {
      const alias = normalizeModelAliasText(item);
      return alias === wantedAlias || alias.indexOf(wantedAlias) >= 0 || wantedAlias.indexOf(alias) >= 0;
    })
    || "";
}

function validatePayload(payload) {
  const workflowName = payload && payload.workflowName;
  if (state.workflows.length && !workflowExists(workflowName)) {
    throw new Error("当前功能资源未安装或桥接未加载，请在启动器更新后重试。");
  }

  const params = payload && payload.params || {};
  if (!params.originalRoute || params.originalRoute === "unmapped") {
    throw new Error(`当前功能缺少 XNH 资源映射，已阻止使用通用流程：${params.feature || "未命名功能"}`);
  }
  if (params.originalRoute === "local-color-only" && (!params.colorOnly || params.localProcessor)) {
    throw new Error(`追色映射错误：${params.feature} 必须只做色彩迁移，不能进入磨皮或重绘模型。`);
  }
  if (params.originalRoute === "local-qwen-replacement" && workflowName !== "XHN-Qwen-Edit-Pro.json" && workflowName !== MAIN_WORKFLOW_FILE) {
    throw new Error(`打字改图模型映射错误：${params.feature} 必须使用 XNH 编辑资源。`);
  }
  if (/generic$/.test(String(params.operation || "")) && (!params.localReplacementFor || params.originalRoute !== "local-custom")) {
    throw new Error(`当前功能没有明确操作类型，已阻止跑通用模型：${params.feature || "未命名功能"}`);
  }
  if (params.requiresReference && !params.referenceImage) {
    throw new Error("当前功能需要参考图，请先选择参考图或点击“当前图参考”。");
  }
  if (params.requiresSelection && !validPayloadSelection(payload)) {
    throw new Error("当前功能需要先在 Photoshop 里创建选区，再点击生成。");
  }
  if (params.colorOnly && params.referenceMode === "color-match" && !params.referenceImage) {
    throw new Error("追色只迁移色彩信息，需要先选择一张参考图。");
  }

  const loras = state.modelCatalog && state.modelCatalog.loras || [];
  const requested = payload && payload.params && payload.params.loras || [];
  if (loras.length && requested.length) {
    for (let index = 0; index < requested.length; index += 1) {
      const name = requested[index] && requested[index].name;
      if (name && !localMatch(loras, name)) {
        throw new Error(`当前 ComfyUI 没找到 LoRA：${name}。请检查模型文件或功能映射。`);
      }
    }
  }

  const checkpoints = state.models || [];
  const checkpoint = payload && payload.params && payload.params.checkpoint;
  if (checkpoint && checkpoints.length && !localMatch(checkpoints, checkpoint)) {
    throw new Error(`当前 ComfyUI 没找到 Checkpoint：${checkpoint}`);
  }
}

function newCustomFeatureDraft() {
  const promptText = controlValue("customFeaturePrompt", "professional photo retouching, high quality, realistic details");
  const name = nextCustomFeatureName();
  state.customFeatures.push({ id: `custom-${Date.now()}`, name: name, prompt: promptText });
  state.activeCategory = CUSTOM_CATEGORY;
  state.activeFeature = name;
  saveCustomFeatures();
  renderAll();
  setLog(`已新增常用按钮：${name}`);
}

function saveCustomFeatureFromInputs() {
  const name = currentFeature();
  const prompt = controlValue("customFeaturePrompt", "");
  if (!name) {
    setLog("请先填写我的常用名称。", true);
    return;
  }
  const existing = customFeatureByName(currentFeature()) || customFeatureByName(name);
  if (existing) {
    existing.name = name;
    existing.prompt = prompt || existing.prompt;
  } else {
    state.customFeatures.push({ id: `custom-${Date.now()}`, name: name, prompt: prompt || "professional photo retouching, high quality" });
  }
  state.activeCategory = CUSTOM_CATEGORY;
  state.activeFeature = name;
  saveCustomFeatures();
  renderAll();
  setLog("我的常用已保存。");
}

function deleteCurrentCustomFeature() {
  const feature = currentFeature();
  state.customFeatures = state.customFeatures.filter(function (item) { return item.name !== feature; });
  if (!state.customFeatures.length) state.customFeatures = DEFAULT_CUSTOM_FEATURES.slice();
  state.activeFeature = state.customFeatures[0].name;
  saveCustomFeatures();
  renderAll();
  setLog("已删除当前常用。");
}

async function pickReferenceImage() {
  try {
    if (!localFileSystem) throw new Error("请选择在 Photoshop 插件里使用参考图。");
    const file = await localFileSystem.getFileForOpening({ types: ["jpg", "jpeg", "png", "webp"] });
    if (!file) return;
    const bytes = await file.read({ format: formats.binary });
    const mime = mimeForFile(file.name);
    if (state.referencePreviewUrl) URL.revokeObjectURL(state.referencePreviewUrl);
    state.referencePreviewUrl = URL.createObjectURL(new Blob([bytes], { type: mime }));
    const uploaded = await bridgeFetch(`/inputs/raw?name=${encodeURIComponent(file.name)}`, {
      method: "POST",
      headers: { "Content-Type": mime },
      body: bytes
    });
    state.referenceName = file.name;
    state.referenceImage = uploaded.file;
    localStorage.setItem(STORAGE_KEYS.referenceName, file.name);
    renderParams();
    setLog(`参考图已选择：${file.name}`);
  } catch (error) {
    setLog(error.message, true);
  }
}

async function useCurrentCanvasAsReference() {
  try {
    if (!app || !core || !localFileSystem) throw new Error("当前图参考需要在 Photoshop 里执行。");
    const document = app.activeDocument;
    if (!document) throw new Error("Photoshop 当前没有打开文档。");
    const folder = await localFileSystem.getTemporaryFolder();
    const file = await folder.createFile(`xhn-reference-${Date.now()}.png`, { overwrite: true });
    await core.executeAsModal(async function () {
      await document.saveAs.png(file, {}, true);
    }, { commandName: "导出当前画布作为参考图" });
    const bytes = await file.read({ format: formats.binary });
    if (state.referencePreviewUrl) URL.revokeObjectURL(state.referencePreviewUrl);
    state.referencePreviewUrl = URL.createObjectURL(new Blob([bytes], { type: "image/png" }));
    const uploaded = await bridgeFetch(`/inputs/raw?name=${encodeURIComponent(file.name)}`, {
      method: "POST",
      headers: { "Content-Type": "image/png" },
      body: bytes
    });
    const info = documentInfo(document);
    state.referenceName = `${info.title}（当前图参考）`;
    state.referenceImage = uploaded.file;
    localStorage.setItem(STORAGE_KEYS.referenceName, state.referenceName);
    renderParams();
    setLog(`已使用当前图作为参考：${info.title}`);
  } catch (error) {
    setLog(error.message, true);
  }
}

function mimeForFile(name) {
  const lower = String(name || "").toLowerCase();
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
  if (lower.endsWith(".webp")) return "image/webp";
  return "image/png";
}

function clearReferenceImage() {
  state.referenceImage = "";
  state.referenceName = "";
  if (state.referencePreviewUrl) URL.revokeObjectURL(state.referencePreviewUrl);
  state.referencePreviewUrl = "";
  localStorage.removeItem(STORAGE_KEYS.referenceName);
  renderParams();
  setLog("已清除参考图。");
}

function documentInfo(document) {
  const title = (document && (document.title || document.name)) || "源文档";
  return { id: document && (document._id || document.id), title: title, name: title };
}

function activeDocumentInfo() {
  try {
    return app && app.activeDocument ? documentInfo(app.activeDocument) : null;
  } catch (error) {
    return null;
  }
}

function numericPixels(value) {
  if (typeof value === "number") return value;
  if (value && typeof value.value === "number") return value.value;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : 0;
}

function documentPixelSize(document) {
  return { width: numericPixels(document && document.width), height: numericPixels(document && document.height) };
}

function descriptorNumber(value) {
  if (typeof value === "number") return value;
  if (value && typeof value._value === "number") return value._value;
  if (value && typeof value.value === "number") return value.value;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : 0;
}

function parseSelectionBounds(descriptor, size) {
  if (!descriptor) return null;
  const selection = descriptor.selection || descriptor.bounds || descriptor;
  const bounds = selection.bounds || selection;
  const left = descriptorNumber(bounds.left || bounds._left);
  const top = descriptorNumber(bounds.top || bounds._top);
  const right = descriptorNumber(bounds.right || bounds._right);
  const bottom = descriptorNumber(bounds.bottom || bounds._bottom);
  const width = right - left;
  const height = bottom - top;
  if (![left, top, right, bottom, width, height].every(Number.isFinite)) return null;
  if (width < 2 || height < 2) return null;
  if (size && size.width && size.height) {
    const isFull = left <= 0.5 && top <= 0.5 && Math.abs(right - size.width) <= 1 && Math.abs(bottom - size.height) <= 1;
    if (isFull) return null;
  }
  return {
    left: Math.max(0, Math.round(left)),
    top: Math.max(0, Math.round(top)),
    right: Math.round(right),
    bottom: Math.round(bottom),
    width: Math.round(width),
    height: Math.round(height)
  };
}

async function getActiveSelectionBounds(document) {
  if (!action || !document) return null;
  try {
    const result = await action.batchPlay([
      {
        _obj: "get",
        _target: [
          { _property: "selection" },
          { _ref: "document", _enum: "ordinal", _value: "targetEnum" }
        ],
        _options: { dialogOptions: "dontDisplay" }
      }
    ], { synchronousExecution: false, modalBehavior: "execute" });
    return parseSelectionBounds(result && result[0], documentPixelSize(document));
  } catch (error) {
    return null;
  }
}

function layerCount(document) {
  try {
    if (!document) return 0;
    if (document.layers && typeof document.layers.length === "number") return document.layers.length;
    if (document.artLayers && typeof document.artLayers.length === "number") return document.artLayers.length;
    return 0;
  } catch (error) {
    return 0;
  }
}

async function actionLayerCount() {
  if (!action) return 0;
  try {
    const result = await action.batchPlay([
      {
        _obj: "get",
        _target: [{ _ref: "document", _enum: "ordinal", _value: "targetEnum" }],
        _options: { dialogOptions: "dontDisplay" }
      }
    ], { synchronousExecution: false, modalBehavior: "execute" });
    const doc = result && result[0];
    if (doc && typeof doc.numberOfLayers === "number") return doc.numberOfLayers;
    if (doc && Array.isArray(doc.layers)) return doc.layers.length;
  } catch (error) {
    return 0;
  }
  return 0;
}

async function renameActiveLayer(name) {
  await action.batchPlay([
    {
      _obj: "set",
      _target: [{ _ref: "layer", _enum: "ordinal", _value: "targetEnum" }],
      to: { _obj: "layer", name: name },
      _options: { dialogOptions: "dontDisplay" }
    }
  ], { synchronousExecution: false, modalBehavior: "execute" });
}

function boundValue(bounds, key) {
  if (!bounds) return 0;
  const altKey = "_" + key;
  const value = Object.prototype.hasOwnProperty.call(bounds, key) ? bounds[key] : bounds[altKey];
  if (typeof value === "number") return value;
  if (value && typeof value.value === "number") return value.value;
  if (value && typeof value._value === "number") return value._value;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : 0;
}

function validPayloadSelection(payload) {
  const selection = payload && payload.params && payload.params.selection;
  if (!selection) return null;
  const left = Number(selection.left);
  const top = Number(selection.top);
  const width = Number(selection.width);
  const height = Number(selection.height);
  if (![left, top, width, height].every(Number.isFinite) || width < 2 || height < 2) return null;
  return { left, top, width, height, right: left + width, bottom: top + height };
}

function isOutpaintPayload(payload) {
  const params = payload && payload.params || {};
  return params.operation === "local.outpaint" || params.localProcessor === "outpaint";
}

function isUpscalePayload(payload) {
  const params = payload && payload.params || {};
  return params.localProcessor === "upscale" || /^quality\.(photo-upscale|high-upscale|old-photo-repair)$/.test(String(params.operation || ""));
}

function resultCoversFullDocument(result, payload) {
  const params = payload && payload.params || {};
  return !!(result && result.fullDocument || params.outputFullDocument || params.localProcessor === "remove");
}

async function getActiveLayerBounds() {
  try {
    const layer = app.activeDocument && app.activeDocument.activeLayers && app.activeDocument.activeLayers[0];
    const bounds = layer && layer.bounds;
    const left = boundValue(bounds, "left");
    const top = boundValue(bounds, "top");
    const right = boundValue(bounds, "right");
    const bottom = boundValue(bounds, "bottom");
    if (right > left && bottom > top) return { left, top, right, bottom, width: right - left, height: bottom - top };
  } catch (error) {
    // Fallback to batchPlay below.
  }
  try {
    const layerInfo = await action.batchPlay([
      { _obj: "get", _target: [{ _ref: "layer", _enum: "ordinal", _value: "targetEnum" }], _options: { dialogOptions: "dontDisplay" } }
    ], { synchronousExecution: false, modalBehavior: "execute" });
    const bounds = layerInfo && layerInfo[0] && layerInfo[0].bounds;
    const left = boundValue(bounds, "left");
    const top = boundValue(bounds, "top");
    const right = boundValue(bounds, "right");
    const bottom = boundValue(bounds, "bottom");
    if (right > left && bottom > top) return { left, top, right, bottom, width: right - left, height: bottom - top };
    const transform = layerInfo && layerInfo[0] && layerInfo[0].smartObjectMore && layerInfo[0].smartObjectMore.transform;
    if (transform && transform.length >= 2) return { left: Number(transform[0]) || 0, top: Number(transform[1]) || 0, right: 0, bottom: 0, width: 0, height: 0 };
  } catch (error) {
    return null;
  }
  return null;
}

async function scaleActiveLayerToBounds(target) {
  if (!target || target.width <= 0 || target.height <= 0) return;
  const bounds = await getActiveLayerBounds();
  if (!bounds || bounds.width <= 0 || bounds.height <= 0) return;
  const scaleX = (target.width / bounds.width) * 100;
  const scaleY = (target.height / bounds.height) * 100;
  if (Math.abs(scaleX - 100) <= 1 && Math.abs(scaleY - 100) <= 1) return;
  await action.batchPlay([
    {
      _obj: "transform",
      _target: [{ _ref: "layer", _enum: "ordinal", _value: "targetEnum" }],
      freeTransformCenterState: { _enum: "quadCenterState", _value: "QCSAverage" },
      width: { _unit: "percentUnit", _value: scaleX },
      height: { _unit: "percentUnit", _value: scaleY },
      _options: { dialogOptions: "dontDisplay" }
    }
  ], { synchronousExecution: false, modalBehavior: "execute" });
}

async function scaleActiveLayerToSelection(payload) {
  const target = validPayloadSelection(payload);
  if (!target) return;
  await scaleActiveLayerToBounds(target);
}

async function scaleActiveLayerToDocument(sourceSize) {
  if (!sourceSize || !sourceSize.width || !sourceSize.height) return;
  await scaleActiveLayerToBounds({ width: sourceSize.width, height: sourceSize.height });
}

async function moveActiveLayerToTarget(payload) {
  const target = validPayloadSelection(payload) || { left: 0, top: 0 };
  const bounds = await getActiveLayerBounds();
  const x = bounds ? bounds.left : 0;
  const y = bounds ? bounds.top : 0;
  const dx = target.left - x;
  const dy = target.top - y;
  if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) return;
  await action.batchPlay([
    {
      _obj: "move",
      _target: [{ _ref: "layer", _enum: "ordinal", _value: "targetEnum" }],
      to: {
        _obj: "offset",
        horizontal: { _unit: "pixelsUnit", _value: dx },
        vertical: { _unit: "pixelsUnit", _value: dy }
      },
      _options: { dialogOptions: "dontDisplay" }
    }
  ], { synchronousExecution: false, modalBehavior: "execute" });
}

async function moveActiveLayerToOrigin() {
  await moveActiveLayerToTarget(null);
}

function resultPixelSize(result, fallback) {
  const width = Number(result && result.width);
  const height = Number(result && result.height);
  if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
    return { width: Math.round(width), height: Math.round(height) };
  }
  return fallback || { width: 0, height: 0 };
}

async function resizeCanvasToSize(size) {
  if (!action || !size || !size.width || !size.height) return false;
  await action.batchPlay([
    {
      _obj: "canvasSize",
      width: { _unit: "pixelsUnit", _value: Math.round(size.width) },
      height: { _unit: "pixelsUnit", _value: Math.round(size.height) },
      horizontal: { _enum: "horizontalLocation", _value: "center" },
      vertical: { _enum: "verticalLocation", _value: "center" },
      _options: { dialogOptions: "dontDisplay" }
    }
  ], { synchronousExecution: false, modalBehavior: "execute" });
  return true;
}

async function fitOutpaintLayerToPayload(payload, sourceSize, result) {
  const bounds = await getActiveLayerBounds();
  const resultSize = resultPixelSize(result, bounds || sourceSize);
  const targetSize = {
    width: Math.max(resultSize.width || 0, sourceSize && sourceSize.width || 0),
    height: Math.max(resultSize.height || 0, sourceSize && sourceSize.height || 0)
  };
  if (targetSize.width > 0 && targetSize.height > 0) {
    await resizeCanvasToSize(targetSize);
  }
  await moveActiveLayerToOrigin();
}

async function fitUpscaleLayerToPayload(payload, sourceSize, result) {
  const bounds = await getActiveLayerBounds();
  const resultSize = resultPixelSize(result, bounds || sourceSize);
  if (resultSize.width > 0 && resultSize.height > 0) {
    await resizeCanvasToSize(resultSize);
    await scaleActiveLayerToBounds(resultSize);
  }
  await moveActiveLayerToOrigin();
}

async function fitActiveLayerToPayload(payload, sourceSize, result) {
  if (isOutpaintPayload(payload)) {
    await fitOutpaintLayerToPayload(payload, sourceSize, result);
    return;
  }
  if (isUpscalePayload(payload)) {
    await fitUpscaleLayerToPayload(payload, sourceSize, result);
    return;
  }
  if (resultCoversFullDocument(result, payload)) {
    await scaleActiveLayerToDocument(sourceSize);
    await moveActiveLayerToOrigin();
    return;
  }
  if (validPayloadSelection(payload)) {
    await scaleActiveLayerToSelection(payload);
  } else {
    await scaleActiveLayerToDocument(sourceSize);
  }
  await moveActiveLayerToTarget(payload);
}

async function setActiveLayerOpacity(opacityPercent) {
  if (!action) return;
  const opacity = clampNumber(opacityPercent, 5, 100, 100);
  await action.batchPlay([
    {
      _obj: "set",
      _target: [{ _ref: "layer", _enum: "ordinal", _value: "targetEnum" }],
      to: {
        _obj: "layer",
        opacity: { _unit: "percentUnit", _value: opacity }
      },
      _options: { dialogOptions: "dontDisplay" }
    }
  ], { synchronousExecution: false, modalBehavior: "execute" });
}

function resultOpacityForPayload(payload) {
  const params = payload && payload.params || {};
  if (params.localProcessor === "remove" || params.localProcessor === "upscale" || params.colorOnly) return 100;
  if (/^(lighting|light)\./.test(String(params.operation || ""))) return 100;
  if (params.feature === "背景提取") return 100;
  const blend = Number(params.blendStrength);
  if (Number.isFinite(blend)) return clampNumber(blend * 100, 15, 100, 100);
  const original = Number(params.originalStrength);
  if (Number.isFinite(original)) return clampNumber((1 - original * 0.45) * 100, 35, 100, 70);
  return 100;
}

async function tryAddMaskFromTransparency(payload) {
  const feature = payload && payload.params && payload.params.feature;
  if (feature !== "背景提取" || !action) return false;
  try {
    await action.batchPlay([
      {
        _obj: "set",
        _target: [{ _ref: "channel", _property: "selection" }],
        to: { _ref: "channel", _enum: "channel", _value: "transparencyEnum" },
        _options: { dialogOptions: "dontDisplay" }
      },
      {
        _obj: "make",
        new: { _class: "channel" },
        at: { _ref: "channel", _enum: "channel", _value: "mask" },
        using: { _enum: "userMaskEnabled", _value: "revealSelection" },
        _options: { dialogOptions: "dontDisplay" }
      },
      {
        _obj: "set",
        _target: [{ _ref: "channel", _property: "selection" }],
        to: { _enum: "ordinal", _value: "none" },
        _options: { dialogOptions: "dontDisplay" }
      }
    ], { synchronousExecution: false, modalBehavior: "execute" });
    return true;
  } catch (error) {
    return false;
  }
}

async function tryAddMaskFromSelection(payload) {
  const selection = validPayloadSelection(payload);
  if (!selection || !action) return false;
  try {
    await action.batchPlay([
      {
        _obj: "set",
        _target: [{ _ref: "channel", _property: "selection" }],
        to: {
          _obj: "rectangle",
          top: { _unit: "pixelsUnit", _value: selection.top },
          left: { _unit: "pixelsUnit", _value: selection.left },
          bottom: { _unit: "pixelsUnit", _value: selection.bottom },
          right: { _unit: "pixelsUnit", _value: selection.right }
        },
        _options: { dialogOptions: "dontDisplay" }
      },
      {
        _obj: "make",
        new: { _class: "channel" },
        at: { _ref: "channel", _enum: "channel", _value: "mask" },
        using: { _enum: "userMaskEnabled", _value: "revealSelection" },
        _options: { dialogOptions: "dontDisplay" }
      },
      {
        _obj: "set",
        _target: [{ _ref: "channel", _property: "selection" }],
        to: { _enum: "ordinal", _value: "none" },
        _options: { dialogOptions: "dontDisplay" }
      }
    ], { synchronousExecution: false, modalBehavior: "execute" });
    return true;
  } catch (error) {
    try {
      await action.batchPlay([
        {
          _obj: "set",
          _target: [{ _ref: "channel", _property: "selection" }],
          to: { _enum: "ordinal", _value: "none" },
          _options: { dialogOptions: "dontDisplay" }
        }
      ], { synchronousExecution: false, modalBehavior: "execute" });
    } catch (cleanupError) {
      // Ignore cleanup errors; the layer placement already succeeded.
    }
    return false;
  }
}

async function finishResultLayer(resultLayerName, payload, sourceSize, result) {
  await renameActiveLayer(resultLayerName);
  await fitActiveLayerToPayload(payload, sourceSize, result);
  await setActiveLayerOpacity(resultOpacityForPayload(payload));
  if (isOutpaintPayload(payload)) return;
  const transparencyMask = await tryAddMaskFromTransparency(payload);
  if (!transparencyMask) await tryAddMaskFromSelection(payload);
}

function normalizeDocumentTitle(value) {
  return safeTrim(value)
    .replace(/\s+\*\s*$/, "")
    .replace(/\s+/g, " ")
    .toLowerCase();
}

function openDocumentsArray() {
  const docs = [];
  if (!app || !app.documents) return docs;
  for (let index = 0; index < app.documents.length; index += 1) {
    if (app.documents[index]) docs.push(app.documents[index]);
  }
  return docs;
}

function findOpenDocument(info) {
  if (!info) return null;
  const docs = openDocumentsArray();
  if (!docs.length) return null;
  if (info.id) {
    for (let index = 0; index < docs.length; index += 1) {
      const docId = docs[index] && (docs[index]._id || docs[index].id);
      if (docId === info.id) return docs[index];
    }
  }
  const wanted = normalizeDocumentTitle(info.title || info.name);
  if (!wanted) return null;
  for (let index = 0; index < docs.length; index += 1) {
    const title = normalizeDocumentTitle(docs[index].title || docs[index].name);
    if (title === wanted) return docs[index];
  }
  return null;
}

function findOpenDocumentByTitle(text) {
  const wanted = normalizeDocumentTitle(text);
  if (!wanted) return null;
  const docs = openDocumentsArray();
  for (let index = 0; index < docs.length; index += 1) {
    const title = normalizeDocumentTitle(docs[index].title || docs[index].name);
    if (title === wanted || title.indexOf(wanted) >= 0 || wanted.indexOf(title) >= 0) {
      return docs[index];
    }
  }
  return null;
}

function isDocumentOpen(info) {
  return !!findOpenDocument(info);
}

async function selectDocument(info, insideModal) {
  if (!action || !info || !info.id) return false;
  const run = async function () {
    return action.batchPlay([{ _obj: "select", _target: [{ _ref: "document", _id: info.id }], makeVisible: true }], { synchronousExecution: false, modalBehavior: "execute" });
  };
  if (insideModal || !core) await run();
  else await core.executeAsModal(run, { commandName: `切换到 ${info.title || "源文档"}` });
  return true;
}

async function exportCurrentCanvas() {
  try {
    persistCurrentParamDraft();
    if (!app || !core || !localFileSystem) throw new Error("导出画布需要在 Photoshop 插件里执行。");
    const document = app.activeDocument;
    if (!document) throw new Error("Photoshop 当前没有打开文档。");
    const folder = await localFileSystem.getTemporaryFolder();
    const file = await folder.createFile(`xhn-input-${Date.now()}.png`, { overwrite: true });
    const selection = await getActiveSelectionBounds(document);
    state.sourceSelection = selection;
    state.inputDocument = Object.assign(documentInfo(document), {
      selection: selection,
      pixelSize: documentPixelSize(document)
    });
    await core.executeAsModal(async function () {
      await document.saveAs.png(file, {}, true);
    }, { commandName: "导出当前画布给 XNH" });
    const bytes = await file.read({ format: formats.binary });
    if (state.inputPreviewUrl) URL.revokeObjectURL(state.inputPreviewUrl);
    state.inputPreviewUrl = URL.createObjectURL(new Blob([bytes], { type: "image/png" }));
    const uploaded = await bridgeFetch(`/inputs/raw?name=${encodeURIComponent(file.name)}`, {
      method: "POST",
      headers: { "Content-Type": "image/png" },
      body: bytes
    });
    state.inputImage = uploaded.file;
    renderParams();
    setLog(selection ? `已导出当前选区：${selection.width}×${selection.height} @ ${selection.left},${selection.top}` : `已导出当前画布：${state.inputDocument.title}`);
    return true;
  } catch (error) {
    setLog(error.message, true);
    return false;
  }
}

function dataUrlToArrayBuffer(dataUrl) {
  const text = String(dataUrl || "");
  const comma = text.indexOf(",");
  const payload = comma >= 0 ? text.slice(comma + 1) : text;
  if (!payload) throw new Error("Result image data is empty.");
  const binary = atob(payload);
  const bytes = new Uint8Array(binary.length);
  for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
  return bytes.buffer;
}

async function resultArrayBuffer(result) {
  if (result && result.dataUrl) return dataUrlToArrayBuffer(result.dataUrl);
  const response = await fetch(resultUrl(result), { cache: "no-store" });
  if (!response.ok) throw new Error(`下载结果失败：${response.status}`);
  return response.arrayBuffer();
}

function resultUrl(result) {
  if (!result || !result.url) return "";
  if (result.dataUrl) return result.dataUrl;
  if (/^https?:\/\//i.test(result.url)) return result.url;
  return `${normalizeBridgeUrl()}${result.url.charAt(0) === "/" ? "" : "/"}${result.url}`;
}

function cleanLayerBaseName(value) {
  const text = safeTrim(value || "XNH结果").replace(/\.[a-z0-9]{2,5}$/i, "");
  return text.replace(/[\\/:*?"<>|]+/g, " ").replace(/\s+/g, " ").slice(0, 60) || "XNH结果";
}

function resultLayerNameForPayload(payload, sourceDocument, resultIndex, resultTotal) {
  const params = payload && payload.params || {};
  const featureName = cleanLayerBaseName(params.feature || "XNH结果");
  const sourceName = cleanLayerBaseName(sourceDocument && (sourceDocument.title || sourceDocument.name));
  const mode = state.settings.layerNameMode || "功能名称 + 时间";
  const suffix = resultTotal > 1 ? ` #${(resultIndex || 0) + 1}` : "";
  const time = new Date().toLocaleTimeString();
  if (mode === "原图名称") return `XNH结果 - ${sourceName}${suffix}`;
  if (mode === "功能名称") return `XNH结果 - ${featureName}${suffix}`;
  return `XNH结果 - ${featureName}${suffix} - ${time}`;
}

async function writeResultToTempFile(result) {
  if (!localFileSystem) throw new Error("当前环境不能写入 Photoshop 临时文件。");
  const folder = await localFileSystem.getTemporaryFolder();
  const file = await folder.createFile(result.filename || `xhn-result-${Date.now()}.png`, { overwrite: true });
  await file.write(await resultArrayBuffer(result), { format: formats.binary });
  return file;
}

async function openResult(result) {
  try {
    const file = await writeResultToTempFile(result);
    await core.executeAsModal(async function () {
      await app.open(file);
    }, { commandName: "打开 XNH 结果" });
    setLog("结果已在 Photoshop 中打开。");
  } catch (error) {
    setLog(error.message, true);
  }
}

async function applyResultToSourceDocument(result, sourceDocument, payload, resultIndex, resultTotal) {
  if (!app || !core || !localFileSystem || !action) {
    setLog("当前环境不能自动回写，可在结果列表中打开。", true);
    return;
  }
  const targetDocument = findOpenDocument(sourceDocument);
  if (!targetDocument) {
    await openResult(result);
    setLog(`没有找到源文档，结果已单独打开：${sourceDocument && sourceDocument.title || "源文档"}`, true);
    return;
  }
  const targetInfo = documentInfo(targetDocument);
  const sourceSize = documentPixelSize(targetDocument);
  const file = await writeResultToTempFile(result);
  const fileToken = await localFileSystem.createSessionToken(file);
  const startLayerCount = layerCount(targetDocument);
  let baselineLayerCount = startLayerCount;
  let wroteLayer = false;
  const resultLayerName = resultLayerNameForPayload(payload, sourceDocument, resultIndex, resultTotal);
  await core.executeAsModal(async function () {
    await selectDocument(targetInfo, true);
    const beforeLayers = Math.max(layerCount(app.activeDocument), await actionLayerCount(), startLayerCount);
    baselineLayerCount = beforeLayers;
    if (!isOutpaintPayload(payload)) try {
      await action.batchPlay([
        {
          _obj: "placeEvent",
          null: { _path: fileToken, _kind: "local" },
          linked: false,
          _options: { dialogOptions: "dontDisplay" }
        }
      ], { synchronousExecution: false, modalBehavior: "execute" });
      const afterPlaceLayers = Math.max(layerCount(app.activeDocument), await actionLayerCount());
      if (afterPlaceLayers <= beforeLayers) throw new Error("置入没有新增图层");
      await finishResultLayer(resultLayerName, payload, sourceSize, result);
      wroteLayer = true;
      return;
    } catch (placeError) {
      await selectDocument(targetInfo, true);
    }

    let generatedInfo = null;
    let generatedSize = { width: 0, height: 0 };
    const ensureGeneratedDocument = async function () {
      let generated = findOpenDocument(generatedInfo);
      if (!generated) generated = await app.open(file);
      if (!generated) generated = app.activeDocument;
      generatedInfo = documentInfo(generated);
      generatedSize = documentPixelSize(generated);
      await selectDocument(generatedInfo, true);
      return generated;
    };
    await ensureGeneratedDocument();
    try {
      await action.batchPlay([
        {
          _obj: "duplicate",
          _target: [{ _ref: "layer", _enum: "ordinal", _value: "targetEnum" }],
          to: { _ref: "document", _id: targetInfo.id },
          _options: { dialogOptions: "dontDisplay" }
        },
        {
          _obj: "close",
          _target: [{ _ref: "document", _id: generatedInfo.id }],
          saving: { _enum: "yesNo", _value: "no" },
          _options: { dialogOptions: "dontDisplay" }
        }
      ], { synchronousExecution: false, modalBehavior: "execute" });
      await selectDocument(targetInfo, true);
      const afterDuplicateLayers = Math.max(layerCount(app.activeDocument), await actionLayerCount());
      if (afterDuplicateLayers <= beforeLayers) throw new Error("复制图层没有新增图层");
      await finishResultLayer(resultLayerName, payload, sourceSize, result);
      wroteLayer = true;
      return;
    } catch (duplicateError) {
      await ensureGeneratedDocument();
    }

    await action.batchPlay([
      { _obj: "set", _target: [{ _ref: "channel", _property: "selection" }], to: { _enum: "ordinal", _value: "all" } },
      { _obj: "copyEvent" }
    ], { synchronousExecution: false, modalBehavior: "execute" });
    await action.batchPlay([
      { _obj: "close", _target: [{ _ref: "document", _id: generatedInfo.id }], saving: { _enum: "yesNo", _value: "no" } }
    ], { synchronousExecution: false, modalBehavior: "execute" });
    await selectDocument(targetInfo, true);
    await action.batchPlay([
      { _obj: "paste", antiAlias: { _enum: "antiAliasType", _value: "antiAliasNone" }, _options: { dialogOptions: "dontDisplay" } },
    ], { synchronousExecution: false, modalBehavior: "execute" });
    const afterPasteLayers = Math.max(layerCount(app.activeDocument), await actionLayerCount());
    if (afterPasteLayers <= beforeLayers) throw new Error("粘贴结果没有新增图层");
    wroteLayer = true;
    if (!isOutpaintPayload(payload) && !isUpscalePayload(payload) && !validPayloadSelection(payload) && sourceSize.width > 0 && sourceSize.height > 0 && generatedSize.width > 0 && generatedSize.height > 0) {
      const scaleX = (sourceSize.width / generatedSize.width) * 100;
      const scaleY = (sourceSize.height / generatedSize.height) * 100;
      if (Math.abs(scaleX - 100) > 2 || Math.abs(scaleY - 100) > 2) {
        await action.batchPlay([
          { _obj: "transform", _target: [{ _ref: "layer", _enum: "ordinal", _value: "targetEnum" }], freeTransformCenterState: { _enum: "quadCenterState", _value: "QCSAverage" }, width: { _unit: "percentUnit", _value: scaleX }, height: { _unit: "percentUnit", _value: scaleY } }
        ], { synchronousExecution: false, modalBehavior: "execute" });
      }
    }
    await finishResultLayer(resultLayerName, payload, sourceSize, result);
  }, { commandName: `回写 XNH 结果到 ${targetInfo.title}` });
  await core.executeAsModal(async function () {
    await selectDocument(targetInfo, true);
    const finalLayerCount = Math.max(layerCount(app.activeDocument), await actionLayerCount());
    if (!wroteLayer || finalLayerCount <= baselineLayerCount) {
      throw new Error("结果已生成，但没有成功贴回源文档。");
    }
  }, { commandName: "确认 XNH 结果已回写" });
  setLog(`结果已自动回写到源图：${targetInfo.title}`);
}

async function generateImage(options) {
  const opts = options || {};
  const button = byId("generateBtn");
  try {
    persistCurrentParamDraft();
    setControlBusy(button, true);
    if (!await ensureLauncherMemberAccess()) return false;
    setProgress(8);
    if (!opts.skipAutoExport && app && core && localFileSystem) {
      setLog("正在导出当前画布...");
      const exported = await exportCurrentCanvas();
      if (!exported) return false;
      if (typeof opts.afterExport === "function") {
        await opts.afterExport(state.inputDocument);
      }
    }
    const currentProfile = operationProfileForCurrent();
    if ((isReferenceColorFeature(currentFeature(), currentCategory()) || currentProfile.requiresReference) && !state.referenceImage) {
      setProgress(0);
      setLog("当前功能需要先选择参考图，或点击“当前图参考”。", true);
      return false;
    }
    setLog("正在提交任务...");
    const startedAt = Date.now();
    const payload = collectPayload();
    const sourceDocument = state.inputDocument ? Object.assign({}, state.inputDocument) : null;
    state.lastPayload = payload;
    validatePayload(payload);
    setLog(`已匹配当前功能：${payload.params.feature || "XNH"}，正在提交任务...`);
    const data = await bridgeFetch("/generate", { method: "POST", json: payload });
    const seconds = ((Date.now() - startedAt) / 1000).toFixed(1);
    const bridgeSeconds = data && data.meta && Number.isFinite(Number(data.meta.elapsedMs)) ? (Number(data.meta.elapsedMs) / 1000).toFixed(1) : "";
    state.results = data.images || [];
    state.latestResult = state.results[0] || null;
    setProgress(100);
    renderResults();
    if (state.results.length && sourceDocument && state.settings.autoSend) {
      for (let index = 0; index < state.results.length; index += 1) {
        await applyResultToSourceDocument(state.results[index], sourceDocument, payload, index, state.results.length);
      }
      setLog(`任务完成，用时 ${bridgeSeconds || seconds} 秒，${state.results.length} 张结果已回写源文档。`);
    } else {
      const holdMessage = state.latestResult && !state.settings.autoSend ? " 自动回写已关闭，结果保留在下方列表。" : "";
      setLog(state.latestResult ? `任务完成，用时 ${bridgeSeconds || seconds} 秒。${holdMessage}` : "任务完成，但没有返回图片。", !state.latestResult);
    }
    return !!state.latestResult;
  } catch (error) {
    setProgress(0);
    setLog(error.message, true);
    return false;
  } finally {
    setControlBusy(button, false);
  }
}

function renderResults() {
  const list = byId("resultList");
  list.innerHTML = "";
  if (!state.results.length) {
    list.className = "result-list empty";
    list.textContent = "暂无结果";
    return;
  }
  list.className = "result-list";
  state.results.forEach(function (result, index) {
    const item = document.createElement("div");
    item.className = "result-item";
    const img = document.createElement("img");
    img.src = resultUrl(result);
    const meta = document.createElement("div");
    meta.innerHTML = `<strong>${escapeText(result.filename || `结果 ${index + 1}`)}</strong><small>${escapeText(result.type || "output")}</small>`;
    item.appendChild(img);
    item.appendChild(meta);
    item.addEventListener("click", function () { openResult(result); });
    list.appendChild(item);
  });
}

async function sleep(ms) {
  return new Promise(function (resolve) { setTimeout(resolve, ms); });
}

async function batchProcessOpenDocuments() {
  const button = byId("batchBtn");
  try {
    if (!app || !core || !action || !localFileSystem) throw new Error("批量处理需要在 Photoshop 插件里执行。");
    const docs = [];
    for (let index = 0; index < app.documents.length; index += 1) {
      const info = documentInfo(app.documents[index]);
      if (info && info.id) docs.push(info);
    }
    if (!docs.length) throw new Error("Photoshop 当前没有打开文档。");
    setControlBusy(button, true);
    if (!await ensureLauncherMemberAccess()) return;
    setLog(`开始批量处理 ${docs.length} 个已打开文档。`);
    for (let index = 0; index < docs.length; index += 1) {
      const info = docs[index];
      if (!isDocumentOpen(info)) continue;
      setProgress(Math.round((index / docs.length) * 90));
      await selectDocument(info);
      await sleep(120);
      state.inputDocument = info;
      const exported = await exportCurrentCanvas();
      if (exported) await generateImage({ skipAutoExport: true });
    }
    setProgress(100);
    setLog("批量处理完成。结果已按原文档回写。");
  } catch (error) {
    setLog(error.message, true);
  } finally {
    setControlBusy(button, false);
  }
}

async function cancelCurrentTask() {
  const button = byId("cancelBtn");
  try {
    setControlBusy(button, true);
    await bridgeFetch("/interrupt", { method: "POST" });
    setProgress(0);
    setLog("已向 ComfyUI 发送中断请求。");
  } catch (error) {
    setLog(`中断失败：${error.message}`, true);
  } finally {
    setControlBusy(button, false);
  }
}

function bindEvents() {
  const connect = byId("connectBtn");
  enableKeyboardClick(connect);
  connect.addEventListener("click", async function () {
    try {
      await checkBridge();
      await refreshOnlineConfig();
      await refreshWorkflows();
      await refreshModels();
    } catch (error) {
      setLog(error.message, true);
    }
  });
  const searchToggle = byId("searchToggleBtn");
  if (searchToggle) {
    enableKeyboardClick(searchToggle);
    searchToggle.addEventListener("click", function () {
      if (!guardMemberInteraction()) return;
      state.searchOpen = !state.searchOpen;
      syncShellControls();
      const input = byId("featureSearchInput");
      if (state.searchOpen && input) input.focus();
    });
  }
  const searchInput = byId("featureSearchInput");
  if (searchInput) {
    searchInput.addEventListener("input", function () {
      state.searchQuery = searchInput.value || "";
      localStorage.setItem(STORAGE_KEYS.searchQuery, state.searchQuery);
      renderAll();
      const freshInput = byId("featureSearchInput");
      if (freshInput) freshInput.focus();
    });
    searchInput.addEventListener("keydown", function (event) {
      if (event.key === "Escape") {
        state.searchQuery = "";
        searchInput.value = "";
        localStorage.removeItem(STORAGE_KEYS.searchQuery);
        renderAll();
      }
    });
  }
  const searchClear = byId("searchClearBtn");
  if (searchClear) {
    enableKeyboardClick(searchClear);
    searchClear.addEventListener("click", function () {
      state.searchQuery = "";
      localStorage.removeItem(STORAGE_KEYS.searchQuery);
      if (searchInput) searchInput.value = "";
      renderAll();
      if (searchInput) searchInput.focus();
    });
  }
  const moreTool = byId("moreToolBtn");
  if (moreTool) {
    enableKeyboardClick(moreTool);
    moreTool.addEventListener("click", function () {
      if (!guardMemberInteraction()) return;
      jumpToMajor("系统设置");
      setLog("已打开系统设置。");
    });
  }
  document.addEventListener("click", function (event) {
    let target = event.target;
    const ids = ["newCustomFeatureBtn", "saveCustomFeatureBtn", "deleteCustomFeatureBtn", "generateBtn", "batchBtn", "cancelBtn"];
    while (target && target !== document && ids.indexOf(target.id) < 0) target = target.parentNode;
    if (!target || target === document) return;
    if (target.classList && target.classList.contains("is-disabled")) return;
    if (!guardMemberInteraction()) return;
    if (target.id === "newCustomFeatureBtn") newCustomFeatureDraft();
    if (target.id === "saveCustomFeatureBtn") saveCustomFeatureFromInputs();
    if (target.id === "deleteCustomFeatureBtn") deleteCurrentCustomFeature();
    if (target.id === "generateBtn") generateImage();
    if (target.id === "batchBtn") batchProcessOpenDocuments();
    if (target.id === "cancelBtn") cancelCurrentTask();
  });
  document.addEventListener("keydown", function (event) {
    if (event.key !== "Enter" && event.key !== " ") return;
    const target = event.target;
    if (!target || !target.id) return;
    if (["newCustomFeatureBtn", "saveCustomFeatureBtn", "deleteCustomFeatureBtn", "generateBtn", "batchBtn", "cancelBtn"].indexOf(target.id) < 0) return;
    event.preventDefault();
    target.click();
  });
}

async function maybeRunAutoTest() {
  let config = null;
  try {
    config = await bridgeFetch("/auto-test");
  } catch (error) {
    return;
  }
  if (!config || !config.run) return;
  const run = async function () {
    try {
      state.activeMajor = config.major || "AI修图";
      const categories = categoriesForMajor(state.activeMajor);
      state.activeCategory = config.category || (categories[0] ? categories[0].name : "人像精修");
      state.activeFeature = config.feature || getCategoryFeatures(currentCategory())[0] || "";
      renderAll();
      setLog("Photoshop 内部自测开始：生成后回写源文档。");
      const ok = await generateImage({
        afterExport: async function () {
          if (!config.switchToTitle) return;
          const doc = findOpenDocumentByTitle(config.switchToTitle);
          if (doc) {
            await selectDocument(documentInfo(doc));
            setLog(`自测已切换到其他文档：${doc.title || doc.name}`);
          }
        }
      });
      await bridgeFetch("/auto-test/finish", {
        method: "POST",
        json: {
          ok: ok,
          source: state.inputDocument,
          switchedTo: config.switchToTitle || "",
          result: state.latestResult && state.latestResult.filename,
          message: ok ? "PS 自测完成，已尝试自动回写源图。" : "PS 自测执行失败。"
        }
      });
    } catch (error) {
      try {
        await bridgeFetch("/auto-test/finish", { method: "POST", json: { ok: false, error: error.message } });
      } catch (finishError) {
        // Ignore finish reporting errors during local self-test.
      }
      setLog(error.message, true);
    }
  };
  window.setTimeout(run, 1200);
}

window.onerror = function (message, source, line, column, error) {
  const detail = error && error.message ? error.message : String(message || "未知脚本错误");
  renderBootError(`脚本错误：${detail}${line ? `（${line}:${column || 0}）` : ""}`);
  return false;
};

async function bootXnhPanel() {
  if (window.__xnhPanelBooted) return;
  window.__xnhPanelBooted = true;
  try {
    renderShell();
    assertBootDomReady();
    byId("bridgeUrl").value = localStorage.getItem(STORAGE_KEYS.bridgeUrl) || state.bridgeUrl;
    state.activeFeature = "";
    state.detailOpen = false;
    renderAll();
    assertBootDomReady();
    bindEvents();
    startBridgePolling();
  } catch (error) {
    renderBootError(error.message);
    return;
  }

  try {
    await checkBridge();
    await refreshOnlineConfig();
    await refreshWorkflows();
    await refreshModels();
    await maybeRunAutoTest();
  } catch (error) {
    setLog(error.message, true);
  }
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", bootXnhPanel);
} else {
  window.setTimeout(bootXnhPanel, 0);
}
