diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..c7e8f97
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,57 @@
+import os
+import re
+
+# 读取版本号
+def get_version():
+ try:
+ toml_path = os.path.join(os.path.dirname(__file__), "pyproject.toml")
+ with open(toml_path, "r", encoding='utf-8') as f:
+ content = f.read()
+ # 使用正则表达式匹配版本号
+ version_match = re.search(r'version\s*=\s*"([^"]+)"', content)
+ if version_match:
+ return version_match.group(1)
+ raise ValueError("未在pyproject.toml中找到版本号")
+ except Exception as e:
+ print(f"读取版本号失败: {str(e)}")
+ raise # 直接抛出异常,不提供保底值
+
+# 声明版本号变量
+VERSION = get_version()
+
+# 注册节点
+NODE_CLASS_MAPPINGS = {}
+NODE_DISPLAY_NAME_MAPPINGS = {}
+
+# 设置Web目录
+WEB_DIRECTORY = "./js"
+
+# 将版本号注入到前端全局变量
+def inject_version_to_frontend():
+ # 创建包含版本号的JavaScript代码
+ js_code = f"""
+window.PromptAssistant_Version = "{VERSION}";
+ """
+
+ # 确保js目录存在
+ js_dir = os.path.join(os.path.dirname(__file__), "js")
+ if not os.path.exists(js_dir):
+ os.makedirs(js_dir)
+
+ # 写入版本号文件
+ version_file = os.path.join(js_dir, "version.js")
+ with open(version_file, "w", encoding='utf-8') as f:
+ f.write(js_code)
+
+
+# 执行版本号注入
+inject_version_to_frontend()
+
+# ANSI颜色代码
+GREEN = "\033[92m" # 绿色
+RESET = "\033[0m" # 重置颜色
+# 打印启动信息,将"已启动"设为绿色
+print(f"✨提示词小助手 V{VERSION} {GREEN}已启动{RESET}")
+
+
+
diff --git a/js/assets/icon-caption-en.svg b/js/assets/icon-caption-en.svg
new file mode 100644
index 0000000..a862871
--- /dev/null
+++ b/js/assets/icon-caption-en.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/assets/icon-caption-zh.svg b/js/assets/icon-caption-zh.svg
new file mode 100644
index 0000000..9812496
--- /dev/null
+++ b/js/assets/icon-caption-zh.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/assets/icon-close.svg b/js/assets/icon-close.svg
new file mode 100644
index 0000000..8ce6c73
--- /dev/null
+++ b/js/assets/icon-close.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/assets/icon-expand.svg b/js/assets/icon-expand.svg
new file mode 100644
index 0000000..b29447d
--- /dev/null
+++ b/js/assets/icon-expand.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/assets/icon-history.svg b/js/assets/icon-history.svg
new file mode 100644
index 0000000..60fdd10
--- /dev/null
+++ b/js/assets/icon-history.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/assets/icon-movedown.svg b/js/assets/icon-movedown.svg
new file mode 100644
index 0000000..55a2032
--- /dev/null
+++ b/js/assets/icon-movedown.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/assets/icon-redo.svg b/js/assets/icon-redo.svg
new file mode 100644
index 0000000..75b1a04
--- /dev/null
+++ b/js/assets/icon-redo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/assets/icon-refresh.svg b/js/assets/icon-refresh.svg
new file mode 100644
index 0000000..539015a
--- /dev/null
+++ b/js/assets/icon-refresh.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/assets/icon-remove.svg b/js/assets/icon-remove.svg
new file mode 100644
index 0000000..735e738
--- /dev/null
+++ b/js/assets/icon-remove.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/assets/icon-tag.svg b/js/assets/icon-tag.svg
new file mode 100644
index 0000000..e7df4c1
--- /dev/null
+++ b/js/assets/icon-tag.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/assets/icon-translate.svg b/js/assets/icon-translate.svg
new file mode 100644
index 0000000..bd3402f
--- /dev/null
+++ b/js/assets/icon-translate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/assets/icon-undo.svg b/js/assets/icon-undo.svg
new file mode 100644
index 0000000..7c4bde3
--- /dev/null
+++ b/js/assets/icon-undo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/config/01起手式.json b/js/config/01起手式.json
new file mode 100644
index 0000000..c16cc1d
--- /dev/null
+++ b/js/config/01起手式.json
@@ -0,0 +1,150 @@
+{
+ "正面起手": {
+ "简单起手": "best quality,masterpiece",
+ "真实风格起手": "best quality,masterpiece,realistic",
+ "提高质量": "HDR,UHD,8K",
+ "最佳质量": "best quality",
+ "高分辨率": "highres",
+ "超高分辨率": "absurdres",
+ "写实": "realistic",
+ "杰作": "masterpiece",
+ "更多细节": "Highly detailed",
+ "简单背景": "simple background",
+ "模糊背景": "blurry background",
+ "演播室灯光": "Studio lighting",
+ "超精细绘画": "ultra-fine painting",
+ "聚焦清晰": "sharp focus",
+ "物理渲染": "physically-based rendering",
+ "极详细刻画": "extreme detail description",
+ "改善细节": "Professional",
+ "添加鲜艳色彩": "Vivid Colors",
+ "虚化模糊景": "Bokeh",
+ "相机设置": "(EOS R8,50mm,F1.2,8K,RAW photo",
+ "扫描": "High resolution scan",
+ "素描": "Sketch",
+ "上半身": "upper body",
+ "全身": "full body",
+ },
+ "负面起手": {
+ "基础负面": "bad proportions, low resolution,worst quality, low quality, normal quality, lowres, inaccurate limb, bad, ugly, terrible, extra fingers, fewer fingers, missing fingers, extra arms, extra legs, inaccurate eyes, bad composition, bad anatomy, error, extra digit, fewer digits, cropped, low res, jpeg artifacts, trademark,artist's name, username, watermarksignature, watermark,text, words",
+ "NSFW": "nsfw",
+ "embeddings": "BadDream,badhandv4,EasyNegative,ng_deepnegative_v1_75t,rev2-badprompt,verybadimagenegative_v1.3,negative_hand-neg,bad-picture-chill-75v",
+ "变异手指": "mutated hands and fingers",
+ "畸形的": "deformed",
+ "解剖不良": "bad anatomy",
+ "毁容": "disfigured",
+ "脸不好": "poorly drawn face",
+ "低分辨率": "lowres",
+ "变异的": "mutated",
+ "多余肢体": "extra limb",
+ "丑陋": "ugly",
+ "手画得差": "poorly drawn hands",
+ "缺少的肢体": "missing limb",
+ "漂浮的四肢": "floating limbs",
+ "肢体不连贯": "disconnected limbs",
+ "畸形的手": "malformed hands",
+ "脱离焦点": "out of focus",
+ "长颈": "long neck",
+ "身体长": "long body",
+ "张嘴": "gape"
+ },
+ "构图视角": {
+ "正面视角": "front view",
+ "侧面视角": "from_side",
+ "从上方↘": "from_above",
+ "从下方↗": "from_below",
+ "左侧": "left side view",
+ "右侧": "right side view",
+ "动态角度": "dynamic angle",
+ "倾斜视角": "dutch angle",
+ "特写": "close-up",
+ "微距": "macro_shot",
+ "近景": "selfie",
+ "中景": " medium view",
+ "中长镜头": "medium long shot",
+ "远景": "wide shot",
+ "全景": "panorama",
+ "近拍": "close shot",
+ "俯视": "vertical view",
+ "鸟瞰": "bird's-eye view",
+ "半身像": "portrait",
+ "全身像": "full_body",
+ "上半身": "upper_body",
+ "下半身": "lower_body",
+ "抱枕": "dakimakura_(medium)",
+ "从裙底瞥": "upshorts",
+ "裙底视角": "phone_screen",
+ "鱼眼镜头": "fisheye",
+ "多视角分解": "multiple_views"
+ },
+ "画风": {
+ "漫画": "comic",
+ "厚涂": "Impasto",
+ "照片": "photorealistic",
+ "拍立得": "polaroid",
+ "马克笔": "marker_(medium)",
+ "马克笔(中性灰色)": "copics",
+ "Q版": "chibi",
+ "水彩": "watercolor",
+ "抱枕": "dakimakura_(medium)",
+ "复古风格": "retro_arf style",
+ "四格": "4koma",
+ "胶片电影": "celluloid",
+ "剪影": "silhouette",
+ "杂志封面": "magazine cover",
+ "海报": "poster",
+ "国风水墨": "chinese monochrome ink",
+ "宫崎骏风格": "Hayao Miyazaki \\(style\\)",
+ "蒂姆伯顿风格": "Tim Burton \\(style\\)"
+ },
+ "构图风格": {
+ "动漫": "Anime",
+ "摄影": "Photographic",
+ "数字艺术": "digital art",
+ "连环漫画": "comic book",
+ "幻想艺术": "fantasyArt",
+ "胶片滤镜": "analog film",
+ "赛博朋克": "cyberpunk",
+ "霓虹朋克": "neon punk",
+ "蒸汽波": "Vaporwave",
+ "等距艺术": "isometric",
+ "低多边形": "low poly",
+ "线条艺术": "lineArt",
+ "电影质感": "cinematic",
+ "像素艺术": "pixelArt",
+ "迷幻艺术": "Psychedelia Art",
+ "酸性风格": "Acid Graphics",
+ "极简主义": "Minimalism",
+ "故障艺术": "Glitch Art",
+ "气泡艺术": "Bubble Art",
+ "玻璃拟态": "Glassmorphism",
+ "新拟物风格": "Neumorphism",
+ "3D插画": "3D graphics",
+ "构成主义": "Constructivism",
+ "孟菲斯风格": "Memphis"
+ },
+ "光影": {
+ "电影光效": "cinematic_lighting",
+ "镜头光晕": "lens_flare",
+ "光追": "reality_ray_tracing",
+ "正面光": "frontlight",
+ "侧面光": "sidelight",
+ "背光": "backlight",
+ "逆光": "backlighting",
+ "边缘光": "rim_light",
+ "强边缘光": "strong_rim_light",
+ "自上而下的光线": "god_rays",
+ "明亮的射线光束": "light_rays",
+ "环境光": "ambient_light",
+ "轮廓光": "rim light",
+ "体积光": "Volumetric Lighting",
+ "霓虹灯": "glowing neon lights",
+ "透镜光晕": "lens flare",
+ "金属光泽": "metallic luster",
+ "氛围光照": "moody lighting",
+ "丁达尔效应": "Tyndall effect",
+ "漏光光效": "light leaks",
+ "背景光": "background light",
+ "自然光": "available light"
+ }
+}
\ No newline at end of file
diff --git a/js/config/02人物.json b/js/config/02人物.json
new file mode 100644
index 0000000..fcf33ba
--- /dev/null
+++ b/js/config/02人物.json
@@ -0,0 +1,128 @@
+{
+ "人设": {
+ "公主": "princess",
+ "舞者": "dancer",
+ "啦啦队": "cheerleader",
+ "芭蕾舞女演员": "ballerina",
+ "体操队队长": "gym leader",
+ "女服务员": "waitress",
+ "和风女仆": "wa maid",
+ "女仆": "maid",
+ "偶像": "idol",
+ "弓道": "kyuudou",
+ "女武神": "valkyrie",
+ "办公室小姐": "office lady",
+ "赛车女郎": "race queen",
+ "魔女": "Witch",
+ "巫女": "miko",
+ "修女": "nun",
+ "牧师": "priest",
+ "神职人员(基督教)": "cleric",
+ "忍者": "ninja",
+ "女警": "policewoman",
+ "警察": "police",
+ "医生": "doctor",
+ "护士": "nurse",
+ "眼镜娘": "glasses",
+ "狐狸精": "kitsune",
+ "公交车": "public use",
+ "女王(SM中)": "dominatrix",
+ "机娘": "mecha musume",
+ "另一种机娘": "gynoid",
+ "类人机器人": "humanoid robot",
+ "半机械人": "cyborg"
+ },
+ "性别/年龄": {
+ "单人": "solo",
+ "女人": "female",
+ "男人": "male",
+ "性转": "genderswap",
+ "扶她": "futanari",
+ "伪娘": "otoko no ko",
+ "儿 童": "child",
+ "未成年": "underage",
+ "年轻": "young",
+ "熟女": "mature female",
+ "老年": "old"
+ },
+ "胸部": {
+ "胸": "chest",
+ "胸肌": "pectorals",
+ "大胸肌": "large pectorals",
+ "小胸部(B": "small breasts",
+ "中等胸部(C": "medium breasts",
+ "大胸部(D": "big breasts",
+ "巨乳(E": "huge breasts",
+ "魔乳(F": "gigantic breasts",
+ "未扣扣子": "unbuttoned clothes"
+ },
+ "脸型": {
+ "瓜子脸": "heart-shaped face",
+ "鹅蛋脸": "oval face",
+ "国字脸": "square face",
+ "可爱脸": "cute face",
+ "菱形脸": "rhomboid face",
+ "圆脸": "round face",
+ "长脸": "long face",
+ "瘦脸": "Thin face",
+ "平脸": "Flat face",
+ "柔和的脸": "Soft face",
+ "棱角分明的脸": "Angular face"
+ },
+ "鼻子": {
+ "蒜头鼻": "pug nose",
+ "肉鼻子": "dumpy nose",
+ "朝天鼻": "upturned nose",
+ "扁鼻": "flat nose",
+ "鹰勾鼻": "aquliline nose",
+ "罗马鼻": "Roman nose",
+ "翘鼻": "Snub nose",
+ "长鼻": "Long nose",
+ "短鼻": "Short nose"
+ },
+ "嘴巴": {
+ "樱桃小嘴": "Cupid mouth",
+ "心形嘴": "Heart-shaped mouth",
+ "薄唇": "Thin mouth",
+ "丰唇": "Full lips",
+ "嘟嘴": "Pouty lips",
+ "方唇": "Square lips",
+ "上扬唇": "Up-turned lips",
+ "下撇唇": "Down-turned lips"
+ },
+ "皮肤": {
+ "有光泽的皮肤": "shiny skin",
+ "苍白皮肤": "pale skin",
+ "白皙皮肤": "white skin",
+ "棕色皮肤": "brown skin",
+ "深色皮肤": "deep skin",
+ "偏黑皮肤": "black skin",
+ "晒日线": "tan lines",
+ "泳装晒痕": "pang",
+ "如白色大理石般有光泽的肌肤": "white marble glowing skin"
+ },
+ "体型": {
+ "模特": "model",
+ "苗条": "slender",
+ "诱惑": "glamor",
+ "丰满": "plump",
+ "肥胖": "fat",
+ "高大": "tall",
+ "纤弱": "petite",
+ "肌肉": "muscular",
+ "细腰": "narrow waist",
+ "宽臀": "wide hips"
+ },
+ "眉毛": {
+ "浓眉": "thick eyebrows",
+ "眉毛翘起": "cocked eyebrow",
+ "短眉毛": "short eyebrows",
+ "V字眉": "v-shaped eyebrows",
+ "剑眉": "dashing eyebrows",
+ "一字眉": "straight united eyebrows ",
+ "柳叶眉": "arched eyebrows",
+ "新月眉": "crescent eyebrows",
+ "描画过的眉毛": "pencilled eyebrows ",
+ "浓密而粗短的眉毛": "bushy and stubby eyebrows"
+ }
+}
\ No newline at end of file
diff --git a/js/config/03服饰.json b/js/config/03服饰.json
new file mode 100644
index 0000000..a348163
--- /dev/null
+++ b/js/config/03服饰.json
@@ -0,0 +1,273 @@
+{
+ "日常服装": {
+ "女式衬衫": "blouse",
+ "有领衬衫": "collared_shirt",
+ "水手服衬衫": "sailor_shirt",
+ "T恤": "t-shirt",
+ "背心(居家)": "tank_top",
+ "背心(正式)": "vest",
+ "休闲": "casual",
+ "卫衣": "hoodie",
+ "居家服": "homewear",
+ "睡衣": "pajamas",
+ "印花睡衣": "print_pajamas",
+ "波点睡衣": "polka_dot_pajamas",
+ "西装外套": "blazer",
+ "大衣": "overcoat",
+ "皮草大衣": "fur_coat",
+ "夹克衫": "jacket",
+ "皮夹克": "leather_jacket",
+ "牛仔夹克": "denim_jacket",
+ "毛衣": "sweater",
+ "高领毛衣": "turtleneck_sweater",
+ "罗纹毛衣": "ribbed_sweater",
+ "毛衣背心": "sweater_vest",
+ "露背毛衣": "backless_sweater",
+ "米色毛衣": "beige_sweater",
+ "棕色毛衣": "brown_sweater",
+ "连帽毛衣": "hooded_sweater",
+ "露肩毛衣": "off-shoulder_sweater",
+ "菱紋毛衣": "ribbed_sweater",
+ "条纹毛衣": "striped_sweater",
+ "处男杀手毛衣": "virgin_killer_sweater",
+ "羽绒服": "puffer_jacket",
+ "雨衣": "raincoat",
+ "连衣裙": "dress",
+ "长连衣裙": "long_dress",
+ "露肩连衣裙": "off-shoulder_dress",
+ "围裙连衣裙": "sweater_dress",
+ "夏日长裙": "summer_dress",
+ "短裙": "skirt",
+ "超短裙": "microskirt",
+ "迷你裙": "miniskirt",
+ "分层裙": " layered skirts",
+ "百褶裙": "pleated_skirt",
+ "短铅笔裙": "pencil_skirt",
+ "蓬蓬裙": "bubble_skirt",
+ "芭蕾舞裙": "tutu",
+ "绷紧的连衣裙": "taut_dress",
+ "铅笔裙": "pencil_dress",
+ "牛仔裙": "denim_skirt",
+ "雪紡裙": "chiffon_skirt",
+ "花边裙子": "frilled_skirt",
+ "毛皮镶边短裙": "fur-trimmed_skirt",
+ "蕾絲短裙": "lace_skirt",
+ "缎带饰边短裙": "ribbon-trimmed_skirt",
+ "短裤": "shorts",
+ "条纹短裤": "striped_shorts",
+ "吊带短裤": "suspender_shorts",
+ "牛仔短裤": "denim_shorts",
+ "瑜伽裤": "yoga_pants",
+ "牛仔裤": "jeans",
+ "七分裤": "capri_pants",
+ "灯笼裤": "bloomers"
+ },
+ "袜子鞋子": {
+ "裸腿": "bare_legs",
+ "裤袜": "pantyhose",
+ "黑丝裤袜": "black_pantyhose",
+ "白丝裤袜": "white_pantyhose",
+ "肉丝裤袜": "nude_pantyhose",
+ "油亮裤袜": "shiny_pantyhose",
+ "有腿环的裤袜": "thighband_pantyhose",
+ "破损的裤袜": "torn_pantyhose",
+ "网袜(材质)": "fishnets",
+ "丝袜": "stockings",
+ "渔网袜": "stirrup_legwear",
+ "长筒袜": "thighhighs",
+ "白色长筒袜": "white_thighhighs",
+ "黑色长筒袜": "black_thighhighs",
+ "粉色长筒袜": "pink_thighhighs",
+ "吊带袜": "garter_straps",
+ "腰带(吊带袜的)": "garter_straps",
+ "损坏了的长筒袜": "torn_thighhighs",
+ "透明的袜子": "see-through_legwear",
+ "花边袜": "frilled_legwear",
+ "蕾丝边袜": "lace-trimmed_legwear",
+ "泡泡袜": "loose_socks",
+ "踝袜": "ankle_socks",
+ "腿套|暖腿袜": "leg_warmers",
+ "横条短袜": "striped_socks",
+ "连体黑丝": "black_bodystocking",
+ "连体白丝": "white_bodystocking",
+ "赤脚": "barefoot",
+ "没有鞋子": "no_shoes",
+ "脱下的鞋子": "shoes_removed",
+ "单鞋": "the_only_shoe",
+ "脱下的鞋": "shoes_removed",
+ "一只脚没穿鞋子": "single_shoe",
+ "高跟鞋": "high_heels",
+ "运动鞋": "sneakers",
+ "裸足凉鞋": "barefoot_sandals",
+ "木屐凉鞋": "clog_sandals",
+ "乐福鞋(小皮鞋)": "black_loafers",
+ "靴子": "boots",
+ "大腿靴": "thigh_boots",
+ "高跟靴": "high_heel_boots",
+ "雪地靴": "snow_boots",
+ "皮靴": "leather_boots",
+ "马丁靴": "doc_martens",
+ "旱冰鞋": "roller_skates",
+ "脚环": "anklet",
+ "镣铐": "shackles"
+ },
+ "佩饰道具": {
+ "颈部饰品": "choker",
+ "黑色颈圈": "black_choker",
+ "领巾": "neckerchief",
+ "红领巾": "red_neckerchief",
+ "领带": "necktie",
+ "蝴蝶结领带": "bowtie",
+ "挂在脖子上的耳机": "headphones_around_neck",
+ "脖子上护目镜": "goggles_around_neck",
+ "颈铃": "neck_bell",
+ "颈部纹身": "neck_tattoo",
+ "十字架项链": "cross_necklace",
+ "珍珠项链": "pearl_necklace",
+ "心形项链": "labcoat",
+ "骷髅项链": "skull_necklace",
+ "花项链": "flower_necklace",
+ "贝壳项链": "shell_necklace",
+ "金项链": "gold_necklace",
+ "羽毛项链": "feather_necklace",
+ "格子围巾": "plaid_scarf",
+ "条纹围巾": "striped_scarf",
+ "格纹围巾": "checkered_scarf",
+ "印花围巾": "print_scarf",
+ "波点围巾": "polka_dot_scarf",
+ "披肩": "capelet",
+ "棒球帽": "Baseball cap",
+ "针织帽": "Beanie",
+ "遮阳帽": "Visor cap",
+ "圆顶礼帽": "Bowler hat",
+ "侦探帽": "Fedora",
+ "牛仔帽": "Cowboy hat",
+ "厨师帽": "Chef hat",
+ "军官帽": "Military hat",
+ "圣诞帽": "Santa hat",
+ "派对帽": "Party hat",
+ "小丑帽": "Jester cap",
+ "女巫帽": "witch hat",
+ "贝雷帽": "beret",
+ "鸭舌帽": "peaked cap",
+ "草帽": "Straw hat"
+ },
+ "制服COS": {
+ "职场制服": "business_suit",
+ "校服": "school_uniform",
+ "水手服": "serafuku",
+ "护士": "nurse",
+ "警服": "police_uniform",
+ "海军制服": "naval_uniform",
+ "陆军制服": "military_uniform",
+ "女仆装": "maid",
+ "围裙": "apron",
+ "厨师工装": "chef_uniform",
+ "宇航服": "space_suit",
+ "实验服": "labcoat",
+ "啦啦队服": "cheerleader",
+ "燕尾服": "tuxedo",
+ "礼服": "evening_gown",
+ "婚纱": "wedding_dress",
+ "巫女服": "miko",
+ "修女服": "nun_cloth",
+ "兔子服装": "bunny_costume",
+ "猫系服装": "cat_costume",
+ "皮套狗": "dog_costume",
+ "熊套装": "bear_costume",
+ "圣诞风格服装": "santa_costume",
+ "盔甲": "armor",
+ "比基尼盔甲": "bikini_armor",
+ "穿着全套盔甲的": "full_armor",
+ "板甲": "plate_armor",
+ "日本铠甲": "japanese_armor",
+ "动力装甲": "power_armor",
+ "机甲": "mecha",
+ "头盔": "helmet",
+ "头盔(日式)": "kabuto",
+ "無肩甲盔甲": "off-shoulder_armor",
+ "肩甲": "shoulder_armor",
+ "日本弓道護胸甲": "muneate",
+ "胸甲": "breastplate",
+ "腹甲": "faulds",
+ "胫甲": "shin_guards",
+ "装甲靴": "armored_boots",
+ "道袍": "Taoist robe",
+ "长袍": "robe",
+ "混合长袍": "robe_of_blending",
+ "斗篷": "cloak",
+ "羽衣": "hagoromo"
+ },
+ "传统服饰": {
+ "中国服饰": "china_dress",
+ "中国风": "chinese_style",
+ "旗袍": "china_dress",
+ "印花旗袍": "print_cheongsam",
+ "汉服": "hanfu",
+ "汉服唐风": "hanfu, tang style",
+ "上杉": "upper shan",
+ "长上杉": "long upper shan",
+ "齐胸破裙": "chest po skirt",
+ "齐胸褶裙": "chest pleated skirt",
+ "系带": "waistband",
+ "披帛": "pibo",
+ "汉服宋风": "hanfu, song style outfits",
+ "短杉": "short shan",
+ "长杉": "long shan",
+ "宋抹": "songmo",
+ "百褶裙": "pleated skirt",
+ "汉服明风": "hanfu, ming style outfits",
+ "短袄": "short coat",
+ "长袄": "long coat",
+ "马面裙": "mamian skirt",
+ "交领": "overlapping collar",
+ "圆领": "round collar",
+ "立领": "standing collar",
+ "方领": "square collar",
+ "云肩": "overlapping collar",
+ "刺绣": "embroidery",
+ "织金": "woven gold",
+ "妆花": "makeup flower",
+ "补服": "bufu",
+ "和服": "kimono",
+ "印花和服": "print_kimono",
+ "衣带(和服用)": "obi",
+ "浴衣": "yukata",
+ "韩服": "hanbok",
+ "朝鲜服饰": "korean_clothes",
+ "西部风格": "western",
+ "德国服装": "german_clothes",
+ "哥特风格": "gothic",
+ "洛丽塔风格": "gothic_lolita",
+ "拜占庭风格": "byzantine_fashion",
+ "热带特征的": "Tropical",
+ "印度风格": "indian_style",
+ "越南校服(奥黛)": "Ao_Dai",
+ "阿伊努人的服饰": "ainu_clothes",
+ "阿拉伯服饰": "arabian_clothes",
+ "埃及风格服饰": "egyptian_clothes"
+ },
+ "运动泳装": {
+ "运动服": "gym_uniform",
+ "体操服": "athletic_leotard",
+ "足球服": "volleyball_uniform",
+ "网球衫": "tennis_uniform",
+ "棒球服": "baseball_uniform",
+ "棒球夹克": "letterman_jacket",
+ "排球服": "volleyball_uniform",
+ "骑行套装": "bikesuit",
+ "泳装": "swimsuit",
+ "学校泳装(死库水)": "school_swimsuit",
+ "赛用泳衣": "competition_swimsuit",
+ "连体泳衣": "casual_one-piece_swimsuit",
+ "高开衩的泳衣": "highleg_swimsuit",
+ "一体式泳衣": "one-piece_swimsuit",
+ "比基尼": "bikini",
+ "微比基尼": "micro_bikini",
+ "女仆比基尼": "maid_bikini",
+ "水手服款比基尼": "sailor_bikini",
+ "贝壳比基尼": "shell_bikini",
+ "运动比基尼": "sports_bikini",
+ "系绳比基尼": "string_bikini"
+ }
+}
\ No newline at end of file
diff --git a/js/config/04人物发型.json b/js/config/04人物发型.json
new file mode 100644
index 0000000..2188bf7
--- /dev/null
+++ b/js/config/04人物发型.json
@@ -0,0 +1,102 @@
+{
+ "头发颜色": {
+ "绿头发": "green hair",
+ "粉色头发": "pink hair",
+ "红头发": "red hair",
+ "铂金色头发": "platinum blonde hair",
+ "青色头发": "azure hair",
+ "水蓝色头发": "aqua hair",
+ "红宝石色头发": "ruby hair",
+ "两色头发": "two-tone hair",
+ "多色的头发": "multicolored hair",
+ "渐变头发": "gradient hair",
+ "分色头发": "split-color hair",
+ "彩虹头发": "rainbow hair"
+ },
+ "头发长度": {
+ "长发": "long hair",
+ "很短的头发": "very short hair",
+ "短发": "short hair",
+ "后短发,前长发": "short hair with long locks",
+ "中等头发": "medium hair",
+ "很长的头发": "very long hair",
+ "超级长的头发": "absurdly long hair"
+ },
+ "马尾": {
+ "马尾": "ponytail",
+ "双马尾": "twintails",
+ "高双马尾": "canonicals",
+ "低双马尾": "low twintails",
+ "披肩单马尾": "one side up",
+ "披肩双马尾": "two side up",
+ "短马尾": "short ponytail",
+ "侧马尾": "side ponytail"
+ },
+ "辫子": {
+ "辫子": "braid",
+ "法式辫子": "french braid",
+ "辫子头发": "braiding hair",
+ "双辫子": "twin braids",
+ "三股辫": "braid",
+ "短辫子": "short braid",
+ "长辫子": "long braid",
+ "辫子刘海": "braided bangs",
+ "辫式发髻": "braided bun",
+ "麻花辫马尾": "braided ponytail",
+ "法式冠编发": "crown braid",
+ "多股(麻花)辫": "multiple braids",
+ "披在一侧的单条辫": "side braid",
+ "披在两侧的两条辫": "side braids",
+ "单股辫": "single braid",
+ "两条辫子": "twin braids"
+ },
+ "刘海": {
+ "进气口发型": "hair intakes",
+ "发瓣": "hair flaps",
+ "刘海": "bangs",
+ "空气刘海": "air bangs",
+ "齐刘海": "blunt bangs",
+ "侧面空气刘海": "side blunt bangs",
+ "中分刘海": "parted bangs",
+ "斜刘海": "swept bangs",
+ "不对称刘海": "asymmetric bangs"
+ },
+ "其他发型": {
+ "眼睛之间的头发": "hair between eyes",
+ "头发覆盖一只眼": "hair over one eye",
+ "头发遮住眉毛": "hair over one eyebrow",
+ "透过头发可见腮红": "blush visible through hair",
+ "透过头发可见眼睛": "eyes visible through hair",
+ "头发撩到耳后": "hair behind ear",
+ "披肩发": "hair over shoulder",
+ "头发遮住三点": "hair censor",
+ "头发披在胸上": "hair over breasts",
+ "直发": "straight hair",
+ "卷发": "curly hair",
+ "波浪卷": "wavy hair",
+ "钻头(配双)": "drill hair",
+ "姬发式(齐刘海后长黑发": "hime cut",
+ "齐而短头发(波波发": "bob cut",
+ "公主发型": "princess head",
+ "上半部分束起": "Half-up",
+ "额头": "forehead",
+ "扎过的头发": "tied hair",
+ "低扎头发": "low tied hair",
+ "多扎头发": "multi-tied hair",
+ "丸子头": "double bun",
+ "圆发髻": "hair bun",
+ "芭蕾髻": "ballet hair bun",
+ "尖头头发": "pointy hair",
+ "羽毛头发": "feather hair",
+ "弓形头发": "bow-shaped hair",
+ "孤颈头发": "lone nape hair",
+ "变换发型": "alternate hairstyle",
+ "与原设不同头发长度": "alternate hair length",
+ "鬓角": "sideburns",
+ "长鬓角": "long sideburns",
+ "侧边发辫": "sidelocks",
+ "秃头": "bald",
+ "鸟窝头|爆炸头": "afro",
+ "尖刺的头发": "spiked hair"
+ }
+}
\ No newline at end of file
diff --git a/js/config/05动作.json b/js/config/05动作.json
new file mode 100644
index 0000000..e21fbbb
--- /dev/null
+++ b/js/config/05动作.json
@@ -0,0 +1,186 @@
+{
+ "热门动作": {
+ "行走": "Walking",
+ "转身": "Turning",
+ "摆动": "Swinging",
+ "弯腰": "Bending",
+ "高抬腿": "High leg lift",
+ "拉伸": "Stretching",
+ "手势": "Gestures",
+ "双手放腰": "Hands on waist",
+ "站立": "standing",
+ "躺": "on back",
+ "趴": "on stomach",
+ "跪": "kneeling",
+ "侧卧": "on side",
+ "靠在物体上": "leaning on object",
+ "躺着的": "lying",
+ "胎儿姿势(躺)": "fetal position"
+ },
+ "手部动作": {
+ "手放在嘴边": "hand to mouth",
+ "爪手势": "claw pose",
+ "招财猫手势(下弯手腕)": "paw pose",
+ "狐狸手势": "fox shadow puppet",
+ "手指枪手势": "finger gun",
+ "胜利手势": "v",
+ "双 v": "double v",
+ "翘大拇指": "thumbs up",
+ "食指抬起": "index finger raised",
+ "国际友好手势": "middle finger",
+ "用手指做出笑脸": "fingersmile",
+ "擦眼泪": "wiping tears",
+ "OK手势": "ok sign",
+ "遮阳手势": "shading eyes",
+ "嘘(手势)": "shushing",
+ "手指伸进嘴里": "finger in mouth",
+ "手指卷着头发": "hair twirling",
+ "双手手指交叉|双手紧握": "hands clasped",
+ "握着头发|手指绕着头发": "holding hair",
+ "扶眼镜": "adjusting eyewear",
+ "拨弄手套": "adjusting gloves",
+ "理头发": "adjusting hair",
+ "整理帽子": "adjusting hat",
+ "掀起自己的衣物": "lifted by self",
+ "拉起掀起卷起衬衫": "shirt lift",
+ "指掀起裙子时形成的篮子形状": "skirt basket",
+ "剥下裙子胸口的部分": "dress pull",
+ "往上剥开的比基尼": "bikini lift",
+ "将裙子掀上去": "skirt lift",
+ "往上掰衣物的吊带": "strap lift",
+ "起风效果|上升气流": "wind lift",
+ "拉着口罩": "mask pull",
+ "拉着裤子": "pants pull",
+ "手抓裆部": "crotch grab",
+ "用手遮住胸部": "covering chest by hand",
+ "捏脸颊": "cheek pinching",
+ "戳脸颊": "cheek poking",
+ "摸下巴": "chin stroking",
+ "展现魅力的姿势": "curvy",
+ "手持辫子": "bunching hair"
+ },
+ "腿部动作": {
+ "张腿": "spread legs",
+ "两腿并拢": "legs together",
+ "二郎腿": "crossed legs",
+ "I字摆腿": "standing split leg up",
+ "屈膝礼(女仆行礼)": "curtsy",
+ "双腿之间的手": "hand between legs",
+ "稍息": "open stance",
+ "挡住关键部位的腿": "convenient leg",
+ "V字张腿": "spread legs",
+ "用双腿夹住": "leg lock",
+ "双腿抬起": "legs up",
+ "双腿交叉站姿": "watson cross",
+ "膝盖合并,两脚分开": "knees together feet apart",
+ "膝盖上有动物": "animal on lap",
+ "手放在自己的膝盖上": "hand on own knee",
+ "顶起膝盖": "knee up",
+ "膝盖蜷到胸前": "knees on chest",
+ "膝盖顶到胸部": "knees to chest",
+ "鸭子坐": "wariza",
+ "正坐": "seiza",
+ "跨坐": "straddling",
+ "侧身坐": "yokozuwari",
+ "蹲下,张开双腿": "squatting, open legs",
+ "一只膝盖": "one knee",
+ "下跪": "kneeling",
+ "四肢着地": "all fours",
+ "凹版姿势": "gravure pose",
+ "高踢": "high kick",
+ "泡脚": "soaking feet"
+ },
+ "多人动作": {
+ "胸部互碰|胸顶着胸": "asymmetrical docking",
+ "背对背": "back-to-back",
+ "眼对眼(对视)": "eye contact",
+ "二人面对面(脸贴得很近)": "facing another",
+ "喂食": "feeding",
+ "口内手指": "finger in another's mouth",
+ "牵手": "holding hands",
+ "拥抱": "hug",
+ "即将接吻": "imminent kiss",
+ "递食物": "incoming food",
+ "递礼物": "incoming gift",
+ "等待接吻|献吻": "incoming kiss",
+ "壁咚": "Kabedon",
+ "膝枕": "lap pillow",
+ "掏耳勺": "mimikaki",
+ "公主抱": "princess carry",
+ "舌头": "tongue",
+ "吐舌头": "tongue out",
+ "小舌头|口盖垂|悬雍垂": "uvula",
+ "咬耳朵": "ear biting"
+ },
+ "常见动作": {
+ "化妆": "applying makeup",
+ "洗澡": "bathing",
+ "入浴": "bathing",
+ "咬": "biting",
+ "鞠躬": "bowing",
+ "刷牙": "brushing teeth",
+ "吹泡泡": "bubble blowing",
+ "追逐": "chasing",
+ "打扫": "cleaning",
+ "攀爬": "climbing",
+ "烹饪": "cooking",
+ "哭": "crying",
+ "拥抱": "cuddling",
+ "跳舞": "dancing",
+ "潜水": "diving",
+ "拉弓": "drawing bow",
+ "喝": "drinking",
+ "驾驶": "driving",
+ "(意外)掉落": "dropping",
+ "弄干(浴后)": "drying",
+ "双持": "dual wielding",
+ "吃饭": "eating",
+ "做运动": "exercise",
+ "战斗姿态|摆着架势的": "fighting stance",
+ "射击": "firing",
+ "钓鱼": "fishing",
+ "秀肌肉": "flexing",
+ "飞踢": "flying kick",
+ "梳头": "hair brushing",
+ "撩头发": "hair tucking",
+ "吊起来的": "hanging",
+ "击打": "hitting",
+ "在想象的": "imagining",
+ "跳跃": "jumping",
+ "踢": "kicking",
+ "演奏乐器": "playing instrument",
+ "钢管舞": "pole dancing",
+ "祈祷": "praying",
+ "展示": "presenting",
+ "挥拳": "punching",
+ "推搡": "pushing",
+ "阅读": "reading",
+ "骑": "riding",
+ "奔跑": "running",
+ "缝纫": "sewing",
+ "购物": "shopping",
+ "淋浴": "showering",
+ "唱歌": "sing",
+ "扑克牌": "playing card",
+ "打游戏": "playing games"
+ },
+ "眼部动作": {
+ "面向镜头": "facing_viewer",
+ "看向镜头": "looking_at_viewer",
+ "眼神接触": "eye-contact",
+ "盯着看": "eyeball",
+ "凝视": "staring",
+ "回眸": "looking_back",
+ "人物倾斜": "gradient",
+ "人物视角向下看↘": "looking_down",
+ "人物视角抬头看↗": "looking_up",
+ "面向别处": "facing_away",
+ "看向侧面": "looking_to_the_side",
+ "看着别处": "looking_away",
+ "遥望": "looking_afar",
+ "向外看": "looking_outside",
+ "歪头": "head_tilt",
+ "低头": "head_down",
+ "照镜子": "looking_at_mirror"
+ }
+}
\ No newline at end of file
diff --git a/js/config/06面部表情.json b/js/config/06面部表情.json
new file mode 100644
index 0000000..1ed7f9e
--- /dev/null
+++ b/js/config/06面部表情.json
@@ -0,0 +1,186 @@
+{
+ "眼睛相关": {
+ "眼袋": "bags under eyes",
+ "缠着绷带的单眼": "bandage over one eye",
+ "遮眼|眼罩": "blindfold",
+ "眼罩": "eyepatch",
+ "眼影": "eyeshadow",
+ "医用眼罩": "medical eyepatch",
+ "眼睛上的疤痕": "scar across eye",
+ "闭眼": "eyes closed",
+ "半闭双眼": "half closed eyes",
+ "眯起眼睛": "narrowed eyes",
+ "拉下眼睑的鬼脸": "akanbe",
+ "睁大眼睛": "wide eyed",
+ "一只眼睛闭着": "one eye closed",
+ "蒙眼": "blindfold",
+ "眨眼": "wink",
+ "失去高光的眼睛": "empty eyes",
+ "翻白眼": "rolling eyes",
+ "眼泪": "tears",
+ "锐利的眼": "slanted eyes",
+ "低眉顺眼": "tareme",
+ "上翘的眼睛": "upturned eyes"
+ },
+ "瞳孔": {
+ "瞳孔": "pupils",
+ "明亮的瞳孔": "bright pupils",
+ "异色瞳": "heterochromia",
+ "竖的瞳孔/猫眼": "slit pupils",
+ "蛇瞳孔": "snake pupils",
+ "瞳孔闪光": "pupils sparkling",
+ "爱心形瞳孔": "heart shaped pupils",
+ "钻石形状瞳孔": "diamond shaped pupils",
+ "五角星形状瞳孔": "star shaped pupils",
+ "瞳孔散大": "dilated pupils",
+ "没有瞳孔": "no pupils",
+ "缩瞳": "constricted pupils",
+ "眼睛里的星星": "star in eye",
+ "星形瞳孔": "star shaped pupils",
+ "X形瞳孔": "x shaped pupils",
+ "水平瞳孔": "horizontal pupils",
+ "虚线的眼睛": "dashed eyes",
+ "蝴蝶形瞳孔": "butterfly shaped pupils",
+ "菱形瞳孔": "diamond shaped pupils",
+ "长方形瞳孔": "rectangular pupils",
+ "方形瞳孔": "square pupils",
+ "点瞳孔": "dot pupils",
+ "额外的瞳孔": "extra pupils",
+ "十字形瞳孔": "cross shaped pupils",
+ "符号形瞳孔": "symbol shaped pupils"
+ },
+ "嘴部表情": {
+ "张嘴": "open mouth",
+ "喘气(张大嘴)": "gasping",
+ "嘴巴微微张开": "Slightly open mouth",
+ "波浪嘴": "wavy mouth",
+ "闭嘴": "close mouth",
+ "点嘴": "dot mouth",
+ "堵嘴": "gag",
+ "啃": "gnaw",
+ "努嘴": "pout",
+ "嘴唇张开": "parted lips",
+ "吃惊": "surprised",
+ "勒住嘴": "bit gag",
+ "栗子嘴": "chestnut mouth",
+ "被封住嘴": "cleave gag",
+ "闭着的嘴": "closed mouth",
+ "蒙住的嘴": "covered mouth",
+ "嘴里有头发": "hair in mouth",
+ "嘟嘴|抿嘴": "homu",
+ "用嘴叼着": "mouth hold",
+ "张开的嘴唇": "parted lips",
+ "撅起的嘴唇": "puckered lips",
+ "把嘴画在侧脸": "sideways mouth",
+ "嘴里含着勺子": "spoon in mouth",
+ "三角嘴": "triangle mouth",
+ "唾液": "saliva",
+ "流口水": "drooling",
+ "嘴角画着口水滴形状的缺口": "mouth drool"
+ },
+ "正面的表情": {
+ "微笑": "smile",
+ "善良的微笑": "kind smile",
+ "大笑": "laughing",
+ "开心": "happy",
+ "露齿咧嘴笑": "grin",
+ "咧嘴傻笑": "teasing smile",
+ "魅惑的微笑": "seductive smile",
+ "自鸣得意的笑": "smirk",
+ "咯咯傻笑": "giggling",
+ "洋洋得意": "smug",
+ "调皮的脸": "naughty face",
+ "邪恶笑": "evil smile",
+ "疯狂的笑": "crazy smile",
+ "快乐|幸福": "happy",
+ "生日快乐": "happy birthday",
+ "万圣节快乐": "happy halloween",
+ "新年快乐": "happy new year",
+ "开心的眼泪": "happy tears",
+ "情人节快乐": "happy valentine"
+ },
+ "其他表情": {
+ "面无表情": "embarrass",
+ "困乏的": "sleepy",
+ "喝醉的": "drunk",
+ "无聊的": "bored",
+ "使困惑": "confused",
+ "思考": "thinking",
+ "孤独": "lonely",
+ "决心的,坚定的": "determined",
+ "阴沉脸": "shaded",
+ "阴影": "shadow",
+ "凝视|盯": "staring",
+ "咬紧牙关": "clenched teeth",
+ "露出虎牙|露出尖牙": "fang out",
+ "尖牙": "fangs",
+ "鲨鱼牙|锯齿牙": "sharp teeth",
+ "慌张的": "flustered",
+ "流汗": "sweat",
+ "害怕的": "scared",
+ "轻微脸红": "light blush",
+ "脸红": "blush",
+ "害羞的": "shy",
+ "害羞的(尴尬的)": "embarrass",
+ "紧张的": "nervous",
+ "捂脸": "facepalm"
+ },
+ "负面情绪表情": {
+ "阿黑颜": "ahegao",
+ "忍耐的表情": "endured face",
+ "忍耐": "restrained",
+ "黑化的": "dark persona",
+ "疯狂的": "crazy",
+ "筋疲力尽的": "exhausted",
+ "傲娇": "Tsundere",
+ "病娇": "yandere",
+ "多重人格": "multiple persona",
+ "颤抖": "trembling",
+ "翻白眼(高潮眼)": "rolling eyes",
+ "嫉妒": "envy",
+ "绝顶": "female orgasm",
+ "重呼吸": "heavy breathing",
+ "淘气": "naughty",
+ "呻吟": "moaning",
+ "嫌弃的眼神": "scowl",
+ "轻蔑": "disdain",
+ "蔑视": "contempt",
+ "脸上有阴影,配合蔑视": "shaded face",
+ "鄙夷的眼神": "jitome",
+ "皱眉/蹙额": "wince",
+ "眉头紧锁": "furrowed brow",
+ "害怕侧目": "fear kubrick",
+ "扬起眉毛": "raised eyebrows",
+ "邪恶的": "evil",
+ "生闷气": "sulking",
+ "尖叫": "screaming",
+ "喊叫": "shouting",
+ "生气的": "angry",
+ "惹恼": "annoy",
+ "怒目而视": "glaring",
+ "严肃的": "serious",
+ "侧头瞪着你": "kubrick stare",
+ "伤心": "sad",
+ "啜泣": "tear",
+ "大哭": "crying",
+ "泪如雨下": "streaming tears",
+ "睁着眼睛哭": "crying with eyes open",
+ "流泪": "streaming tears",
+ "泪珠": "teardrop",
+ "撕破衣服": "tearing clothes",
+ "要哭的表情": "tearing up",
+ "眼泪": "tears",
+ "擦眼泪": "wiping tears",
+ "心情不好": "badmood",
+ "不开心的": "unamused",
+ "沮丧": "frustrated",
+ "沮丧的眉头": "frustrated brow",
+ "苦恼的": "annoyed",
+ "苦闷": "anguish",
+ "叹气": "sigh",
+ "忧郁的": "gloom",
+ "失望的": "disappointed",
+ "绝望": "despair",
+ "疼痛": "pain"
+ }
+}
\ No newline at end of file
diff --git a/js/config/features.js b/js/config/features.js
new file mode 100644
index 0000000..898f015
--- /dev/null
+++ b/js/config/features.js
@@ -0,0 +1,193 @@
+/**
+ * 小助手功能特性管理模块
+ * 负责管理所有功能开关、按钮可见性、功能状态变更等
+ */
+
+import { logger } from '../utils/logger.js';
+
+// 外部注入的 promptAssistant 实例
+let promptAssistant = null;
+// 外部注入的 PromptAssistant 类
+let PromptAssistant = null;
+// 外部注入的 UIToolkit
+let UIToolkit = null;
+// 外部注入的 HistoryCacheService
+let HistoryCacheService = null;
+// 外部注入的 imageCaption 实例
+let imageCaption = null;
+// 外部注入的 ImageCaption 类
+let ImageCaption = null;
+
+/**
+ * 注入依赖实例(由主入口调用)
+ */
+export function setFeatureModuleDeps({ promptAssistant: pa, PromptAssistant: PAC, UIToolkit: ui, HistoryCacheService: hc, imageCaption: ic, ImageCaption: ICC }) {
+ promptAssistant = pa;
+ PromptAssistant = PAC;
+ UIToolkit = ui;
+ HistoryCacheService = hc;
+ imageCaption = ic;
+ ImageCaption = ICC;
+ // 初始化时同步日志级别
+ try {
+ if (typeof window !== 'undefined' && window.FEATURES) {
+ if (typeof window.FEATURES.logLevel === 'undefined') {
+ window.FEATURES.logLevel = 0;
+ }
+ if (typeof logger.setLevel === 'function') {
+ logger.setLevel(window.FEATURES.logLevel);
+ }
+ }
+ } catch (e) { }
+}
+
+/**
+ * 功能特性配置对象
+ * 控制各个功能的启用状态
+ */
+export const FEATURES = {
+ // 基础功能开关
+ enabled: true,
+
+ // 具体功能开关
+ history: true, // 历史功能(包含历史、撤销、重做)
+ tag: true,
+ expand: true,
+ translate: true,
+ imageCaption: true, // 图像反推提示词功能
+
+ /**
+ * 更新所有实例的按钮显示状态
+ * 根据功能开关状态控制UI元素的显示和隐藏
+ */
+ updateButtonsVisibility() {
+ if (!PromptAssistant) return;
+ // 遍历所有助手实例
+ PromptAssistant.instances.forEach((instance) => {
+ if (instance.buttons) {
+ // 历史相关按钮 - 由单一的history开关控制
+ if (instance.buttons['history']) {
+ instance.buttons['history'].style.display = this.history ? 'block' : 'none';
+ }
+ if (instance.buttons['undo']) {
+ instance.buttons['undo'].style.display = this.history ? 'block' : 'none';
+ }
+ if (instance.buttons['redo']) {
+ instance.buttons['redo'].style.display = this.history ? 'block' : 'none';
+ }
+
+ // 分隔线1 - 在历史功能之后
+ if (instance.buttons['divider1']) {
+ const hasHistoryFeature = this.history;
+ const hasOtherFeatures = this.tag || this.expand || this.translate;
+ const showDivider1 = hasHistoryFeature && hasOtherFeatures;
+ instance.buttons['divider1'].style.display = showDivider1 ? 'block' : 'none';
+ }
+
+ // 其他功能按钮
+ if (instance.buttons['tag']) {
+ instance.buttons['tag'].style.display = this.tag ? 'block' : 'none';
+ }
+ if (instance.buttons['expand']) {
+ instance.buttons['expand'].style.display = this.expand ? 'block' : 'none';
+ }
+ if (instance.buttons['translate']) {
+ instance.buttons['translate'].style.display = this.translate ? 'block' : 'none';
+ }
+
+ // 记录日志
+ logger.debug(`按钮更新 | 节点ID: ${instance.nodeId}`);
+ }
+ });
+
+ // 处理图像小助手的按钮显示
+ if (ImageCaption) {
+ ImageCaption.instances.forEach((assistant) => {
+ if (assistant.buttons) {
+ // 图像反推按钮
+ if (assistant.buttons['caption_zh']) {
+ assistant.buttons['caption_zh'].style.display = this.imageCaption ? 'block' : 'none';
+ }
+ if (assistant.buttons['caption_en']) {
+ assistant.buttons['caption_en'].style.display = this.imageCaption ? 'block' : 'none';
+ }
+
+ // 如果图像反推功能被禁用,隐藏整个小助手
+ if (assistant.element) {
+ if (!this.imageCaption) {
+ assistant.element.style.display = 'none';
+ } else if (assistant.isMouseOver || assistant.isFirstCreate || assistant.isActive) {
+ assistant.element.style.display = 'flex';
+ }
+ }
+ }
+ });
+ }
+ }
+};
+
+/**
+ * 处理功能开关状态变化
+ * @param {string} featureName
+ * @param {boolean} value
+ * @param {boolean} oldValue
+ */
+export function handleFeatureChange(featureName, value, oldValue) {
+ if (!PromptAssistant || !promptAssistant) return;
+ // 无论总开关状态如何,功能开关始终独立工作
+ // 如果是从禁用变为启用,需要重新创建按钮
+ if (value && !oldValue) {
+ // 只有当小助手系统已初始化时才重建按钮
+ if (PromptAssistant.instances.size > 0) {
+ // 重新创建所有实例的按钮
+ PromptAssistant.instances.forEach((instance) => {
+ if (instance.element && instance.innerContent) {
+ // 清空现有按钮容器
+ instance.innerContent.innerHTML = '';
+ instance.buttons = {};
+ // 重新创建所有按钮
+ promptAssistant.addFunctionButtons(instance);
+ }
+ });
+ logger.debug(`功能重建 | 结果:完成 | 功能: ${featureName}`);
+ }
+
+ // 如果是图像反推功能被启用
+ if (featureName === '图像反推' && imageCaption) {
+ // 启用图像小助手功能
+ if (imageCaption.initialized) {
+ // 重置节点初始化标记
+ if (app.canvas && app.canvas.graph) {
+ const nodes = app.canvas.graph._nodes || [];
+ nodes.forEach(node => {
+ if (node) {
+ node._imageCaptionInitialized = false;
+ }
+ });
+ }
+
+ // 如果有当前选中的节点,立即处理
+ if (app.canvas && app.canvas.selected_nodes && Object.keys(app.canvas.selected_nodes).length > 0) {
+ app.canvas._imageCaptionSelectionHandler(app.canvas.selected_nodes);
+ }
+ } else {
+ // 如果图像小助手尚未初始化,则初始化它
+ imageCaption.initialize().then(() => {
+ // 初始化完成后处理当前选中的节点
+ if (app.canvas && app.canvas.selected_nodes && Object.keys(app.canvas.selected_nodes).length > 0) {
+ app.canvas._imageCaptionSelectionHandler(app.canvas.selected_nodes);
+ }
+ });
+ }
+ }
+ } else {
+ // 否则只更新显示状态
+ FEATURES.updateButtonsVisibility();
+
+ // 如果是图像反推功能被禁用
+ if (featureName === '图像反推' && !value && imageCaption) {
+ // 清理所有图像小助手实例
+ imageCaption.cleanup();
+ }
+ }
+}
\ No newline at end of file
diff --git a/js/config/system_prompts.json b/js/config/system_prompts.json
new file mode 100644
index 0000000..658e610
--- /dev/null
+++ b/js/config/system_prompts.json
@@ -0,0 +1,26 @@
+{
+ "expand_prompt": {
+ "role": "system",
+ "content": "你是AI绘画提示词优化专家,将用户简单描述转换为完整的专业提示词。\n\n## 核心要素(必须包含)\n1. **主体**:人物/物体的详细特征、动作、表情\n2. **构图**:视角、景深、构图法则 \n3. **环境**:场所、背景、空间氛围\n4. **光影**:光源、色调、明暗效果\n5. **风格**:画风、技法、艺术流派\n6. **质量**:分辨率、细节度、专业术语\n\n## 处理规则\n- **语言匹配**:用户输入中文则输出中文提示词,输入英文则输出英文提示词\n- **智能补充**:自动填补用户遗漏的要素\n- **结构重组**:按\"主体→构图→环境→光影→风格→质量\"顺序排列\n- **画面逻辑**:确保所有要素在视觉上合理协调,避免逻辑矛盾冲突\n- **美感营造**:注重整体画面的艺术美感和情感表达,而非生硬拼接关键词\n\n## 输出格式\n- 用流畅的描述性语言组织提示词,避免生硬的关键词堆砌\n- 重要词汇可加权重:(关键词:1.2)\n- 控制长度在150-200词\n- 创造具有画面感和情感共鸣的描述\n\n## 创作原则\n- **视觉连贯性**:每个要素都应该在同一个画面中和谐存在\n- **情感一致性**:光影、色调、氛围要与主题情感相匹配 \n- **细节丰富性**:添加能增强画面故事性和趣味性的生动细节\n- **艺术性表达**:用诗意而精准的语言描述,而非术语罗列\n\n## 示例\n**输入**:\"一个女孩在看书\"\n**输出**:\"阳光透过古老图书馆的彩色玻璃窗,轻柔地洒在一位沉浸在书本世界中的年轻女性身上,她安静地坐在雕花木椅上,手指轻抚书页,表情专注而宁静,画面采用中景平仰拍,突出人物与书本的互动,周围高耸的书架仿佛知识的守护者在画面两侧形成天然框架,温暖的金色光线在书脊上跳跃,营造出梦幻般的学者氛围,浅景深让背景柔和虚化,细腻的光影层次,如电影般的质感,超高清细节展现每一缕发丝和纸张纹理。\"\n\n现在请处理用户的绘画需求。"
+ },
+ "translate_prompts": {
+ "TO_EN": {
+ "role": "system",
+ "content": "你是一位专业的AI绘画提示词翻译专家,负责将中文提示词准确翻译成英文。\n\n## 翻译原则\n1. **保持专业性**:准确理解和翻译AI绘画领域的专业术语\n2. **维持结构**:保持原文的描述结构和重点强调\n3. **权重保留**:确保保留原文中的权重标记,如(关键词:1.2)\n4. **风格统一**:使用AI绘画社区常用的英文表达方式\n5. **语言地道**:输出自然流畅的英文表达\n\n## 处理规则\n- 保持提示词的逻辑顺序和层次关系\n- 准确翻译特定艺术风格、渲染技术等专业术语\n- 保留原文的修饰语和细节描述\n- 确保权重标记格式统一:(keyword:1.2)\n- 直接输出翻译结果,不需要解释或备注\n\n## 特殊处理\n- 对于中文特有的表达,使用合适的英文替代表达\n- 对于无法直译的文化元素,使用英语世界能理解的相近概念\n- 保持数字、符号等格式的一致性\n\n现在请将用户提供的中文提示词翻译成英文。"
+ },
+ "TO_ZH": {
+ "role": "system",
+ "content": "你是一位专业的AI绘画提示词翻译专家,负责将英文提示词准确翻译成中文。\n\n## 翻译原则\n1. **保持专业性**:准确理解和翻译AI绘画领域的专业术语\n2. **维持结构**:保持原文的描述结构和重点强调\n3. **权重保留**:确保保留原文中的权重标记,如(keyword:1.2)\n4. **风格统一**:使用AI绘画社区常用的中文表达方式\n5. **语言地道**:输出自然流畅的中文表达\n\n## 处理规则\n- 保持提示词的逻辑顺序和层次关系\n- 准确翻译特定艺术风格、渲染技术等专业术语\n- 保留原文的修饰语和细节描述\n- 确保权重标记格式统一:(关键词:1.2)\n- 直接输出翻译结果,不需要解释或备注\n\n## 特殊处理\n- 对于英文特有的表达,使用合适的中文替代表达\n- 对于艺术风格和技术术语,优先使用约定俗成的中文翻译\n- 保持数字、符号等格式的一致性\n\n现在请将用户提供的英文提示词翻译成中文。"
+ }
+ },
+ "vision_prompts": {
+ "ZH": {
+ "role": "system",
+ "content": "请对图像进行详细描述,包括画面类型(写实、卡通、抽象、超现实等)、主要元素(人物、动物、物品、场景)的特征及细节、色彩、构图、光影效果。"
+ },
+ "EN": {
+ "role": "system",
+ "content": "Please provide a detailed description of the image, including the type of imagery (realistic, cartoon, abstract, surreal, etc.), the characteristics and details of the main elements (characters, animals, objects, scenes), colors, composition, and lighting effects."
+ }
+ }
+}
\ No newline at end of file
diff --git a/js/config/tags.json b/js/config/tags.json
new file mode 100644
index 0000000..828ff03
--- /dev/null
+++ b/js/config/tags.json
@@ -0,0 +1,1920 @@
+{
+ "常规标签": {
+ "画质": {
+ "杰作": "masterpiece",
+ "写实": "realistic",
+ "提高质量": "HDR,UHD,8K",
+ "最佳质量": "best quality",
+ "高分辨率": "highres",
+ "超高分辨率": "absurdres",
+ "超高清": "ultra-highres",
+ "更多细节": "Highly detailed",
+ "简单背景": "simple background",
+ "模糊背景": "blurry background",
+ "清晰背景": "clear background",
+ "清晰细节": "clear details",
+ "超精细绘画": "ultra-fine painting",
+ "聚焦清晰": "sharp focus",
+ "物理渲染": "physically-based rendering",
+ "极详细刻画": "extreme detail description",
+ "改善细节": "Professional",
+ "添加鲜艳色彩": "Vivid Colors",
+ "扫描": "High resolution scan"
+ },
+ "负面标签": {
+ "基础负面": "low resolution,worst quality, low quality, normal quality, lowres,jpeg, cropped, bad composition, artifacts bad proportions, error, inaccurate limb, bad anatomy, bad, ugly, terrible, extra fingers, fewer fingers, missing fingers, extra arms, extra legs, inaccurate eyes,extra digit, fewer digits, trademark, artist's name, username, watermarksignature, watermark,text, words",
+ "低分辨率": "lowres",
+ "NSFW": "nsfw",
+ "embeddings": "BadDream,badhandv4,EasyNegative,ng_deepnegative_v1_75t,rev2-badprompt,verybadimagenegative_v1.3,negative_hand-neg,bad-picture-chill-75v",
+ "变异手指": "mutated hands and fingers",
+ "畸形的": "deformed",
+ "解剖不良": "bad anatomy",
+ "毁容": "disfigured",
+ "脸不好": "poorly drawn face",
+ "变异的": "mutated",
+ "多余肢体": "extra limb",
+ "丑陋": "ugly",
+ "手画得差": "poorly drawn hands",
+ "缺少的肢体": "missing limb",
+ "漂浮的四肢": "floating limbs",
+ "肢体不连贯": "disconnected limbs",
+ "畸形的手": "malformed hands",
+ "脱离焦点": "out of focus",
+ "长颈": "long neck",
+ "身体长": "long body",
+ "张嘴": "gape"
+ },
+ "摄影": {
+ "相机设置": "(EOS R8,50mm,F1.2,8K,RAW photo",
+ "散景": "Bokeh",
+ "景深": "depth of field",
+ "广角": "wide angle",
+ "微距": "macro",
+ "大光圈": "wide aperture"
+ },
+ "光影": {
+ "演播室灯光": "Studio lighting",
+ "电影光效": "cinematic_lighting",
+ "镜头光晕": "lens_flare",
+ "光追": "reality_ray_tracing",
+ "正面光": "frontlight",
+ "侧面光": "sidelight",
+ "背光": "backlight",
+ "逆光": "backlighting",
+ "边缘光": "rim_light",
+ "强边缘光": "strong_rim_light",
+ "自上而下的光线": "god_rays",
+ "明亮的射线光束": "light_rays",
+ "环境光": "ambient_light",
+ "轮廓光": "rim light",
+ "体积光": "Volumetric Lighting",
+ "霓虹灯": "glowing neon lights",
+ "透镜光晕": "lens flare",
+ "金属光泽": "metallic luster",
+ "氛围光照": "moody lighting",
+ "丁达尔效应": "Tyndall effect",
+ "漏光光效": "light leaks",
+ "背景光": "background light",
+ "自然光": "available light"
+ },
+ "构图": {
+ "全身": "full body",
+ "半身像": "portrait",
+ "上半身": "upper body",
+ "正面视角": "front view",
+ "侧面视角": "from_side",
+ "从上方往下看": "from_above",
+ "从下方往上看": "from_below",
+ "左侧": "left side view",
+ "右侧": "right side view",
+ "动态角度": "dynamic angle",
+ "倾斜视角": "dutch angle",
+ "特写": "close-up",
+ "微距": "macro_shot",
+ "近景": "selfie",
+ "中景": " medium view",
+ "中长镜头": "medium long shot",
+ "远景": "wide shot",
+ "全景": "panorama",
+ "近拍": "close shot",
+ "俯视": "vertical view",
+ "鸟瞰": "bird's-eye view",
+ "鱼眼镜头": "fisheye",
+ "多视角分解": "multiple_views",
+ "裙底视角": "phone_screen"
+ }
+ },
+ "艺术题材": {
+ "表现形式": {
+ "动漫": "Anime",
+ "Q版": "Chibi",
+ "摄影": "Photographic",
+ "数字艺术": "Digital Art",
+ "连环漫画": "Comic Book",
+ "四格漫画": "4koma",
+ "幻想艺术": "Fantasy Art"
+ },
+ "绘画技法": {
+ "素描": "Sketch",
+ "漫画": "Comic",
+ "厚涂": "Impasto",
+ "油画": "Oil Painting",
+ "水彩": "Watercolor",
+ "国风水墨": "Chinese Monochrome Ink",
+ "剪影": "Silhouette",
+ "马克笔": "Marker",
+ "超写实": "Photorealism"
+ },
+ "技术风格": {
+ "像素艺术": "Pixel Art",
+ "低多边形": "Low Poly",
+ "等距艺术": "Isometric",
+ "线条艺术": "Line Art",
+ "故障艺术": "Glitch Art",
+ "气泡艺术": "Bubble Art",
+ "玻璃拟态": "Glassmorphism",
+ "新拟物风格": "Neumorphism",
+ "3D插画": "3D Graphics"
+ },
+ "艺术家风格": {
+ "宫崎骏风格": "Hayao Miyazaki Style",
+ "蒂姆伯顿风格": "Tim Burton Style"
+ },
+ "艺术流派": {
+ "赛博朋克": "Cyberpunk",
+ "霓虹朋克": "Neon Punk",
+ "蒸汽波": "Vaporwave",
+ "迷幻艺术": "Psychedelia Art",
+ "酸性风格": "Acid Graphics",
+ "极简主义": "Minimalism",
+ "构成主义": "Constructivism",
+ "孟菲斯风格": "Memphis"
+ },
+ "媒介与效果": {
+ "胶片滤镜": "Analog Film",
+ "电影质感": "Cinematic",
+ "胶片电影": "Celluloid",
+ "复古风格": "Retro Style"
+ },
+ "载体与应用": {
+ "杂志封面": "Magazine Cover",
+ "海报": "Poster",
+ "拍立得": "Polaroid"
+ }
+ },
+ "人物类": {
+ "人设": {
+ "职业": {
+ "公主": "princess",
+ "舞者": "dancer",
+ "啦啦队": "cheerleader",
+ "芭蕾舞女演员": "ballerina",
+ "体操队队长": "gym leader",
+ "女服务员": "waitress",
+ "和风女仆": "wa maid",
+ "女仆": "maid",
+ "偶像": "idol",
+ "弓道": "kyuudou",
+ "女武神": "valkyrie",
+ "办公室小姐": "office lady",
+ "赛车女郎": "race queen",
+ "魔女": "Witch",
+ "巫女": "miko",
+ "修女": "nun",
+ "牧师": "priest",
+ "神职人员(基督教)": "cleric",
+ "忍者": "ninja",
+ "女警": "policewoman",
+ "警察": "police",
+ "医生": "doctor",
+ "护士": "nurse",
+ "眼镜娘": "glasses",
+ "狐狸精": "kitsune",
+ "公交车": "public use",
+ "女王(SM中)": "dominatrix",
+ "机娘": "mecha musume",
+ "另一种机娘": "gynoid",
+ "类人机器人": "humanoid robot",
+ "半机械人": "cyborg"
+ },
+ "性别/年龄": {
+ "单人": "solo",
+ "女人": "female",
+ "男人": "male",
+ "性转": "genderswap",
+ "扶她": "futanari",
+ "伪娘": "otoko no ko",
+ "儿童": "child",
+ "未成年": "underage",
+ "年轻": "young",
+ "熟女": "mature female",
+ "老年": "old"
+ },
+ "胸部": {
+ "胸": "chest",
+ "胸肌": "pectorals",
+ "大胸肌": "large pectorals",
+ "小胸部(B": "small breasts",
+ "中等胸部(C": "medium breasts",
+ "大胸部(D": "big breasts",
+ "巨乳(E": "huge breasts",
+ "魔乳(F": "gigantic breasts",
+ "未扣扣子": "unbuttoned clothes"
+ },
+ "脸型": {
+ "瓜子脸": "heart-shaped face",
+ "鹅蛋脸": "oval face",
+ "国字脸": "square face",
+ "可爱脸": "cute face",
+ "菱形脸": "rhomboid face",
+ "圆脸": "round face",
+ "长脸": "long face",
+ "瘦脸": "Thin face",
+ "平脸": "Flat face",
+ "柔和的脸": "Soft face",
+ "棱角分明的脸": "Angular face"
+ },
+ "鼻子": {
+ "蒜头鼻": "pug nose",
+ "肉鼻子": "dumpy nose",
+ "朝天鼻": "upturned nose",
+ "扁鼻": "flat nose",
+ "鹰勾鼻": "aquliline nose",
+ "罗马鼻": "Roman nose",
+ "翘鼻": "Snub nose",
+ "长鼻": "Long nose",
+ "短鼻": "Short nose"
+ },
+ "嘴巴": {
+ "樱桃小嘴": "Cupid mouth",
+ "心形嘴": "Heart-shaped mouth",
+ "薄唇": "Thin mouth",
+ "丰唇": "Full lips",
+ "嘟嘴": "Pouty lips",
+ "方唇": "Square lips",
+ "上扬唇": "Up-turned lips",
+ "下撇唇": "Down-turned lips"
+ },
+ "皮肤": {
+ "有光泽的皮肤": "shiny skin",
+ "苍白皮肤": "pale skin",
+ "白皙皮肤": "white skin",
+ "棕色皮肤": "brown skin",
+ "深色皮肤": "deep skin",
+ "偏黑皮肤": "black skin",
+ "晒日线": "tan lines",
+ "泳装晒痕": "pang",
+ "如白色大理石般有光泽的肌肤": "white marble glowing skin"
+ },
+ "体型": {
+ "模特": "model",
+ "苗条": "slender",
+ "诱惑": "glamor",
+ "丰满": "plump",
+ "肥胖": "fat",
+ "高大": "tall",
+ "纤弱": "petite",
+ "肌肉": "muscular",
+ "细腰": "narrow waist",
+ "宽臀": "wide hips"
+ },
+ "眉毛": {
+ "浓眉": "thick eyebrows",
+ "眉毛翘起": "cocked eyebrow",
+ "短眉毛": "short eyebrows",
+ "V字眉": "v-shaped eyebrows",
+ "剑眉": "dashing eyebrows",
+ "一字眉": "straight united eyebrows ",
+ "柳叶眉": "arched eyebrows",
+ "新月眉": "crescent eyebrows",
+ "描画过的眉毛": "pencilled eyebrows ",
+ "浓密而粗短的眉毛": "bushy and stubby eyebrows"
+ },
+ "头发": {
+ "颜色": {
+ "绿头发": "green hair",
+ "粉色头发": "pink hair",
+ "红头发": "red hair",
+ "铂金色头发": "platinum blonde hair",
+ "青色头发": "azure hair",
+ "水蓝色头发": "aqua hair",
+ "红宝石色头发": "ruby hair",
+ "两色头发": "two-tone hair",
+ "多色的头发": "multicolored hair",
+ "渐变头发": "gradient hair",
+ "分色头发": "split-color hair",
+ "彩虹头发": "rainbow hair"
+ },
+ "长度": {
+ "长发": "long hair",
+ "很短的头发": "very short hair",
+ "短发": "short hair",
+ "后短发,前长发": "short hair with long locks",
+ "中等头发": "medium hair",
+ "很长的头发": "very long hair",
+ "超级长的头发": "absurdly long hair"
+ },
+ "马尾": {
+ "马尾": "ponytail",
+ "双马尾": "twintails",
+ "高双马尾": "canonicals",
+ "低双马尾": "low twintails",
+ "披肩单马尾": "one side up",
+ "披肩双马尾": "two side up",
+ "短马尾": "short ponytail",
+ "侧马尾": "side ponytail"
+ },
+ "辫子": {
+ "辫子": "braid",
+ "法式辫子": "french braid",
+ "辫子头发": "braiding hair",
+ "双辫子": "twin braids",
+ "三股辫": "braid",
+ "短辫子": "short braid",
+ "长辫子": "long braid",
+ "辫子刘海": "braided bangs",
+ "辫式发髻": "braided bun",
+ "麻花辫马尾": "braided ponytail",
+ "法式冠编发": "crown braid",
+ "多股(麻花)辫": "multiple braids",
+ "披在一侧的单条辫": "side braid",
+ "披在两侧的两条辫": "side braids",
+ "单股辫": "single braid",
+ "两条辫子": "twin braids"
+ },
+ "刘海": {
+ "进气口发型": "hair intakes",
+ "发瓣": "hair flaps",
+ "刘海": "bangs",
+ "空气刘海": "air bangs",
+ "齐刘海": "blunt bangs",
+ "侧面空气刘海": "side blunt bangs",
+ "中分刘海": "parted bangs",
+ "斜刘海": "swept bangs",
+ "不对称刘海": "asymmetric bangs"
+ },
+ "其他发型": {
+ "眼睛之间的头发": "hair between eyes",
+ "头发覆盖一只眼": "hair over one eye",
+ "头发遮住眉毛": "hair over one eyebrow",
+ "透过头发可见腮红": "blush visible through hair",
+ "透过头发可见眼睛": "eyes visible through hair",
+ "头发撩到耳后": "hair behind ear",
+ "披肩发": "hair over shoulder",
+ "头发遮住三点": "hair censor",
+ "头发披在胸上": "hair over breasts",
+ "直发": "straight hair",
+ "卷发": "curly hair",
+ "波浪卷": "wavy hair",
+ "钻头(配双)": "drill hair",
+ "姬发式(齐刘海后长黑发": "hime cut",
+ "齐而短头发(波波发": "bob cut",
+ "公主发型": "princess head",
+ "上半部分束起": "Half-up",
+ "额头": "forehead",
+ "扎过的头发": "tied hair",
+ "低扎头发": "low tied hair",
+ "多扎头发": "multi-tied hair",
+ "丸子头": "double bun",
+ "圆发髻": "hair bun",
+ "芭蕾髻": "ballet hair bun",
+ "尖头头发": "pointy hair",
+ "羽毛头发": "feather hair",
+ "弓形头发": "bow-shaped hair",
+ "孤颈头发": "lone nape hair",
+ "变换发型": "alternate hairstyle",
+ "与原设不同头发长度": "alternate hair length",
+ "鬓角": "sideburns",
+ "长鬓角": "long sideburns",
+ "侧边发辫": "sidelocks",
+ "秃头": "bald",
+ "鸟窝头|爆炸头": "afro",
+ "尖刺的头发": "spiked hair"
+ }
+ },
+ "眼睛相关": {
+ "眼袋": "bags under eyes",
+ "缠着绷带的单眼": "bandage over one eye",
+ "遮眼|眼罩": "blindfold",
+ "眼罩": "eyepatch",
+ "眼影": "eyeshadow",
+ "医用眼罩": "medical eyepatch",
+ "眼睛上的疤痕": "scar across eye",
+ "闭眼": "eyes closed",
+ "半闭双眼": "half closed eyes",
+ "眯起眼睛": "narrowed eyes",
+ "拉下眼睑的鬼脸": "akanbe",
+ "睁大眼睛": "wide eyed",
+ "一只眼睛闭着": "one eye closed",
+ "蒙眼": "blindfold",
+ "眨眼": "wink",
+ "失去高光的眼睛": "empty eyes",
+ "翻白眼": "rolling eyes",
+ "眼泪": "tears",
+ "锐利的眼": "slanted eyes",
+ "低眉顺眼": "tareme",
+ "上翘的眼睛": "upturned eyes"
+ },
+ "瞳孔": {
+ "瞳孔": "pupils",
+ "明亮的瞳孔": "bright pupils",
+ "异色瞳": "heterochromia",
+ "竖的瞳孔/猫眼": "slit pupils",
+ "蛇瞳孔": "snake pupils",
+ "瞳孔闪光": "pupils sparkling",
+ "爱心形瞳孔": "heart shaped pupils",
+ "钻石形状瞳孔": "diamond shaped pupils",
+ "五角星形状瞳孔": "star shaped pupils",
+ "瞳孔散大": "dilated pupils",
+ "没有瞳孔": "no pupils",
+ "缩瞳": "constricted pupils",
+ "眼睛里的星星": "star in eye",
+ "星形瞳孔": "star shaped pupils",
+ "X形瞳孔": "x shaped pupils",
+ "水平瞳孔": "horizontal pupils",
+ "虚线的眼睛": "dashed eyes",
+ "蝴蝶形瞳孔": "butterfly shaped pupils",
+ "菱形瞳孔": "diamond shaped pupils",
+ "长方形瞳孔": "rectangular pupils",
+ "方形瞳孔": "square pupils",
+ "点瞳孔": "dot pupils",
+ "额外的瞳孔": "extra pupils",
+ "十字形瞳孔": "cross shaped pupils",
+ "符号形瞳孔": "symbol shaped pupils"
+ }
+ },
+ "服饰": {
+ "服装": {
+ "上装": {
+ "连衣裙": "dress",
+ "裙子": "skirt",
+ "衬衫": "blouse",
+ "t 恤": "t-shirt",
+ "毛衣": "sweater",
+ "夹克": "jacket",
+ "背心": "tank top",
+ "卫衣": "hoodie",
+ "马甲": "vest",
+ "风衣": "trench coat",
+ "西装": "suit",
+ "骑士夹克": "biker jacket",
+ "外套": "coat",
+ "罩衫": "shawl",
+ "披肩": "cape",
+ "庞乔斗篷": "poncho",
+ "大衣": "overcoat",
+ "毛皮大衣": "fur coat",
+ "斗篷": "cape",
+ "披风": "cloak",
+ "皮衣": "leather jacket"
+ },
+ "下装": {
+ "牛仔裤": "jeans",
+ "裤子": "pants",
+ "连体裤": "jumpsuit",
+ "短裤": "shorts",
+ "长裤": "trousers",
+ "裙子": "skirt",
+ "打底裤": "leggings",
+ "紧身裤": "leggings",
+ "便裤": "slacks",
+ "阔腿裤": "wide-leg pants",
+ "七分裤": "capri pants",
+ "短裙": "mini skirt",
+ "中裙": "midi skirt",
+ "长裙": "maxi skirt",
+ "运动裤": "sweatpants",
+ "卡其裤": "khaki pants",
+ "牛仔短裤": "denim shorts",
+ "裙裤": "skort",
+ "束脚裤": "joggers",
+ "瑜伽裤": "yoga pants",
+ "哈伦裤": "harem pants"
+ },
+ "泳装": {
+ "泳衣": "swimsuit",
+ "比基尼": "bikini",
+ "连体泳衣": "one-piece swimsuit",
+ "泳裙": "swim skirt",
+ "沙滩裙": "beach cover-up",
+ "遮阳衣": "rash guard",
+ "泳裤": "swim shorts",
+ "泳帽": "swim cap",
+ "浮潜套装": "snorkel set",
+ "潜水衣": "wetsuit",
+ "潜水裤": "wetsuit pants",
+ "冲浪裤": "boardshorts"
+ },
+ "运动装": {
+ "运动文胸": "sports bra",
+ "运动上衣": "sports top",
+ "运动裤": "sports pants",
+ "运动短裤": "athletic shorts",
+ "运动紧身衣": "compression suit",
+ "运动套装": "athletic set",
+ "运动连衣裙": "athletic dress",
+ "健身服": "gym wear",
+ "跑步裤": "running pants",
+ "篮球裤": "basketball shorts",
+ "网球裤": "tennis shorts",
+ "足球裤": "soccer shorts",
+ "高尔夫服": "golf attire",
+ "瑜伽服": "yoga wear",
+ "太极服": "tai chi suit",
+ "拳击装": "boxing attire",
+ "羽毛球服": "badminton attire",
+ "游泳裤": "swim trunks",
+ "健身紧身衣": "fitness tights",
+ "长跑服": "marathon suit"
+ },
+ "内衣": {
+ "文胸": "bra",
+ "内裤": "panties",
+ "丁字裤": "thong",
+ "连体内衣": "bodysuit",
+ "睡衣": "pajamas",
+ "底裤": "long johns",
+ "吊袜带": "garter belt",
+ "情趣内衣": "lingerie",
+ "背心": "camisole",
+ "泳衣内衣": "swimwear bra",
+ "塑身内衣": "shapewear",
+ "胸衣": "bustier",
+ "纹身文胸": "tattoo bra",
+ "丰胸垫": "push-up pad",
+ "吊带内衣": "strapless bra",
+ "无肩带文胸": "convertible bra",
+ "无线文胸": "wireless bra",
+ "无痕内衣": "seamless underwear",
+ "泳装内衣": "swimwear bra",
+ "小胸文胸": "small cup bra",
+ "大胸文胸": "large cup bra"
+ },
+ "配饰": {
+ "项链": "necklace",
+ "耳环": "earrings",
+ "手链": "bracelet",
+ "戒指": "ring",
+ "围巾": "scarf",
+ "帽子": "hat",
+ "手套": "gloves",
+ "皮带": "belt",
+ "太阳镜": "sunglasses",
+ "包包": "handbag",
+ "披肩": "shawl",
+ "发夹": "hairpin",
+ "发带": "headband",
+ "项链套装": "necklace set",
+ "手表": "watch",
+ "腰链": "waist chain",
+ "胸针": "brooch",
+ "手镯": "bangle",
+ "项链链子": "chain necklace",
+ "胸花": "corsage",
+ "围巾固定器": "scarf clip"
+ },
+ "鞋类": {
+ "高跟鞋": "high heels",
+ "运动鞋": "sneakers",
+ "平底鞋": "flats",
+ "靴子": "boots",
+ "凉鞋": "sandals",
+ "拖鞋": "slippers",
+ "尖头鞋": "pointed-toe shoes",
+ "帆布鞋": "canvas shoes",
+ "平底便鞋": "loafers",
+ "牛仔鞋": "cowboy boots",
+ "坡跟鞋": "wedges",
+ "男鞋": "oxfords",
+ "马丁靴": "ankle boots",
+ "水鞋": "water shoes",
+ "人字拖": "flip-flops",
+ "木底拖鞋": "clogs",
+ "运动凉鞋": "sport sandals",
+ "豆豆鞋": "penny loafers"
+ },
+ "睡衣": {
+ "睡衣": "pajamas",
+ "睡袍": "robe",
+ "睡裙": "nightgown",
+ "睡袍套装": "robe set",
+ "家居服": "loungewear",
+ "居家裤": "lounging pants",
+ "睡裤": "sleep shorts",
+ "睡裙套装": "nightgown set",
+ "短睡裤套装": "sleep shorts set",
+ "居家内衣": "lounging lingerie",
+ "睡裤套装": "sleep shorts set",
+ "夜袍": "night robe",
+ "夜裙": "evening dress"
+ },
+ "帽子": {
+ "帽子": "hat",
+ "棒球帽": "baseball cap",
+ "遮阳帽": "sun hat",
+ "渔夫帽": "bucket hat",
+ "毛线帽": "beanie",
+ "礼帽": "top hat",
+ "鸭舌帽": "duckbill cap",
+ "贝雷帽": "beret",
+ "军帽": "military cap",
+ "斗篷": "cape",
+ "围巾": "scarf",
+ "披肩": "shawl",
+ "发带": "headband",
+ "发夹": "hairpin",
+ "草帽": "straw hat",
+ "兔毛帽": "fur hat",
+ "军官帽": "officer's cap",
+ "帽套": "hat cover",
+ "卡其帽": "newsboy cap",
+ "洪堡帽": "homburg hat",
+ "英式帽": "bowler hat",
+ "圆顶礼帽": "derby hat"
+ },
+ "连袜裤": {
+ "连袜裤": "tights",
+ "裤袜": "pantyhose",
+ "厚连裤袜": "opaque tights",
+ "薄连裤袜": "sheer tights",
+ "蕾丝连袜裤": "lace tights",
+ "鱼网袜": "fishnet tights",
+ "中筒袜": "knee-high socks",
+ "长筒袜": "thigh-high socks",
+ "踝筒袜": "ankle socks",
+ "脚趾袜": "toe socks",
+ "冬季连袜裤": "winter tights",
+ "体育袜": "sport socks",
+ "格子连袜裤": "plaid tights",
+ "格子袜": "argyle socks",
+ "袜套": "sock covers",
+ "睡袜": "sleep socks",
+ "美腿袜": "leggings"
+ },
+ "围巾": {
+ "围巾": "scarf",
+ "丝巾": "silk scarf",
+ "披肩": "shawl",
+ "围巾固定器": "scarf clip",
+ "领巾": "neckerchief",
+ "围巾环": "scarf ring",
+ "围脖": "snood",
+ "飘带": "streamer",
+ "绒线围巾": "knit scarf",
+ "披风": "cape",
+ "莱卡围巾": "lycra scarf",
+ "领巾固定器": "neckerchief clip",
+ "鸟巾": "boa",
+ "围巾链子": "scarf chain"
+ },
+ "丝袜": {
+ "丝袜连裤袜": "pantyhose",
+ "网纹丝袜": "fishnet stockings",
+ "压力丝袜": "compression stockings",
+ "无缝丝袜": "seamless stockings",
+ "花边丝袜": "lace stockings",
+ "大网格丝袜": "large fishnet stockings",
+ "薄款丝袜": "sheer stockings",
+ "加强型丝袜": "reinforced stockings",
+ "超薄丝袜": "ultra-sheer stockings",
+ "丝袜袜子": "stocking socks",
+ "丝袜裤袜": "tights",
+ "蕾丝丝袜": "lace tights",
+ "鱼网丝袜": "fishnet tights",
+ "肉色丝袜": "nude stockings",
+ "彩色丝袜": "colored stockings",
+ "黑色丝袜": "black stockings",
+ "白色丝袜": "white stockings",
+ "红色丝袜": "red stockings",
+ "粉色丝袜": "pink stockings",
+ "紫色丝袜": "purple stockings",
+ "蓝色丝袜": "blue stockings",
+ "绿色丝袜": "green stockings",
+ "黄色丝袜": "yellow stockings",
+ "灰色丝袜": "gray stockings",
+ "褐色丝袜": "brown stockings",
+ "条纹丝袜": "striped stockings",
+ "格子丝袜": "checkered stockings",
+ "花卉丝袜": "floral stockings",
+ "圆点丝袜": "polka dot stockings",
+ "复古丝袜": "vintage stockings",
+ "半透明丝袜": "semi-opaque stockings",
+ "珠光丝袜": "shimmer stockings",
+ "亮片丝袜": "glitter stockings",
+ "珍珠丝袜": "pearl stockings",
+ "水钻丝袜": "rhinestone stockings",
+ "厚款丝袜": "opaque stockings",
+ "坎肩丝袜": "cowl neck stockings",
+ "连体丝袜": "bodystocking",
+ "性感丝袜": "sexy stockings",
+ "丝袜内衣套装": "lingerie and stockings set",
+ "高腰丝袜": "high-waisted stockings",
+ "丝袜配饰": "stocking accessories",
+ "网纹袜套": "fishnet leg warmers",
+ "丝袜腿套": "stocking leg warmers",
+ "丝袜吊带": "stocking garters"
+ },
+ "深V": {
+ "深V领连衣裙": "deep v-neck dress",
+ "深V领毛衣": "deep v-neck sweater",
+ "深V领上衣": "deep v-neck top",
+ "深V领连体裤": "deep v-neck jumpsuit",
+ "深V领紧身衣": "deep v-neck bodysuit",
+ "深V领背心": "deep v-neck camisole",
+ "深V领吊带连衣裙": "deep v-neck halter dress",
+ "深V领连身短裤": "deep v-neck romper",
+ "深V领礼服连衣裙": "deep v-neck evening gown",
+ "深V领薄款针织衫": "deep v-neck lightweight knit sweater",
+ "深V领运动上衣": "deep v-neck athletic top",
+ "深V领丝绒连衣裙": "deep v-neck velvet dress",
+ "深V领夏季连衣裙": "deep v-neck summer dress",
+ "深V领短袖衬衫": "deep v-neck short sleeve shirt",
+ "深V领包裹裙": "deep v-neck bodycon skirt",
+ "深V领流苏裙": "deep v-neck fringe skirt",
+ "深V领蕾丝连衣裙": "deep v-neck lace dress",
+ "深V领连衣裙套装": "deep v-neck dress set",
+ "深V领吊带上衣": "deep v-neck halter top",
+ "深V领连衣裙配饰": "deep v-neck dress accessories",
+ "深V领连衣裙外套": "deep v-neck dress coat",
+ "深V领连衣裙帽衫": "deep v-neck dress hooded sweater",
+ "深V领拼接连衣裙": "deep v-neck patchwork dress",
+ "深V领卫衣连衣裙": "deep v-neck hoodie dress",
+ "深V领夜店装": "deep v-neck clubwear",
+ "深V领网纱连衣裙": "deep v-neck mesh dress",
+ "深V领迷彩连衣裙": "deep v-neck camouflage dress",
+ "深V领蕾丝上衣": "deep v-neck lace top",
+ "深V领透明连衣裙": "deep v-neck sheer dress",
+ "深V领复古连衣裙": "deep v-neck vintage dress",
+ "深V领长款连衣裙": "deep v-neck long dress",
+ "深V领迷你连衣裙": "deep v-neck mini dress",
+ "深V领宴会装": "deep v-neck cocktail dress",
+ "深V领无袖连衣裙": "deep v-neck sleeveless dress",
+ "深V领绒线连衣裙": "deep v-neck knit dress",
+ "深V领运动连衣裙": "deep v-neck athletic dress",
+ "深V领修身连衣裙": "deep v-neck fitted dress",
+ "深V领皮质连衣裙": "deep v-neck leather dress",
+ "深V领连衣裙裙子": "deep v-neck dress skirt",
+ "深V领裹胸连衣裙": "deep v-neck wrap dress",
+ "深V领露肩连衣裙": "deep v-neck off-shoulder dress",
+ "深V领背带连衣裙": "deep v-neck suspender dress",
+ "深V领夏日连衣裙": "deep v-neck summer dress",
+ "深V领金属纹裙": "deep v-neck metallic skirt",
+ "深V领高腰连衣裙": "deep v-neck high-waisted dress",
+ "深V领包裹上衣": "deep v-neck bodycon top"
+ },
+ "包臀": {
+ "包臀连衣裙": "bodycon dress",
+ "包臀裙": "bodycon skirt",
+ "包臀上衣": "bodycon top",
+ "包臀连体裤": "bodycon jumpsuit",
+ "包臀紧身裙": "bodycon skirt",
+ "包臀礼服": "bodycon gown",
+ "包臀针织裙": "knit bodycon dress",
+ "包臀运动短裤": "sports booty shorts",
+ "包臀派对服": "party bodycon dress",
+ "包臀露背连衣裙": "backless bodycon dress",
+ "包臀剧院装": "theater bodycon dress",
+ "包臀半身裙": "mini bodycon skirt",
+ "包臀连衣裙套装": "bodycon dress set",
+ "包臀纹理裙": "textured bodycon skirt",
+ "包臀夏季连衣裙": "summer bodycon dress",
+ "包臀针织上衣": "knit bodycon top",
+ "包臀夏日裙子": "summer bodycon dress",
+ "包臀毛织裙": "knit bodycon skirt",
+ "包臀花纹裙": "patterned bodycon dress",
+ "包臀网纱连衣裙": "mesh bodycon dress",
+ "包臀迷你连衣裙": "mini bodycon dress",
+ "包臀薄款上衣": "lightweight bodycon top",
+ "包臀复古连衣裙": "vintage bodycon dress",
+ "包臀夜店装": "clubwear bodycon dress",
+ "包臀蕾丝裙": "lace bodycon skirt",
+ "包臀宴会装": "cocktail bodycon dress",
+ "包臀皮质短裙": "leather bodycon skirt",
+ "包臀纯色连衣裙": "solid color bodycon dress",
+ "包臀迷你半身裙": "mini bodycon skirt",
+ "包臀吊带连衣裙": "strapless bodycon dress",
+ "包臀连衣裙配饰": "bodycon dress accessories",
+ "包臀高腰裙": "high-waisted bodycon skirt",
+ "包臀透明连衣裙": "sheer bodycon dress",
+ "包臀金属纹裙": "metallic bodycon skirt",
+ "包臀卫衣连衣裙": "hooded bodycon dress",
+ "包臀深V领连衣裙": "deep v-neck bodycon dress",
+ "包臀丝绒连衣裙": "velvet bodycon dress",
+ "包臀迷你连衣裙套装": "mini bodycon dress set",
+ "包臀牛仔短裙": "denim bodycon skirt",
+ "包臀低背连衣裙": "low-back bodycon dress",
+ "包臀修身连衣裙": "fitted bodycon dress",
+ "包臀雪纺连衣裙": "chiffon bodycon dress",
+ "包臀长款连衣裙": "long bodycon dress",
+ "包臀背心上衣": "sleeveless bodycon top",
+ "包臀无袖连衣裙": "sleeveless bodycon dress",
+ "包臀蕾丝连衣裙": "lace bodycon dress",
+ "包臀紧身衬衫": "bodycon shirt",
+ "包臀绒线连衣裙": "knit bodycon dress",
+ "包臀薄纱连衣裙": "chiffon bodycon dress",
+ "包臀高领连衣裙": "high-neck bodycon dress",
+ "包臀纯色半身裙": "solid color bodycon skirt"
+ },
+ "蕾丝": {
+ "蕾丝连衣裙": "lace dress",
+ "蕾丝上衣": "lace top",
+ "蕾丝裙子": "lace skirt",
+ "蕾丝连体裤": "lace jumpsuit",
+ "蕾丝裤子": "lace pants",
+ "蕾丝外套": "lace jacket",
+ "蕾丝背心": "lace camisole",
+ "蕾丝内衣": "lace lingerie",
+ "蕾丝袜子": "lace stockings",
+ "蕾丝长袜": "lace thigh-high socks",
+ "蕾丝长袖上衣": "lace long-sleeve top",
+ "蕾丝吊带上衣": "lace camisole top",
+ "蕾丝连衣裙配饰": "lace dress accessories",
+ "蕾丝短袖上衣": "lace short-sleeve top",
+ "蕾丝披肩": "lace shawl",
+ "蕾丝长袖裙": "lace long-sleeve dress",
+ "蕾丝袖套": "lace sleeves",
+ "蕾丝贴边裙": "lace-edged skirt",
+ "蕾丝花边上衣": "lace trim top",
+ "蕾丝贴边连衣裙": "lace-edged dress",
+ "蕾丝夏季裙": "lace summer dress",
+ "蕾丝春季外套": "lace spring jacket",
+ "蕾丝晚礼服": "lace evening gown",
+ "蕾丝迷你裙": "lace mini skirt",
+ "蕾丝修身裙": "lace bodycon skirt",
+ "蕾丝冬季外套": "lace winter jacket",
+ "蕾丝紧身上衣": "lace fitted top",
+ "蕾丝包臀连衣裙": "lace bodycon dress",
+ "蕾丝裙裤套装": "lace skirt and pants set",
+ "蕾丝夜店装": "lace clubwear",
+ "蕾丝流苏裙": "lace fringe skirt",
+ "蕾丝露背连衣裙": "lace backless dress",
+ "蕾丝深V领上衣": "lace deep v-neck top",
+ "蕾丝高腰裙": "lace high-waisted skirt",
+ "蕾丝透明连衣裙": "lace sheer dress",
+ "蕾丝圆领上衣": "lace round neck top",
+ "蕾丝半袖连衣裙": "lace short-sleeve dress",
+ "蕾丝花边连衣裙": "lace trim dress",
+ "蕾丝低背连衣裙": "lace low-back dress",
+ "蕾丝包臀上衣": "lace bodycon top",
+ "蕾丝薄纱连衣裙": "lace chiffon dress",
+ "蕾丝针织裙": "lace knit skirt",
+ "蕾丝连衣裙外套": "lace dress coat",
+ "蕾丝背心上衣": "lace camisole top"
+ },
+ "裙子": {
+ "连衣裙": "dress",
+ "迷你裙": "miniskirt",
+ "中裙": "midi skirt",
+ "长裙": "maxi skirt",
+ "半身裙": "skirt",
+ "包臀裙": "bodycon skirt",
+ "百褶裙": "pleated skirt",
+ "牛仔裙": "denim skirt",
+ "礼服裙": "gown",
+ "夏季裙": "summer dress",
+ "冬季裙": "winter dress",
+ "花卉裙": "floral dress",
+ "印花裙": "printed dress",
+ "纱裙": "chiffon dress",
+ "针织裙": "knit skirt",
+ "派对裙": "party dress",
+ "背心裙": "camisole dress",
+ "吊带裙": "halter dress",
+ "圆领裙": "round neck dress",
+ "蕾丝裙": "lace dress",
+ "修身裙": "fitted skirt",
+ "流苏裙": "fringe skirt",
+ "包裹裙": "wrap dress",
+ "无袖裙": "sleeveless dress",
+ "宴会裙": "cocktail dress",
+ "露肩裙": "off-shoulder dress",
+ "连体裙": "jumpsuit",
+ "迷你连衣裙": "minidress",
+ "深V领连衣裙": "deep v-neck dress",
+ "丝绒裙": "velvet dress",
+ "复古裙": "vintage dress",
+ "亮片裙": "sequin skirt",
+ "亮丝裙": "shimmer dress",
+ "格子裙": "plaid skirt",
+ "牛仔迷你裙": "denim miniskirt",
+ "棉质连衣裙": "cotton dress",
+ "运动裙": "athletic skirt",
+ "低背连衣裙": "low-back dress",
+ "高领连衣裙": "turtleneck dress",
+ "珍珠裙": "pearl skirt",
+ "水钻裙": "rhinestone skirt",
+ "条纹裙": "striped skirt",
+ "亮片迷你裙": "sequin miniskirt",
+ "印花迷你裙": "printed miniskirt",
+ "薄纱连衣裙": "chiffon minidress",
+ "无袖连衣裙": "sleeveless minidress",
+ "背带连衣裙": "suspender dress",
+ "麂皮迷你裙": "suede miniskirt",
+ "纯色连衣裙": "solid color dress"
+ }
+ },
+ "制服COS": {
+ "职场制服": "business_suit",
+ "校服": "school_uniform",
+ "水手服": "serafuku",
+ "护士": "nurse",
+ "警服": "police_uniform",
+ "海军制服": "naval_uniform",
+ "陆军制服": "military_uniform",
+ "女仆装": "maid",
+ "围裙": "apron",
+ "厨师工装": "chef_uniform",
+ "宇航服": "space_suit",
+ "实验服": "labcoat",
+ "啦啦队服": "cheerleader",
+ "燕尾服": "tuxedo",
+ "礼服": "evening_gown",
+ "婚纱": "wedding_dress",
+ "巫女服": "miko",
+ "修女服": "nun_cloth",
+ "兔子服装": "bunny_costume",
+ "猫系服装": "cat_costume",
+ "皮套狗": "dog_costume",
+ "熊套装": "bear_costume",
+ "圣诞风格服装": "santa_costume",
+ "盔甲": "armor",
+ "比基尼盔甲": "bikini_armor",
+ "穿着全套盔甲的": "full_armor",
+ "板甲": "plate_armor",
+ "日本铠甲": "japanese_armor",
+ "动力装甲": "power_armor",
+ "机甲": "mecha",
+ "头盔": "helmet",
+ "头盔(日式)": "kabuto",
+ "無肩甲盔甲": "off-shoulder_armor",
+ "肩甲": "shoulder_armor",
+ "日本弓道護胸甲": "muneate",
+ "胸甲": "breastplate",
+ "腹甲": "faulds",
+ "胫甲": "shin_guards",
+ "装甲靴": "armored_boots",
+ "道袍": "Taoist robe",
+ "长袍": "robe",
+ "混合长袍": "robe_of_blending",
+ "斗篷": "cloak",
+ "羽衣": "hagoromo"
+ },
+ "传统服饰": {
+ "中国服饰": "china_dress",
+ "中国风": "chinese_style",
+ "旗袍": "china_dress",
+ "印花旗袍": "print_cheongsam",
+ "汉服": "hanfu",
+ "汉服唐风": "hanfu, tang style",
+ "上杉": "upper shan",
+ "长上杉": "long upper shan",
+ "齐胸破裙": "chest po skirt",
+ "齐胸褶裙": "chest pleated skirt",
+ "系带": "waistband",
+ "披帛": "pibo",
+ "汉服宋风": "hanfu, song style outfits",
+ "短杉": "short shan",
+ "长杉": "long shan",
+ "宋抹": "songmo",
+ "百褶裙": "pleated skirt",
+ "汉服明风": "hanfu, ming style outfits",
+ "短袄": "short coat",
+ "长袄": "long coat",
+ "马面裙": "mamian skirt",
+ "交领": "overlapping collar",
+ "圆领": "round collar",
+ "立领": "standing collar",
+ "方领": "square collar",
+ "云肩": "overlapping collar",
+ "刺绣": "embroidery",
+ "织金": "woven gold",
+ "妆花": "makeup flower",
+ "补服": "bufu",
+ "和服": "kimono",
+ "印花和服": "print_kimono",
+ "衣带(和服用)": "obi",
+ "浴衣": "yukata",
+ "韩服": "hanbok",
+ "朝鲜服饰": "korean_clothes",
+ "西部风格": "western",
+ "德国服装": "german_clothes",
+ "哥特风格": "gothic",
+ "洛丽塔风格": "gothic_lolita",
+ "拜占庭风格": "byzantine_fashion",
+ "热带特征的": "Tropical",
+ "印度风格": "indian_style",
+ "越南校服(奥黛)": "Ao_Dai",
+ "阿伊努人的服饰": "ainu_clothes",
+ "阿拉伯服饰": "arabian_clothes",
+ "埃及风格服饰": "egyptian_clothes"
+ }
+ },
+ "情景": {
+ "化妆": "applying makeup",
+ "洗澡": "bathing",
+ "入浴": "bathing",
+ "咬": "biting",
+ "鞠躬": "bowing",
+ "刷牙": "brushing teeth",
+ "吹泡泡": "bubble blowing",
+ "追逐": "chasing",
+ "打扫": "cleaning",
+ "攀爬": "climbing",
+ "烹饪": "cooking",
+ "哭": "crying",
+ "拥抱": "cuddling",
+ "跳舞": "dancing",
+ "潜水": "diving",
+ "拉弓": "drawing bow",
+ "喝": "drinking",
+ "驾驶": "driving",
+ "(意外)掉落": "dropping",
+ "弄干(浴后)": "drying",
+ "双持": "dual wielding",
+ "吃饭": "eating",
+ "做运动": "exercise",
+ "战斗姿态|摆着架势的": "fighting stance",
+ "射击": "firing",
+ "钓鱼": "fishing",
+ "秀肌肉": "flexing",
+ "飞踢": "flying kick",
+ "梳头": "hair brushing",
+ "撩头发": "hair tucking",
+ "吊起来的": "hanging",
+ "击打": "hitting",
+ "在想象的": "imagining",
+ "跳跃": "jumping",
+ "踢": "kicking",
+ "演奏乐器": "playing instrument",
+ "钢管舞": "pole dancing",
+ "祈祷": "praying",
+ "展示": "presenting",
+ "挥拳": "punching",
+ "推搡": "pushing",
+ "阅读": "reading",
+ "骑": "riding",
+ "奔跑": "running",
+ "缝纫": "sewing",
+ "购物": "shopping",
+ "淋浴": "showering",
+ "唱歌": "sing",
+ "扑克牌": "playing card",
+ "打游戏": "playing games"
+ },
+ "动作/表情": {
+ "单人姿态": {
+ "行走": "Walking",
+ "转身": "Turning",
+ "摆动": "Swinging",
+ "弯腰": "Bending",
+ "高抬腿": "High leg lift",
+ "拉伸": "Stretching",
+ "手势": "Gestures",
+ "双手放腰": "Hands on waist",
+ "站立": "standing",
+ "躺": "on back",
+ "趴": "on stomach",
+ "跪": "kneeling",
+ "侧卧": "on side",
+ "靠在物体上": "leaning on object",
+ "躺着的": "lying",
+ "胎儿姿势(躺)": "fetal position"
+ },
+ "多人姿态": {
+ "胸部互碰|胸顶着胸": "asymmetrical docking",
+ "背对背": "back-to-back",
+ "眼对眼(对视)": "eye contact",
+ "二人面对面(脸贴得很近)": "facing another",
+ "喂食": "feeding",
+ "口内手指": "finger in another's mouth",
+ "牵手": "holding hands",
+ "拥抱": "hug",
+ "即将接吻": "imminent kiss",
+ "递食物": "incoming food",
+ "递礼物": "incoming gift",
+ "等待接吻|献吻": "incoming kiss",
+ "壁咚": "Kabedon",
+ "膝枕": "lap pillow",
+ "掏耳勺": "mimikaki",
+ "公主抱": "princess carry",
+ "舌头": "tongue",
+ "吐舌头": "tongue out",
+ "小舌头|口盖垂|悬雍垂": "uvula",
+ "咬耳朵": "ear biting"
+ },
+ "手部动作": {
+ "手放在嘴边": "hand to mouth",
+ "爪手势": "claw pose",
+ "招财猫手势(下弯手腕)": "paw pose",
+ "狐狸手势": "fox shadow puppet",
+ "手指枪手势": "finger gun",
+ "胜利手势": "v",
+ "双 v": "double v",
+ "翘大拇指": "thumbs up",
+ "食指抬起": "index finger raised",
+ "国际友好手势": "middle finger",
+ "用手指做出笑脸": "fingersmile",
+ "擦眼泪": "wiping tears",
+ "OK手势": "ok sign",
+ "遮阳手势": "shading eyes",
+ "嘘(手势)": "shushing",
+ "手指伸进嘴里": "finger in mouth",
+ "手指卷着头发": "hair twirling",
+ "双手手指交叉|双手紧握": "hands clasped",
+ "握着头发|手指绕着头发": "holding hair",
+ "扶眼镜": "adjusting eyewear",
+ "拨弄手套": "adjusting gloves",
+ "理头发": "adjusting hair",
+ "整理帽子": "adjusting hat",
+ "掀起自己的衣物": "lifted by self",
+ "拉起掀起卷起衬衫": "shirt lift",
+ "指掀起裙子时形成的篮子形状": "skirt basket",
+ "剥下裙子胸口的部分": "dress pull",
+ "往上剥开的比基尼": "bikini lift",
+ "将裙子掀上去": "skirt lift",
+ "往上掰衣物的吊带": "strap lift",
+ "起风效果|上升气流": "wind lift",
+ "拉着口罩": "mask pull",
+ "拉着裤子": "pants pull",
+ "手抓裆部": "crotch grab",
+ "用手遮住胸部": "covering chest by hand",
+ "捏脸颊": "cheek pinching",
+ "戳脸颊": "cheek poking",
+ "摸下巴": "chin stroking",
+ "展现魅力的姿势": "curvy",
+ "手持辫子": "bunching hair"
+ },
+ "腿部动作": {
+ "张腿": "spread legs",
+ "两腿并拢": "legs together",
+ "二郎腿": "crossed legs",
+ "I字摆腿": "standing split leg up",
+ "屈膝礼(女仆行礼)": "curtsy",
+ "双腿之间的手": "hand between legs",
+ "稍息": "open stance",
+ "挡住关键部位的腿": "convenient leg",
+ "V字张腿": "spread legs",
+ "用双腿夹住": "leg lock",
+ "双腿抬起": "legs up",
+ "双腿交叉站姿": "watson cross",
+ "膝盖合并,两脚分开": "knees together feet apart",
+ "膝盖上有动物": "animal on lap",
+ "手放在自己的膝盖上": "hand on own knee",
+ "顶起膝盖": "knee up",
+ "膝盖蜷到胸前": "knees on chest",
+ "膝盖顶到胸部": "knees to chest",
+ "鸭子坐": "wariza",
+ "正坐": "seiza",
+ "跨坐": "straddling",
+ "侧身坐": "yokozuwari",
+ "蹲下,张开双腿": "squatting, open legs",
+ "一只膝盖": "one knee",
+ "下跪": "kneeling",
+ "四肢着地": "all fours",
+ "凹版姿势": "gravure pose",
+ "高踢": "high kick",
+ "泡脚": "soaking feet"
+ },
+ "眼部动作": {
+ "面向镜头": "facing_viewer",
+ "看向镜头": "looking_at_viewer",
+ "眼神接触": "eye-contact",
+ "盯着看": "eyeball",
+ "凝视": "staring",
+ "回眸": "looking_back",
+ "人物倾斜": "gradient",
+ "人物视角向下看↘": "looking_down",
+ "人物视角抬头看↗": "looking_up",
+ "面向别处": "facing_away",
+ "看向侧面": "looking_to_the_side",
+ "看着别处": "looking_away",
+ "遥望": "looking_afar",
+ "向外看": "looking_outside",
+ "歪头": "head_tilt",
+ "低头": "head_down",
+ "照镜子": "looking_at_mirror"
+ },
+ "表情": {
+ "正面的表情": {
+ "微笑": "smile",
+ "善良的微笑": "kind smile",
+ "大笑": "laughing",
+ "开心": "happy",
+ "露齿咧嘴笑": "grin",
+ "咧嘴傻笑": "teasing smile",
+ "魅惑的微笑": "seductive smile",
+ "自鸣得意的笑": "smirk",
+ "咯咯傻笑": "giggling",
+ "洋洋得意": "smug",
+ "调皮的脸": "naughty face",
+ "邪恶笑": "evil smile",
+ "疯狂的笑": "crazy smile",
+ "快乐|幸福": "happy",
+ "生日快乐": "happy birthday",
+ "万圣节快乐": "happy halloween",
+ "新年快乐": "happy new year",
+ "开心的眼泪": "happy tears",
+ "情人节快乐": "happy valentine"
+ },
+ "负面情绪表情": {
+ "阿黑颜": "ahegao",
+ "忍耐的表情": "endured face",
+ "忍耐": "restrained",
+ "黑化的": "dark persona",
+ "疯狂的": "crazy",
+ "筋疲力尽的": "exhausted",
+ "傲娇": "Tsundere",
+ "病娇": "yandere",
+ "多重人格": "multiple persona",
+ "颤抖": "trembling",
+ "翻白眼(高潮眼)": "rolling eyes",
+ "嫉妒": "envy",
+ "绝顶": "female orgasm",
+ "重呼吸": "heavy breathing",
+ "淘气": "naughty",
+ "呻吟": "moaning",
+ "嫌弃的眼神": "scowl",
+ "轻蔑": "disdain",
+ "蔑视": "contempt",
+ "脸上有阴影,配合蔑视": "shaded face",
+ "鄙夷的眼神": "jitome",
+ "皱眉/蹙额": "wince",
+ "眉头紧锁": "furrowed brow",
+ "害怕侧目": "fear kubrick",
+ "扬起眉毛": "raised eyebrows",
+ "邪恶的": "evil",
+ "生闷气": "sulking",
+ "尖叫": "screaming",
+ "喊叫": "shouting",
+ "生气的": "angry",
+ "惹恼": "annoy",
+ "怒目而视": "glaring",
+ "严肃的": "serious",
+ "侧头瞪着你": "kubrick stare",
+ "伤心": "sad",
+ "啜泣": "tear",
+ "大哭": "crying",
+ "泪如雨下": "streaming tears",
+ "睁着眼睛哭": "crying with eyes open",
+ "流泪": "streaming tears",
+ "泪珠": "teardrop",
+ "撕破衣服": "tearing clothes",
+ "要哭的表情": "tearing up",
+ "眼泪": "tears",
+ "擦眼泪": "wiping tears",
+ "心情不好": "badmood",
+ "不开心的": "unamused",
+ "沮丧": "frustrated",
+ "沮丧的眉头": "frustrated brow",
+ "苦恼的": "annoyed",
+ "苦闷": "anguish",
+ "叹气": "sigh",
+ "忧郁的": "gloom",
+ "失望的": "disappointed",
+ "绝望": "despair",
+ "疼痛": "pain"
+ },
+ "其他表情": {
+ "面无表情": "embarrass",
+ "困乏的": "sleepy",
+ "喝醉的": "drunk",
+ "无聊的": "bored",
+ "使困惑": "confused",
+ "思考": "thinking",
+ "孤独": "lonely",
+ "决心的,坚定的": "determined",
+ "阴沉脸": "shaded",
+ "阴影": "shadow",
+ "凝视|盯": "staring",
+ "咬紧牙关": "clenched teeth",
+ "露出虎牙|露出尖牙": "fang out",
+ "尖牙": "fangs",
+ "鲨鱼牙|锯齿牙": "sharp teeth",
+ "慌张的": "flustered",
+ "流汗": "sweat",
+ "害怕的": "scared",
+ "轻微脸红": "light blush",
+ "脸红": "blush",
+ "害羞的": "shy",
+ "害羞的(尴尬的)": "embarrass",
+ "紧张的": "nervous",
+ "捂脸": "facepalm"
+ },
+ "嘴部表情": {
+ "张嘴": "open mouth",
+ "喘气(张大嘴)": "gasping",
+ "嘴巴微微张开": "Slightly open mouth",
+ "波浪嘴": "wavy mouth",
+ "闭嘴": "close mouth",
+ "点嘴": "dot mouth",
+ "堵嘴": "gag",
+ "啃": "gnaw",
+ "努嘴": "pout",
+ "嘴唇张开": "parted lips",
+ "吃惊": "surprised",
+ "勒住嘴": "bit gag",
+ "栗子嘴": "chestnut mouth",
+ "被封住嘴": "cleave gag",
+ "闭着的嘴": "closed mouth",
+ "蒙住的嘴": "covered mouth",
+ "嘴里有头发": "hair in mouth",
+ "嘟嘴|抿嘴": "homu",
+ "用嘴叼着": "mouth hold",
+ "张开的嘴唇": "parted lips",
+ "撅起的嘴唇": "puckered lips",
+ "把嘴画在侧脸": "sideways mouth",
+ "嘴里含着勺子": "spoon in mouth",
+ "三角嘴": "triangle mouth",
+ "唾液": "saliva",
+ "流口水": "drooling",
+ "嘴角画着口水滴形状的缺口": "mouth drool"
+ }
+ }
+ },
+ "道具": {
+ "翅膀": {
+ "翅膀": "wings",
+ "蝙蝠翅膀": "bat wings",
+ "天使翅膀": "feathered wings",
+ "恶魔翅膀": "demon wings",
+ "蝴蝶翅膀": "butterfly wings",
+ "冰纹理翅膀": "ice crystal texture wings",
+ "龙翅膀": "dragon wings",
+ "妖精翅膀": "fairy wings",
+ "假翅膀": "fake wings",
+ "头上翅膀": "head wings",
+ "低位翅膀": "low wings",
+ "迷你翅膀": "mini wings",
+ "分离的翅膀": "detached wings"
+ },
+ "尾巴": {
+ "尾巴": "tail",
+ "兔尾": "bunny tail",
+ "猫尾": "cat tail",
+ "狗尾": "dog tail",
+ "鼠尾": "mouse tail",
+ "狐狸尾": "fox tail",
+ "老虎尾": "tiger tail",
+ "松鼠尾": "squirrel tail",
+ "狼尾": "wolf tail",
+ "龙尾": "dragon tail",
+ "马尾巴": "horse tail",
+ "浣熊尾": "raccoon tail",
+ "牛尾": "cow tail",
+ "恶魔尾巴": "demon tail"
+ },
+ "耳朵": {
+ "兽耳": "animal ears",
+ "蝙蝠耳": "bat ears",
+ "猫耳": "cat ears",
+ "狗耳": "dog ears",
+ "狐耳": "fox ears",
+ "兔耳": "bunny ears",
+ "浣熊耳": "raccoon ears",
+ "老鼠耳": "mouse ears",
+ "松鼠耳": "squirrel ears",
+ "熊耳": "bear ears",
+ "虎耳": "tiger ears",
+ "狼耳": "wolf ears",
+ "马耳": "horse ears",
+ "牛耳": "cow ears",
+ "羊耳": "sheep ears",
+ "山羊耳": "goat ears",
+ "狮耳": "lion ears",
+ "熊猫耳": "panda ears",
+ "鹿耳": "deer ears",
+ "猴耳": "monkey ears",
+ "猪耳": "pig ears",
+ "鼬耳": "ferret ears",
+ "羊驼耳": "alpaca ears",
+ "尖耳": "pointy ears",
+ "长尖耳": "long pointy ears",
+ "垂耳": "floppy ears"
+ },
+ "角": {
+ "山羊角": "goat horns",
+ "羊角": "sheep horns",
+ "机械角": "mechanical horns",
+ "奶牛角": "cow horns",
+ "恶魔之角": "demon horns",
+ "断角": "broken horn",
+ "龙之角": "dragon horns"
+ }
+ }
+ },
+ "场景": {
+ "自然环境": {
+ "大海": "ocean",
+ "海滩": "beach",
+ "湖泊": "lake",
+ "瀑布": "waterfall",
+ "夜空": "night_sky",
+ "星空": "starry_sky",
+ "星云": "nebula",
+ "超级银河": "hyper_galaxy",
+ "星星轨迹": "star_trail",
+ "落日": "sunset",
+ "月亮": "moon",
+ "月牙": "crescent",
+ "新月": "crescent_moon",
+ "满月": "full_moon",
+ "月光": "moonlight",
+ "猩红月亮": "scarlet_moon",
+ "宇宙": "universe",
+ "太空": "space",
+ "星球": "planet",
+ "地平线": "horizon",
+ "多山的地平线": "mountainous horizon",
+ "牧场": "pasture",
+ "高原": "plateau",
+ "花园": "garden",
+ "田园": "fields",
+ "浮岛": "floating_island",
+ "森林": "forest",
+ "草原": "grasslands",
+ "花田": "flower field",
+ "花海": "flower ocean",
+ "火山": "volcano",
+ "悬崖": "cliff",
+ "雪山": "snowy mountain",
+ "春": "spring",
+ "秋": "autumn",
+ "冬": "winter",
+ "夏": "summer"
+ },
+ "室外场景": {
+ "公园": "park",
+ "吊架;秋千": "Trapeze",
+ "面包店": "bakery",
+ "咖啡厅": "cafe",
+ "植物园": "greenhouse",
+ "城堡": "castle",
+ "竞技场": "arena",
+ "礼堂": "Auditorium",
+ "音乐会": "concert",
+ "书店": "bookstore",
+ "日本旅馆": "Japanese_hotel",
+ "监狱酒吧": "jail_bars",
+ "在电影院里": "in_the_movie_theatre",
+ "舞台": "stage",
+ "港口": "harbor",
+ "赌场": "casino",
+ "台球桌": "billiard",
+ "地牢": "dungeon",
+ "坟墓": "grave",
+ "泰姬陵": "Taj_Mahal",
+ "马丘比丘": "Machu_Picchu",
+ "明石海峡大桥": "Akashi_Kaikyo_Bridge",
+ "东京天空树": "Skytree",
+ "富士山": "Fuji_Mountain",
+ "东京塔": "Tokyo_Tower",
+ "伏见稻荷": "fushimi_inari",
+ "大峡谷": "grand_canyon",
+ "长城": "great_wall_of_china",
+ "希腊_圣托里尼": "santorini",
+ "意大利_威尼斯": "venice",
+ "铁路": "railway"
+ },
+ "城市风景": {
+ "天际线": "city",
+ "城市风景": "cityscape",
+ "街道": "street",
+ "市中心": "downtown",
+ "人群": "crowd",
+ "小巷": "alleyway",
+ "大道": "in_main_street",
+ "路口": "Intersection",
+ "草坪": "lawn",
+ "路径": "path",
+ "路面": "pavement",
+ "路标": "road_sign",
+ "路障": "traffic_cone",
+ "路灯": "lamppost",
+ "灯柱": "lamppost",
+ "电线杆和电线": "power_lines",
+ "长凳": "bench",
+ "自动贩卖机": "vending_machine",
+ "摩天楼": "Skyscraper"
+ },
+ "建筑": {
+ "建筑": "magnificent_architecture",
+ "摩天楼": "Skyscraper",
+ "东亚建筑": "east_asian_architecture",
+ "鸟居/牌坊": "torii",
+ "大教堂": "cathedral",
+ "中式阁楼": "chinese_style_loft",
+ "传统中式客房": "traditional_chinese_room",
+ "城堡": "castle",
+ "塔楼": "turret",
+ "清真寺": "mosque",
+ "水库": "reservoir",
+ "铁路": "railway",
+ "桥": "bridge",
+ "桥下": "under_bridge",
+ "遗迹": "ruins",
+ "废墟堆": "remains",
+ "建筑废墟": "rubble_ruins"
+ },
+ "室内环境及装饰": {
+ "室内泳池": "indoor pool,",
+ "混合控制台": "mixing_console",
+ "汽车驾驶室内": "car_seat,_seatbelt",
+ "更衣室": "locker_room",
+ "在泳池边": "poolside",
+ "洗衣店": "on laundry",
+ "在房间内": "in the room,",
+ "研究所": "on laboratory",
+ "灯笼环境": "lantern_floating",
+ "豪华房间内": "mansion",
+ "厕所里": "on toilet",
+ "在床上": "on_bed",
+ "在沙发上": "on_couch",
+ "在桌子上": "on_desk",
+ "在巴士内": "on_the_bus",
+ "电车内": "on_the_train",
+ "咖啡厅": "cafe",
+ "居酒屋": "Izakaya",
+ "宴会": "banquet",
+ "木质地板": "tile_floor",
+ "榻榻米": "tatami",
+ "床": "bed",
+ "沙发": "safa",
+ "被炉": "kotatsu",
+ "壁炉": "fireplace",
+ "窗": "window",
+ "窗帘": "curtains",
+ "门": "door",
+ "推拉门": "sliding_doors",
+ "衣架": "clothes_rack",
+ "空调": "air_conditioner",
+ "床单": "bed_sheet",
+ "床垫": "futon",
+ "坐垫": "cushion",
+ "抱枕": "pillow_hug",
+ "毛绒兔子": "stuffed_bunny",
+ "小熊": "koakuma"
+ },
+ "简单背景": {
+ "单色背景": "monochrome_background",
+ "简单的背景": "simple_background",
+ "无背景|平铺的背景": "flats",
+ "黑色背景": "black_background",
+ "白色背景": "white_background",
+ "透明背景": "transparent_background",
+ "米色背景": "beige_background",
+ "棕色背景": "brown_background",
+ "棕褐色背景": "tan_background",
+ "灰色背景": "grey_background",
+ "双色调背景": "two-tone_background",
+ "渐变的背景": "gradient_background",
+ "多彩的背景": "multicolored_background",
+ "彩虹背景": "rainbow_background",
+ "抽象背景": "abstract_background",
+ "菱形背景": "argyle_background",
+ "方格背景": "checkered_background",
+ "花朵点缀的背景": "floral_background",
+ "网点图背景": "halftone_background",
+ "桃色背景": "heart_background",
+ "蜂窝风格背景": "honeycomb_background",
+ "格子呢背景": "plaid_background",
+ "圆斑背景": "striped_background"
+ },
+ "自然景观": {
+ "黄石国家公园": "yellowstone national park",
+ "大峡谷": "grand canyon",
+ "珠穆朗玛峰": "mount everest",
+ "大堡礁": "great barrier reef",
+ "撒哈拉沙漠": "sahara desert",
+ "亚马逊雨林": "amazon rainforest",
+ "伊瓜苏大瀑布": "iguazu falls",
+ "维多利亚瀑布": "victoria falls",
+ "死海": "dead sea",
+ "科莫多国家公园": "komodo national park",
+ "加拉帕戈斯群岛": "galapagos islands",
+ "乞力马扎罗山": "mount kilimanjaro",
+ "诺罗威国家公园": "norwegian fjords",
+ "巴厘岛": "bali",
+ "安第斯山脉": "andes",
+ "摩洛哥蓝色小镇": "chefchaouen",
+ "澳大利亚珊瑚海": "coral sea",
+ "阿尔卑斯山": "alps",
+ "巴塔哥尼亚": "patagonia",
+ "哈里森湖洞穴": "harrison's cave",
+ "泰姬陵": "taj mahal",
+ "尼亚加拉大瀑布": "niagara falls",
+ "极光": "aurora borealis",
+ "喜马拉雅山脉": "himalayas",
+ "乌鲁鲁": "uluru",
+ "黄金海岸": "gold coast",
+ "马尔代夫": "maldives",
+ "马丘比丘": "machu picchu",
+ "塞尔维亚的德里纳河": "drina river"
+ },
+ "人造景观": {
+ "万里长城": "great wall of china",
+ "自由女神像": "statue of liberty",
+ "埃菲尔铁塔": "eiffel tower",
+ "大本钟": "big ben",
+ "白金汉宫": "buckingham palace",
+ "圣彼得大教堂": "st. peter's basilica",
+ "金门桥": "golden gate bridge",
+ "古罗马斗兽场": "colosseum",
+ "悉尼歌剧院": "sydney opera house",
+ "伦敦眼": "london eye",
+ "克里姆林宫": "kremlin",
+ "红场": "red square",
+ "莫斯科圣瓦西里大教堂": "saint basil's cathedral",
+ "比萨斜塔": "leaning tower of pisa",
+ "卢浮宫": "louvre",
+ "凡尔赛宫": "palace of versailles",
+ "尼斯湖": "loch ness",
+ "圣保罗大教堂": "st. paul's cathedral",
+ "埃德蒙顿购物中心": "west edmonton mall",
+ "布拉格城堡": "prague castle",
+ "新天鹅堡": "neuschwanstein castle",
+ "泰姬陵": "taj mahal",
+ "马丘比丘": "machu picchu",
+ "克里斯特救世像": "christ the redeemer",
+ "巴比伦空中花园": "hanging gardens of babylon",
+ "亚历山大灯塔": "lighthouse of alexandria",
+ "宙斯神庙": "temple of artemis",
+ "摩洛哥蓝色小镇": "chefchaouen",
+ "阿尔罕布拉宫": "alhambra",
+ "佩特拉古城": "petra",
+ "布拉格天文钟": "prague astronomical clock",
+ "春秋战国时期的秦始皇兵马俑": "terracotta army",
+ "埃及金字塔": "egyptian pyramids",
+ "吉萨大金字塔": "great pyramid of giza",
+ "斯芬克斯": "sphinx",
+ "巴黎圣母院": "notre-dame de paris",
+ "威尼斯水城": "venice",
+ "阿姆斯特丹运河": "amsterdam canals",
+ "巴塞罗那圣家堂": "sagrada familia",
+ "米兰大教堂": "milan cathedral",
+ "维也纳国家歌剧院": "vienna state opera",
+ "比利时布鲁日的布鲁日广场": "markt square bruges",
+ "荷兰风车": "dutch windmills",
+ "巴塞罗那哥特区": "gothic quarter barcelona",
+ "佛罗伦萨大教堂": "florence cathedral",
+ "慕尼黑啤酒节": "oktoberfest munich",
+ "巴拿马运河": "panama canal",
+ "好莱坞标志": "hollywood sign",
+ "帝国大厦": "empire state building",
+ "自然历史博物馆": "natural history museum",
+ "史密森尼博物馆": "smithsonian museum",
+ "卢浮宫金字塔": "louvre pyramid",
+ "迪士尼乐园": "disneyland",
+ "百老汇": "broadway",
+ "时代广场": "times square",
+ "拉斯维加斯大道": "las vegas strip",
+ "旧金山唐人街": "chinatown san francisco",
+ "尼亚加拉瀑布": "niagara falls",
+ "肯辛顿宫": "kensington palace",
+ "伦敦塔桥": "tower bridge",
+ "圣马可广场": "st. mark's square"
+ }
+ },
+ "颜色": {
+ "常用": {
+ "藕色 ": "bisque",
+ "酱色 ": "caramel",
+ "茶色 ": "umber",
+ "赫色 ": "ocher",
+ "琥珀色 ": "amber",
+ "栗色": "chestnut",
+ "金色 ": "gold",
+ "古金色": "old gold",
+ "锌色 ": "zinc",
+ "铁锈色": " rust",
+ "青古铜色 ": "bronze",
+ "黑古铜色 ": "dark bronze",
+ "紫铜色": " purple bronze",
+ "黄铜色 ": "brassiness",
+ "木色 ": "wood",
+ "卡其色": " khaki",
+ "奶油色": " cream",
+ "豆沙色": " cameo",
+ "浅豆沙色": " pale mauve",
+ "肉桂色 ": "cinnamon",
+ "肉色 ": "flesh ",
+ "水晶色 ": "crystal",
+ "荧光色 ": "iridescent",
+ "彩虹色 ": "rainbow",
+ "陶土": "Terra Cotta ",
+ "秋叶棕": "Autumn Brown",
+ "淡茶褐": "Teak ",
+ "琥珀黄": "Amber ",
+ "釉菜": "underglaze ",
+ "银粉色": "silver pink",
+ "米色 ": "beige"
+ },
+ "红色系": {
+ "红色": "red",
+ "暗红": "dark red",
+ "淡红 ": "light red",
+ "铬红 ": "chrome red",
+ "血红 ": "blood red",
+ "紫红 ": "purplish red",
+ "莲红": "lotus red",
+ "金红 ": "bronze red",
+ "品红": "magenta",
+ "朱红": "ponceau",
+ "粉红": "pink",
+ "梅红": "plum",
+ "桃红": "peach blossom",
+ "浅莲红": "fuchsia pink",
+ "辣椒红": "capsicum red",
+ "海螺红 ": "cadmium orange",
+ "宝石红": " ruby red",
+ "玛瑙红 ": "agate red",
+ "珊瑚红": " coral",
+ "石榴红 ": "garnet",
+ "樱桃红": "cherry",
+ "石榴红": "garnet",
+ "鲑鱼红": "salmon",
+ "深紫红": "mulberry",
+ "深藕红 ": "conch shell",
+ "玫瑰红": "rose red"
+ },
+ "黄色系": {
+ "黄色": " yellow",
+ "深黄": "deep yellow",
+ "浅黄 ": "light yellow",
+ "深橙": "deep orange",
+ "浅橙 ": "clear orange",
+ "桔黄 ": "orange",
+ "杏黄": " apricot",
+ "蛋黄 ": "vitellin",
+ "藤黄 ": "rattan yellow",
+ "石黄": " mineral yellow",
+ "土黄 ": "earth yellow",
+ "金黄": "golden yellow",
+ "青黄 ": "bluish yellow",
+ "钴黄 ": "cobalt yellow",
+ "砂黄": " sand yellow",
+ "米黄 ": "apricot cream",
+ "玉米黄 ": "maize",
+ "柠檬黄": " lemon yellow",
+ "橄榄黄": "olive yellow",
+ "樱草黄 ": "primrose yellow",
+ "稻草黄": "straw yellow",
+ "芥末黄": "mustard",
+ "鳝鱼黄 ": "eel yellow",
+ "象牙黄": "ivory",
+ "日光黄 ": "sunny yellow"
+ },
+ "绿色系": {
+ "绿色 ": "green",
+ "深绿 ": "dark green",
+ "淡绿 ": "pale green",
+ "铜绿": " verdigris",
+ "墨绿 ": "blackish green",
+ "蓝绿": "blue green",
+ "水绿 ": "aqua green",
+ "草绿": " grass green",
+ "茶绿": "tea green",
+ "葱绿": "onion green",
+ "玉绿 ": "jade green",
+ "石绿": " mineral green",
+ "豆绿": "pea green",
+ "浅豆绿": "light bean green",
+ "橄榄绿": "olive green",
+ "苹果绿": "apple green",
+ "原野绿": " field green",
+ "森林绿 ": "forest green",
+ "洋蓟绿": " artichoke green",
+ "苔藓绿 ": "moss green",
+ "水草绿 ": "water grass green",
+ "深草绿": " jungle green",
+ "灰湖绿 ": "agate green",
+ "海水绿 ": "marine green",
+ "酸性绿": " acid green",
+ "水晶绿": " crystal green",
+ "松石绿 ": "spearmint"
+ },
+ "青色系": {
+ "青色 ": "cyan",
+ "暗青": "dark blue",
+ "浅青": "light blue",
+ "淡青 ": "pale cerulean",
+ "豆青": " bean green",
+ "花青": " flower blue",
+ "葱青 ": "onion green",
+ "天青": "cerulean blue",
+ "霁青 ": "sky-clearing blue",
+ "石青 ": "mineral blue",
+ "铁青 ": "electric blue",
+ "蟹青": " turquoise ",
+ "蛋青 ": "egg blue",
+ "影青": " misty blue",
+ "黛青": " bluish",
+ "群青": "ultramarine",
+ "藏青 ": "navy blue",
+ "靛青 ": "indigo",
+ "大青 ": "smalt",
+ "粉青": "light greenish blue",
+ "鲜青 ": "clear cerulean",
+ "鳝鱼青 ": "eel green"
+ },
+ "蓝色系": {
+ "蓝色": " blue",
+ "深蓝 ": "dark blue",
+ "浅蓝": " light blue",
+ "鲜蓝 ": "clear blue",
+ "天蓝 ": "sky blue",
+ "蔚蓝 ": "azure",
+ "月光蓝 ": "moon blue",
+ "海洋蓝 ": "ocean blue",
+ "海蓝 ": "sea blue",
+ "湖蓝 ": "acid blue",
+ "青蓝 ": "ultramarine",
+ "铁蓝": " iron blue",
+ "海军蓝 ": "navy blue",
+ "深灰蓝 ": "blue ashes",
+ "深湖蓝": " vivid blue",
+ "中湖蓝 ": "bright blue",
+ "浅湖蓝 ": "canal blue",
+ "清水蓝 ": "water blue",
+ "冰雪蓝 ": "ice-snow blue",
+ "孔雀蓝": " peacock blue",
+ "宝石蓝 ": "sapphire",
+ "粉末蓝 ": "powder blue",
+ "普鲁士蓝 ": "Prussian blue"
+ },
+ "紫色系": {
+ "紫色": " purple",
+ "深紫 ": "amaranth",
+ "浅紫 ": "grey violet",
+ "淡紫": " pale purple",
+ "墨紫 ": "violet black",
+ "绛紫 ": "dark reddish purple",
+ "暗紫 ": "violet deep",
+ "乌紫": " raisin",
+ "蓝紫": " royal light",
+ "鲜紫 ": "violet light",
+ "钴紫": "cobalt violet",
+ "青莲紫": "pale purple",
+ "紫罗兰": " violet",
+ "紫水晶 ": "amethyst",
+ "葡萄紫": " grape",
+ "茄皮紫 ": "aubergine",
+ "玫瑰紫 ": "rose violet",
+ "丁香紫 ": "lilac",
+ "淡白紫": " violet ash",
+ "深青莲": "amaranth purple",
+ "墨绛红": "purple black",
+ "暗绛红": " purple deep",
+ "浅绛红 ": "purple light"
+ },
+ "灰色系": {
+ "灰色 ": "grey",
+ "银灰 ": "silver grey",
+ "深灰": " dark grey",
+ "浅灰": " light grey",
+ "铁灰 ": "iron grey",
+ "铅灰 ": "lividity",
+ "碳灰 ": "charcoal grey",
+ "豆灰 ": "rose dust",
+ "藕灰": " zephyr",
+ "莲灰": " elderberry",
+ "天灰": " sky grey",
+ "土灰 ": "dust grey",
+ "烟灰": " smoky grey",
+ "雾灰": " misty grey",
+ "黑灰 ": "grey black",
+ "紫灰": "purple grey",
+ "蓝灰": "blue grey",
+ "青灰": " lividity",
+ "白灰": " pale grey",
+ "浅莲灰": " pale lilac",
+ "鸽子灰 ": "dove grey",
+ "水泥灰 ": "concrete grey",
+ "深紫灰": " heron",
+ "淡紫灰": " lilac grey",
+ "浅绿灰 ": "eucalyptus",
+ "浅米灰": " moon light",
+ "卡其灰 ": "khaki light"
+ },
+ "白色系": {
+ "白色": " white",
+ "银白 ": "silver white",
+ "铅白 ": "flake white",
+ "锌白 ": "zinc white",
+ "米白": " off-white",
+ "乳白 ": "milky-white",
+ "雪白": " snow-white",
+ "灰白": " greyish white",
+ "青白": " bluish white",
+ "纯白 ": "crisp-white",
+ "本白": " raw white",
+ "象牙白": " ivory white",
+ "牡蛎白": " oyster white",
+ "珍珠白 ": " pear white",
+ "玉石白 ": "jade white",
+ "锌钡白": "lithopone",
+ "羊毛白 ": "wool white",
+ "粉红白 ": "pinky white",
+ "淡紫白 ": "lilac white"
+ }
+ }
+}
\ No newline at end of file
diff --git a/js/config/动物装饰.json b/js/config/动物装饰.json
new file mode 100644
index 0000000..f6ab8cc
--- /dev/null
+++ b/js/config/动物装饰.json
@@ -0,0 +1,70 @@
+{
+ "翅膀": {
+ "翅膀": "wings",
+ "蝙蝠翅膀": "bat wings",
+ "天使翅膀": "feathered wings",
+ "恶魔翅膀": "demon wings",
+ "蝴蝶翅膀": "butterfly wings",
+ "冰纹理翅膀": "ice crystal texture wings",
+ "龙翅膀": "dragon wings",
+ "妖精翅膀": "fairy wings",
+ "假翅膀": "fake wings",
+ "头上翅膀": "head wings",
+ "低位翅膀": "low wings",
+ "迷你翅膀": "mini wings",
+ "分离的翅膀": "detached wings"
+ },
+ "尾巴": {
+ "尾巴": "tail",
+ "兔尾 ": "bunny tail",
+ "猫尾 ": "cat tail",
+ "狗尾 ": "dog tail",
+ "鼠尾 ": "mouse tail",
+ "狐狸尾 ": "fox tail",
+ "老虎尾 ": "tiger tail",
+ "松鼠尾 ": "squirrel tail",
+ "狼尾 ": "wolf tail",
+ "龙尾 ": "dragon tail",
+ "马尾巴 ": "horse tail",
+ "浣熊尾 ": "raccoon tail",
+ "牛尾 ": "cow tail",
+ "恶魔尾巴": "demon tail"
+ },
+ "耳朵": {
+ "兽耳 ": "animal ears",
+ "蝙蝠耳": "bat ears",
+ "猫耳 ": "cat ears",
+ "狗耳 ": "dog ears",
+ "狐耳 ": "fox ears",
+ "兔耳 ": "bunny ears",
+ "浣熊耳": "raccoon ears",
+ "老鼠耳": "mouse ears",
+ "松鼠耳": "squirrel ears",
+ "熊耳 ": "bear ears",
+ "虎耳 ": "tiger ears",
+ "狼耳 ": "wolf ears",
+ "马耳 ": "horse ears",
+ "牛耳 ": "cow ears",
+ "羊耳 ": "sheep ears",
+ "山羊耳": "goat ears",
+ "狮耳 ": "lion ears",
+ "熊猫耳": "panda ears",
+ "鹿耳 ": "deer ears",
+ "猴耳 ": "monkey ears",
+ "猪耳 ": "pig ears",
+ "鼬耳 ": "ferret ears",
+ "羊驼耳": "alpaca ears",
+ "尖耳 ": "pointy ears",
+ "长尖耳": "long pointy ears",
+ "垂耳 ": "floppy ears"
+ },
+ "角": {
+ "山羊角": "goat horns",
+ "羊角": "sheep horns",
+ "机械角": "mechanical horns",
+ "奶牛角": "cow horns",
+ "恶魔之角": "demon horns",
+ "断角": "broken horn",
+ "龙之角": "dragon horns"
+ }
+}
\ No newline at end of file
diff --git a/js/config/场景道具.json b/js/config/场景道具.json
new file mode 100644
index 0000000..5e27899
--- /dev/null
+++ b/js/config/场景道具.json
@@ -0,0 +1,178 @@
+{
+ "自然环境": {
+ "大海": "ocean",
+ "海滩": "beach",
+ "湖泊": "lake",
+ "瀑布": "waterfall",
+ "夜空": "night_sky",
+ "星空": "starry_sky",
+ "星云": "nebula",
+ "超级银河": "hyper_galaxy",
+ "星星轨迹": "star_trail",
+ "落日": "sunset",
+ "月亮": "moon",
+ "月牙": "crescent",
+ "新月": "crescent_moon",
+ "满月": "full_moon",
+ "月光": "moonlight",
+ "猩红月亮": "scarlet_moon",
+ "宇宙": "universe",
+ "太空": "space",
+ "星球": "planet",
+ "地平线": "horizon",
+ "多山的地平线": "mountainous horizon",
+ "牧场": "pasture",
+ "高原": "plateau",
+ "花园": "garden",
+ "田园": "fields",
+ "浮岛": "floating_island",
+ "森林": "forest",
+ "草原": "grasslands",
+ "花田": "flower field",
+ "花海": "flower ocean",
+ "火山": "volcano",
+ "悬崖": "cliff",
+ "雪山": "snowy mountain",
+ "春": "spring",
+ "秋": "autumn",
+ "冬": "winter",
+ "夏": "summer"
+ },
+ "室外场景": {
+ "公园": "park",
+ "吊架;秋千": "Trapeze",
+ "面包店": "bakery",
+ "咖啡厅": "cafe",
+ "植物园": "greenhouse",
+ "城堡": "castle",
+ "竞技场": "arena",
+ "礼堂": "Auditorium",
+ "音乐会": "concert",
+ "书店": "bookstore",
+ "日本旅馆": "Japanese_hotel",
+ "监狱酒吧": "jail_bars",
+ "在电影院里": "in_the_movie_theatre",
+ "舞台": "stage",
+ "港口": "harbor",
+ "赌场": "casino",
+ "台球桌": "billiard",
+ "地牢": "dungeon",
+ "坟墓": "grave",
+ "泰姬陵": "Taj_Mahal",
+ "马丘比丘": "Machu_Picchu",
+ "明石海峡大桥": "Akashi_Kaikyo_Bridge",
+ "东京天空树": "Skytree",
+ "富士山": "Fuji_Mountain",
+ "东京塔": "Tokyo_Tower",
+ "伏见稻荷": "fushimi_inari",
+ "大峡谷": "grand_canyon",
+ "长城": "great_wall_of_china",
+ "希腊_圣托里尼": "santorini",
+ "意大利_威尼斯": "venice",
+ "铁路": "railway"
+ },
+ "城市风景": {
+ "天际线": "city",
+ "城市风景": "cityscape",
+ "街道": "street",
+ "市中心": "downtown",
+ "人群": "crowd",
+ "小巷": "alleyway",
+ "大道": "in_main_street",
+ "路口": "Intersection",
+ "草坪": "lawn",
+ "路径": "path",
+ "路面": "pavement",
+ "路标": "road_sign",
+ "路障": "traffic_cone",
+ "路灯": "lamppost",
+ "灯柱": "lamppost",
+ "电线杆和电线": "power_lines",
+ "长凳": "bench",
+ "自动贩卖机": "vending_machine",
+ "摩天楼": "Skyscraper"
+ },
+ "建筑": {
+ "建筑": "magnificent_architecture",
+ "摩天楼": "Skyscraper",
+ "东亚建筑": "east_asian_architecture",
+ "鸟居/牌坊": "torii",
+ "大教堂": "cathedral",
+ "中式阁楼": "chinese_style_loft",
+ "传统中式客房": "traditional_chinese_room",
+ "城堡": "castle",
+ "塔楼": "turret",
+ "清真寺": "mosque",
+ "水库": "reservoir",
+ "铁路": "railway",
+ "桥": "bridge",
+ "桥下": "under_bridge",
+ "遗迹": "ruins",
+ "废墟堆": "remains",
+ "建筑废墟": "rubble_ruins"
+ },
+ "室内环境及装饰": {
+ "室内泳池": "indoor pool,",
+ "混合控制台": "mixing_console",
+ "汽车驾驶室内": "car_seat,_seatbelt",
+ "更衣室": "locker_room",
+ "在泳池边": "poolside",
+ "洗衣店": "on laundry",
+ "在房间内": "in the room,",
+ "研究所": "on laboratory",
+ "灯笼环境": "lantern_floating",
+ "豪华房间内": "mansion",
+ "厕所里": "on toilet",
+ "在床上": "on_bed",
+ "在沙发上": "on_couch",
+ "在桌子上": "on_desk",
+ "在巴士内": "on_the_bus",
+ "电车内": "on_the_train",
+ "咖啡厅": "cafe",
+ "居酒屋": "Izakaya",
+ "宴会": "banquet",
+ "木质地板": "tile_floor",
+ "榻榻米": "tatami",
+ "床": "bed",
+ "沙发": "safa",
+ "被炉": "kotatsu",
+ "壁炉": "fireplace",
+ "窗": "window",
+ "窗帘": "curtains",
+ "门": "door",
+ "推拉门": "sliding_doors",
+ "衣架": "clothes_rack",
+ "空调": "air_conditioner",
+ "床单": "bed_sheet",
+ "床垫": "futon",
+ "坐垫": "cushion",
+ "抱枕": "pillow_hug",
+ "毛绒兔子": "stuffed_bunny",
+ "小熊": "koakuma"
+ },
+ "简单背景": {
+ "单色背景": "monochrome_background",
+ "简单的背景": "simple_background",
+ "无背景|平铺的背景": "flats",
+ "黑色背景": "black_background",
+ "白色背景": "white_background",
+ "透明背景": "transparent_background",
+ "米色背景": "beige_background",
+ "棕色背景": "brown_background",
+ "棕褐色背景": "tan_background",
+ "灰色背景": "grey_background",
+ "双色调背景": "two-tone_background",
+ "渐变的背景": "gradient_background",
+ "多彩的背景": "multicolored_background",
+ "彩虹背景": "rainbow_background",
+ "抽象背景": "abstract_background",
+ "菱形背景": "argyle_background",
+ "方格背景": "checkered_background",
+ "花朵点缀的背景": "floral_background",
+ "网点图背景": "halftone_background",
+ "桃色背景": "heart_background",
+ "蜂窝风格背景": "honeycomb_background",
+ "格子呢背景": "plaid_background",
+ "圆斑背景": "striped_background"
+ }
+}
\ No newline at end of file
diff --git a/js/config/抽卡机.json b/js/config/抽卡机.json
new file mode 100644
index 0000000..1f7a6e6
--- /dev/null
+++ b/js/config/抽卡机.json
@@ -0,0 +1,194 @@
+{
+ "Muggle穿搭设计": {
+ "经典白色衬衫, 裁剪黑色长裤, 金链项链": "(Classic_white_blouse:1.6),(Tailored_black_trousers:1.5),(Gold_chain_necklace:1.4)",
+ "花卉夏日连衣裙, 绑带凉鞋, 草编帽": "(Floral_summer_dress:1.5),(Strappy_sandals:1.4),(Straw_hat:1.3)",
+ "牛仔夹克, 高腰短裤, 运动鞋": "(Denim_jacket:1.4),(High-waisted_shorts:1.3),(Sneakers:1.2)",
+ "丝绸吊带上衣, 飘逸长裙, 大号耳环": "(Silk_cami_top:1.5),(Flowy_maxi_skirt:1.4),(Statement_earrings:1.3)",
+ "露肩上衣, 宽松七分裤, 编织手提包": "(Off-the-shoulder_blouse:1.4),(Wide-leg_cropped_pants:1.3),(Woven_handbag:1.2)",
+ "条纹T恤, 牛仔背带裤, 白色运动鞋": "(Striped_tee:1.5),(Denim_overalls:1.4),(White_sneakers:1.3)",
+ "粗针织毛衣, 皮质紧身裤, 大号围巾": "(Chunky_knit_sweater:1.4),(Leather_leggings:1.3),(Oversized_scarf:1.2)",
+ "波点上衣, 褶皱中长裙, 尖头高跟鞋": "(Polka_dot_blouse:1.5),(Pleated_midi_skirt:1.4),(Pointed-toe_pumps:1.3)",
+ "皮质夹克, 黑色紧身牛仔裤, 短靴": "(Leather_jacket:1.4),(Black_skinny_jeans:1.3),(Ankle_boots:1.2)",
+ "交叉上衣, 花卉印花裙, 块跟凉鞋": "(Wrap_top:1.5),(Floral_printed_skirt:1.4),(Block_heel_sandals:1.3)",
+ "白色衬衫, 阔腿长裤, 珍珠项链": "(Crisp_white_button-down:1.6),(Wide-leg_trousers:1.5),(Pearl_necklace:1.4)",
+ "波西米亚风印花连衣裙, 草编楔形凉鞋, 珠子手链": "(Boho_printed_dress:1.5),(Straw_wedge_sandals:1.4),(Beaded_bracelets:1.3)",
+ "皮质摩托夹克, 破洞牛仔裤, 镶钉短靴": "(Leather_biker_jacket:1.4),(Distressed_jeans:1.3),(Studded_ankle_boots:1.2)",
+ "针织毛衣连衣裙, 过膝靴, 毛呢宽檐帽": "(Knit_sweater_dress:1.5),(Thigh-high_boots:1.4),(Felt_fedora_hat:1.3)",
+ "修身西装外套, 铅笔裙, 尖头平底鞋": "(Fitted_blazer:1.4),(Pencil_skirt:1.3),(Pointed-toe_flats:1.2)",
+ "浪漫蕾丝上衣, 飘逸长裙, 精致手链": "(Romantic_lace_top:1.5),(Flowy_maxi_skirt:1.4),(Delicate_bracelet:1.3)",
+ "露肩毛衣, 破洞牛仔裤, 白色运动鞋": "(Off-the-shoulder_sweater:1.4),(Distressed_jeans:1.3),(White_sneakers:1.2)",
+ "格子衬衫, 牛仔裙, 军靴": "(Plaid_button-down_shirt:1.5),(Denim_skirt:1.4),(Combat_boots:1.3)",
+ "羊绒毛衣, 阔腿长裤, 大围巾": "(Cashmere_sweater:1.4),(Wide-leg_trousers:1.3),(Blanket_scarf:1.2)",
+ "雪纺上衣, 褶皱七分裤, 块跟绑带凉鞋": "(Chiffon_blouse:1.5),(Pleated_culottes:1.4),(Strappy_block_heels:1.3)",
+ "短款毛衣, 高腰直筒牛仔裤, 厚底运动鞋": "(Cropped_sweater:1.4),(Mom_jeans:1.3),(Chunky_sneakers:1.2)",
+ "裁剪西装外套, 皮质裤子, 大号项链": "(Tailored_blazer:1.5),(Leather_pants:1.4),(Statement_necklace:1.3)",
+ "波西米亚风印花上衣, 飘逸长裙, 叠戴项链": "(Bohemian_printed_top:1.4),(Flowy_maxi_skirt:1.3),(Layered_necklaces:1.2)",
+ "绒面夹克, 阔腿裤, 短靴": "(Suede_jacket:1.5),(Wide-leg_pants:1.4),(Ankle_boots:1.3)",
+ "交叉连衣裙, 块跟绑带高跟鞋, 流苏耳环": "(Wrap_dress:1.4),(Strappy_block_heels:1.3),(Tassel_earrings:1.2)",
+ "条纹长袖上衣, 牛仔短裤, 帆布运动鞋": "(Striped_long_sleeve_top:1.5),(Denim_shorts:1.4),(Canvas_sneakers:1.3)",
+ "羊绒开衫, 七分裤, 皮质平底鞋": "(Cashmere_cardigan:1.4),(Cropped_trousers:1.3),(Leather_loafers:1.2)",
+ "丝绸衬衫, 褶皱中长裙, 珍珠耳环": "(Silk_blouse:1.5),(Pleated_midi_skirt:1.4),(Pearl_earrings:1.3)",
+ "军旅风外套, 装饰裤, 军靴": "(Utility_jacket:1.4),(Cargo_pants:1.3),(Combat_boots:1.2)",
+ "露肩连身短裤, 草编楔形凉鞋, 贝壳项链": "(Off-the-shoulder_romper:1.5),(Straw_wedge_sandals:1.4),(Shell_necklace:1.3)",
+ "修身西装外套, 烟管裤, 尖头高跟鞋": "(Structured_blazer:1.4),(Cigarette_pants:1.3),(Pointed-toe_pumps:1.2)",
+ "波西米亚风花卉长裙, 赤足凉鞋, 木珠手链": "(Boho_floral_maxi_dress:1.5),(Barefoot_sandals:1.4),(Wooden_beaded_bracelets:1.3)",
+ "高领毛衣, 皮质裙子, 过膝靴": "(Turtleneck_sweater:1.4),(Leather_skirt:1.3),(Over-the-knee_boots:1.2)",
+ "条纹衬衫, 阔腿长裤, 滑步运动鞋": "(Striped_button-down_shirt:1.5),(Wide-leg_trousers:1.4),(Slip-on_sneakers:1.3)",
+ "针织短款上衣, 高腰喇叭牛仔裤, 厚底凉鞋": "(Knit_cropped_top:1.4),(High-waisted_flared_jeans:1.3),(Platform_sandals:1.2)",
+ "修身上衣, 裁剪七分裤, 块跟穆勒鞋": "(Structured_blouse:1.5),(Tailored_culottes:1.4),(Chunky_heel_mules:1.3)",
+ "丝质睡袍裙, 绒面短靴, 叠戴项链": "(Silk_slip_dress:1.4),(Suede_ankle_boots:1.3),(Layered_necklaces:1.2)",
+ "短款牛仔夹克, A字裙, 白色运动鞋": "(Cropped_denim_jacket:1.5),(A-line_skirt:1.4),(White_sneakers:1.3)",
+ "波西米亚风印花连衣裙, 绑带凉鞋, 羽毛耳环": "(Bohemian_printed_dress:1.4),(Strappy_sandals:1.3),(Feather_earrings:1.2)",
+ "飘逸衬衫, 阔腿裤, 帆绳楔形凉鞋": "(Flowy_blouse:1.5),(Wide-leg_pants:1.4),(Espadrille_wedges:1.3)",
+ "针织开衫, 紧身牛仔裤, 短靴": "(Knit_cardigan:1.4),(Skinny_jeans:1.3),(Ankle_boots:1.2)",
+ "花卉交叉上衣, 中长裙, 珠子手链": "(Floral_wrap_top:1.5),(Midi_skirt:1.4),(Beaded_bracelets:1.3)",
+ "皮夹克, 黑色紧身裤, 军靴": "(Leather_moto_jacket:1.4),(Black_leggings:1.3),(Combat_boots:1.2)",
+ "高领毛衣连衣裙, 过膝靴, 宽檐帽": "(Turtleneck_sweater_dress:1.5),(Thigh-high_boots:1.4),(Wide-brim_hat:1.3)",
+ "修身西装外套, 七分裤, 尖头平底鞋": "(Structured_blazer:1.4),(Cropped_trousers:1.3),(Pointed-toe_flats:1.2)",
+ "波西米亚风印花上衣, 牛仔短裤, 珠饰发带": "(Bohemian_printed_top:1.5),(Denim_shorts:1.4),(Beaded_headband:1.3)",
+ "宽松毛衣, 紧身打底裤, 便携运动鞋": "(Oversized_sweater:1.4),(Leggings:1.3),(Slip-on_sneakers:1.2)",
+ "花卉印花连身短裤, 楔形凉鞋, 叠戴手镯": "(Floral_printed_romper:1.5),(Wedge_sandals:1.4),(Stacked_bangles:1.3)",
+ "羊绒高领毛衣, 阔腿喇叭牛仔裤, 白色运动鞋": "(Cashmere_turtleneck:1.4),(Cropped_flare_jeans:1.3),(White_sneakers:1.2)",
+ "露肩上衣, 铅笔裙, 尖头高跟鞋": "(Off-the-shoulder_blouse:1.5),(Pencil_skirt:1.4),(Pointed-toe_pumps:1.3)",
+ "黑色天鹅绒连衣裙, 针织渔网袜, 铆钉短靴": "(Black_velvet_dress:1.5),(Fishnet_tights:1.4),(Studded_ankle_boots:1.3)",
+ "条纹短款上衣, 牛仔工装裤, 帆布运动鞋": "(Striped_crop_top:1.4),(Denim_overalls:1.3),(Canvas_sneakers:1.2)",
+ "丝质和服, 阔腿长裤, 厚底凉鞋": "(Silk_kimono:1.5),(Wide-leg_trousers:1.4),(Platform_sandals:1.3)",
+ "带流苏皮夹克, 破洞宽松牛仔裤, 机车靴": "(Leather_jacket_with_fringe:1.5),(Distressed_boyfriend_jeans:1.4),(Biker_boots:1.3)",
+ "波西米亚风长裙, 刺绣绒面背心, 流苏短靴": "(Bohemian_maxi_dress:1.4),(Embroidered_suede_vest:1.3),(Fringe_ankle_boots:1.2)",
+ "宽松印花T恤, 牛仔短裤, 帆布高帮运动鞋": "(Oversized_graphic_tee:1.5),(Denim_shorts_with_patchwork:1.4),(Canvas_high-top_sneakers:1.3)",
+ "简约黑色连体裤, 尖头踝靴, 金属手拿包": "(Sleek_black_jumpsuit:1.5),(Pointed-toe_ankle_boots:1.4),(Metallic_clutch:1.3)",
+ "格纹西装外套, 高领毛衣, 七分裤": "(Plaid_blazer:1.4),(Turtleneck_sweater:1.3),(Cropped_trousers:1.2)",
+ "天鹅绒中长裙, 缎面蝴蝶结衬衫, 尖头穆勒鞋": "(Velvet_midi_skirt:1.5),(Satin_blouse_with_bow_tie:1.4),(Pointed-toe_mules:1.3)",
+ "刺绣波西米亚风上衣, 花卉印花裙, 编织凉鞋": "(Embroidered_boho_top:1.4),(Floral_print_skirt:1.3),(Woven_sandals:1.2)",
+ "荷叶边露肩连衣裙, 草帽, 篮编斜挎包": "(Ruffled_off-the-shoulder_dress:1.5),(Straw_hat:1.4),(Basket_woven_bag:1.3)",
+ "拼接皮夹克, 破洞紧身牛仔裤, 机车靴": "(Leather_jacket_with_patchwork:1.4),(Skinny_ripped_jeans:1.3),(Motorcycle_boots:1.2)",
+ "复古蕾丝连衣裙, 珍珠项链, 缎面手套": "(Vintage_lace_dress:1.5),(Pearl_necklace:1.4),(Satin_gloves:1.3)",
+ "刺绣和服外套, 阔腿连身裤, 细带高跟鞋": "(Embroidered_kimono_jacket:1.4),(Wide-leg_jumpsuit:1.3),(Strappy_heels:1.2)",
+ "针织毛衣连衣裙, 过膝靴, 羊毛礼帽": "(Knit_sweater_dress:1.5),(Over-the-knee_boots:1.4),(Felt_fedora_hat:1.3)",
+ "合身西装外套, 褶皱长裤, 尖头平底便鞋": "(Tailored_blazer:1.4),(Pleated_trousers:1.3),(Pointed-toe_loafers:1.2)",
+ "带拼布艺术的牛仔夹克, 图案T恤, 装饰口袋长裤": "(Denim_jacket_with_patched_artwork:1.5),(Graphic_tee:1.4),(Cargo_pants:1.3)",
+ "涂鸦印花连帽衫, 运动裤, 厚底运动鞋": "(Graffiti_printed_hoodie:1.4),(Track_pants:1.3),(Chunky_sneakers:1.2)",
+ "露肩比基尼上衣, 高腰短裤, 草帽": "(Strapless_bikini_top:1.5),(High-waisted_shorts:1.4),(Straw_hat:1.3)",
+ "艺术家罩衫, 溅墨牛仔裤, 帆布运动鞋": "(Artist_smock:1.4),(Paint-splattered_jeans:1.3),(Canvas_sneakers:1.2)",
+ "礼服西装外套, 阔腿长裤, 尖头高跟鞋": "(Tuxedo_blazer:1.5),(Wide-leg_trousers:1.4),(Pointed-toe_pumps:1.3)",
+ "不对称上衣, 褶皱中长裙, 小跟鞋": "(Asymmetrical_blouse:1.4),(Pleated_midi_skirt:1.3),(Kitten_heels:1.2)",
+ "热带印花长裙, 宽檐草帽, 编织凉拖": "(Tropical_print_maxi_dress:1.5),(Straw_wide-brim_hat:1.4),(Woven_slides:1.3)",
+ "麻质衬衫连衣裙, 草编斜挎包, 懒人绣花鞋": "(Linen_shirt_dress:1.4),(Straw_crossbody_bag:1.3),(Slip-on_espadrilles:1.2)",
+ "格纹风衣大衣, 复古高领毛衣, 阔腿牛仔裤": "(Plaid_trench_coat:1.5),(Vintage_turtleneck:1.4),(Wide-leg_jeans:1.3)",
+ "条纹贝雷帽, 圆点中长裙, 复古踝靴": "(Striped_beret:1.4),(Polka_dot_midi_skirt:1.3),(Retro_ankle_boots:1.2)",
+ "军绿色军装外套, 迷彩军装长裤, 作战靴": "(Army_green_cargo_jacket:1.5),(Camouflage_cargo_pants:1.4),(Combat_boots:1.3)",
+ "编织比基尼, 飘逸长裙, 草编海滩包": "(Crochet_bikini:1.4),(Flowy_maxi_skirt:1.3),(Straw_beach_bag:1.2)",
+ "别致的粗花呢西装外套, 烟管裤, 尖头高跟鞋": "(Chic_tweed_blazer:1.5),(Cigarette_trousers:1.4),(Pointed-toe_pumps:1.3)",
+ "印花和服开衫, 阔腿连身裤, 厚底帆绳凉鞋": "(Printed_kimono_cardigan:1.4),(Wide-leg_jumpsuit:1.3),(Platform_espadrilles:1.2)",
+ "简约黑色连衣裙, 雕塑跟高跟凉鞋, 个性手拿包": "(Minimalist_black_dress:1.5),(Sculptural_heeled_sandals:1.4),(Statement_clutch:1.3)",
+ "复古波点连衣裙, 复古太阳镜, 块跟凉鞋": "(Retro_polka_dot_dress:1.4),(Vintage_sunglasses:1.3),(Block-heel_sandals:1.2)",
+ "修身西装裙, 尖头穆勒鞋, 结构感手提包": "(Tailored_blazer_dress:1.5),(Pointed-toe_mules:1.4),(Structured_handbag:1.3)",
+ "皮夹克, 紧身黑色牛仔裤, 带扣短靴": "(Leather_biker_jacket:1.4),(Skinny_black_jeans:1.3),(Ankle_boots_with_buckles:1.2)",
+ "运动休闲套装, 彩色鞋底运动鞋, 棒球帽": "(Athleisure_tracksuit:1.5),(Sneakers_with_colorful_soles:1.4),(Baseball_cap:1.3)",
+ "波西米亚风婚纱, 花冠, 赤足凉鞋": "(Bohemian_wedding_gown:1.4),(Flower_crown:1.3),(Barefoot_sandals:1.2)",
+ "刺绣牛仔夹克, 阔腿七分裤, 帆布运动鞋": "(Denim_jacket_with_embroidery:1.5),(Wide-leg_culottes:1.4),(Canvas_sneakers:1.3)",
+ "工业风连身工装裤, 皮质踝靴, 个性腰带": "(Industrial-style_jumpsuit:1.4),(Leather_ankle_boots:1.3),(Statement_belt:1.2)",
+ "舒适针织毛衣, 灯芯绒裙, 过膝靴": "(Cozy_knit_sweater:1.5),(Corduroy_skirt:1.4),(Knee-high_boots:1.3),witch costume, pointed hat, flowing robes, Halloween gathering ",
+ "经典黑色西装外套, 铅笔裙, 尖头高跟鞋": "(Classic_black_blazer:1.4),(Pencil_skirt:1.3),(Pointed-toe_pumps:1.2)",
+ "花卉印花连身短裤, 宽檐草帽, 藤编斜挎包": "(Floral_printed_romper:1.5),(Straw_wide-brim_hat:1.4),(Rattan_crossbody_bag:1.3)",
+ "街头艺术风连帽衫, 破洞牛仔裤, 高帮运动鞋": "(Street_art_inspired_hoodie:1.4),(Distressed_jeans:1.3),(High-top_sneakers:1.2)",
+ "波西米亚风雪纺长裙, 羽毛发饰, 赤足凉鞋": "(Boho_chiffon_maxi_dress:1.5),(Feather_headpiece:1.4),(Barefoot_sandals:1.3)",
+ "印花牛仔夹克, 印花背心, 装饰口袋短裤": "(Denim_jacket_with_painting:1.4),(Printed_tank_top:1.3),(Cargo_shorts:1.2)",
+ "人造毛皮外套, 蕾丝镶边睡裙, 天鹅绒踝靴": "(Faux_fur_coat:1.5),(Lace_trimmed_slip_dress:1.4),(Velvet_ankle_boots:1.3)",
+ "细条纹西装外套, 阔腿长裤, 块跟便鞋": "(Polished_pinstripe_blazer:1.4),(Wide-leg_trousers:1.3),(Block-heel_loafers:1.2)",
+ "装饰皮夹克, 亮片迷你裙, 铆钉高跟鞋": "(Leather_jacket_with_embellishments:1.5),(Sequin_mini_skirt:1.4),(Studded_heels:1.3)",
+ "节日风流苏马甲, 破洞牛仔短裤, 牛仔靴": "(Festival_fringe_vest:1.4),(Ripped_denim_shorts:1.3),(Cowboy_boots:1.2)",
+ "条纹比基尼, 飘逸裙包藤编手提包": "(Striped_bikini:1.5),(Flowy_wrap_skirt:1.4),(Straw_tote_bag:1.3)",
+ "针织开衫, 印花中长裙, 便携运动鞋": "(Knit_cardigan:1.4),(Printed_midi_skirt:1.3),(Slip-on_sneakers:1.2)"
+ },
+ "Muggle奇装异服": {
+ "迷彩服, 伪装图案, 军装风格, 户外探险": "camo costume, camouflage pattern, military-inspired, outdoor adventure",
+ "糖果服, 艳丽色彩, 可爱图案, 卖糖果": "candy costume, vibrant colors, sweet motifs, trick-or-treating",
+ "船长服, 海洋主题, 金色点缀, 服装派对": "captain costume, nautical theme, gold accents, costume party",
+ "嘉年华服, 羽毛, 亮片, 炫彩嘉年华氛围, 化装舞会": "carnival costume, feathers, sequins, vibrant carnival atmosphere, masquerade ball",
+ "猫咪服, 蓬松尾巴, 胡须, 猫咪的优雅, 万圣节派对": "cat costume, fluffy tail, whiskers, feline grace, Halloween party",
+ "猫女服, 紧身连身衣, 神秘诱惑, 角色扮演大会": "catwoman costume, sleek bodysuit, mysterious allure, cosplay convention",
+ "啦啦队服, 褶皱裙, 摇旗, 团队精神, 热身集会": "cheerleader costume, pleated skirt, pom-poms, team spirit, pep rally",
+ "圣诞服, 红绿装, 圣诞老人帽, 节日庆典": "christmas costume, red and green attire, Santa hat, holiday festivities",
+ "克莉奥佩特拉服, 华丽头饰, 长裙, 古埃及主题活动": "cleopatra costume, ornate headdress, flowing gown, ancient Egypt-themed event",
+ "小丑服, 多彩连体衣, 超大鞋子, 马戏表演": "clown costume, colorful jumpsuit, oversized shoes, circus performance",
+ "漫画角色服, 标志性服装, 超级英雄/恶棍大会": "comic book character costume, iconic outfit, superhero/villain convention",
+ "指挥服, 细条纹西装, 指挥帽, 音乐表演": "conductor costume, pinstripe suit, conductor's hat, musical performance",
+ "囚犯服, 黑白条纹, 手铐, 服装竞赛": "convict costume, black and white stripes, handcuffs, costume contest",
+ "警察服, 警察制服, 胸牌, 犯罪主题派对": "cop costume, police uniform, badge, crime-themed party",
+ "牛仔服, 牛仔帽, 靴子, 西部主题庆祝活动": "cowboy costume, cowboy hat, boots, western-themed celebration",
+ "斑点狗服, 斑点皮毛, 软耳朵, 动物主题活动": "dalmatian costume, spotted fur, floppy ears, animal-themed event",
+ "十年代服, 复古装, 主题派对, 怀旧": "decade costume, retro attire, themed party, nostalgia",
+ "鹿服, 鹿角, 柔软褐色皮毛, 优雅, 大自然主题聚会": "deer costume, antlers, soft brown fur, graceful, nature-themed gathering",
+ "恶魔服, 黑色斗篷, 有角, 不祥气息, 万圣节化装舞会": "demon costume, dark cloak, horned , ominous presence, Halloween masquerade",
+ "恶魔服, 红色披风, 钉耙, 调皮魅力, 服装派对": "devil costume, red cape, pitchfork, mischievous allure, costume party",
+ "迪斯科服, 闪闪发光的亮片, 铃形喇叭裤, 时髦氛围, 复古主题舞会": "disco costume, sparkling sequins, bell-bottom pants, groovy vibes, retro-themed dance",
+ "洋娃娃服, 褶皱裙装, 服装游行": "doll costume, frilly dress, costume parade",
+ "德古拉服, 黑色披风, 尖牙, 吸血鬼主题活动": "dracula costume, black cape, fangs, vampire-themed event",
+ "矮人服, 超大帽子, 胡须, 幻想主题聚会": "dwarf costume, oversized hat, beard, fantasy-themed gathering",
+ "复活节服, 兔耳朵, 淡色调, 寻找复活节彩蛋": "easter costume, bunny ears, pastel colors, Easter egg hunt",
+ "埃及服, 法老头饰, 长袍, 历史重现活动": "egyptian costume, pharaoh headdress, flowing robes, historical reenactment",
+ "仙女服, 飘逸翅膀, 闪闪发光的长袍, 魔幻森林派对": "fairy costume, ethereal wings, shimmering gown, enchanted forest party",
+ "童话故事服, 故事书角色, 魔法道具, 童话主题舞会": "fairy tale costume, storybook characters, magical props, fairytale-themed ball",
+ "消防员服, 防火服, 头盔, 社区活动": "firefighter costume, fire-resistant suit, helmet, community event",
+ "健身服, 运动装, 吸汗带, 健身主题募捐活动": "fitness costume, athletic attire, sweatbands, fitness-themed fundraiser",
+ "嬉皮士服, 珠片礼服, 羽毛头带, 狂欢的二十年代派对": "flapper costume, beaded dress, feather headband, Roaring Twenties party",
+ "森林仙子服, 叶状翅膀, 花环头冠, 大自然主题庆祝活动": "forest fairy costume, leafy wings, floral crown, nature-themed celebration",
+ "法国服, 贝雷帽, 条纹衬衫, 巴士底日游行": "french costume, beret, striped shirt, Bastille Day parade",
+ "水果服, 超大水果形状服装, 色彩缤纷, 水果节": "fruits costume, oversized fruit-shaped attire, colorful and fun, fruit festival",
+ "搞笑服, 滑稽服装, 幽默道具, 服装竞赛": "funny costume, comedic outfit, humorous props, costume contest",
+ "星际战士服, 未来战甲, 激光枪, 科幻大会": "galactic warrior costume, futuristic armor, laser blaster, sci-fi convention",
+ "匪徒服, 细条纹西装, 牛仔帽, 二十年代派对": "gangster costume, pinstripe suit, fedora hat, 1920s-themed party",
+ "羚羊服, 鹿角, 优雅的连身衣, 野生动物保护活动": "gazelle costume, antlers, graceful jumpsuit, wildlife conservation event",
+ "艺伎服, 华丽的和服, 文化节庆活动": "geisha costume, ornate kimono,cultural festival",
+ "哥特式服, 黑色蕾丝, 哥特主题活动": "gothic costume, black lace, Gothic-themed event",
+ "毕业服, 帽子和学士服, 流动, 毕业典礼": "graduation costume, cap and gown, tassel, graduation ceremony",
+ "希腊服, 长袍, 月桂花环, 罗马派对": "greek costume, flowing robes, laurel wreath, toga party",
+ "吉普赛服, 多彩裙子, 硬币饰品, 文化博览会": "gypsy costume, colorful skirts, coin accessories, cultural fair",
+ "老巫婆服, 破烂的长裙, 疣疤, 闹鬼屋": "hag costume, tattered dress, warts, haunted house",
+ "万圣节服, 鬼怪装束, 恐怖妆容, 不给糖就捣蛋": "halloween costume, spooky attire, , trick-or-treating",
+ "传令官服, 帝王装束, 喇叭, 中世纪节日": "herald costume, regal attire, trumpet, medieval festival",
+ "嬉皮服, 扎染衬衫, 铃形喇叭裤, 音乐节": "hippie costume, tie-dye shirt, bell-bottom pants, music festival",
+ "呼拉舞者服, 草裙, 花环, 起波派对": "hula dancer costume, grass skirt, flower lei, luau party",
+ "印第安服, 羽毛头饰, 流苏裙, 文化展览": "indian costume, feather headdress, fringed dress, cultural exhibition",
+ "爱尔兰服, 绿色装束, 三叶草饰品, 圣帕特里克节游行": "irish costume, green attire, shamrock accessories, St. Patrick's Day parade",
+ "南瓜灯服, 南瓜形状装束, 发光的眼睛, 万圣节游行": "jack-o-lantern costume, pumpkin-shaped outfit, glowing eyes, Halloween parade",
+ "日本服, 和服, 带子, 樱花节": "japanese costume, kimono, obi belt, cherry blossom festival",
+ "小丑帽服, 多彩的杂色衣, 叮当铃铛, 文艺复兴集市": "jester costume, colorful motley, jingling bells, Renaissance fair",
+ "国王服, 帝王袍, 金色皇冠, 服装舞会": "king costume, regal robe, golden crown, costume ball",
+ "小猫服, 蓬松耳朵, 胡须, 宠物服装竞赛": "kitty costume, fluffy ears, whiskers, pet costume contest",
+ "骑士服, 闪闪发光的盔甲, 头盔, 中世纪城堡": "knight costume, shining armor, helmet, medieval castle",
+ "妖精服, 绿色西装, 高顶礼帽, 圣帕特里克节庆祝活动": "leprechaun costume, green suit, top hat, St. Patrick's Day celebration",
+ "狮子服, 毛茸茸的鬃毛, 狮子造型, 野生动物主题派对": "lion costume, furry mane, lion , wildlife-themed party",
+ "小红帽服, 红色斗篷, 篮子, 童话主题派对": "little red riding hood costume, red cloak, basket, fairy tale-themed party",
+ "夏威夷服, 草裙, 花环冠, 夏威夷派对": "luau costume, grass skirt, flower crown, Hawaiian-themed luau",
+ "女仆服, 褶边围裙, 蕾丝细节, 服装派对": "maid costume, frilled apron, lace details, costume party",
+ "狂欢节服, 鲜艳色彩, 羽毛饰品, 节日游行": "mardi gras costume, vibrant colors, feathered , festive parade",
+ "中世纪服, 铁链甲, 剑, 文艺复兴集市": "medieval costume, chainmail, sword, Renaissance fair",
+ "美人鱼服, 色彩斑斓的鳞片, 贝壳胸罩, 水下主题活动": "mermaid costume, iridescent scales, seashell bra, underwater-themed event",
+ "红磨坊服, 紧身胸衣, 羽毛头饰, 卡巴莱派对": "moulin rouge costume, corset, feathered headpiece, cabaret-style party",
+ "美洲原住民服, 流苏裙, 羽毛头饰, 文化节庆活动": "native american costume, fringe dress, feather headdress, cultural festival",
+ "忍者服, 黑色服装, 面罩, 武术表演": "ninja costume, black attire, , martial arts demonstration",
+ "修女服, 黑色修道服, 白色领子, 戏剧表演": "nun costume, black habit, white collar, theatrical performance",
+ "护士服, 白色连衣裙, 护士帽, 医学主题派对": "nurse costume, white dress, nurse cap, medical-themed party",
+ "东方服, 丝质长袍, 扇子, 文化展览": "oriental costume, silk robe, fan, cultural exhibition",
+ "选美服, 亮片礼服, 王冠, 选美比赛": "pageant costume, sequined gown, tiara, beauty pageant",
+ "皇宫卫兵服, 帝宫制服, 熊皮帽, 皇家仪式": "palace guard costume, regal uniform, bearskin hat, royal ceremony",
+ "熊猫服, 毛茸茸, 竹子, 服装竞赛": "panda costume, furry, bamboo, costume contest",
+ "伞兵服, 迷彩连体服, 降落伞, 军事主题活动": "paratrooper costume, camo jumpsuit, parachute, military-themed event",
+ "爱国服, 受国旗启发的装束, 帽子, 独立日庆祝": "patriotic costume, flag-inspired attire, hat, Independence Day celebration",
+ "农民服, 简单裙装, 草帽, 历史重现活动": "peasant costume, simple dress, straw hat, historical reenactment",
+ "法老服, 华丽头饰, 金色配饰, 古埃及主题派对": "pharaoh costume, ornate headdress, golden accessories, ancient Egypt-themed party",
+ "清教徒服, 黑色帽子, 白色领子, 感恩节表演": "pilgrim costume, black hat, white collar, Thanksgiving play",
+ "海盗服, 眼罩, 三角帽, 海盗主题派对": "pirate costume, eye patch, tricorn hat, pirate-themed party",
+ "海盗女郎服, 褶边裙装, 大头巾, 海盗主题活动": "pirate wench costume, ruffled dress, bandana, pirate-themed event",
+ "警察官服, 制服, 胸牌, 服装游行": "police officer costume, uniform, badge, costume parade",
+ "女警官服, 警察制服, 帽子, 破案活动": "policewoman costume, police uniform, hat, crime-solving event",
+ "公主服, 花瓣裙, 王冠, 皇家舞会": "princess costume, ballgown, tiara, royal ball",
+ "皇后服, 优雅礼服, 皇冠, 服装盛会": "queen costume, elegant gown, crown, costume gala",
+ "校园女生服, 格纹裙, 白衬衫, 服装派对": "school girl costume, plaid skirt, white blouse, costume party",
+ "蜘蛛女侠服, 蜘蛛网纹连体衣, 蜘蛛标志, 漫画大会": "spider-woman costume, web-patterned suit, spider symbol, comic convention",
+ "超级英雄服, 彩色披风, 有特殊标志, 超级英雄主题活动": "superhero costume, colorful cape, , superhero-themed event",
+ "吸血鬼服, 黑色披风, 服装舞会": "vampire costume, black cape, costume ball"
+ }
+}
\ No newline at end of file
diff --git a/js/config/新服饰.json b/js/config/新服饰.json
new file mode 100644
index 0000000..5d91c6e
--- /dev/null
+++ b/js/config/新服饰.json
@@ -0,0 +1,472 @@
+{
+ "上装": {
+ "连衣裙": "dress",
+ "裙子": "skirt",
+ "衬衫": "blouse",
+ "t 恤": "t-shirt",
+ "毛衣": "sweater",
+ "夹克": "jacket",
+ "背心": "tank top",
+ "卫衣": "hoodie",
+ "马甲": "vest",
+ "风衣": "trench coat",
+ "西装": "suit",
+ "骑士夹克": "biker jacket",
+ "外套": "coat",
+ "罩衫": "shawl",
+ "披肩": "cape",
+ "庞乔斗篷": "poncho",
+ "大衣": "overcoat",
+ "毛皮大衣": "fur coat",
+ "斗篷": "cape",
+ "披风": "cloak",
+ "皮衣": "leather jacket"
+ },
+ "下装": {
+ "牛仔裤": "jeans",
+ "裤子": "pants",
+ "连体裤": "jumpsuit",
+ "短裤": "shorts",
+ "长裤": "trousers",
+ "裙子": "skirt",
+ "打底裤": "leggings",
+ "紧身裤": "leggings",
+ "便裤": "slacks",
+ "阔腿裤": "wide-leg pants",
+ "七分裤": "capri pants",
+ "短裙": "mini skirt",
+ "中裙": "midi skirt",
+ "长裙": "maxi skirt",
+ "运动裤": "sweatpants",
+ "卡其裤": "khaki pants",
+ "牛仔短裤": "denim shorts",
+ "裙裤": "skort",
+ "束脚裤": "joggers",
+ "瑜伽裤": "yoga pants",
+ "哈伦裤": "harem pants"
+ },
+ "泳装": {
+ "泳衣": "swimsuit",
+ "比基尼": "bikini",
+ "连体泳衣": "one-piece swimsuit",
+ "泳裙": "swim skirt",
+ "沙滩裙": "beach cover-up",
+ "遮阳衣": "rash guard",
+ "泳裤": "swim shorts",
+ "泳帽": "swim cap",
+ "浮潜套装": "snorkel set",
+ "潜水衣": "wetsuit",
+ "潜水裤": "wetsuit pants",
+ "冲浪裤": "boardshorts"
+ },
+ "运动装": {
+ "运动文胸": "sports bra",
+ "运动上衣": "sports top",
+ "运动裤": "sports pants",
+ "运动短裤": "athletic shorts",
+ "运动紧身衣": "compression suit",
+ "运动套装": "athletic set",
+ "运动连衣裙": "athletic dress",
+ "健身服": "gym wear",
+ "跑步裤": "running pants",
+ "篮球裤": "basketball shorts",
+ "网球裤": "tennis shorts",
+ "足球裤": "soccer shorts",
+ "高尔夫服": "golf attire",
+ "瑜伽服": "yoga wear",
+ "太极服": "tai chi suit",
+ "拳击装": "boxing attire",
+ "羽毛球服": "badminton attire",
+ "游泳裤": "swim trunks",
+ "健身紧身衣": "fitness tights",
+ "长跑服": "marathon suit"
+ },
+ "内衣": {
+ "文胸": "bra",
+ "内裤": "panties",
+ "丁字裤": "thong",
+ "连体内衣": "bodysuit",
+ "睡衣": "pajamas",
+ "底裤": "long johns",
+ "吊袜带": "garter belt",
+ "情趣内衣": "lingerie",
+ "背心": "camisole",
+ "泳衣内衣": "swimwear bra",
+ "塑身内衣": "shapewear",
+ "胸衣": "bustier",
+ "纹身文胸": "tattoo bra",
+ "丰胸垫": "push-up pad",
+ "吊带内衣": "strapless bra",
+ "无肩带文胸": "convertible bra",
+ "无线文胸": "wireless bra",
+ "无痕内衣": "seamless underwear",
+ "泳装内衣": "swimwear bra",
+ "小胸文胸": "small cup bra",
+ "大胸文胸": "large cup bra"
+ },
+ "配饰": {
+ "项链": "necklace",
+ "耳环": "earrings",
+ "手链": "bracelet",
+ "戒指": "ring",
+ "围巾": "scarf",
+ "帽子": "hat",
+ "手套": "gloves",
+ "皮带": "belt",
+ "太阳镜": "sunglasses",
+ "包包": "handbag",
+ "披肩": "shawl",
+ "发夹": "hairpin",
+ "发带": "headband",
+ "项链套装": "necklace set",
+ "手表": "watch",
+ "腰链": "waist chain",
+ "胸针": "brooch",
+ "手镯": "bangle",
+ "项链链子": "chain necklace",
+ "胸花": "corsage",
+ "围巾固定器": "scarf clip"
+ },
+ "鞋类": {
+ "高跟鞋": "high heels",
+ "运动鞋": "sneakers",
+ "平底鞋": "flats",
+ "靴子": "boots",
+ "凉鞋": "sandals",
+ "拖鞋": "slippers",
+ "尖头鞋": "pointed-toe shoes",
+ "帆布鞋": "canvas shoes",
+ "平底便鞋": "loafers",
+ "牛仔鞋": "cowboy boots",
+ "坡跟鞋": "wedges",
+ "男鞋": "oxfords",
+ "马丁靴": "ankle boots",
+ "水鞋": "water shoes",
+ "人字拖": "flip-flops",
+ "木底拖鞋": "clogs",
+ "运动凉鞋": "sport sandals",
+ "豆豆鞋": "penny loafers"
+ },
+ "睡衣": {
+ "睡衣": "pajamas",
+ "睡袍": "robe",
+ "睡裙": "nightgown",
+ "睡袍套装": "robe set",
+ "家居服": "loungewear",
+ "居家裤": "lounging pants",
+ "睡裤": "sleep shorts",
+ "睡裙套装": "nightgown set",
+ "短睡裤套装": "sleep shorts set",
+ "居家内衣": "lounging lingerie",
+ "睡裤套装": "sleep shorts set",
+ "夜袍": "night robe",
+ "夜裙": "evening dress"
+ },
+ "帽子": {
+ "帽子": "hat",
+ "棒球帽": "baseball cap",
+ "遮阳帽": "sun hat",
+ "渔夫帽": "bucket hat",
+ "毛线帽": "beanie",
+ "礼帽": "top hat",
+ "鸭舌帽": "duckbill cap",
+ "贝雷帽": "beret",
+ "军帽": "military cap",
+ "斗篷": "cape",
+ "围巾": "scarf",
+ "披肩": "shawl",
+ "发带": "headband",
+ "发夹": "hairpin",
+ "草帽": "straw hat",
+ "兔毛帽": "fur hat",
+ "军官帽": "officer's cap",
+ "帽套": "hat cover",
+ "卡其帽": "newsboy cap",
+ "洪堡帽": "homburg hat",
+ "英式帽": "bowler hat",
+ "圆顶礼帽": "derby hat"
+ },
+ "连袜裤": {
+ "连袜裤": "tights",
+ "裤袜": "pantyhose",
+ "厚连裤袜": "opaque tights",
+ "薄连裤袜": "sheer tights",
+ "蕾丝连袜裤": "lace tights",
+ "鱼网袜": "fishnet tights",
+ "中筒袜": "knee-high socks",
+ "长筒袜": "thigh-high socks",
+ "踝筒袜": "ankle socks",
+ "脚趾袜": "toe socks",
+ "冬季连袜裤": "winter tights",
+ "体育袜": "sport socks",
+ "格子连袜裤": "plaid tights",
+ "格子袜": "argyle socks",
+ "袜套": "sock covers",
+ "睡袜": "sleep socks",
+ "美腿袜": "leggings"
+ },
+ "围巾": {
+ "围巾": "scarf",
+ "丝巾": "silk scarf",
+ "披肩": "shawl",
+ "围巾固定器": "scarf clip",
+ "领巾": "neckerchief",
+ "围巾环": "scarf ring",
+ "围脖": "snood",
+ "飘带": "streamer",
+ "绒线围巾": "knit scarf",
+ "披风": "cape",
+ "莱卡围巾": "lycra scarf",
+ "领巾固定器": "neckerchief clip",
+ "鸟巾": "boa",
+ "围巾链子": "scarf chain"
+
+
+ },
+ "丝袜":{
+ "丝袜连裤袜": "pantyhose",
+ "网纹丝袜": "fishnet stockings",
+ "压力丝袜": "compression stockings",
+ "无缝丝袜": "seamless stockings",
+ "花边丝袜": "lace stockings",
+ "大网格丝袜": "large fishnet stockings",
+ "薄款丝袜": "sheer stockings",
+ "加强型丝袜": "reinforced stockings",
+ "超薄丝袜": "ultra-sheer stockings",
+ "丝袜袜子": "stocking socks",
+ "丝袜裤袜": "tights",
+ "蕾丝丝袜": "lace tights",
+ "鱼网丝袜": "fishnet tights",
+ "肉色丝袜": "nude stockings",
+ "彩色丝袜": "colored stockings",
+ "黑色丝袜": "black stockings",
+ "白色丝袜": "white stockings",
+ "红色丝袜": "red stockings",
+ "粉色丝袜": "pink stockings",
+ "紫色丝袜": "purple stockings",
+ "蓝色丝袜": "blue stockings",
+ "绿色丝袜": "green stockings",
+ "黄色丝袜": "yellow stockings",
+ "灰色丝袜": "gray stockings",
+ "褐色丝袜": "brown stockings",
+ "条纹丝袜": "striped stockings",
+ "格子丝袜": "checkered stockings",
+ "花卉丝袜": "floral stockings",
+ "圆点丝袜": "polka dot stockings",
+ "复古丝袜": "vintage stockings",
+ "半透明丝袜": "semi-opaque stockings",
+ "珠光丝袜": "shimmer stockings",
+ "亮片丝袜": "glitter stockings",
+ "珍珠丝袜": "pearl stockings",
+ "水钻丝袜": "rhinestone stockings",
+ "厚款丝袜": "opaque stockings",
+ "坎肩丝袜": "cowl neck stockings",
+ "连体丝袜": "bodystocking",
+ "性感丝袜": "sexy stockings",
+ "丝袜内衣套装": "lingerie and stockings set",
+ "高腰丝袜": "high-waisted stockings",
+ "丝袜配饰": "stocking accessories",
+ "网纹袜套": "fishnet leg warmers",
+ "丝袜腿套": "stocking leg warmers",
+ "丝袜吊带": "stocking garters"
+ },
+ "深V":{
+ "深V领连衣裙": "deep v-neck dress",
+ "深V领毛衣": "deep v-neck sweater",
+ "深V领上衣": "deep v-neck top",
+ "深V领连体裤": "deep v-neck jumpsuit",
+ "深V领紧身衣": "deep v-neck bodysuit",
+ "深V领背心": "deep v-neck camisole",
+ "深V领吊带连衣裙": "deep v-neck halter dress",
+ "深V领连身短裤": "deep v-neck romper",
+ "深V领礼服连衣裙": "deep v-neck evening gown",
+ "深V领薄款针织衫": "deep v-neck lightweight knit sweater",
+ "深V领运动上衣": "deep v-neck athletic top",
+ "深V领丝绒连衣裙": "deep v-neck velvet dress",
+ "深V领夏季连衣裙": "deep v-neck summer dress",
+ "深V领短袖衬衫": "deep v-neck short sleeve shirt",
+ "深V领包裹裙": "deep v-neck bodycon skirt",
+ "深V领流苏裙": "deep v-neck fringe skirt",
+ "深V领蕾丝连衣裙": "deep v-neck lace dress",
+ "深V领连衣裙套装": "deep v-neck dress set",
+ "深V领吊带上衣": "deep v-neck halter top",
+ "深V领连衣裙配饰": "deep v-neck dress accessories",
+ "深V领连衣裙外套": "deep v-neck dress coat",
+ "深V领连衣裙帽衫": "deep v-neck dress hooded sweater",
+ "深V领拼接连衣裙": "deep v-neck patchwork dress",
+ "深V领卫衣连衣裙": "deep v-neck hoodie dress",
+ "深V领夜店装": "deep v-neck clubwear",
+ "深V领网纱连衣裙": "deep v-neck mesh dress",
+ "深V领迷彩连衣裙": "deep v-neck camouflage dress",
+ "深V领蕾丝上衣": "deep v-neck lace top",
+ "深V领透明连衣裙": "deep v-neck sheer dress",
+ "深V领复古连衣裙": "deep v-neck vintage dress",
+ "深V领长款连衣裙": "deep v-neck long dress",
+ "深V领迷你连衣裙": "deep v-neck mini dress",
+ "深V领宴会装": "deep v-neck cocktail dress",
+ "深V领无袖连衣裙": "deep v-neck sleeveless dress",
+ "深V领绒线连衣裙": "deep v-neck knit dress",
+ "深V领运动连衣裙": "deep v-neck athletic dress",
+ "深V领修身连衣裙": "deep v-neck fitted dress",
+ "深V领皮质连衣裙": "deep v-neck leather dress",
+ "深V领连衣裙裙子": "deep v-neck dress skirt",
+ "深V领裹胸连衣裙": "deep v-neck wrap dress",
+ "深V领露肩连衣裙": "deep v-neck off-shoulder dress",
+ "深V领背带连衣裙": "deep v-neck suspender dress",
+ "深V领夏日连衣裙": "deep v-neck summer dress",
+ "深V领金属纹裙": "deep v-neck metallic skirt",
+ "深V领高腰连衣裙": "deep v-neck high-waisted dress",
+ "深V领包裹上衣": "deep v-neck bodycon top"
+ },
+ "包臀":{
+ "包臀连衣裙": "bodycon dress",
+ "包臀裙": "bodycon skirt",
+ "包臀上衣": "bodycon top",
+ "包臀连体裤": "bodycon jumpsuit",
+ "包臀紧身裙": "bodycon skirt",
+ "包臀礼服": "bodycon gown",
+ "包臀针织裙": "knit bodycon dress",
+ "包臀运动短裤": "sports booty shorts",
+ "包臀派对服": "party bodycon dress",
+ "包臀露背连衣裙": "backless bodycon dress",
+ "包臀剧院装": "theater bodycon dress",
+ "包臀半身裙": "mini bodycon skirt",
+ "包臀连衣裙套装": "bodycon dress set",
+ "包臀纹理裙": "textured bodycon skirt",
+ "包臀夏季连衣裙": "summer bodycon dress",
+ "包臀针织上衣": "knit bodycon top",
+ "包臀夏日裙子": "summer bodycon dress",
+ "包臀毛织裙": "knit bodycon skirt",
+ "包臀花纹裙": "patterned bodycon dress",
+ "包臀网纱连衣裙": "mesh bodycon dress",
+ "包臀迷你连衣裙": "mini bodycon dress",
+ "包臀薄款上衣": "lightweight bodycon top",
+ "包臀复古连衣裙": "vintage bodycon dress",
+ "包臀夜店装": "clubwear bodycon dress",
+ "包臀蕾丝裙": "lace bodycon skirt",
+ "包臀宴会装": "cocktail bodycon dress",
+ "包臀皮质短裙": "leather bodycon skirt",
+ "包臀纯色连衣裙": "solid color bodycon dress",
+ "包臀迷你半身裙": "mini bodycon skirt",
+ "包臀吊带连衣裙": "strapless bodycon dress",
+ "包臀连衣裙配饰": "bodycon dress accessories",
+ "包臀高腰裙": "high-waisted bodycon skirt",
+ "包臀透明连衣裙": "sheer bodycon dress",
+ "包臀金属纹裙": "metallic bodycon skirt",
+ "包臀卫衣连衣裙": "hooded bodycon dress",
+ "包臀深V领连衣裙": "deep v-neck bodycon dress",
+ "包臀丝绒连衣裙": "velvet bodycon dress",
+ "包臀迷你连衣裙套装": "mini bodycon dress set",
+ "包臀牛仔短裙": "denim bodycon skirt",
+ "包臀低背连衣裙": "low-back bodycon dress",
+ "包臀修身连衣裙": "fitted bodycon dress",
+ "包臀雪纺连衣裙": "chiffon bodycon dress",
+ "包臀长款连衣裙": "long bodycon dress",
+ "包臀背心上衣": "sleeveless bodycon top",
+ "包臀无袖连衣裙": "sleeveless bodycon dress",
+ "包臀蕾丝连衣裙": "lace bodycon dress",
+ "包臀紧身衬衫": "bodycon shirt",
+ "包臀绒线连衣裙": "knit bodycon dress",
+ "包臀薄纱连衣裙": "chiffon bodycon dress",
+ "包臀高领连衣裙": "high-neck bodycon dress",
+ "包臀纯色半身裙": "solid color bodycon skirt"
+ },
+ "蕾丝":{
+ "蕾丝连衣裙": "lace dress",
+ "蕾丝上衣": "lace top",
+ "蕾丝裙子": "lace skirt",
+ "蕾丝连体裤": "lace jumpsuit",
+ "蕾丝裤子": "lace pants",
+ "蕾丝外套": "lace jacket",
+ "蕾丝背心": "lace camisole",
+ "蕾丝内衣": "lace lingerie",
+ "蕾丝袜子": "lace stockings",
+ "蕾丝长袜": "lace thigh-high socks",
+ "蕾丝长袖上衣": "lace long-sleeve top",
+ "蕾丝吊带上衣": "lace camisole top",
+ "蕾丝连衣裙配饰": "lace dress accessories",
+ "蕾丝短袖上衣": "lace short-sleeve top",
+ "蕾丝披肩": "lace shawl",
+ "蕾丝长袖裙": "lace long-sleeve dress",
+ "蕾丝袖套": "lace sleeves",
+ "蕾丝贴边裙": "lace-edged skirt",
+ "蕾丝花边上衣": "lace trim top",
+ "蕾丝贴边连衣裙": "lace-edged dress",
+ "蕾丝夏季裙": "lace summer dress",
+ "蕾丝春季外套": "lace spring jacket",
+ "蕾丝晚礼服": "lace evening gown",
+ "蕾丝迷你裙": "lace mini skirt",
+ "蕾丝修身裙": "lace bodycon skirt",
+ "蕾丝冬季外套": "lace winter jacket",
+ "蕾丝紧身上衣": "lace fitted top",
+ "蕾丝包臀连衣裙": "lace bodycon dress",
+ "蕾丝裙裤套装": "lace skirt and pants set",
+ "蕾丝夜店装": "lace clubwear",
+ "蕾丝流苏裙": "lace fringe skirt",
+ "蕾丝露背连衣裙": "lace backless dress",
+ "蕾丝深V领上衣": "lace deep v-neck top",
+ "蕾丝高腰裙": "lace high-waisted skirt",
+ "蕾丝透明连衣裙": "lace sheer dress",
+ "蕾丝圆领上衣": "lace round neck top",
+ "蕾丝半袖连衣裙": "lace short-sleeve dress",
+ "蕾丝花边连衣裙": "lace trim dress",
+ "蕾丝低背连衣裙": "lace low-back dress",
+ "蕾丝包臀上衣": "lace bodycon top",
+ "蕾丝薄纱连衣裙": "lace chiffon dress",
+ "蕾丝针织裙": "lace knit skirt",
+ "蕾丝连衣裙外套": "lace dress coat",
+ "蕾丝背心上衣": "lace camisole top"
+ },
+ "裙子":{
+ "连衣裙": "dress",
+ "迷你裙": "miniskirt",
+ "中裙": "midi skirt",
+ "长裙": "maxi skirt",
+ "半身裙": "skirt",
+ "包臀裙": "bodycon skirt",
+ "百褶裙": "pleated skirt",
+ "牛仔裙": "denim skirt",
+ "礼服裙": "gown",
+ "夏季裙": "summer dress",
+ "冬季裙": "winter dress",
+ "花卉裙": "floral dress",
+ "印花裙": "printed dress",
+ "纱裙": "chiffon dress",
+ "针织裙": "knit skirt",
+ "派对裙": "party dress",
+ "背心裙": "camisole dress",
+ "吊带裙": "halter dress",
+ "圆领裙": "round neck dress",
+ "蕾丝裙": "lace dress",
+ "修身裙": "fitted skirt",
+ "流苏裙": "fringe skirt",
+ "包裹裙": "wrap dress",
+ "无袖裙": "sleeveless dress",
+ "宴会裙": "cocktail dress",
+ "露肩裙": "off-shoulder dress",
+ "连体裙": "jumpsuit",
+ "迷你连衣裙": "minidress",
+ "深V领连衣裙": "deep v-neck dress",
+ "丝绒裙": "velvet dress",
+ "复古裙": "vintage dress",
+ "亮片裙": "sequin skirt",
+ "亮丝裙": "shimmer dress",
+ "格子裙": "plaid skirt",
+ "牛仔迷你裙": "denim miniskirt",
+ "棉质连衣裙": "cotton dress",
+ "运动裙": "athletic skirt",
+ "低背连衣裙": "low-back dress",
+ "高领连衣裙": "turtleneck dress",
+ "珍珠裙": "pearl skirt",
+ "水钻裙": "rhinestone skirt",
+ "条纹裙": "striped skirt",
+ "亮片迷你裙": "sequin miniskirt",
+ "印花迷你裙": "printed miniskirt",
+ "薄纱连衣裙": "chiffon minidress",
+ "无袖连衣裙": "sleeveless minidress",
+ "背带连衣裙": "suspender dress",
+ "麂皮迷你裙": "suede miniskirt",
+ "纯色连衣裙": "solid color dress"
+ }
+
+}
\ No newline at end of file
diff --git a/js/config/景观.json b/js/config/景观.json
new file mode 100644
index 0000000..63d8784
--- /dev/null
+++ b/js/config/景观.json
@@ -0,0 +1,99 @@
+{
+ "自然景观":{
+ "黄石国家公园": "yellowstone national park",
+ "大峡谷": "grand canyon",
+ "珠穆朗玛峰": "mount everest",
+ "大堡礁": "great barrier reef",
+ "撒哈拉沙漠": "sahara desert",
+ "亚马逊雨林": "amazon rainforest",
+ "伊瓜苏大瀑布": "iguazu falls",
+ "维多利亚瀑布": "victoria falls",
+ "死海": "dead sea",
+ "科莫多国家公园": "komodo national park",
+ "加拉帕戈斯群岛": "galapagos islands",
+ "乞力马扎罗山": "mount kilimanjaro",
+ "诺罗威国家公园": "norwegian fjords",
+ "巴厘岛": "bali",
+ "安第斯山脉": "andes",
+ "摩洛哥蓝色小镇": "chefchaouen",
+ "澳大利亚珊瑚海": "coral sea",
+ "阿尔卑斯山": "alps",
+ "巴塔哥尼亚": "patagonia",
+ "哈里森湖洞穴": "harrison's cave",
+ "泰姬陵": "taj mahal",
+ "尼亚加拉大瀑布": "niagara falls",
+ "极光": "aurora borealis",
+ "喜马拉雅山脉": "himalayas",
+ "乌鲁鲁": "uluru",
+ "黄金海岸": "gold coast",
+ "马尔代夫": "maldives",
+ "马丘比丘": "machu picchu",
+ "塞尔维亚的德里纳河": "drina river"
+ },
+ "人造景观":{
+ "万里长城": "great wall of china",
+ "自由女神像": "statue of liberty",
+ "埃菲尔铁塔": "eiffel tower",
+ "大本钟": "big ben",
+ "白金汉宫": "buckingham palace",
+ "圣彼得大教堂": "st. peter's basilica",
+ "金门桥": "golden gate bridge",
+ "古罗马斗兽场": "colosseum",
+ "悉尼歌剧院": "sydney opera house",
+ "伦敦眼": "london eye",
+ "克里姆林宫": "kremlin",
+ "红场": "red square",
+ "莫斯科圣瓦西里大教堂": "saint basil's cathedral",
+ "比萨斜塔": "leaning tower of pisa",
+ "卢浮宫": "louvre",
+ "凡尔赛宫": "palace of versailles",
+ "尼斯湖": "loch ness",
+ "圣保罗大教堂": "st. paul's cathedral",
+ "埃德蒙顿购物中心": "west edmonton mall",
+ "布拉格城堡": "prague castle",
+ "新天鹅堡": "neuschwanstein castle",
+ "泰姬陵": "taj mahal",
+ "马丘比丘": "machu picchu",
+ "克里斯特救世像": "christ the redeemer",
+ "巴比伦空中花园": "hanging gardens of babylon",
+ "亚历山大灯塔": "lighthouse of alexandria",
+ "宙斯神庙": "temple of artemis",
+ "摩洛哥蓝色小镇": "chefchaouen",
+ "阿尔罕布拉宫": "alhambra",
+ "佩特拉古城": "petra",
+ "布拉格天文钟": "prague astronomical clock",
+ "春秋战国时期的秦始皇兵马俑": "terracotta army",
+ "埃及金字塔": "egyptian pyramids",
+ "吉萨大金字塔": "great pyramid of giza",
+ "斯芬克斯": "sphinx",
+ "巴黎圣母院": "notre-dame de paris",
+ "威尼斯水城": "venice",
+ "阿姆斯特丹运河": "amsterdam canals",
+ "巴塞罗那圣家堂": "sagrada familia",
+ "米兰大教堂": "milan cathedral",
+ "维也纳国家歌剧院": "vienna state opera",
+ "比利时布鲁日的布鲁日广场": "markt square bruges",
+ "荷兰风车": "dutch windmills",
+ "巴塞罗那哥特区": "gothic quarter barcelona",
+ "佛罗伦萨大教堂": "florence cathedral",
+ "慕尼黑啤酒节": "oktoberfest munich",
+ "巴拿马运河": "panama canal",
+ "好莱坞标志": "hollywood sign",
+ "帝国大厦": "empire state building",
+ "自然历史博物馆": "natural history museum",
+ "史密森尼博物馆": "smithsonian museum",
+ "卢浮宫金字塔": "louvre pyramid",
+ "迪士尼乐园": "disneyland",
+ "百老汇": "broadway",
+ "时代广场": "times square",
+ "拉斯维加斯大道": "las vegas strip",
+ "旧金山唐人街": "chinatown san francisco",
+ "尼亚加拉瀑布": "niagara falls",
+ "肯辛顿宫": "kensington palace",
+ "伦敦塔桥": "tower bridge",
+ "圣马可广场": "st. mark's square"
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/js/config/艺术、魔法.json b/js/config/艺术、魔法.json
new file mode 100644
index 0000000..48423b4
--- /dev/null
+++ b/js/config/艺术、魔法.json
@@ -0,0 +1,129 @@
+{
+ "艺术风格": {
+ "8bit游戏": "8 Bit Game",
+ "80动画": "1980s anime",
+ "迪士尼电影": "disney movie",
+ "哥特摇滚": "goth",
+ "80电影": "80s movie",
+ "泡泡龙": "bubble bobble",
+ "皮克斯动画": "style of Pixar",
+ "宝丽来艺术": "Polaroid art",
+ "万花筒摄影": "Kaleidoscope Photography",
+ "欧泊渲染": "opal render",
+ "色谱图": "chemigram",
+ "吉卜力风格": "Studio Ghibli",
+ "梦幻": "dreamlike",
+ "签绘风格": "(faux traditional media)",
+ "原神冲击": "genshin impact",
+ "碧蓝航线": "azur lane",
+ "舰队收藏": "kantai collection",
+ "边缘行者": "rebecca (cyberpunk)",
+ "电锯人": "chainsaw man",
+ "魔法旋涡": "Magic Vortex",
+ "柴油朋克": "((dieselpunk))",
+ "四格": "4koma",
+ "杂志扫描": "magazine scan",
+ "专辑封面": "album cover",
+ "线条变粗": "(lineart)",
+ "蒸汽波": "synthwave",
+ "洛可可": "(illustration),(paper figure),(lococo),((impasto)),(shiny skin)"
+ },
+ "艺术类型": {
+ "单色图片": "monochrome",
+ "拼贴艺术": "Collage",
+ "彩色玻璃": "Dalle de verre",
+ "像素画": "pixel art",
+ "瓷画": "Encaustic painting",
+ "水墨画": "Ink wash painting",
+ "铜版雕刻": "Mezzotint",
+ "剪影": "silhouette",
+ "插画": "illustration",
+ "水彩画": "(((ink))), ((watercolor))",
+ "浮世绘": "illustration,(((ukiyoe))),((sketch)),((japanese_art))",
+ "油画": "((dyeing)),((oil painting)),((impasto))",
+ "黑白草图": "(posing sketch), (monochrome)",
+ "手画草稿": "sketch",
+ "铅笔速写": "(monochrome), (gray scale), (pencil sketch lines)",
+ "彩铅画": "(watercolor pencil)"
+ },
+ "艺术派系": {
+ "新艺术主义": "((art nouveau))",
+ "古典主义": "((classicism))",
+ "未来主义": "((futurism))",
+ "达达主义": "((Dadaism))",
+ "抽象艺术": "((abstract art))",
+ "ASCII艺术": "((ASCII art))",
+ "穆夏风格": "((alphonse mucha))",
+ "莫奈风格": "((Monet style))"
+ },
+ "魔法1.0": {
+ "水魔法": "((water drops)),wet clothes,((beautiful detailed water)),((floating)),dynamic angle",
+ "冰魔法": "beautiful detailed glow, (detailed ice), beautiful detailed water",
+ "冰系改": "beautiful detailed glow,detailed ice,beautiful detailed water,(floating palaces:1.2),(ice crystal texture wings),(Iridescence and rainbow hair:2.5)",
+ "星冰乐": "beautiful detailed glow,detailed ice,beautiful detailed water, (floating palaces:1.3),(((ice crystal texture wings)))",
+ "森林冰": "((detailed beautiful snow forest with trees)), ((snowflakes)), floating",
+ "结晶法": "(((crystals texture Hair))),((beautiful detailed glass hair)),((glass shaped texture hand)),((crystallize texture body)),gem body,hands as clear as jewels,crystallization of clothes,((crystals texture skin)),sparkle, lens flare, light leaks,broken glass,detailed glass shaped clothes,beautiful detailed gemstone sky, gemstone sea, crystals texture flowers,((detailed crystallized clothing))",
+ "核爆法": "beautiful detailed glow,((flames of war)),(((nuclear explosion behide)))",
+ "风魔法": "((breeze)), flying splashes, flying petals, wind",
+ "流沙法": "((surrounded by heavy floating sand flow and floating sharp stones)),((((ink)))),((illustration)),((watercolor))",
+ "雷电法": "(detailed light), ((lightning in hand)),lightning surrounds,(((lightning chain)))",
+ "圣光法": "(sunlight),(angel),dynamic angle, floating, wing, halo, floating white silk,(Holy Light),silver stars",
+ "苇名法": "beautiful detailed pampas grass field, open hakama, surrounded by floating sakura, yellow full moon, beautiful detailed dark midnight sky, messy white long hair",
+ "自然法": "beautiful and delicate water, the finest grass, very delicate light, nature, painting, water spray, breeze, flowers and grass meadow, near the water edge, (sunset, starry sky in a circle), randomly distributed clouds, river, splashing water, falling petals",
+ "森林法": "(detailed light) , feather, leaves, nature, (sunlight), river, (forest),(bloom)",
+ "虹彩法": "floating and rainbow long hair,Iridescence and rainbow, beautiful detailed starry sky",
+ "暗锁法": "chain ring, chain storm, dark chain,((wholeblack bloomer)), darkside, night, deep dark, darkness, ((dark clouds)),((ruins)),shadow, death garden",
+ "火烧云": "beautiful detailed glow, floating ashes, beautiful and detailed explosion, red moon, fire, fire cloud, wings on fire, a cloudy sky, smoke of gunpowder, burning, black dress, dove of peace, (floating cloud:1.2)",
+ "城堡法": "beautiful detailed glow, detailed ice, beautiful detailed water, (magic circle:1.2), (floating palaces:1.3)",
+ "黄昏法": "(water bloom), (delicate glow), (breeze), long Flowers meadow,(((sunset)), (less stars form a circle), randomly distributed clouds, (rivers), (willows with branches falling into the water)",
+ "泡泡法": "((colorful bubble)),(floating),detailed light",
+ "蔷薇法": "((rose)), (vine), cage, bandage, red rope, (detail light), falling rose petals",
+ "星空法": "(starry tornado:1.4), starry Nebula, beautiful detailed sky",
+ "月亮法": "((moon)), starry sky, (lighting particle), fog, snow,(bloom)",
+ "雪月法": "beautiful detailed glow, detailed ice, beautiful detailed water, (cold full moon), snowflake, (floating cloud:1.2)",
+ "森火法": "((burning forest)), spark, light leaks, burning sky, flame, flames burning around, (flying sparks)",
+ "废土法": "((destroyed)),explosion, buildings in disarray, The residual eaves DuanBi, cumulus, mouldy, floating, wind, Dead end machine,(broken robot),(Mechanical robot girl), in the rubble of a devastated city",
+ "战甲法": "mecha clothes, robot girl, sliver bodysuit,((sliver)) and (broken) body",
+ "黄金法": "Extremely gorgeous metal style, Metal crown with ornate stripes, Various metals background, Sputtered molten iron, floating hair, Hair like melted metal, Clothes made of silver, Clothes with gold lace, flowing gold and silver, everything flowing and melt, flowing iron, flowing silver, lace flowing and melt",
+ "机娘法": "mecha clothes, robot girl",
+ "死灵法": "ink,(((bone))), (((ribs))), rose, black hair, blue eyes, greyscale, no shadow, simple background, bright skin",
+ "水晶法": "(((gorgeous crystal armor))),(((crystal wings))),((altocumulus)),(clear_sky),(snow mountain),((flowery flowers)),(flowery bubbles),cloud map plane, crystal, crystal poppies,Brilliant light, thick_coating, glass tint,(watercolor)",
+ "水森法": "((an extremely delicate and beautiful)),floating, (detailed wet clothes), (detailed light),feather, nature, (sunlight), river, floating palace, beautiful and delicate water,(bloom),(shine)",
+ "冰火法": "(blue spark),(red and blue hair),blue eyes, burning sky,flame,Rainbow in the sky,((Flames burning ice)),(((fire butterflies))),(((ice crystal texture wings))),(Flying sparks),(detailed ice),((a lot of luminous ice crystals)),((burning feathers)),(feathers made of ice),(frozen feathers),(((ice and fire together)))",
+ "龙骑士": "(anger), dragon horns, (silver armor), metal, complex pattern, cape, indifference",
+ "坠落法": "((full body)),(helpless),tear, crying,(((((falling from the sky))))),((Weathering With You)),((falling)),((face towards the sky)),(hair flows upwards),((disheveled hair)),(1 girl), floating, beautiful detailed sky",
+ "水下法": "((underwater)),(beautiful detailed water),((coral)),dynamic angle, floating,(detailed light),floating hair,(splash),((fishes)),leaves dress, feather, nature,(sunlight),(underwater forest),(bloom),(detailed glow),drenched, seaweed, fish,(((Tyndall effect)))",
+ "秘境法": "(extremely detailed CG unity 8k wallpaper),(((masterpiece))), (((best quality))), ((ultra-detailed)), (best illustration),(best shadow), ((an extremely delicate and beautiful)),dynamic angle,floating, fairyland,dynamic angle,sea of flowers,beautiful detailed garden,wind,classic,spring, (detailed light),feather, nature, (sunlight), river, forest,(((floating palace))),((the best building)),beautiful and delicate water,(painting),(sketch),(bloom),(shine)",
+ "摄影法": "(((masterpiece))), ((the best quality, super fine illustrations, beautiful and delicate water)), ((very delicate light)), ((nature, painting)), ((fine lighting, more transparent stars, high-quality snowflakes, high-quality mountains, very fine 8KCG wallpapers)), (plateau), (((snow mountain))), sunrise, randomly distributed clouds, (snow field), cliff, ((rotating star sky)), ((lake in mountain stream)), luminous particles",
+ "摩登法": "1980s (style),simple background, retro artstyle",
+ "血魔法": "(white hair), red long hair, red eyes, (full body), (((with sword))), angry face, (beautiful detailed eyes), Blood drop,Blood fog, floating hair, disheveled hair, Splashing blood,(Bloodstain)",
+ "唤龙术": "((dragon)), ((dragon background))",
+ "战姬法": "hair fluttering in the wind,((mechanical arm armor)),((mechanical body armor)),riding motor, bodysuit,((ruins of city in war, fire, burning cars, burning buildings)),air force fleet in the sky",
+ "龙机法": "mecha clothes, robot girl, sliver bodysuit, dragon wings, ((a dragon stands behind the girl)), beautiful detailed sliver dragon armor",
+ "星蝶术": "((Beautiful butterflies in detail)),((Beautiful stars in detail)),(((halter dress ))),particle,(Starry sky in beautiful detail),(Hazy fog),(Ruins of beautiful details),(((Standing on the surface of the sea)))",
+ "学院法": "blonde wavy hair, (shiny long hair), ((Gothic Lolita)), blue white skirt, ((short skirt)), black Headdress, bowknot, (((hair ornament))), (hair flower), Lace, cross-laced footwear, ribbon-trimmed sleeves, building architecture, ((gothic architecture)), starry sky, outdoors, church, (castle)",
+ "星霞海": "walking, waves, wind,(((glistening light of waves))),(detailed sunset glow),(floating flow),((coral)),(Luminous),coast,(floating colorful bubbles),beautiful detailed sky,(fluorescence),detailed shadow,(conch),beautiful detailed water, starfish, meteor, rainbow,(seabirds),(glinting stars), (glowworm),(splash),detailed cloud, shell,(fireworks)",
+ "冬雪法": "beautiful detailed sky, night, stars, (red plum blossom),((winter)),(((snowflakes))), ((red and white flowers)),(starry sky),(sitting),((colorful)),scenery, lantern,(starfall)"
+ },
+ "魔法1.5": {
+ "万物熔炉": "((((melt)))),(((((black and white melt))))),(((wear Black and white Taoist robes))),((((gold and silver lace)))),(((gold and silver lace lace))),(((flowing ((black)) and white background))),extremely detailed gorgeous tiara",
+ "暗鸦法": "Floating black ashes, Beautiful and detailed black, red moon, ((The black clouds)), (black Wings) , a black cloudy sky, burning, black dress, ((Black fog)), Red eyes, (black smoke), ((Black feathers floating in the air)),bat, (floating black cloud:1.5),",
+ "花火基础": "dynamic angle, finely detail, (bloom), (shine), glinting stars, ((((best detailed fireworks)))), ((((depth of field)))), (((hanabi))),(((backlight))),",
+ "星之彩": "stars in the eyes, messy floating hair, colored inner hair, Starry sky adorns hair, (lots of big colorful Bubble), (pearl), (Galaxy), depth of field",
+ "沉入星海": "stars in the eyes, messy floating hair, colored inner hair, Starry sky adorns hair, depth of field",
+ "百溺法": "dynamic angle, detailed wet clothes, blank stare, overexplosure, floating, black long straight, red eyes, aqua eyes, gradient eyes, ((blood)), white dress, frills, ((expressionless)), extremely beautiful detailed water, ((lying on the lake)), (bloodred water:1.5), (red background:1.3)",
+ "辉煌阳光法": "((messy hair)),(grassland),(yellow eyes),incredibly absurdres,(gold hair),floating hair,Large number of environments,the medieval ,grace,prospect,water eyes,wind,breeze,god ray,lawn,Mountains and lakes in the distance,The sun shines through the trees,A vast expanse of grassland",
+ "星鬓法": "Starry sky adorns hair, sparkling anime eyes,beautiful detailed stars,blighting stars,emerging dark purple across with white hair,multicolored hair,beautiful detailed sky, beautiful detailed water, cinematic lighting, dramatic angle,",
+ "森罗法": "There are many scattered luminous petals,Hidden in the light yellow flowers,Depth of field,Many flying drops of water,Many scattered leaves,branch ,angle ,contour deepening,cinematic angle",
+ "星天使": "sunlight, extremely light, extremely clothes, Holy Light, dynamic angle, Light particle, very long hair, white hair, yellow eyes, glowing eyes, expressionless, ((light smile)), ((((white Tulle skirt)))), white silk, looking at viewer, angel wings, large wings, multiple wings, angel halo, (((starry sky))), dusk sky, Floating light spot, Lots of feathers",
+ "黄金律": "blonde hair, yellow eyes, white skin, mature female, sunrise, golden sky, magnificent architecture, beautiful detailed sky, overexposure, delicate gold metal decorations",
+ "机械姬": "mechanical prosthesis,mecha coverage,emerging dark purple across with white hair,fluorescent purple,cool movement,rose red eyes,beatiful detailed cyberpunk city, hd semirealistic anime cg concept art digital painting",
+ "人鱼法": "(incredibly absurdres), (highly detailed beautiful fishtail:1.6), (((human hands))), (the lower body is a fish:1.9)AND(no human thigh:1.2), seaweed, (full body), (white seashell), (curved and slender fish tail), (the lower body is bare:1.1), {beautiful tailfin}, ((underwater)), (illustration), detailed water, ((fishes)), (floating), watercolor (medium), (small whirlpool), ((The sensation of water flowing)), (detailed scales on a mermaid)",
+ "末日": "Blood Mist, background Urban rooftop,despair,Blood Cherry Blossom,torn clothes,crying with eyes open,solo,Blood Rain,bandages,Gunpowder smoke,beautiful deatailed shadow, Splashing blood,dust,tyndall effect",
+ "碎梦": "Space opera,Space port,robot arm,elbow gloves,night,glisten,stare,cyberpunk,((((citylight)))),science fiction,bodysuit,Mechanical armor headdress,(bare shoulders)",
+ "幻碎梦": "dynamic angle,rainbow hair,detailed cute anime face,((loli)),flower,cry,water,corrugated,flowers tire,broken glass,(broken screen),atlantis,transparent glass",
+ "血改法": "(white hair),(((red streaked hair))), red eyes, (((full body))),(red hair), (((((Hold a red sword))))), (angry face),((Blood drop)),((Blood fog)),light shafts, soft focus, ((Splashing blood))),Long hair,((Bloodstain)),Fighting stance,{{{{{watercolor (medium)}}}},(((masterpiece))),((white clock)),((ultradetailed)),((Covered in blood)),flowing hair,Exquisite Flame,dynamic angle, floating, (shine), extremely delicate and beautiful girls, bright skin, lying red petals,Holy lighting",
+ "留影术": "dramatic shadows,black and white,monochrome,{{comic}},cross necklace,Cassock",
+ "飘花法": "((ink)),(water color),bloom effect,detailed beautiful grassland with petal,flower,butterfly,necklace,smile,petal,(((surrounded by heavy floating petal flow)))"
+ }
+}
diff --git a/js/config/颜色.json b/js/config/颜色.json
new file mode 100644
index 0000000..752aebf
--- /dev/null
+++ b/js/config/颜色.json
@@ -0,0 +1,243 @@
+{
+ "颜色综合": {
+ "藕色 ": "bisque",
+ "酱色 ": "caramel",
+ "茶色 ": "umber",
+ "赫色 ": "ocher",
+ "琥珀色 ": "amber",
+ "栗色": "chestnut",
+ "金色 ": "gold",
+ "古金色": "old gold",
+ "锌色 ": "zinc",
+ "铁锈色": " rust",
+ "青古铜色 ": "bronze",
+ "黑古铜色 ": "dark bronze",
+ "紫铜色": " purple bronze",
+ "黄铜色 ": "brassiness",
+ "木色 ": "wood",
+ "卡其色": " khaki",
+ "奶油色": " cream",
+ "豆沙色": " cameo",
+ "浅豆沙色": " pale mauve",
+ "肉桂色 ": "cinnamon",
+ "肉色 ": "flesh ",
+ "水晶色 ": "crystal",
+ "荧光色 ": "iridescent",
+ "彩虹色 ": "rainbow",
+ "陶土": "Terra Cotta ",
+ "秋叶棕": "Autumn Brown",
+ "淡茶褐": "Teak ",
+ "琥珀黄": "Amber ",
+ "釉菜": "underglaze ",
+ "银粉色": "silver pink",
+ "米色 ": "beige"
+ },
+ "红色类": {
+ "红色": "red",
+ "暗红": "dark red",
+ "淡红 ": "light red",
+ "铬红 ": "chrome red",
+ "血红 ": "blood red",
+ "紫红 ": "purplish red",
+ "莲红": "lotus red",
+ "金红 ": "bronze red",
+ "品红": "magenta",
+ "朱红": "ponceau",
+ "粉红": "pink",
+ "梅红": "plum",
+ "桃红": "peach blossom",
+ "浅莲红": "fuchsia pink",
+ "辣椒红": "capsicum red",
+ "海螺红 ": "cadmium orange",
+ "宝石红": " ruby red",
+ "玛瑙红 ": "agate red",
+ "珊瑚红": " coral",
+ "石榴红 ": "garnet",
+ "樱桃红": "cherry",
+ "石榴红": "garnet",
+ "鲑鱼红": "salmon",
+ "深紫红": "mulberry",
+ "深藕红 ": "conch shell",
+ "玫瑰红": "rose red"
+ },
+ "黄色类": {
+ "黄色": " yellow",
+ "深黄": "deep yellow",
+ "浅黄 ": "light yellow",
+ "深橙": "deep orange",
+ "浅橙 ": "clear orange",
+ "桔黄 ": "orange",
+ "杏黄": " apricot",
+ "蛋黄 ": "vitellin",
+ "藤黄 ": "rattan yellow",
+ "石黄": " mineral yellow",
+ "土黄 ": "earth yellow",
+ "金黄": "golden yellow",
+ "青黄 ": "bluish yellow",
+ "钴黄 ": "cobalt yellow",
+ "砂黄": " sand yellow",
+ "米黄 ": "apricot cream",
+ "玉米黄 ": "maize",
+ "柠檬黄": " lemon yellow",
+ "橄榄黄": "olive yellow",
+ "樱草黄 ": "primrose yellow",
+ "稻草黄": "straw yellow",
+ "芥末黄": "mustard",
+ "鳝鱼黄 ": "eel yellow",
+ "象牙黄": "ivory",
+ "日光黄 ": "sunny yellow"
+ },
+ "绿色类": {
+ "绿色 ": "green",
+ "深绿 ": "dark green",
+ "淡绿 ": "pale green",
+ "铜绿": " verdigris",
+ "墨绿 ": "blackish green",
+ "蓝绿": "blue green",
+ "水绿 ": "aqua green",
+ "草绿": " grass green",
+ "茶绿": "tea green",
+ "葱绿": "onion green",
+ "玉绿 ": "jade green",
+ "石绿": " mineral green",
+ "豆绿": "pea green",
+ "浅豆绿": "light bean green",
+ "橄榄绿": "olive green",
+ "苹果绿": "apple green",
+ "原野绿": " field green",
+ "森林绿 ": "forest green",
+ "洋蓟绿": " artichoke green",
+ "苔藓绿 ": "moss green",
+ "水草绿 ": "water grass green",
+ "深草绿": " jungle green",
+ "灰湖绿 ": "agate green",
+ "海水绿 ": "marine green",
+ "酸性绿": " acid green",
+ "水晶绿": " crystal green",
+ "松石绿 ": "spearmint"
+ },
+ "青色类": {
+ "青色 ": "cyan",
+ "暗青": "dark blue",
+ "浅青": "light blue",
+ "淡青 ": "pale cerulean",
+ "豆青": " bean green",
+ "花青": " flower blue",
+ "葱青 ": "onion green",
+ "天青": "cerulean blue",
+ "霁青 ": "sky-clearing blue",
+ "石青 ": "mineral blue",
+ "铁青 ": "electric blue",
+ "蟹青": " turquoise ",
+ "蛋青 ": "egg blue",
+ "影青": " misty blue",
+ "黛青": " bluish",
+ "群青": "ultramarine",
+ "藏青 ": "navy blue",
+ "靛青 ": "indigo",
+ "大青 ": "smalt",
+ "粉青": "light greenish blue",
+ "鲜青 ": "clear cerulean",
+ "鳝鱼青 ": "eel green"
+ },
+ "蓝色类": {
+ "蓝色": " blue",
+ "深蓝 ": "dark blue",
+ "浅蓝": " light blue",
+ "鲜蓝 ": "clear blue",
+ "天蓝 ": "sky blue",
+ "蔚蓝 ": "azure",
+ "月光蓝 ": "moon blue",
+ "海洋蓝 ": "ocean blue",
+ "海蓝 ": "sea blue",
+ "湖蓝 ": "acid blue",
+ "青蓝 ": "ultramarine",
+ "铁蓝": " iron blue",
+ "海军蓝 ": "navy blue",
+ "深灰蓝 ": "blue ashes",
+ "深湖蓝": " vivid blue",
+ "中湖蓝 ": "bright blue",
+ "浅湖蓝 ": "canal blue",
+ "清水蓝 ": "water blue",
+ "冰雪蓝 ": "ice-snow blue",
+ "孔雀蓝": " peacock blue",
+ "宝石蓝 ": "sapphire",
+ "粉末蓝 ": "powder blue",
+ "普鲁士蓝 ": "Prussian blue"
+ },
+ "紫色类": {
+ "紫色": " purple",
+ "深紫 ": "amaranth",
+ "浅紫 ": "grey violet",
+ "淡紫": " pale purple",
+ "墨紫 ": "violet black",
+ "绛紫 ": "dark reddish purple",
+ "暗紫 ": "violet deep",
+ "乌紫": " raisin",
+ "蓝紫": " royal light",
+ "鲜紫 ": "violet light",
+ "钴紫": "cobalt violet",
+ "青莲紫": "pale purple",
+ "紫罗兰": " violet",
+ "紫水晶 ": "amethyst",
+ "葡萄紫": " grape",
+ "茄皮紫 ": "aubergine",
+ "玫瑰紫 ": "rose violet",
+ "丁香紫 ": "lilac",
+ "淡白紫": " violet ash",
+ "深青莲": "amaranth purple",
+ "墨绛红": "purple black",
+ "暗绛红": " purple deep",
+ "浅绛红 ": "purple light"
+ },
+ "灰色类": {
+ "灰色 ": "grey",
+ "银灰 ": "silver grey",
+ "深灰": " dark grey",
+ "浅灰": " light grey",
+ "铁灰 ": "iron grey",
+ "铅灰 ": "lividity",
+ "碳灰 ": "charcoal grey",
+ "豆灰 ": "rose dust",
+ "藕灰": " zephyr",
+ "莲灰": " elderberry",
+ "天灰": " sky grey",
+ "土灰 ": "dust grey",
+ "烟灰": " smoky grey",
+ "雾灰": " misty grey",
+ "黑灰 ": "grey black",
+ "紫灰": "purple grey",
+ "蓝灰": "blue grey",
+ "青灰": " lividity",
+ "白灰": " pale grey",
+ "浅莲灰": " pale lilac",
+ "鸽子灰 ": "dove grey",
+ "水泥灰 ": "concrete grey",
+ "深紫灰": " heron",
+ "淡紫灰": " lilac grey",
+ "浅绿灰 ": "eucalyptus",
+ "浅米灰": " moon light",
+ "卡其灰 ": "khaki light"
+ },
+ "白色类": {
+ "白色": " white",
+ "银白 ": "silver white",
+ "铅白 ": "flake white",
+ "锌白 ": "zinc white",
+ "米白": " off-white",
+ "乳白 ": "milky-white",
+ "雪白": " snow-white",
+ "灰白": " greyish white",
+ "青白": " bluish white",
+ "纯白 ": "crisp-white",
+ "本白": " raw white",
+ "象牙白": " ivory white",
+ "牡蛎白": " oyster white",
+ "珍珠白 ": " pear white",
+ "玉石白 ": "jade white",
+ "锌钡白": "lithopone",
+ "羊毛白 ": "wool white",
+ "粉红白 ": "pinky white",
+ "淡紫白 ": "lilac white"
+ }
+}
\ No newline at end of file
diff --git a/js/css/assistant.css b/js/css/assistant.css
new file mode 100644
index 0000000..b078eb2
--- /dev/null
+++ b/js/css/assistant.css
@@ -0,0 +1,289 @@
+/**
+ * 提示词小助手 (PromptAssistant) 样式表
+ * 包含所有组件样式和动画定义
+ */
+
+/* ======== 组件样式 ======== */
+
+/* 输入框高亮动画 */
+@keyframes inputHighlight {
+ 0% {
+ background-color: transparent;
+ }
+
+ 20% {
+ background-color: rgba(93, 243, 146, 0.07);
+ }
+
+ 100% {
+ background-color: transparent;
+ }
+}
+
+.input-highlight {
+ animation: inputHighlight 0.2s ease-out;
+}
+
+/* 小助手容器 */
+.prompt-assistant-container {
+ /* 基础布局 */
+ position: absolute;
+ pointer-events: all;
+ display: none;
+ /* 默认隐藏 */
+ align-items: center;
+ justify-content: center;
+ padding: 4px;
+ /* 四周统一内边距 */
+
+ background-color: color-mix(in srgb, var(--bg-color), transparent 20%);
+ /* border: 1px solid rgba(100, 100, 100, 0.25); */
+ border: 1px solid var(--p-panel-border-color);
+ border-radius: 6px;
+ z-index: 9999;
+ opacity: 0.95;
+ backdrop-filter: blur(8px);
+ /* 动画与变换 */
+ transform-origin: bottom right;
+ user-select: none;
+ will-change: transform, opacity;
+
+ /* 尺寸设置 */
+ width: auto;
+ height: 28px !important;
+ /* 强制高度 */
+ max-width: fit-content;
+ box-sizing: border-box !important;
+ /* 优化性能 */
+ overflow: visible;
+ /* 允许内容溢出 */
+}
+
+/* 内部内容容器 */
+.prompt-assistant-inner {
+ display: flex;
+ gap: 2px;
+ /* align-items: center;
+ justify-content: center;
+
+ width: 100%;
+ height: 100%;
+ min-height: 0; */
+ /* 防止内容撑开容器 */
+}
+
+/* 容器的悬停效果 */
+.prompt-assistant-container:hover {
+ opacity: 1;
+ /* border-color: var(--p-inputtext-focus-border-color); */
+ /* transform: translateZ(0) scale(1.02); */
+}
+
+/* 按钮分割线样式 */
+.prompt-assistant-divider {
+ width: 1px;
+ height: 10px;
+ background-color: var(--p-content-border-color);
+ margin: 0 2px;
+ align-self: center;
+ flex-shrink: 0;
+ transform: translateZ(0);
+ /* 确保硬件加速 */
+ border-radius: 0.5px;
+ /* 添加圆角效果 */
+}
+
+/* 小助手显示和隐藏动画类 */
+.assistant-show {
+ display: flex !important;
+ animation: clipFadeInDown 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+.assistant-hide {
+ display: flex !important;
+ animation: clipFadeOutUp 0.2s cubic-bezier(0.55, 0.085, 0.68, 0.53) forwards !important;
+ pointer-events: none;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+/* 状态提示的入场和退场动画类 */
+.statustip-show {
+ animation: tipScaleIn 0.1s ease-out forwards !important;
+}
+
+.statustip-hide {
+ animation: tipFloatUp 0.4s ease-out forwards !important;
+}
+
+/* 按钮状态类 */
+/* 通用按钮按钮样式 */
+.prompt-assistant-button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+ background-size: contain;
+ background-repeat: no-repeat;
+ opacity: 0.8;
+ border: none;
+ padding: 0;
+ margin: 0 1px;
+ background-color: transparent;
+ transition: transform 0.15s, opacity 0.2s, background-color 0.2s;
+ flex-shrink: 0;
+ /* 防止按钮被压缩 */
+ border-radius: 3px;
+ color: var(--p-dialog-color);
+ /* 添加颜色属性用于SVG图标 */
+}
+
+/* SVG图标容器样式 */
+.svg-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ position: relative;
+ z-index: 1;
+}
+
+/* SVG元素样式 */
+.svg-icon svg {
+ width: 100%;
+ height: 100%;
+ fill: currentColor;
+ color: inherit;
+ transition: all 0.2s;
+ vertical-align: middle;
+}
+
+/* 按钮内部图标样式 - 兼容旧的img和新的svg */
+.prompt-assistant-button img,
+.prompt-assistant-button .svg-icon {
+ width: 18px;
+ height: 18px;
+ color: inherit;
+ transition: all 0.2s;
+ position: relative;
+ z-index: 1;
+ vertical-align: middle;
+ /* 确保图标垂直居中 */
+ margin: 0;
+ /* 移除默认边距 */
+}
+
+/* 按钮悬停效果 */
+.prompt-assistant-button:hover {
+ opacity: 1;
+ transform: scale(1.15);
+ background-color: rgba(255, 255, 255, 0.1);
+ /* 添加背景高亮 */
+ box-shadow: 0 0 5px rgba(255, 255, 255, 0.2);
+ /* 添加轻微阴影 */
+}
+
+.prompt-assistant-button:active {
+ transform: scale(0.95);
+}
+
+/* 执行中状态 - 添加呼吸效果 */
+.prompt-assistant-button.button-processing {
+ animation: buttonBreathing 1.5s infinite ease-in-out;
+ background-color: rgba(100, 100, 255, 0.15) !important;
+ /* border: 1px solid rgba(100, 150, 255, 0.3); */
+ position: relative;
+ z-index: 1;
+ opacity: 1 !important;
+}
+
+/* 激活状态 */
+.prompt-assistant-button.button-active {
+ opacity: 1;
+ transform: scale(1.15);
+ background-color: rgba(255, 255, 255, 0.1);
+ /* 添加背景高亮 */
+ box-shadow: 0 0 5px rgba(255, 255, 255, 0.2);
+ /* 添加轻微阴影 */
+ color: var(--p-listbox-option-selected-color);
+ /* 激活状态颜色 */
+}
+
+.prompt-assistant-button.button-active img,
+.prompt-assistant-button.button-active .svg-icon svg {
+ filter: brightness(1.8) !important;
+}
+
+/* 置灰状态 - 降低亮度且不可点击 */
+.prompt-assistant-button.button-disabled {
+ opacity: 0.4 !important;
+ filter: grayscale(60%);
+ cursor: not-allowed !important;
+ pointer-events: none;
+ transform: none !important;
+ color: var(--p-text-muted-color);
+ /* 禁用状态颜色 */
+}
+
+.prompt-assistant-button.button-disabled:hover {
+ transform: none !important;
+ background-color: transparent !important;
+ box-shadow: none !important;
+}
+
+.prompt-assistant-button.button-disabled img,
+.prompt-assistant-button.button-disabled .svg-icon svg {
+ filter: brightness(0.9) grayscale(100%);
+}
+
+
+/* statustip状态提示样式 */
+.statustip {
+
+ transform-origin: center bottom;
+ font-weight: bold;
+ letter-spacing: 0.5px;
+ position: absolute;
+ /* 使用absolute代替fixed */
+ padding: 4px 8px;
+ border-radius: 8px;
+ font-size: 14px;
+ z-index: 10000;
+ text-align: center;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ /* box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); */
+ pointer-events: none;
+ /* 确保提示不会阻挡交互 */
+ backdrop-filter: blur(8px);
+ white-space: nowrap;
+ /* 防止文本换行 */
+ will-change: transform, opacity;
+ /* 优化动画性能 */
+}
+
+/* 不同状态的颜色效果 */
+.statustip.success {
+ color: color-mix(in srgb, var(--p-inputtext-color), rgb(18, 255, 18) 40%) !important;
+ background-color: color-mix(in srgb, var(--p-content-background), rgb(4, 58, 4)30%);
+}
+
+.statustip.error {
+ color: color-mix(in srgb, var(--p-inputtext-color), rgb(255, 0, 0) 60%) !important;
+ background-color: color-mix(in srgb, var(--p-content-background), rgb(74, 12, 12)30%);
+}
+
+.statustip.loading {
+ color: color-mix(in srgb, var(--p-inputtext-color), rgb(249, 255, 69) 30%) !important;
+ background-color: color-mix(in srgb, var(--p-content-background), rgb(114, 117, 22)30%);
+}
+
+.statustip.info {
+ color: color-mix(in srgb, var(--p-inputtext-color), rgb(82, 70, 255) 60%) !important;
+ background-color: color-mix(in srgb, var(--p-content-background), rgb(55, 8, 113)30%);
+
+}
\ No newline at end of file
diff --git a/js/css/common.css b/js/css/common.css
new file mode 100644
index 0000000..32976b5
--- /dev/null
+++ b/js/css/common.css
@@ -0,0 +1,123 @@
+/**
+ * 通用样式及动画库
+ * 提供全局共享的动画效果和基础样式
+ */
+
+/* ======== 动画定义 ======== */
+
+/* 向上淡入动画 */
+@keyframes clipFadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* 向下淡入动画 */
+@keyframes clipFadeInDown {
+ from {
+ opacity: 0;
+ transform: translateY(10px) scale(1);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+/* 向上淡出动画 */
+@keyframes clipFadeOutUp {
+ from {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+
+ to {
+ opacity: 0;
+ transform: translateY(10px) scale(1);
+ }
+}
+
+/* 向下淡出动画 */
+@keyframes clipFadeOutDown {
+ from {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+
+ to {
+ opacity: 0;
+ transform: translateY(-10px) scale(1);
+ }
+}
+
+/* 淡入动画 */
+@keyframes clipFadeIn {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+/* 按钮呼吸缩放动画 */
+@keyframes buttonBreathing {
+ 0% {
+ transform: scale(1);
+ box-shadow: 0 0 0 rgba(100, 100, 255, 0);
+ }
+
+ 50% {
+ transform: scale(1.15);
+ box-shadow: 0 0 8px rgba(100, 100, 255, 0.6);
+ }
+
+ 100% {
+ transform: scale(1);
+ box-shadow: 0 0 0 rgba(100, 100, 255, 0);
+ }
+}
+
+/* 状态提示浮动效果 - 向上淡出 */
+@keyframes tipFloatUp {
+ 0% {
+ opacity: 1;
+ transform: translate(-50%, -100%) translateY(-8px);
+ }
+
+ 20% {
+ opacity: 0.9;
+ transform: translate(-50%, -100%) translateY(-15px);
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translate(-50%, -100%) translateY(-40px);
+ }
+}
+
+/* 状态提示缩放出现动画 */
+@keyframes tipScaleIn {
+ 0% {
+ opacity: 0;
+ transform: translate(-50%, -100%) translateY(-8px) scale(0.8);
+ }
+
+ 70% {
+ opacity: 1;
+ transform: translate(-50%, -100%) translateY(-8px) scale(1.05);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translate(-50%, -100%) translateY(-8px) scale(1);
+ }
+}
\ No newline at end of file
diff --git a/js/css/popup.css b/js/css/popup.css
new file mode 100644
index 0000000..43cb69d
--- /dev/null
+++ b/js/css/popup.css
@@ -0,0 +1,802 @@
+/**
+ * 通用弹窗样式
+ * 提供可复用的弹窗组件样式
+ */
+
+/* 弹窗基础样式 */
+.popup_container {
+ position: fixed;
+ z-index: 10000;
+ /* background-color: rgba(30, 30, 30, 0.95); */
+ background-color: var(--p-content-background);
+ border: 1px solid var(--p-content-border-color);
+ /* border: 1px solid rgba(255, 255, 255, 0.2); */
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
+ min-width: 600px;
+ max-width: 600px;
+ max-height: 400px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ backdrop-filter: blur(8px) !important;
+ padding: 0;
+ transform-origin: top center;
+ will-change: transform, opacity, left, top;
+ transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out;
+}
+
+/* 弹窗动画类 */
+.popup-up {
+ animation: clipFadeInDown 0.2s ease-in-out;
+}
+
+.popup-down {
+ animation: clipFadeInUp 0.2s ease-in-out;
+}
+
+.popup-closing-up {
+ animation: clipFadeOutUp 0.2s ease-in-out forwards;
+ pointer-events: none;
+}
+
+.popup-closing-down {
+ animation: clipFadeOutDown 0.2s ease-in-out forwards;
+ pointer-events: none;
+}
+
+/* 弹窗标题栏 */
+.popup_title_bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 6px 10px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ margin-bottom: 0;
+ user-select: none;
+ gap: 10px;
+ flex-shrink: 0;
+ position: relative;
+}
+
+.popup_title {
+ font-size: 12px;
+ font-weight: bold;
+ /* color: rgba(255, 255, 255, 0.8); */
+ color: var(--p-text-color);
+ white-space: nowrap;
+}
+
+/* 弹窗按钮基础样式 */
+.popup_btn {
+ background: none;
+ border: none;
+ /* color: rgba(255, 255, 255, 0.6); */
+ color: var(--p-button-text-secondary-color);
+ cursor: pointer;
+ padding: 2px;
+ line-height: 1;
+ transition: all 0.2s ease;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+}
+
+.popup_btn:hover {
+ /* color: rgba(255, 255, 255, 0.9); */
+ color: var(--p-button-text-secondary-color);
+ background-color: color-mix(in srgb, var(--comfy-menu-secondary-bg), transparent 50%);
+}
+
+/* SVG图标样式 */
+.svg-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ position: relative;
+}
+
+.svg-icon svg {
+ width: 100%;
+ height: 100%;
+ fill: currentColor;
+ color: inherit;
+ transition: all 0.2s;
+}
+
+/* 弹窗按钮图标样式 - 兼容旧的img和新的svg */
+.popup_btn_icon {
+ width: 12px;
+ height: 12px;
+}
+
+/* 搜索框容器 */
+.popup_search_container {
+ flex: 1;
+ position: relative;
+ display: flex;
+ align-items: center;
+ min-width: 100px;
+ justify-content: flex-end;
+ z-index: 2;
+ /* 确保搜索框在标题栏之上 */
+}
+
+.popup_search_input {
+ width: 200px;
+ padding: 4px 28px 4px 8px;
+ background-color: color-mix(in srgb, var(--comfy-menu-secondary-bg), transparent 60%);
+ border: none;
+ border-radius: 10px;
+ color: var(--p-inputtext-color);
+ font-size: 12px;
+ outline: none;
+ transition: all 0.2s ease;
+ position: relative;
+ z-index: 2;
+ /* 确保输入框在标题栏之上 */
+}
+
+.popup_search_input:focus {
+ border-color: rgba(255, 255, 255, 0.4);
+ background-color: color-mix(in srgb, var(--p-primary-500), transparent 95%);
+ width: 260px;
+ z-index: 2;
+ /* 确保聚焦时在标题栏之上 */
+}
+
+/* 搜索框清除按钮 */
+.popup_search_container .popup_btn {
+ position: absolute;
+ right: 2px;
+ top: 50%;
+ transform: translateY(-50%);
+ display: none;
+ z-index: 3;
+ /* 确保清除按钮在最上层 */
+}
+
+/* 操作按钮容器 */
+.popup_actions {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ position: relative;
+ z-index: 2;
+ /* 确保操作按钮在标题栏之上 */
+}
+
+/* 搜索结果列表 */
+.tag_search_result_list {
+ padding: 8px;
+ overflow-y: auto;
+ max-height: 290px;
+}
+
+.search_result_tag_item {
+ display: flex;
+ align-items: center;
+ padding: 6px 12px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.search_result_tag_item:hover {
+ background-color: rgba(255, 255, 255, 0.1);
+}
+
+.search_result_tag_item .tag_category_path {
+ margin-left: 8px;
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.search_result_tag_item.used {
+ background-color: rgba(40, 120, 40, 0.4);
+}
+
+.search_result_tag_item.used:hover {
+ background-color: rgba(40, 120, 40, 0.6);
+}
+
+/* 弹窗操作按钮 */
+.popup_action_btn {
+ background: none;
+ border: none;
+ color: var(--p-text-muted-color);
+ cursor: pointer;
+ font-size: 10px;
+ padding: 5px 8px;
+ border-radius: 16px;
+ transition: all 0.2s ease;
+}
+
+.popup_action_btn:hover {
+ background-color: color-mix(in srgb, var(--comfy-menu-secondary-bg), transparent 50%);
+ color: var(--p-text-muted-color);
+}
+
+.popup_action_btn:active {
+ transform: scale(0.95);
+}
+
+/* 弹窗内容区域 */
+.popup_content {
+ max-height: 300px;
+ overflow-y: auto;
+ scrollbar-width: thin;
+ scrollbar-color: color-mix(in srgb, var(--p-button-text-secondary-color), transparent 50%) transparent;
+ flex: 1;
+
+}
+
+.popup_content::-webkit-scrollbar {
+ width: 6px;
+}
+
+.popup_content::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.popup_content::-webkit-scrollbar-thumb {
+ background-color: color-mix(in srgb, var(--p-button-text-secondary-color), transparent 50%);
+ border-radius: 3px;
+}
+
+/* 弹窗列表项 */
+.popup_list_item {
+ padding: 10px 10px;
+ font-size: 14px;
+ cursor: pointer;
+ /* color: rgba(255, 255, 255, 0.7); */
+ color: var(--p-text-muted-color);
+ transition: all 0.2s ease;
+ /* border-bottom: 0px solid rgba(255, 255, 255, 0.05); */
+ background-color: color-mix(in srgb, var(--comfy-menu-secondary-bg), transparent 70%);
+ position: relative;
+ user-select: none;
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ min-height: 24px;
+ word-break: break-word;
+ overflow-wrap: break-word;
+}
+
+/* 添加原生tooltip的样式优化 */
+.popup_list_item[title] {
+ position: relative;
+}
+
+/* 确保tooltip在其他元素之上 */
+.popup_list_item[title]:hover {
+ z-index: 1000;
+}
+
+/* 交替背景色 */
+.popup_list_item:nth-child(even) {
+ background-color: var(--p-dialog-background);
+}
+
+/* 当前节点的标识(左侧色条) */
+.popup_list_item.current_node::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 3px;
+ background-color: rgba(0, 255, 100, 0.7);
+ z-index: 1;
+ /* 确保色条在内容之上 */
+}
+
+/* 鼠标悬停效果 */
+.popup_list_item:hover {
+ /* background-color: rgba(211, 16, 16, 0.862); */
+ background-color: color-mix(in srgb, var(--p-primary-500), transparent 84%);
+}
+
+/* 历史记录标识信息 */
+.popup_list_item .history_meta {
+ display: flex;
+ gap: 4px;
+ font-size: 10px;
+ /* color: rgba(255, 255, 255, 0.5); */
+ color: var(--p-text-muted-color);
+ flex-shrink: 0;
+ min-width: 4px;
+ margin-top: 2px;
+}
+
+.popup_list_item .history_meta .node_id,
+.popup_list_item .history_meta .input_id,
+.popup_list_item .history_meta .translated,
+.popup_list_item .history_meta .original,
+.popup_list_item .history_meta .expanded {
+ background-color: color-mix(in srgb, var(--comfy-menu-secondary-bg), transparent 30%);
+ padding: 1px 4px;
+ border-radius: 3px;
+ font-family: monospace;
+}
+
+.popup_list_item .history_meta .node_id {
+ color: rgba(100, 200, 255, 0.8);
+}
+
+.popup_list_item .history_meta .input_id {
+ color: rgba(135, 133, 131, 0.8);
+}
+
+/* 译文标签样式 - 紫色 */
+.popup_list_item .history_meta .translated {
+ color: rgba(180, 120, 255, 0.8);
+}
+
+/* 反推提示词标签样式 - 蓝色 */
+.popup_list_item .history_meta .caption {
+ color: rgba(64, 158, 255, 0.8);
+}
+
+/* 原文标签样式 - 蓝色 */
+.popup_list_item .history_meta .original {
+ color: rgba(100, 160, 255, 0.8);
+}
+
+/* 扩写标签样式 - 绿色 */
+.popup_list_item .history_meta .expanded {
+ color: rgba(100, 220, 120, 0.8);
+}
+
+/* 历史内容样式 */
+.popup_list_item .history_content {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: 1.4;
+ font-size: 14px;
+ min-width: 0;
+}
+
+/* 弹窗空状态提示 */
+.popup_empty {
+ padding: 20px;
+ text-align: center;
+ /* color: rgba(255, 255, 255, 0.4); */
+ color: var(--p-text-muted-color);
+ font-size: 14px;
+ user-select: none;
+}
+
+/* 危险操作按钮样式 */
+.popup_action_btn.danger {
+ color: rgba(255, 100, 100, 0.8);
+}
+
+.popup_action_btn.danger:hover {
+ background-color: rgba(255, 100, 100, 0.2);
+ color: rgba(255, 100, 100, 1);
+}
+
+/* 手风琴样式 */
+.tag_accordion {
+ transition: all 0.3s ease;
+ width: 100%;
+ margin-bottom: 4px;
+ border-radius: 6px;
+ overflow: hidden;
+
+ /* background-color: rgba(40, 40, 40, 0.5); */
+}
+
+.tag_accordion_header {
+ padding: 4px 14px;
+ background-color: color-mix(in srgb, var(--comfy-menu-secondary-bg), transparent 60%);
+ cursor: pointer;
+ user-select: none;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ transition: all 0.2s ease;
+ border-radius: 6px;
+}
+
+.tag_accordion_header:hover {
+ background-color: color-mix(in srgb, var(--p-primary-500), transparent 84%);
+
+}
+
+.tag_accordion_header.active {
+ background-color: color-mix(in srgb, var(--comfy-menu-secondary-bg), transparent 50%);
+ ;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.tag_accordion_title {
+ flex: 1;
+ font-size: 12px;
+ /* color: rgba(255, 255, 255, 0.9); */
+ color: var(--p-text-muted-color);
+ font-weight: 500;
+}
+
+.tag_accordion_icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ /* color: rgba(255, 255, 255, 0.6); */
+ color: var(--p-text-muted-color);
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.tag_accordion_icon img,
+.tag_accordion_icon .svg-icon,
+.accordion_arrow_icon {
+ width: 12px;
+ height: 12px;
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.tag_accordion_icon img,
+.tag_accordion_icon .svg-icon svg {
+ filter: brightness(1.5);
+}
+
+.tag_accordion_content {
+ display: block;
+ max-height: 0;
+ padding: 0;
+ background-color: color-mix(in srgb, var(--comfy-menu-secondary-bg), transparent 90%);
+ overflow: hidden;
+ transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1), padding 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.tag_accordion_content.active {
+ display: block;
+ max-height: 200px;
+ padding: 2px 0;
+ overflow-y: auto;
+}
+
+/* 标签容器样式 */
+.tag_category_container {
+ max-height: 300px;
+ overflow-y: auto;
+ padding: 4px;
+ position: relative;
+ flex: 1;
+}
+
+/* 标签项样式 */
+.tag_item {
+ display: inline-block;
+ margin: 4px;
+ padding: 4px 10px;
+ background-color: color-mix(in srgb, var(--comfy-menu-secondary-bg), transparent 10%);
+ /* border: 1px solid rgba(255, 255, 255, 0.1); */
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ position: relative;
+ z-index: 1;
+ user-select: none;
+}
+
+.tag_item:hover {
+ background-color: color-mix(in srgb, var(--p-primary-500), transparent 84%);
+ ;
+ border-color: rgba(255, 255, 255, 0.2);
+ z-index: 99999;
+}
+
+.tag_item_text {
+ font-size: 14px;
+ /* color: rgba(255, 255, 255, 0.731); */
+ color: var(--p-text-muted-color);
+}
+
+/* 标签使用状态样式 */
+.tag_item.used {
+ background-color: rgba(40, 120, 40, 0.8);
+ border-color: rgba(100, 255, 100, 0.2);
+}
+
+.tag_item.used:hover {
+ background-color: rgba(50, 140, 50, 0.9);
+}
+
+.tag_item.used .tag_item_text {
+ color: rgba(200, 255, 200, 0.9);
+}
+
+/* 自定义滚动条样式 */
+.tag_category_container::-webkit-scrollbar {
+ width: 6px;
+}
+
+.tag_category_container::-webkit-scrollbar-track {
+ background: rgba(0, 0, 0, 0.1);
+}
+
+.tag_category_container::-webkit-scrollbar-thumb {
+ background-color: color-mix(in srgb, var(--p-button-text-secondary-color), transparent 50%);
+ border-radius: 3px;
+}
+
+.tag_category_container::-webkit-scrollbar-thumb:hover {
+ background-color: color-mix(in srgb, var(--p-button-text-secondary-color), transparent 50%);
+}
+
+/* 自定义 tooltip 样式 */
+.tag_tooltip {
+ position: fixed;
+ padding: 8px 12px;
+ background-color: color-mix(in srgb, var(--bg-color), transparent 10%);
+ /* color: white; */
+ color: var(--p-inputtext-color);
+ font-size: 12px;
+ border-radius: 8px;
+ white-space: pre-line;
+ max-width: 300px;
+ word-wrap: break-word;
+ line-height: 1.6;
+ z-index: 1000000;
+ pointer-events: none;
+ animation: clipFadeInDown 0.2s ease-in-out;
+ transform-origin: top center;
+ will-change: transform, opacity;
+ backdrop-filter: blur(8px);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+/* 添加 tooltip 中分类路径的样式 */
+.tag_tooltip .tooltip_path {
+ display: block;
+ font-size: 12px;
+ color: color-mix(in srgb, var(--p-inputtext-color), transparent 60%);
+ margin-top: 4px;
+}
+
+/* ---可滚动Tabs栏样式--- */
+.popup_tabs_container {
+ position: relative;
+ border-bottom: 1px solid var(--p-content-border-color);
+ /* background-color: rgba(30, 30, 30, 0.8); */
+ background-color: color-mix(in srgb, var(--comfy-menu-secondary-bg), transparent 60%);
+ user-select: none;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ padding: 0;
+}
+
+/* 滚动区域 */
+.popup_tabs_scroll {
+ overflow-x: auto;
+ overflow-y: hidden;
+ white-space: nowrap;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ scroll-behavior: smooth;
+ -webkit-overflow-scrolling: touch;
+ padding: 2px 0 0;
+ position: relative;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ margin: 0 2px;
+}
+
+/* 隐藏滚动条 */
+.popup_tabs_scroll::-webkit-scrollbar {
+ display: none;
+ /* Chrome, Safari, Opera */
+}
+
+/* Tabs栏 */
+.popup_tabs {
+ display: inline-flex;
+ flex-wrap: nowrap;
+ gap: 2px;
+ padding: 4px 0 0;
+ position: relative;
+}
+
+/* 为第一个标签添加左侧边距 */
+.popup_tabs .popup_tab:first-child {
+ margin-left: 6px;
+}
+
+/* 单个Tab标签 */
+.popup_tab {
+ display: inline-block;
+ padding: 4px 10px 8px 10px;
+ font-size: 12px;
+ /* color: rgba(255, 255, 255, 0.6); */
+ color: var(--p-text-muted-color);
+ background-color: transparent;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ max-width: 120px;
+ position: relative;
+ border-radius: 0;
+}
+
+/* Tab激活状态 - 添加底部高亮条 */
+.popup_tab.active {
+ /* color: rgba(255, 255, 255, 0.9); */
+ color: var(--p-listbox-option-selected-color);
+ background-color: transparent;
+ box-shadow: none;
+}
+
+/* 底部高亮条基础样式 */
+.popup_tab::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ width: 100%;
+ height: 2px;
+ background-color: rgba(100, 180, 255, 0.8);
+ border-radius: 2px 2px 0 0;
+ transform-origin: center;
+ transform: translateX(-50%) scaleX(0);
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ will-change: transform;
+}
+
+/* 底部高亮条激活状态 */
+.popup_tab.active::after {
+ transform: translateX(-50%) scaleX(1);
+}
+
+/* 底部高亮条退出动画 */
+.popup_tab.active.exiting::after {
+ transform: translateX(-50%) scaleX(0);
+}
+
+/* 当前节点的底部高亮条颜色 */
+.popup_tab.current_node::after {
+ background-color: var(--p-green-500);
+}
+
+/* Tab悬停效果 */
+.popup_tab:hover {
+ /* color: rgba(255, 255, 255, 0.9); */
+ color: var(--p-inputtext-color);
+
+}
+
+/* 当前节点标识 */
+.popup_tab.current_node {
+ background-color: transparent;
+ /* color: rgba(200, 255, 200, 0.8); */
+ color: var(--p-listbox-option-selected-color);
+}
+
+.popup_tab.current_node.active {
+ background-color: transparent;
+ color: var(--p-green-500);
+ box-shadow: none;
+}
+
+.popup_tab.current_node:hover {
+ background-color: rgba(0, 170, 80, 0.1);
+ border-radius: 8px 8px 0 0;
+}
+
+/* Tab内容容器 */
+.popup_tab_content {
+ display: none;
+ max-height: 290px;
+ overflow-y: auto;
+ scrollbar-width: thin;
+ scrollbar-color: color-mix(in srgb, var(--p-button-text-secondary-color), transparent 50%) transparent;
+ padding-bottom: 8px;
+}
+
+.popup_tab_content::-webkit-scrollbar {
+ width: 6px;
+}
+
+.popup_tab_content::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.popup_tab_content::-webkit-scrollbar-thumb {
+ background-color: color-mix(in srgb, var(--p-button-text-secondary-color), transparent 50%);
+ border-radius: 3px;
+}
+
+.popup_tab_content.active {
+ display: block;
+}
+
+/* 滚动指示器 - PrimeVue 风格 */
+.tabs_scroll_indicator {
+ position: relative;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ /* color: rgba(255, 255, 255, 0.7); */
+ color: var(--p-text-muted-color);
+ font-size: 16px;
+ cursor: pointer;
+ z-index: 5;
+ transition: all 0.2s ease;
+ flex-shrink: 0;
+ margin: 0 2px;
+ border: none;
+ border-radius: 50%;
+}
+
+/* 左侧滚动指示器 */
+.tabs_scroll_indicator.left {
+ order: -1;
+ /* 确保在滚动区域左侧 */
+ margin: 0 4px;
+}
+
+/* 右侧滚动指示器 */
+.tabs_scroll_indicator.right {
+ order: 1;
+ /* 确保在滚动区域右侧 */
+ margin: 0 4px;
+}
+
+.tabs_scroll_indicator:hover {
+ color: rgba(255, 255, 255, 1);
+ background-color: rgba(60, 60, 60, 0.7);
+ transform: scale(1.1);
+ background-color: rgba(255, 255, 255, 0.1);
+ box-shadow: 0 0 5px rgba(255, 255, 255, 0.2);
+}
+
+/* 滚动指示器图标样式 */
+.scroll_indicator_icon {
+ width: 12px;
+ height: 12px;
+ display: block;
+ filter: brightness(1.5);
+}
+
+/* 左侧滚动指示器图标旋转 */
+.rotate_left {
+ transform: rotate(-90deg);
+}
+
+/* 右侧滚动指示器图标旋转 */
+.rotate_right {
+ transform: rotate(-90deg);
+}
+
+.rotate-180 {
+ transform: rotate(180deg);
+}
+
+/* 拖动时的样式 */
+.popup_container.dragging {
+ opacity: 0.95;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
+}
+
+.popup_title_bar.dragging {
+ cursor: grabbing !important;
+}
\ No newline at end of file
diff --git a/js/index.js b/js/index.js
new file mode 100644
index 0000000..1357783
--- /dev/null
+++ b/js/index.js
@@ -0,0 +1,244 @@
+/**
+ * 提示词小助手 (PromptAssistant) 主入口文件
+ * 负责扩展初始化、节点检测和功能注入
+ */
+
+import { app } from "../../../scripts/app.js";
+import { promptAssistant, PromptAssistant } from './modules/PromptAssistant.js';
+import { registerSettings } from './modules/settings.js';
+import { FEATURES as ASSISTANT_FEATURES, handleFeatureChange, setFeatureModuleDeps } from './config/features.js';
+import { EventManager } from './utils/eventManager.js';
+import { ResourceManager } from './utils/resourceManager.js';
+import { UIToolkit } from "./utils/UIToolkit.js";
+import { logger } from './utils/logger.js';
+import { HistoryCacheService } from './services/cache.js';
+import { imageCaption, ImageCaption } from './modules/imageCaption.js';
+
+// ====================== 全局配置与状态 ======================
+
+// 设置全局对象供其他模块访问
+window.FEATURES = ASSISTANT_FEATURES;
+
+// 将实例添加到全局app对象
+app.promptAssistant = promptAssistant;
+app.imageCaption = imageCaption;
+
+// ====================== 扩展注册 ======================
+
+/**
+ * 注册ComfyUI扩展
+ */
+app.registerExtension({
+ name: "Comfy.PromptAssistant",
+
+ // ---扩展生命周期钩子---
+ /**
+ * 初始化扩展
+ */
+ async setup() {
+ try {
+ // 注册设置选项
+ registerSettings();
+
+ // 初始化提示词小助手(内部会处理版本号检查和总开关状态)
+ await promptAssistant.initialize();
+ // 初始化图像小助手
+ await imageCaption.initialize();
+
+ // 清理旧引用
+ if (app.canvas) {
+ app.canvas.updateNodeAssistantsVisibility = null;
+ app.canvas._onNodeSelectionChange = null;
+ }
+
+ // 将管理器添加到app对象,使其可以通过window.app访问
+ app.EventManager = EventManager;
+ app.ResourceManager = ResourceManager;
+ app.UIToolkit = UIToolkit;
+
+ // 先初始化 features.js 依赖
+ setFeatureModuleDeps({
+ promptAssistant,
+ PromptAssistant,
+ UIToolkit,
+ HistoryCacheService,
+ imageCaption,
+ ImageCaption
+ });
+
+ // 然后再自动注册服务功能
+ if (window.FEATURES.enabled) {
+ await promptAssistant.toggleGlobalFeature(true, true);
+ await imageCaption.toggleGlobalFeature(true, true);
+ }
+
+ logger.log("扩展初始化完成");
+ } catch (error) {
+ logger.error(`扩展初始化失败: ${error.message}`);
+ }
+ },
+
+ // ---节点生命周期钩子---
+ /**
+ * 节点创建钩子
+ * 在节点创建时初始化特定类型节点的小助手
+ */
+ async nodeCreated(node) {
+ try {
+ // 始终检查总开关状态
+ if (!window.FEATURES.enabled) {
+ return;
+ }
+
+ // 只在节点被选中时进行检测和初始化
+ if (!node || !node.id || node.id === -1) {
+ return;
+ }
+
+ // 检查是否为提示词节点
+ if (PromptAssistant.isValidNode(node)) {
+ if (!node._promptAssistantInitialized) {
+ node._promptAssistantInitialized = true;
+ promptAssistant.checkAndSetupNode(node);
+ }
+ return;
+ }
+
+ // 检查是否为图像节点,同时检查图像反推功能开关
+ if (window.FEATURES.imageCaption && imageCaption.hasValidImage(node)) {
+ if (!node._imageCaptionInitialized) {
+ node._imageCaptionInitialized = true;
+ imageCaption.checkAndSetupNode(node);
+ }
+ return;
+ }
+ } catch (error) {
+ logger.error(`节点创建处理失败: ${error.message}`);
+ }
+ },
+
+ /**
+ * 节点移除钩子
+ * 在节点被删除时清理对应的小助手实例
+ */
+ async nodeRemoved(node) {
+ try {
+ if (!node || !node.id || node.id === -1) return;
+
+ // 添加清理标记,避免重复清理
+ node._promptAssistantCleaned = false;
+ node._imageCaptionCleaned = false;
+
+ // 清理提示词小助手
+ if (node._promptAssistantInitialized) {
+ promptAssistant.cleanup(node.id);
+ node._promptAssistantCleaned = true;
+ logger.debug(`[节点移除钩子] 提示词小助手清理完成 | 节点ID: ${node.id}`);
+ }
+
+ // 清理图像小助手
+ if (node._imageCaptionInitialized) {
+ imageCaption.cleanup(node.id);
+ node._imageCaptionCleaned = true;
+ logger.debug(`[节点移除钩子] 图像小助手清理完成 | 节点ID: ${node.id}`);
+ }
+ } catch (error) {
+ logger.error(`节点移除处理失败: ${error.message}`);
+ }
+ },
+
+ /**
+ * 节点定义注册前钩子
+ * 向所有节点类型注入小助手相关功能
+ */
+ async beforeRegisterNodeDef(nodeType, nodeData) {
+ // 保存原始方法
+ const origOnNodeCreated = nodeType.prototype.onNodeCreated;
+ const origOnRemoved = nodeType.prototype.onRemoved;
+ const origOnSelected = nodeType.prototype.onSelected;
+
+ // 注入节点创建方法
+ nodeType.prototype.onNodeCreated = function () {
+ if (origOnNodeCreated) {
+ origOnNodeCreated.apply(this, arguments);
+ }
+
+ // 始终检查最新的总开关状态
+ const currentEnabled = app.ui.settings.getSettingValue("PromptAssistant.Features.Enabled");
+ window.FEATURES.enabled = currentEnabled !== undefined ? currentEnabled : true;
+
+ // 总开关关闭时,直接返回
+ if (!window.FEATURES.enabled) {
+ return;
+ }
+
+ // 设置未初始化标记,等待nodeCreated钩子处理
+ this._promptAssistantInitialized = false;
+ this._imageCaptionInitialized = false;
+ };
+
+ // 注入节点选择方法
+ nodeType.prototype.onSelected = function () {
+ if (origOnSelected) {
+ origOnSelected.apply(this, arguments);
+ }
+
+ // 确保选择事件能触发到图像小助手,同时检查图像反推功能开关
+ if (window.FEATURES.enabled && window.FEATURES.imageCaption &&
+ app.canvas && app.canvas._imageCaptionSelectionHandler) {
+ const selected_nodes = {};
+ selected_nodes[this.id] = this;
+ app.canvas._imageCaptionSelectionHandler(selected_nodes);
+ }
+ };
+
+ // 注入节点移除方法
+ nodeType.prototype.onRemoved = function () {
+ try {
+ // 清理提示词小助手(如果尚未清理)
+ if (this._promptAssistantInitialized && !this._promptAssistantCleaned) {
+ promptAssistant.cleanup(this.id);
+ this._promptAssistantCleaned = true;
+ logger.debug(`[onRemoved方法] 提示词小助手清理完成 | 节点ID: ${this.id}`);
+ }
+
+ // 清理图像小助手(如果尚未清理)
+ if (this._imageCaptionInitialized && !this._imageCaptionCleaned) {
+ imageCaption.cleanup(this.id);
+ this._imageCaptionCleaned = true;
+ logger.debug(`[onRemoved方法] 图像小助手清理完成 | 节点ID: ${this.id}`);
+ }
+
+ // 即使没有初始化标记,也尝试清理(关键修复)
+ // 这是为了处理可能的边缘情况,确保完全清理
+ if (!this._promptAssistantCleaned) {
+ promptAssistant.cleanup(this.id, true);
+ }
+ if (!this._imageCaptionCleaned) {
+ imageCaption.cleanup(this.id, true);
+ }
+
+ if (origOnRemoved) {
+ origOnRemoved.apply(this, arguments);
+ }
+ } catch (error) {
+ logger.error(`[onRemoved方法] 清理失败 | 节点ID: ${this.id} | 错误: ${error.message}`);
+ // 确保原始方法仍然被调用
+ if (origOnRemoved) {
+ origOnRemoved.apply(this, arguments);
+ }
+ }
+ };
+ },
+
+ /**
+ * 扩展卸载钩子
+ * 在扩展被卸载时清理所有资源
+ */
+ async beforeExtensionUnload() {
+ promptAssistant.cleanup();
+ imageCaption.cleanup();
+ }
+});
+
+export { EventManager, UIToolkit };
\ No newline at end of file
diff --git a/js/modules/PromptAssistant.js b/js/modules/PromptAssistant.js
new file mode 100644
index 0000000..c13723f
--- /dev/null
+++ b/js/modules/PromptAssistant.js
@@ -0,0 +1,1808 @@
+/**
+ * 提示词小助手核心类
+ * 统一管理小助手的生命周期、实例创建、UI交互等功能
+ */
+
+import { app } from "../../../../scripts/app.js";
+import { logger } from '../utils/logger.js';
+import { FEATURES } from "../config/features.js";
+import { HistoryManager } from "./history.js";
+import { TagManager } from "./tag.js";
+import { HistoryCacheService, TagCacheService, TranslateCacheService, CACHE_CONFIG, CacheService } from "../services/cache.js";
+import { EventManager } from "../utils/eventManager.js";
+import { ResourceManager } from "../utils/resourceManager.js";
+import { UIToolkit } from "../utils/UIToolkit.js";
+import { PromptFormatter } from "../utils/promptFormatter.js";
+import { BaiduTranslateService } from "../services/baidu.js";
+import { llmService } from "../services/llm.js";
+
+// ====================== 工具函数 ======================
+
+/**
+ * 生成唯一请求ID
+ * @param {string} prefix 前缀
+ * @param {string} type 请求类型,用于确定前缀
+ * @returns {string} 唯一request_id
+ */
+function generateRequestId(prefix = 'baidu_trans', type = null) {
+ // 如果提供了type,根据type确定前缀
+ if (type) {
+ switch (type) {
+ case 'llm':
+ prefix = 'llm_trans';
+ break;
+ case 'baidu':
+ prefix = 'baidu_trans';
+ break;
+ case 'expand':
+ prefix = 'glm4_expand';
+ break;
+ default:
+ // 保持传入的prefix不变
+ break;
+ }
+ }
+
+ const timestamp = Date.now();
+ const random = Math.random().toString(36).substr(2, 6);
+ return `${prefix}_${timestamp}_${random}`;
+}
+
+/**
+ * 防抖函数
+ * 限制函数调用频率,避免频繁触发导致性能问题
+ */
+function debounce(func, wait = 100) {
+ return EventManager.debounce(func, wait);
+}
+
+// ====================== 主类实现 ======================
+
+/**
+ * 提示词小助手主类
+ * 统一管理小助手的生命周期、实例和资源
+ */
+class PromptAssistant {
+ /** 存储所有小助手实例的Map集合 */
+ static instances = new Map();
+
+ constructor() {
+ this.initialized = false;
+ }
+
+ // ---生命周期管理功能---
+ /**
+ * 判断功能是否被禁用
+ */
+ areAllFeaturesDisabled() {
+ return !window.FEATURES.enabled;
+ }
+
+ /**
+ * 初始化提示词小助手
+ */
+ initialize() {
+ if (this.initialized) return;
+
+ try {
+ // 检查版本号
+ if (!window.PromptAssistant_Version) {
+ logger.error("初始化时未找到版本号!这可能导致UI显示异常");
+ } else {
+ logger.debug(`初始化时检测到版本号: ${window.PromptAssistant_Version}`);
+ }
+
+ // 初始化事件管理器
+ EventManager.init();
+
+ // 检查总开关的初始状态
+ const initialEnabled = app.ui.settings.getSettingValue("PromptAssistant.Features.Enabled");
+ window.FEATURES.enabled = initialEnabled !== undefined ? initialEnabled : true;
+
+ // 记录总开关状态
+ logger.log(`总开关状态 | 状态:${window.FEATURES.enabled ? "启用" : "禁用"}`);
+
+ // 迁移旧的缓存数据到新的键名格式
+ CacheService.migrateCache();
+
+ // 初始化资源管理器
+ ResourceManager.init();
+
+ // 只有在总开关打开时才做完整初始化
+ if (window.FEATURES.enabled) {
+ // 注册全局鼠标监听
+ this.registerGlobalMouseListener();
+ }
+
+ this.initialized = true;
+ logger.log("初始化完成 | 小助手已完全启动");
+ } catch (error) {
+ logger.error(`初始化失败 | 错误: ${error.message}`);
+ // 重置状态
+ this.initialized = false;
+ window.FEATURES.enabled = false;
+ // 确保清理
+ this.cleanup();
+ }
+ }
+
+ /**
+ * 统一控制总开关功能
+ * 集中管理所有受总开关控制的服务功能
+ * @param {boolean} enable - 是否启用总开关
+ * @param {boolean} force - 是否强制执行(跳过状态判断)
+ */
+ async toggleGlobalFeature(enable, force = false) {
+ // 更新状态
+ const oldValue = window.FEATURES.enabled;
+ window.FEATURES.enabled = enable;
+
+ // 状态未变化时不执行操作,除非force为true
+ if (!force && oldValue === enable) {
+ return;
+ }
+
+ logger.log(`总开关 | 动作:${enable ? "启用" : "禁用"}`);
+
+ try {
+ if (enable) {
+ // === 启用所有服务 ===
+ // 确保管理器已初始化
+ if (!EventManager.initialized) {
+ EventManager.init();
+ }
+
+ if (!ResourceManager.isInitialized()) {
+ ResourceManager.init();
+ }
+
+ // 1. 重置节点初始化标记,准备重新检测
+ if (app.canvas && app.canvas.graph) {
+ const nodes = app.canvas.graph._nodes || [];
+ nodes.forEach(node => {
+ if (node) {
+ node._promptAssistantInitialized = false;
+ }
+ });
+ }
+
+ // 2. 设置或恢复节点选择事件监听
+ if (app.canvas) {
+ // 避免重复设置监听器
+ if (!app.canvas._promptAssistantSelectionHandler) {
+ app.canvas._promptAssistantSelectionHandler = function (selected_nodes) {
+ // 当总开关关闭时,跳过所有节点处理
+ if (!window.FEATURES.enabled) {
+ return;
+ }
+
+ if (selected_nodes && Object.keys(selected_nodes).length > 0) {
+ Object.keys(selected_nodes).forEach(nodeId => {
+ const node = app.canvas.graph.getNodeById(nodeId);
+ if (!node) return;
+
+ // 初始化未初始化的节点
+ if (!node._promptAssistantInitialized) {
+ node._promptAssistantInitialized = true;
+ this.checkAndSetupNode(node);
+ }
+ });
+ }
+ }.bind(this);
+ }
+
+ // 保存当前监听器并设置新的
+ if (app.canvas.onSelectionChange && app.canvas.onSelectionChange !== app.canvas._promptAssistantSelectionHandler) {
+ app.canvas._originalSelectionChange = app.canvas.onSelectionChange;
+ }
+
+ app.canvas.onSelectionChange = app.canvas._promptAssistantSelectionHandler;
+ }
+
+ // 3. 注册全局鼠标监听
+ await this.registerGlobalMouseListener();
+ } else {
+ // === 禁用所有服务 ===
+
+ // 1. 计数并清理所有实例
+ const instanceCount = PromptAssistant.instances.size;
+ this.cleanup(null, true);
+
+ // 2. 恢复原始节点选择事件监听
+ if (app.canvas) {
+ if (app.canvas._originalSelectionChange) {
+ app.canvas.onSelectionChange = app.canvas._originalSelectionChange;
+ } else {
+ app.canvas.onSelectionChange = null;
+ }
+ }
+
+ // 3. 移除全局鼠标监听
+ this.removeGlobalMouseListener();
+ }
+
+ // 按钮可见性更新在features中单独处理
+ window.FEATURES.updateButtonsVisibility();
+ } catch (error) {
+ logger.error(`总开关操作失败 | 错误: ${error.message}`);
+ // 恢复原始状态
+ window.FEATURES.enabled = oldValue;
+ }
+ }
+
+ // ---资源管理功能---
+ /**
+ * 清理所有资源
+ * @param {string} [nodeId] - 可选的节点ID,如果提供则只清理该节点的实例
+ * @param {boolean} [silent=false] - 是否静默清理(不输出日志)
+ */
+ cleanup(nodeId = null, silent = false) {
+ // 清理特定节点
+ if (nodeId) {
+ // 获取当前节点的所有实例键
+ const keysToDelete = Array.from(PromptAssistant.instances.keys())
+ .filter(key => key === String(nodeId) || key.startsWith(`${nodeId}_`));
+
+ // 如果有实例需要清理
+ if (keysToDelete.length > 0) {
+ let historyCount = 0;
+ let tagCount = 0;
+ let instanceNames = [];
+
+ try {
+ // 统计并清理历史记录
+ const allHistory = HistoryCacheService.getAllHistory();
+ historyCount = allHistory.filter(item => item.node_id === nodeId).length;
+ HistoryCacheService.clearNodeHistory(nodeId);
+
+ // 统计并清理标签缓存
+ keysToDelete.forEach(key => {
+ const instance = PromptAssistant.getInstance(key);
+ if (instance && instance.inputId) {
+ const tags = TagCacheService.getAllRawTags(nodeId, instance.inputId);
+ tagCount += tags ? tags.length : 0;
+ TagCacheService.clearCache(nodeId, instance.inputId);
+ instanceNames.push(instance.inputId);
+ }
+ });
+
+ // 清理实例
+ keysToDelete.forEach(key => {
+ const instance = PromptAssistant.getInstance(key);
+ if (instance) {
+ this._cleanupInstance(instance, key, true);
+ PromptAssistant.instances.delete(key);
+ }
+ });
+
+ if (!silent) {
+ // 获取当前剩余的统计信息
+ const remainingInstances = PromptAssistant.instances.size;
+ const remainingTags = Object.keys(localStorage)
+ .filter(key => key.startsWith(CACHE_CONFIG.TAG_KEY_PREFIX))
+ .reduce((total, key) => {
+ try {
+ const cacheData = JSON.parse(localStorage.getItem(key));
+ return total + (cacheData ? Object.keys(cacheData).length : 0);
+ } catch (e) {
+ return total;
+ }
+ }, 0);
+ const remainingHistory = HistoryCacheService.getAllHistory().length;
+
+ logger.log(`[节点清理] 节点ID: ${nodeId} | 实例: ${instanceNames.join(', ')} | 历史: ${historyCount}条 | 标签: ${tagCount}个`);
+ logger.log(`[剩余统计] 小助手实例: ${remainingInstances}个 | 标签缓存: ${remainingTags}个 | 节点历史缓存: ${remainingHistory}条`);
+ }
+ } catch (error) {
+ logger.error(`[节点清理] 失败 | 节点ID: ${nodeId} | 错误: ${error.message}`);
+ }
+ }
+ return;
+ }
+
+ // 清理所有实例和历史
+ const beforeCleanupSize = PromptAssistant.instances.size;
+ if (beforeCleanupSize > 0) {
+ let totalHistoryCount = 0;
+ let totalTagCount = 0;
+ let allInstanceNames = [];
+
+ try {
+ // 统计并清理所有历史记录
+ const allHistory = HistoryCacheService.getAllHistory();
+ totalHistoryCount = allHistory.length;
+ HistoryCacheService.clearAllHistory();
+
+ // 统计并清理所有标签缓存和实例
+ for (const [key, instance] of PromptAssistant.instances) {
+ if (instance && instance.nodeId && instance.inputId) {
+ const tags = TagCacheService.getAllRawTags(instance.nodeId, instance.inputId);
+ totalTagCount += tags ? tags.length : 0;
+ TagCacheService.clearCache(instance.nodeId, instance.inputId);
+ allInstanceNames.push(instance.inputId);
+ this._cleanupInstance(instance, key, true);
+ }
+ }
+
+ // 清空实例集合
+ PromptAssistant.instances.clear();
+
+ if (!silent) {
+ logger.log(`[全局清理] 实例: ${allInstanceNames.join(', ')} | 历史: ${totalHistoryCount}条 | 标签: ${totalTagCount}个`);
+ logger.log(`[剩余统计] 小助手实例: 0个 | 标签缓存: 0个 | 节点历史缓存: 0条`);
+ }
+ } catch (error) {
+ logger.error(`[全局清理] 失败 | 错误: ${error.message}`);
+ }
+ }
+ }
+
+ // ---实例管理功能---
+ /**
+ * 检查节点是否有效
+ */
+ static isValidNode(node) {
+ return node &&
+ typeof node.id !== 'undefined' &&
+ node.id !== -1 && // 防止id为-1的无效节点
+ node.widgets && // 确保有widgets属性
+ typeof node.type === 'string'; // 确保有类型
+ }
+
+ /**
+ * 添加实例到管理器
+ */
+ static addInstance(nodeId, widget) {
+ if (nodeId != null && widget != null) {
+ this.instances.set(String(nodeId), widget);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 获取实例
+ */
+ static getInstance(nodeId) {
+ if (nodeId == null) return null;
+ return this.instances.get(String(nodeId));
+ }
+
+ /**
+ * 检查实例是否存在
+ */
+ static hasInstance(nodeId) {
+ if (nodeId == null) return false;
+ return this.instances.has(String(nodeId));
+ }
+
+ /**
+ * 检查节点并设置小助手
+ * 查找节点中的有效输入控件并创建小助手
+ */
+ checkAndSetupNode(node) {
+ // 始终首先检查总开关状态
+ if (!window.FEATURES.enabled || !PromptAssistant.isValidNode(node) || (node.flags && node.flags.collapsed)) {
+ return;
+ }
+
+ // 查找有效的文本输入控件
+ const validInputs = [];
+ if (node.widgets) {
+ node.widgets.forEach(widget => {
+ // 不再直接设置 node 引用,因为它现在是只读的
+ if (UIToolkit.isValidInput(widget)) {
+ validInputs.push(widget);
+ }
+ });
+ }
+
+ // 如果没有找到有效的文本输入控件,直接返回
+ if (validInputs.length === 0) {
+ return;
+ }
+
+ // 为每个有效控件创建小助手
+ validInputs.forEach(inputWidget => {
+ const inputId = inputWidget.name || inputWidget.id;
+ const assistantKey = `${node.id}_${inputId}`;
+
+ // 检查实例是否已存在
+ if (PromptAssistant.hasInstance(assistantKey)) {
+ return; // 跳过已存在的实例
+ }
+
+ // 再次检查总开关状态,确保在创建过程中没有被禁用
+ if (!window.FEATURES.enabled) {
+ return;
+ }
+
+ // 创建小助手实例
+ const assistant = this.setupNodeAssistant(node, inputWidget);
+ if (assistant) {
+ logger.log(`创建小助手 | 节点:${node.id} | 控件:${inputId} | 实例:${assistantKey}`);
+ }
+ });
+ }
+
+ /**
+ * 为节点设置小助手
+ * 创建小助手实例并初始化显示状态
+ */
+ setupNodeAssistant(node, inputWidget) {
+ // 简化参数检查
+ if (!node || !inputWidget) {
+ return null;
+ }
+
+ try {
+ const nodeId = node.id;
+ const inputId = inputWidget.name || inputWidget.id || Math.random().toString(36).substring(2, 10);
+ const isNoteNode = node.type === "Note";
+
+ // 简化节点信息
+ const nodeInfo = {
+ nodeType: node.type,
+ inputType: inputId,
+ isNoteNode: isNoteNode
+ };
+
+ // 创建小助手实例
+ const assistant = this.createAssistant(
+ node,
+ inputId,
+ isNoteNode ? { ...inputWidget, inputEl: inputWidget.element } : inputWidget, // Note节点特殊处理
+ nodeInfo
+ );
+
+ if (assistant) {
+ // 初始化显示状态
+ assistant.isFirstCreate = true;
+ this.showAssistantUI(assistant);
+
+ // 设置初始悬停状态,2秒后恢复正常行为
+ assistant.isMouseOver = true;
+ setTimeout(() => {
+ if (assistant) {
+ assistant.isFirstCreate = false;
+ if (!assistant.isMouseOver) {
+ this.updateAssistantVisibility(assistant);
+ }
+ }
+ }, 2000);
+
+ return assistant;
+ }
+
+ return null;
+ } catch (error) {
+ logger.error(`创建小助手 | 结果:异常 | 节点ID: ${node.id}, 错误: ${error.message}`);
+ return null;
+ }
+ }
+
+ /**
+ * 创建小助手实例
+ * 根据节点和输入控件构建小助手对象并初始化UI
+ */
+ createAssistant(node, inputId, inputWidget, nodeInfo = {}) {
+ // 简化前置检查
+ if (!window.FEATURES.enabled || !node || !inputId || !inputWidget || !UIToolkit.isValidInput(inputWidget)) {
+ return null;
+ }
+
+ // 确保输入元素存在
+ const inputEl = inputWidget.inputEl || inputWidget.element;
+ if (!inputEl) {
+ return null;
+ }
+
+ const nodeId = node.id;
+ const widgetKey = `${nodeId}_${inputId}`;
+
+ // 检查是否已存在实例
+ if (PromptAssistant.hasInstance(widgetKey)) {
+ return PromptAssistant.getInstance(widgetKey);
+ }
+
+ // 创建小助手对象
+ const widget = {
+ type: "prompt_assistant",
+ name: inputId,
+ nodeId,
+ inputId,
+ widgetKey,
+ buttons: {},
+ text_element: inputEl,
+ inputEl: inputEl, // 直接添加inputEl引用,方便历史记录回写
+ // 不再直接设置 node 引用,而是保存 nodeId
+ nodeInfo: {
+ ...nodeInfo,
+ nodeId: node.id,
+ nodeType: node.type
+ },
+ isMouseOver: false, // 鼠标悬停状态
+ isFirstCreate: false, // 首次创建标记
+ isTransitioning: false // 添加状态标记,避免频繁切换
+ };
+
+ // 创建全局输入框映射(如果不存在)
+ if (!window.PromptAssistantInputWidgetMap) {
+ window.PromptAssistantInputWidgetMap = {};
+ }
+
+ // 将当前输入框添加到映射
+ window.PromptAssistantInputWidgetMap[widgetKey] = {
+ inputEl: inputEl,
+ widget: widget
+ };
+
+ logger.debug(`输入框映射 | 添加映射 | 键:${widgetKey}`);
+
+ // 创建UI并添加到实例集合
+ this.createAssistantUI(widget, inputWidget);
+ PromptAssistant.addInstance(widgetKey, widget);
+
+ // 初始化撤销状态
+ HistoryCacheService.initUndoState(nodeId, inputId, inputEl.value);
+ // 初始化时立即更新撤销/重做按钮状态
+ UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService);
+
+ // 绑定输入框失焦事件,写入历史
+ if (inputWidget.inputEl) {
+ // 使用事件管理器添加DOM事件监听
+ const removeBlurListener = EventManager.addDOMListener(inputWidget.inputEl, 'blur', async () => {
+ logger.debug(`历史写入准备| 原因:失焦事件触发 node_id=${node.id} input_id=${inputId}`);
+ HistoryCacheService.addHistory({
+ workflow_id: nodeInfo?.workflow_id || '',
+ node_id: node.id,
+ input_id: inputId,
+ content: inputWidget.inputEl.value,
+ operation_type: 'input',
+ timestamp: Date.now()
+ });
+ // 重置撤销状态
+ HistoryCacheService.initUndoState(node.id, inputId, inputWidget.inputEl.value);
+ // 更新按钮状态
+ UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService);
+ logger.debug(`历史写入完成|原因:输入框失焦 node_id=${node.id} input_id=${inputId}`);
+ });
+
+ // 保存清理函数引用,以便后续清理
+ widget._eventCleanupFunctions = widget._eventCleanupFunctions || [];
+ widget._eventCleanupFunctions.push(removeBlurListener);
+
+ // 添加输入事件监听,实时更新撤销/重做按钮状态
+ const removeInputListener = EventManager.addDOMListener(inputWidget.inputEl, 'input', () => {
+ UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService);
+ });
+ widget._eventCleanupFunctions.push(removeInputListener);
+ }
+
+ return widget;
+ }
+
+ // ---UI管理功能---
+ /**
+ * 创建小助手UI
+ * 构建DOM元素并设置事件监听和初始样式
+ */
+ createAssistantUI(widget, inputWidget) {
+ const nodeId = widget.nodeId;
+ const inputId = widget.inputId;
+
+ try {
+ // 获取DOM相关信息
+ const inputEl = inputWidget.inputEl;
+ const graphCanvasContainer = document.querySelector('.graphcanvas');
+ const canvasContainerRect = graphCanvasContainer?.getBoundingClientRect();
+
+ // 创建内部内容容器(分离视觉效果)
+ const innerContentDiv = document.createElement('div');
+ innerContentDiv.className = 'prompt-assistant-inner';
+
+ // 创建主容器 - 简化样式,减少触发栅格化的属性
+ const containerDiv = document.createElement('div');
+ containerDiv.className = 'prompt-assistant-container';
+ containerDiv.dataset.nodeId = nodeId;
+ containerDiv.dataset.inputId = inputId;
+
+ // 添加内容容器到主容器
+ containerDiv.appendChild(innerContentDiv);
+
+ // 保存引用并初始化
+ widget.element = containerDiv;
+ widget.innerContent = innerContentDiv;
+ widget.buttons = {};
+
+ // 初始化UI组件和事件
+ this.addFunctionButtons(widget);
+ this._setupUIEventHandling(widget, inputEl, containerDiv);
+ this._setupUIPosition(widget, inputEl, containerDiv, canvasContainerRect);
+
+ // 默认隐藏状态
+ containerDiv.style.display = 'none';
+
+ logger.log(`UI创建 | 结果:完成 | 节点ID: ${nodeId}`);
+ return containerDiv;
+ } catch (error) {
+ logger.error(`UI创建 | 结果:异常 | 节点ID: ${nodeId}, 错误: ${error.message}`);
+ return null;
+ }
+ }
+
+ /**
+ * 显示小助手UI
+ * 控制UI显示动画和状态
+ */
+ showAssistantUI(widget, forceAnimation = false) {
+ if (!widget?.element) return;
+
+ // 避免重复显示和不必要的动画
+ const isCurrentlyShown = widget.element.classList.contains('assistant-show');
+ if (isCurrentlyShown && !forceAnimation) {
+ // 如果已经显示且不需要强制动画,则仅确保显示状态
+ widget.element.style.display = 'flex';
+ widget.element.style.opacity = '1';
+ return;
+ }
+
+ // 取消任何隐藏计时器
+ if (widget.hideTimeout) {
+ clearTimeout(widget.hideTimeout);
+ widget.hideTimeout = null;
+ }
+
+ // 设置过渡状态
+ widget.isTransitioning = true;
+
+ // 优化渲染性能
+ widget.element.style.willChange = 'auto';
+ widget.element.style.transform = 'translateZ(0)';
+ widget.element.style.opacity = '1';
+
+ // 显示元素并应用动画类
+ widget.element.style.display = 'flex';
+ void widget.element.offsetWidth; // 触发回流,确保动画生效
+ widget.element.classList.remove('assistant-hide');
+ widget.element.classList.add('assistant-show');
+
+ // 动画结束后重置过渡状态
+ setTimeout(() => {
+ widget.isTransitioning = false;
+ // 动画结束后检查鼠标状态
+ setTimeout(() => this.forceUpdateMouseState(widget), 10);
+ }, 300); // 与CSS动画时长匹配
+ }
+
+ /**
+ * 隐藏小助手UI
+ * 控制UI隐藏动画和状态
+ */
+ hideAssistantUI(widget) {
+ if (!widget?.element) return;
+
+ // 避免重复隐藏
+ if (!widget.element.classList.contains('assistant-show')) return;
+
+ // 设置过渡状态
+ widget.isTransitioning = true;
+
+ // 应用隐藏动画类
+ widget.element.classList.add('assistant-hide');
+ widget.element.classList.remove('assistant-show');
+
+ // 触发回流确保动画生效
+ void widget.element.offsetWidth;
+
+ // 动画结束后隐藏元素
+ widget.hideTimeout = setTimeout(() => {
+ if (widget.element) {
+ widget.element.style.display = 'none';
+ }
+
+ // 重置过渡状态
+ widget.isTransitioning = false;
+
+ // 动画结束后检查鼠标状态
+ setTimeout(() => this.forceUpdateMouseState(widget), 10);
+ }, 300); // 与CSS动画时长匹配
+ }
+
+ /**
+ * 更新小助手可见性
+ * 根据鼠标悬停和首次创建状态决定是否显示
+ */
+ updateAssistantVisibility(widget) {
+ if (!widget) return;
+
+ // 总开关关闭时不处理可见性更新
+ if (!window.FEATURES || !window.FEATURES.enabled) {
+ return;
+ }
+
+ // 检查是否有按钮处于激活或处理中状态
+ const hasActiveButtons = this._checkAssistantActiveState(widget);
+
+ // 如果有激活的按钮,强制显示小助手(带动画)
+ if (hasActiveButtons) {
+ this.showAssistantUI(widget, true);
+ return;
+ }
+
+ const isMouseOver = widget.isMouseOver === true;
+ const isFirstCreate = widget.isFirstCreate === true;
+
+ // 显示条件:首次创建或鼠标悬停
+ const shouldShow = isFirstCreate || isMouseOver;
+ const isCurrentlyShown = widget.element?.classList.contains('assistant-show');
+
+ // 仅在状态需变化时更新
+ if (shouldShow !== isCurrentlyShown) {
+ const reason = isFirstCreate ? "首次创建" : (isMouseOver ? "鼠标悬停" : "鼠标离开");
+
+ if (shouldShow) {
+ // 首次创建时使用动画,鼠标悬停时不使用动画
+ this.showAssistantUI(widget, isFirstCreate);
+ logger.debug(`UI显示 | 节点:${widget.nodeId} | 原因:${reason}`);
+ } else {
+ this.hideAssistantUI(widget);
+ logger.debug(`UI隐藏 | 节点:${widget.nodeId} | 原因:${reason}`);
+ }
+ }
+ }
+
+ /**
+ * 检查小助手是否有按钮处于激活状态
+ */
+ _checkAssistantActiveState(widget) {
+ if (!widget || !widget.buttons) return false;
+
+ // 首先检查中央按钮状态管理器是否有该widget的激活按钮
+ const activeButtonInfo = UIToolkit.getActiveButtonInfo();
+ if (activeButtonInfo && activeButtonInfo.widget === widget) {
+ return true;
+ }
+
+ // 再遍历所有按钮,检查是否有按钮处于active或processing状态
+ for (const buttonId in widget.buttons) {
+ const button = widget.buttons[buttonId];
+ if (button.classList.contains('button-active') ||
+ button.classList.contains('button-processing')) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 更新所有实例的可见性
+ * 在按钮状态变化时调用
+ */
+ updateAllInstancesVisibility() {
+ PromptAssistant.instances.forEach(widget => {
+ if (!widget.isTransitioning) {
+ this.updateAssistantVisibility(widget);
+ }
+ });
+ }
+
+ /**
+ * 显示状态提示
+ * 创建临时提示信息气泡
+ */
+ showStatusTip(anchorElement, type, message, position = null) {
+ return UIToolkit.showStatusTip(anchorElement, type, message, position);
+ }
+
+ // ---事件处理功能---
+ /**
+ * 设置UI事件处理
+ * 配置鼠标悬停相关的事件监听 - 简化版本
+ */
+ _setupUIEventHandling(widget, inputEl, containerDiv) {
+ // 设置容器的事件穿透
+ containerDiv.style.pointerEvents = 'none';
+ widget.innerContent.style.pointerEvents = 'auto'; // 确保内容容器可以接收事件
+
+ // 确保所有按钮可点击
+ const buttons = containerDiv.querySelectorAll('button');
+ buttons.forEach(button => {
+ button.style.pointerEvents = 'auto';
+ button.classList.add('prompt-assistant-button-active');
+ });
+
+ // 初始化过渡状态标记
+ widget.isTransitioning = false;
+
+ // 使用清理函数数组存储所有事件清理函数
+ widget._eventCleanupFunctions = widget._eventCleanupFunctions || [];
+
+ // 添加清理函数
+ widget.cleanupListeners = () => {
+ // 执行所有保存的清理函数
+ if (widget._eventCleanupFunctions && widget._eventCleanupFunctions.length) {
+ widget._eventCleanupFunctions.forEach(cleanupFn => {
+ if (typeof cleanupFn === 'function') {
+ cleanupFn();
+ }
+ });
+ widget._eventCleanupFunctions = [];
+ }
+ };
+ }
+
+ /**
+ * 判断鼠标是否在元素上方
+ */
+ isMouseOverElement(element) {
+ if (!element) return false;
+
+ try {
+ // 直接使用计算而非委托给EventManager,减少一层调用
+ const mousePos = EventManager.getMousePosition();
+ const rect = element.getBoundingClientRect();
+
+ return (
+ mousePos.x >= rect.left &&
+ mousePos.x <= rect.right &&
+ mousePos.y >= rect.top &&
+ mousePos.y <= rect.bottom
+ );
+ } catch (error) {
+ logger.error(`鼠标位置检测失败 | 错误: ${error.message}`);
+ return false;
+ }
+ }
+
+ /**
+ * 强制更新鼠标悬停状态
+ */
+ forceUpdateMouseState(widget) {
+ if (!widget || !widget.node || !widget.text_element || !widget.element) return;
+
+ // 总开关关闭时不处理鼠标状态更新
+ if (!window.FEATURES || !window.FEATURES.enabled) {
+ return;
+ }
+
+ // 使用直接检测方法检查鼠标是否在元素上方
+ const isOverInput = this.isMouseOverElement(widget.text_element);
+ const isOverContainer = widget.element.style.display !== 'none' &&
+ this.isMouseOverElement(widget.element);
+
+ // 更新状态
+ const oldState = widget.isMouseOver;
+ widget.isMouseOver = isOverInput || isOverContainer;
+
+ // 如果状态变化,更新可见性
+ if (oldState !== widget.isMouseOver) {
+ logger.debug(`鼠标状态 | 手动更新 | 节点:${widget.nodeId} | 原状态:${oldState} | 新状态:${widget.isMouseOver}`);
+ this.updateAssistantVisibility(widget);
+ }
+
+ return widget.isMouseOver;
+ }
+
+ // ---鼠标监听功能---
+ /**
+ * 判断全局鼠标监听器是否已注册
+ */
+ isGlobalMouseListenerRegistered() {
+ try {
+ return EventManager.listeners.has('global_mouse_move') &&
+ EventManager.listeners.get('global_mouse_move').has('assistant_manager');
+ } catch (error) {
+ logger.error(`鼠标监听器状态检查失败 | 错误: ${error.message}`);
+ return false;
+ }
+ }
+
+ /**
+ * 注册全局鼠标移动事件监听
+ */
+ async registerGlobalMouseListener() {
+ try {
+ // 确保EventManager已初始化
+ EventManager.init();
+
+ // 检查是否已注册,避免重复
+ if (this.isGlobalMouseListenerRegistered()) {
+ return true;
+ }
+
+ // 创建去抖动的全局监听函数
+ const debouncedListener = EventManager.debounce((e) => {
+ // 检查总开关状态,如果总开关已关闭,则不处理
+ if (!window.FEATURES || !window.FEATURES.enabled) {
+ return;
+ }
+
+ try {
+ // 获取当前实例数量
+ const instanceCount = PromptAssistant.instances.size;
+ if (instanceCount === 0) return;
+
+ // 遍历所有实例,更新鼠标状态
+ PromptAssistant.instances.forEach(widget => {
+ // 跳过正在过渡的实例
+ if (widget.isTransitioning) return;
+
+ // 检查鼠标是否在实例上
+ const isOver = this.isMouseOverElement(widget.text_element) ||
+ (widget.element?.style.display !== 'none' && this.isMouseOverElement(widget.element));
+
+ // 如果状态变化,更新可见性
+ if (widget.isMouseOver !== isOver) {
+ widget.isMouseOver = isOver;
+ this.updateAssistantVisibility(widget);
+ }
+ });
+ } catch (error) {
+ logger.error(`鼠标处理异常 | 错误: ${error.message}`);
+ }
+ }, 50);
+
+ // 注册全局鼠标移动事件监听器
+ EventManager.on('global_mouse_move', 'assistant_manager', debouncedListener);
+
+ return true;
+ } catch (error) {
+ logger.error(`全局鼠标监听注册失败 | 错误: ${error.message}`);
+ return false;
+ }
+ }
+
+ /**
+ * 移除全局鼠标移动事件监听
+ */
+ removeGlobalMouseListener() {
+ try {
+ // 检查是否已注册,未注册则不需要移除
+ if (!this.isGlobalMouseListenerRegistered()) {
+ return true;
+ }
+
+ // 移除事件监听器
+ EventManager.off('global_mouse_move', 'assistant_manager');
+ return true;
+ } catch (error) {
+ logger.error(`全局鼠标监听移除失败 | 错误: ${error.message}`);
+ return false;
+ }
+ }
+
+ // ---辅助功能---
+ /**
+ * 更新输入框内容并添加高亮效果
+ */
+ updateInputWithHighlight(widget, content) {
+ if (!widget?.inputEl) return;
+
+ try {
+ // 更新输入框内容
+ widget.inputEl.value = content;
+
+ // 触发输入事件
+ widget.inputEl.dispatchEvent(new Event('input', { bubbles: true }));
+ widget.inputEl.dispatchEvent(new Event('change', { bubbles: true }));
+
+ // 添加高亮效果
+ widget.inputEl.classList.add('input-highlight');
+ // 动画结束后移除类
+ setTimeout(() => {
+ widget.inputEl.classList.remove('input-highlight');
+ }, 200); // 与 CSS 动画时长匹配
+
+ logger.debug(`输入框更新 | 结果:成功 | 内容长度:${content.length}`);
+ } catch (error) {
+ logger.error(`输入框更新 | 结果:异常 | 错误:${error.message}`);
+ }
+ }
+
+ // ---按钮管理功能---
+ /**
+ * 添加功能按钮
+ */
+ addFunctionButtons(widget) {
+ if (!widget?.element) {
+ logger.error('添加按钮 | 结果:失败 | 原因: 容器不存在');
+ return;
+ }
+
+ // 检查总开关状态
+ if (!FEATURES.enabled) {
+ logger.debug('添加按钮 | 结果:跳过 | 原因: 总功能已禁用');
+ return;
+ }
+
+ // 检查是否有至少一个功能启用
+ const hasEnabledFeatures = FEATURES.history || FEATURES.tag || FEATURES.expand || FEATURES.translate;
+ if (!hasEnabledFeatures) {
+ logger.debug('添加按钮 | 结果:跳过 | 原因: 没有启用任何功能');
+ return;
+ }
+
+ // 检查是否是Note节点
+ const isNoteNode = widget.nodeInfo && widget.nodeInfo.isNoteNode === true;
+
+ // 获取历史状态(用于初始化撤销/重做按钮状态)
+ const canUndo = HistoryCacheService.canUndo(widget.nodeId, widget.inputId);
+ const canRedo = HistoryCacheService.canRedo(widget.nodeId, widget.inputId);
+
+ // 按钮配置
+ const buttonConfigs = [
+ {
+ id: 'history',
+ title: '历史',
+ icon: 'icon-history.svg',
+ onClick: (e, widget) => {
+ UIToolkit.handlePopupButtonClick(
+ e,
+ widget,
+ 'history',
+ HistoryManager.showHistoryPopup.bind(HistoryManager),
+ HistoryManager.hideHistoryPopup.bind(HistoryManager)
+ );
+ },
+ visible: !isNoteNode && FEATURES.history, // Note节点不显示此按钮
+ initialState: { disabled: false }
+ },
+ {
+ id: 'undo',
+ title: '撤销',
+ icon: 'icon-undo.svg',
+ onClick: (e, widget) => {
+ e.preventDefault();
+ e.stopPropagation();
+ logger.debug('按钮点击 | 动作: 撤销');
+
+ // 执行撤销操作
+ const undoContent = HistoryCacheService.undo(widget.nodeId, widget.inputId);
+ if (undoContent !== null) {
+ // 更新输入框内容并添加高亮效果
+ this.updateInputWithHighlight(widget, undoContent);
+
+ // 更新按钮状态
+ UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService);
+
+ logger.debug(`撤销操作 | 结果:成功 | 节点:${widget.nodeId}`);
+ } else {
+ logger.debug(`撤销操作 | 结果:失败 | 节点:${widget.nodeId} | 原因:无可用内容`);
+ }
+ },
+ visible: !isNoteNode && FEATURES.history,
+ initialState: { disabled: !canUndo }
+ },
+ {
+ id: 'redo',
+ title: '重做',
+ icon: 'icon-redo.svg',
+ onClick: (e, widget) => {
+ e.preventDefault();
+ e.stopPropagation();
+ logger.debug('按钮点击 | 动作: 重做');
+
+ // 执行重做操作
+ const redoContent = HistoryCacheService.redo(widget.nodeId, widget.inputId);
+ if (redoContent !== null) {
+ // 更新输入框内容并添加高亮效果
+ this.updateInputWithHighlight(widget, redoContent);
+
+ // 更新按钮状态
+ UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService);
+
+ logger.debug(`重做操作 | 结果:成功 | 节点:${widget.nodeId}`);
+ } else {
+ logger.debug(`重做操作 | 结果:失败 | 节点:${widget.nodeId} | 原因:无可用内容`);
+ }
+ },
+ visible: !isNoteNode && FEATURES.history,
+ initialState: { disabled: !canRedo }
+ },
+ {
+ id: 'divider1',
+ type: 'divider',
+ visible: !isNoteNode // Note节点不显示分割线
+ },
+ {
+ id: 'tag',
+ title: '标签工具',
+ icon: 'icon-tag.svg',
+ onClick: (e, widget) => {
+ // 创建一个带有标签选择功能的显示函数
+ const showTagPopup = (options) => {
+ // 处理标签选择功能
+ const enhancedOptions = {
+ ...options,
+ onTagSelect: (tag) => {
+ // 获取当前输入框的值和光标位置
+ const currentValue = widget.inputEl.value;
+ const cursorPos = widget.inputEl.selectionStart;
+ const beforeText = currentValue.substring(0, cursorPos);
+ const afterText = currentValue.substring(widget.inputEl.selectionEnd);
+
+ // 添加标签(英文值)
+ const newValue = beforeText + tag.en + afterText;
+
+ // 更新输入框内容并添加高亮效果
+ this.updateInputWithHighlight(widget, newValue);
+
+ // 更新光标位置
+ const newPos = cursorPos + tag.en.length;
+ widget.inputEl.setSelectionRange(newPos, newPos);
+
+ // 保持焦点在输入框
+ widget.inputEl.focus();
+ }
+ };
+
+ // 调用标签管理器显示弹窗
+ TagManager.showTagPopup(enhancedOptions);
+ };
+
+ // 使用统一的弹窗按钮点击处理
+ UIToolkit.handlePopupButtonClick(
+ e,
+ widget,
+ 'tag',
+ showTagPopup,
+ TagManager.hideTagPopup.bind(TagManager)
+ );
+ },
+ visible: !isNoteNode && FEATURES.tag // Note节点不显示此按钮
+ },
+ {
+ id: 'expand',
+ title: '扩写',
+ icon: 'icon-expand.svg',
+ onClick: async (e, widget) => {
+ logger.debug('按钮点击 | 动作: 扩写');
+
+ // 如果按钮处于 processing 状态且被点击,显示"扩写中"提示并直接返回
+ if (e.currentTarget.classList.contains('button-processing')) {
+ const btnRect = e.currentTarget.getBoundingClientRect();
+ UIToolkit.showStatusTip(
+ e.currentTarget,
+ 'loading',
+ '扩写中',
+ { x: btnRect.left + btnRect.width / 2, y: btnRect.top }
+ );
+ return;
+ }
+
+ await UIToolkit.handleAsyncButtonOperation(
+ widget,
+ 'expand',
+ e.currentTarget,
+ async () => {
+ try {
+ // 获取输入值
+ const inputValue = widget.inputEl.value;
+ if (!inputValue || inputValue.trim() === '') {
+ throw new Error('请输入要扩写的内容');
+ }
+
+ // 生成唯一request_id
+ const request_id = generateRequestId('glm4_expand');
+
+ // 显示扩写中提示
+ const btnRect = e.currentTarget.getBoundingClientRect();
+ UIToolkit.showStatusTip(
+ e.currentTarget,
+ 'loading',
+ '扩写中',
+ { x: btnRect.left + btnRect.width / 2, y: btnRect.top }
+ );
+
+ // 调用LLM服务进行扩写
+ const result = await llmService.expandPrompt(inputValue, request_id);
+
+ if (result.success) {
+ // 更新输入框内容并添加高亮效果
+ this.updateInputWithHighlight(widget, result.data.expanded);
+
+ // 添加扩写结果到历史记录
+ HistoryCacheService.addHistory({
+ workflow_id: widget.nodeInfo?.workflow_id || '',
+ node_id: widget.nodeId,
+ input_id: widget.inputId,
+ content: result.data.expanded,
+ operation_type: 'expand',
+ request_id: request_id,
+ timestamp: Date.now()
+ });
+
+ // 重置撤销状态
+ HistoryCacheService.initUndoState(widget.nodeId, widget.inputId, result.data.expanded);
+
+ // 更新按钮状态
+ UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService);
+
+ return {
+ success: true,
+ useCache: false,
+ tipType: 'success',
+ tipMessage: '扩写完成'
+ };
+ } else {
+ // 不在这里显示错误提示,直接抛出错误让 handleAsyncButtonOperation 处理
+ throw new Error(result.error);
+ }
+ } catch (error) {
+ // 不在这里显示错误提示,直接抛出错误让 handleAsyncButtonOperation 处理
+ throw error;
+ }
+ }
+ );
+ },
+ visible: !isNoteNode && FEATURES.expand // Note节点不显示此按钮
+ },
+ {
+ id: 'translate',
+ title: '翻译',
+ icon: 'icon-translate.svg',
+ onClick: async (e, widget) => {
+ logger.debug('按钮点击 | 动作: 翻译');
+
+ // 如果按钮处于 processing 状态且被点击,显示"翻译中"提示并直接返回
+ if (e.currentTarget.classList.contains('button-processing')) {
+ const btnRect = e.currentTarget.getBoundingClientRect();
+ UIToolkit.showStatusTip(
+ e.currentTarget,
+ 'loading',
+ '翻译中',
+ { x: btnRect.left + btnRect.width / 2, y: btnRect.top }
+ );
+ return;
+ }
+
+ await UIToolkit.handleAsyncButtonOperation(
+ widget,
+ 'translate',
+ e.currentTarget,
+ async () => {
+ try {
+ // 获取输入值
+ const inputValue = widget.inputEl.value;
+ if (!inputValue || inputValue.trim() === '') {
+ throw new Error('请输入要翻译的内容');
+ }
+
+ // 显示翻译中提示
+ const btnRect = e.currentTarget.getBoundingClientRect();
+ UIToolkit.showStatusTip(
+ e.currentTarget,
+ 'loading',
+ '翻译中',
+ { x: btnRect.left + btnRect.width / 2, y: btnRect.top }
+ );
+
+ // 首先查询翻译缓存
+ const cacheResult = TranslateCacheService.queryTranslateCache(inputValue);
+
+ // 缓存命中情况处理
+ if (cacheResult) {
+ let resultText = '';
+ let tipMessage = '';
+ let useCache = true;
+
+ // 根据缓存匹配类型处理
+ if (cacheResult.type === 'source') {
+ // 命中原文,返回译文
+ resultText = cacheResult.translatedText;
+ tipMessage = '译文';
+ } else if (cacheResult.type === 'translated') {
+ // 命中译文,返回原文
+ resultText = cacheResult.sourceText;
+ tipMessage = '原文';
+ }
+
+ // 更新输入框内容并添加高亮效果
+ this.updateInputWithHighlight(widget, resultText);
+
+ // 添加翻译结果到历史记录
+ HistoryCacheService.addHistory({
+ workflow_id: widget.nodeInfo?.workflow_id || '',
+ node_id: widget.nodeId,
+ input_id: widget.inputId,
+ content: resultText,
+ operation_type: 'translate',
+ timestamp: Date.now()
+ });
+
+ // 重置撤销状态
+ HistoryCacheService.initUndoState(widget.nodeId, widget.inputId, resultText);
+
+ // 更新按钮状态
+ UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService);
+
+ return {
+ success: true,
+ useCache: useCache,
+ tipType: 'info',
+ tipMessage: tipMessage,
+ buttonElement: e.currentTarget // 传递按钮元素
+ };
+ }
+
+ // 缓存未命中,使用API翻译
+ // 获取翻译类型
+ const translateType = localStorage.getItem("PromptAssistant_Settings_translate_type") || "baidu";
+
+ // 生成唯一request_id,根据翻译类型生成对应的前缀
+ const request_id = generateRequestId(null, translateType);
+
+ // 检测语言
+ const langResult = PromptFormatter.detectLanguage(inputValue);
+
+ // 根据翻译类型选择服务
+ let result;
+ try {
+ if (translateType === "baidu") {
+ // 使用百度翻译服务
+ result = await BaiduTranslateService.translate(
+ inputValue,
+ langResult.from,
+ langResult.to,
+ request_id
+ );
+ } else {
+ // 使用LLM翻译服务
+ result = await llmService.translate(
+ inputValue,
+ langResult.from,
+ langResult.to,
+ request_id
+ );
+ }
+
+ if (!result) {
+ throw new Error('翻译服务返回空结果');
+ }
+ } catch (error) {
+ logger.error(`翻译失败 | 服务:${translateType} | 错误:${error.message}`);
+ throw new Error(`翻译失败: ${error.message}`);
+ }
+
+ if (result.success) {
+ // 格式化翻译结果(转换标点符号)
+ const formattedText = PromptFormatter.formatTranslatedText(result.data.translated);
+
+ // 添加翻译结果到历史记录
+ HistoryCacheService.addHistory({
+ workflow_id: widget.nodeInfo?.workflow_id || '',
+ node_id: widget.nodeId,
+ input_id: widget.inputId,
+ content: formattedText,
+ operation_type: 'translate',
+ request_id: request_id,
+ timestamp: Date.now()
+ });
+
+ // 更新输入框内容并添加高亮效果
+ this.updateInputWithHighlight(widget, formattedText);
+
+ // 重置撤销状态
+ HistoryCacheService.initUndoState(widget.nodeId, widget.inputId, formattedText);
+
+ // 更新按钮状态
+ UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService);
+
+ // 添加到翻译缓存
+ TranslateCacheService.addTranslateCache(inputValue, formattedText);
+
+ return {
+ success: true,
+ useCache: false,
+ tipType: 'success',
+ tipMessage: '翻译完成'
+ };
+ } else {
+ // 不在这里显示错误提示,直接抛出错误让 handleAsyncButtonOperation 处理
+ throw new Error(result.error);
+ }
+ } catch (error) {
+ // 不在这里显示错误提示,直接抛出错误让 handleAsyncButtonOperation 处理
+ throw error;
+ }
+ }
+ );
+ },
+ visible: FEATURES.translate // Note节点只显示此按钮
+ },
+ ];
+
+ // 记录添加的按钮
+ let historyButtons = [];
+ let otherButtons = [];
+ let divider = null;
+
+ buttonConfigs.forEach(config => {
+ if (config.type === 'divider') {
+ // 创建分割线但暂不添加
+ divider = document.createElement('div');
+ divider.className = 'prompt-assistant-divider';
+ divider.dataset.id = config.id || `divider_${Date.now()}`;
+ // 保存引用
+ if (config.id) {
+ widget.buttons[config.id] = divider;
+ }
+ return;
+ }
+
+ // 检查按钮是否可见
+ if (config.visible === false) {
+ // 区分节点类型和功能禁用的情况
+ const reason = (widget.nodeInfo?.isNoteNode && config.id !== 'translate') ? "Note节点限制" : "功能禁用";
+ // logger.debug(`按钮跳过 | 按钮: ${config.id || 'unknown'} | 原因: ${reason}`);
+ return;
+ }
+
+ // 创建按钮
+ const button = this.addButtonWithIcon(widget, config);
+ if (!button) return;
+
+ // 设置初始状态
+ if (config.initialState) {
+ Object.entries(config.initialState).forEach(([stateType, value]) => {
+ UIToolkit.setButtonState(widget, config.id, stateType, value);
+ });
+ }
+
+ // 根据按钮类型分组
+ if (['history', 'undo', 'redo'].includes(config.id)) {
+ historyButtons.push(button);
+ } else {
+ otherButtons.push(button);
+ }
+ });
+
+ // 添加按钮到DOM,并在需要时添加分割线
+ let addedButtonCount = 0;
+
+ // 添加历史相关按钮
+ historyButtons.forEach(button => {
+ widget.innerContent.appendChild(button);
+ addedButtonCount++;
+ });
+
+ // 如果两种类型的按钮都存在,添加分割线
+ if (historyButtons.length > 0 && otherButtons.length > 0 && divider) {
+ widget.innerContent.appendChild(divider);
+ }
+
+ // 添加其他按钮
+ otherButtons.forEach(button => {
+ widget.innerContent.appendChild(button);
+ addedButtonCount++;
+ });
+
+ // 判断节点类型并记录日志
+ if (widget.nodeInfo?.isNoteNode) {
+ logger.debug(`按钮添加 | 结果:完成 | 节点ID: ${widget.nodeId} | Note节点: 仅显示翻译按钮 | 添加数量: ${addedButtonCount}`);
+ } else {
+ logger.debug(`按钮添加 | 结果:完成 | 节点ID: ${widget.nodeId} | 标准节点 | 添加数量: ${addedButtonCount}`);
+ }
+ }
+
+ /**
+ * 添加带图标的按钮
+ */
+ addButtonWithIcon(widget, config) {
+ if (!widget?.element || !widget?.innerContent) {
+ logger.error('添加按钮 | 结果:失败 | 原因: 容器不存在');
+ return null;
+ }
+
+ const { id, title, icon, onClick } = config;
+
+ // 创建按钮
+ const button = document.createElement('button');
+ button.className = 'prompt-assistant-button';
+ button.title = title || '';
+ button.dataset.id = id || `btn_${Date.now()}`;
+ button.dataset.icon = icon?.replace('.svg', ''); // 保存不带后缀的图标名,用于CSS选择器
+
+ // 按钮样式在CSS中定义
+
+ // 添加图标
+ if (icon) {
+ UIToolkit.addIconToButton(button, icon, title || id);
+ }
+
+ // 添加事件
+ if (typeof onClick === 'function') {
+ button.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onClick(e, widget);
+ });
+ }
+
+ // 保存引用并添加到内部内容容器
+ if (id) {
+ widget.buttons[id] = button;
+ }
+ widget.innerContent.appendChild(button);
+
+ return button;
+ }
+
+ /**
+ * 设置UI位置
+ * 使用绝对定位方式
+ */
+ _setupUIPosition(widget, inputEl, containerDiv, canvasContainerRect) {
+ // 查找dom-widget父容器(ComfyUI 0.3.27及以上版本的标准容器)
+ let domWidgetContainer = null;
+
+ // 向上查找dom-widget容器
+ let parent = inputEl.parentElement;
+ while (parent) {
+ if (parent.classList && parent.classList.contains('dom-widget')) {
+ domWidgetContainer = parent;
+ break;
+ }
+ parent = parent.parentElement;
+ }
+
+ // 检查是否需要使用兼容模式
+ const needCompatibilityMode = !domWidgetContainer;
+
+ // 清理函数列表
+ widget._eventCleanupFunctions = widget._eventCleanupFunctions || [];
+
+ if (needCompatibilityMode) {
+ // === 兼容方案定位 (ComfyUI 0.3.26及以下) ===
+ logger.log("定位方式 - 兼容方案");
+
+ // 使用固定定位样式,不添加额外类
+ containerDiv.style.position = 'fixed';
+ containerDiv.style.zIndex = '9999';
+ document.body.appendChild(containerDiv);
+
+ // 设置初始显示
+ containerDiv.style.display = 'flex';
+
+ // 更新位置的函数,确保跟随输入框并位于右下角
+ const updatePosition = () => {
+ if (!widget.element || !inputEl || !containerDiv) return;
+
+ try {
+ // 获取输入框的位置信息
+ const inputRect = inputEl.getBoundingClientRect();
+
+ // 设置容器样式 - 首先确保容器可见以便获得正确的尺寸
+ containerDiv.style.display = 'flex';
+ containerDiv.style.visibility = 'hidden'; // 暂时隐藏以避免闪烁
+
+ // 先设置样式,使其能正确计算尺寸
+ Object.assign(containerDiv.style, {
+ width: 'auto',
+ height: '24px',
+ pointerEvents: 'none'
+ });
+
+ // 确保小部件样式正确
+ Object.assign(widget.element.style, {
+ transformOrigin: 'right center',
+ margin: '0',
+ pointerEvents: 'auto'
+ });
+
+ // 强制回流以获取正确尺寸
+ void containerDiv.offsetWidth;
+
+ // 现在设置位置 - 放置在输入框右下角
+ const offsetRight = 12; // 右侧偏移
+ const offsetBottom = 8;
+
+ containerDiv.style.left = `${inputRect.right - containerDiv.offsetWidth - offsetRight}px`;
+ containerDiv.style.top = `${inputRect.bottom - containerDiv.offsetHeight - offsetBottom}px`;
+ containerDiv.style.visibility = 'visible'; // 恢复可见性
+
+ // 最终触发回流,确保样式更新
+ void containerDiv.offsetWidth;
+ } catch (error) {
+ logger.error("更新小助手位置出错:", error);
+ // 错误恢复 - 确保组件仍然可见
+ if (containerDiv) containerDiv.style.visibility = 'visible';
+ }
+ };
+
+ // 初始更新位置
+ updatePosition();
+
+ // 使用防抖函数优化位置更新
+ const debouncedUpdatePosition = EventManager.debounce(updatePosition, 100);
+
+ // 添加窗口resize事件监听
+ const removeResizeListener = EventManager.addDOMListener(window, 'resize', debouncedUpdatePosition);
+ widget._eventCleanupFunctions.push(removeResizeListener);
+
+ // 监听画布变化
+ const app = window.app || null;
+
+ if (!app || !app.canvas) {
+ logger.error("错误:无法获取app对象,小助手无法正常工作");
+ return;
+ }
+
+ // 1. 使用MutationObserver监听画布变化
+ const observer = new MutationObserver(debouncedUpdatePosition);
+ const canvasParent = app.canvas.canvas.parentElement;
+
+ if (canvasParent) {
+ observer.observe(canvasParent, {
+ attributes: true,
+ attributeFilter: ['style', 'transform']
+ });
+
+ // 添加Observer清理函数
+ widget._eventCleanupFunctions.push(() => observer.disconnect());
+ }
+
+ // 2. 监听画布重绘
+ const originalDrawBackground = app.canvas.onDrawBackground;
+ app.canvas.onDrawBackground = function () {
+ const ret = originalDrawBackground?.apply(this, arguments);
+ debouncedUpdatePosition();
+ return ret;
+ };
+
+ // 添加画布重绘清理函数
+ widget._eventCleanupFunctions.push(() => {
+ if (originalDrawBackground) {
+ app.canvas.onDrawBackground = originalDrawBackground;
+ }
+ });
+
+ // 3. 监听节点移动 - 使用 nodeInfo 而不是直接的 node 引用
+ const nodeId = widget.nodeInfo?.nodeId;
+ if (nodeId && app.canvas && app.canvas.graph) {
+ const node = app.canvas.graph.getNodeById(nodeId);
+ if (node) {
+ const originalOnNodeMoved = node.onNodeMoved;
+ node.onNodeMoved = function () {
+ const ret = originalOnNodeMoved?.apply(this, arguments);
+ debouncedUpdatePosition();
+ return ret;
+ };
+
+ // 添加节点移动清理函数
+ widget._eventCleanupFunctions.push(() => {
+ if (node && originalOnNodeMoved) {
+ node.onNodeMoved = originalOnNodeMoved;
+ }
+ });
+ }
+ }
+
+ // 添加DOM元素清理函数
+ widget._eventCleanupFunctions.push(() => {
+ // 移除DOM元素
+ if (containerDiv && document.body.contains(containerDiv)) {
+ document.body.removeChild(containerDiv);
+ }
+ });
+ } else {
+ // === 标准方案定位 (ComfyUI 0.3.27及以上) ===
+ logger.log("定位方式 - 标准方案");
+
+ // 标准模式使用默认的绝对定位,添加位置参数
+ containerDiv.style.right = '6px';
+ containerDiv.style.bottom = '8px';
+
+ // 直接添加到dom-widget容器,但不修改dom-widget本身
+ domWidgetContainer.appendChild(containerDiv);
+
+ // 触发回流,确保样式更新
+ void containerDiv.offsetWidth;
+ }
+
+ // 强制应用容器高度
+ containerDiv.style.height = '20px';
+ containerDiv.style.minHeight = '20px';
+ void containerDiv.offsetWidth; // 触发回流,确保样式应用
+ }
+
+ /**
+ * 清理单个实例的资源
+ * @param {Object} instance - 要清理的实例
+ * @param {string} instanceKey - 实例的键
+ * @param {boolean} [skipRemove=false] - 是否跳过从实例集合中移除
+ */
+ _cleanupInstance(instance, instanceKey, skipRemove = false) {
+ try {
+ if (!instance) return;
+
+ // 1. 重置所有按钮状态
+ if (instance.buttons) {
+ Object.keys(instance.buttons).forEach(buttonId => {
+ try {
+ const button = instance.buttons[buttonId];
+ if (button) {
+ // 移除所有状态类
+ button.classList.remove('button-active', 'button-processing', 'button-disabled');
+ // 移除所有事件监听器
+ button.replaceWith(button.cloneNode(true));
+ }
+ } catch (err) {
+ logger.debug(`按钮清理 | 按钮:${buttonId} | 错误:${err.message}`);
+ }
+ });
+ // 清空按钮引用
+ instance.buttons = {};
+ }
+
+ // 2. 清理事件监听器
+ if (instance.cleanupListeners) {
+ instance.cleanupListeners();
+ }
+
+ // 3. 清理所有保存的事件清理函数
+ if (instance._eventCleanupFunctions) {
+ instance._eventCleanupFunctions.forEach(cleanup => {
+ if (typeof cleanup === 'function') {
+ try {
+ cleanup();
+ } catch (err) {
+ logger.debug(`事件清理 | 错误:${err.message}`);
+ }
+ }
+ });
+ instance._eventCleanupFunctions = [];
+ }
+
+ // 4. 从DOM中移除元素
+ if (instance.element && instance.element.parentNode) {
+ // 确保在移除元素前清理所有子元素的事件
+ const allButtons = instance.element.querySelectorAll('button');
+ allButtons.forEach(button => {
+ button.replaceWith(button.cloneNode(true));
+ });
+ instance.element.parentNode.removeChild(instance.element);
+ }
+
+ // 5. 清理输入框映射
+ if (window.PromptAssistantInputWidgetMap && instanceKey) {
+ delete window.PromptAssistantInputWidgetMap[instanceKey];
+ }
+
+ // 6. 清理弹窗状态
+ if (window.FEATURES && window.FEATURES.updateButtonsVisibility) {
+ try {
+ window.FEATURES.updateButtonsVisibility();
+ } catch (err) {
+ logger.debug(`按钮可见性更新 | 错误:${err.message}`);
+ }
+ }
+
+ // 7. 从实例集合中移除(除非明确指定跳过)
+ if (!skipRemove && instanceKey) {
+ PromptAssistant.instances.delete(instanceKey);
+ }
+
+ // 8. 清理实例属性
+ Object.keys(instance).forEach(key => {
+ try {
+ delete instance[key];
+ } catch (err) {
+ logger.debug(`属性清理 | 属性:${key} | 错误:${err.message}`);
+ }
+ });
+
+ logger.debug(`实例清理 | 结果:成功 | 实例:${instanceKey}`);
+ } catch (error) {
+ logger.error(`实例清理失败 | 实例:${instanceKey} | 错误:${error.message}`);
+ }
+ }
+}
+
+// 创建单例实例
+const promptAssistant = new PromptAssistant();
+
+// 导出
+export { promptAssistant, PromptAssistant };
\ No newline at end of file
diff --git a/js/modules/history.js b/js/modules/history.js
new file mode 100644
index 0000000..20a5835
--- /dev/null
+++ b/js/modules/history.js
@@ -0,0 +1,681 @@
+/**
+ * 历史记录管理器
+ * 负责管理历史记录的显示和操作
+ */
+
+import { app } from "../../../../scripts/app.js";
+import { logger } from '../utils/logger.js';
+import { HistoryCacheService } from "../services/cache.js";
+import { PopupManager } from "../utils/popupManager.js";
+import { EventManager } from "../utils/eventManager.js";
+import { UIToolkit } from "../utils/UIToolkit.js";
+import { ResourceManager } from "../utils/resourceManager.js";
+
+/**
+ * 历史记录管理器类
+ * 管理历史记录弹窗和历史记录选择
+ */
+class HistoryManager {
+ static popupInstance = null;
+ static onCloseCallback = null;
+ static currentNodeId = null; // 当前节点ID
+ static currentInputId = null; // 当前输入框ID
+ static eventCleanups = []; // 事件清理函数数组
+ static activeTooltip = null; // 当前活动的 tooltip
+
+ /**
+ * 显示历史记录弹窗
+ * @param {Object} params 显示参数
+ */
+ static async showHistoryPopup(params) {
+ const { anchorButton, nodeId, inputId, onClose } = params;
+
+ try {
+ // 保存当前节点和输入框ID
+ this.currentNodeId = nodeId;
+ this.currentInputId = inputId;
+
+ logger.debug(`历史弹窗 | 触发显示 | 节点:${nodeId} | 输入框:${inputId}`);
+
+ // 清理现有事件监听
+ this._cleanupEvents();
+
+ // 获取历史数据
+ const historyList = HistoryCacheService.getHistoryList({
+ nodeId: nodeId,
+ limit: 100 // 增加限制以确保有足够的历史数据
+ });
+
+ // 创建新弹窗
+ const popup = this._createHistoryPopup({ historyList, nodeId });
+
+ // 使用PopupManager显示弹窗
+ await PopupManager.showPopup({
+ popup: popup,
+ anchorButton: anchorButton,
+ buttonInfo: params.buttonInfo,
+ onClose: () => {
+ // 清理事件监听
+ this._cleanupEvents();
+ // 执行传入的关闭回调
+ if (typeof onClose === 'function') {
+ onClose();
+ }
+ }
+ });
+
+ logger.debug(`历史弹窗 | 结果:显示成功 | 节点:${nodeId}`);
+ } catch (error) {
+ logger.error(`历史弹窗 | 结果:失败 | 错误:${error.message}`);
+ this._cleanupAll();
+ }
+ }
+
+ /**
+ * 隐藏历史记录弹窗
+ */
+ static hideHistoryPopup() {
+ // 清理事件监听
+ this._cleanupEvents();
+
+ // 使用PopupManager关闭所有弹窗
+ PopupManager.closeAllPopups();
+ }
+
+ /**
+ * 清理所有事件监听
+ */
+ static _cleanupEvents() {
+ // 执行并清空所有事件清理函数
+ if (this.eventCleanups.length > 0) {
+ this.eventCleanups.forEach(cleanup => {
+ if (typeof cleanup === 'function') {
+ cleanup();
+ }
+ });
+ this.eventCleanups = [];
+ }
+ }
+
+ /**
+ * 强制清理所有相关资源
+ */
+ static _cleanupAll() {
+ // 清理事件监听
+ this._cleanupEvents();
+
+ // 使用PopupManager关闭所有弹窗
+ PopupManager.closeAllPopups();
+ }
+
+ /**
+ * 格式化历史内容,进行适当的截断和处理
+ * @param {string} content 原始内容
+ * @param {string} operationType 操作类型
+ * @returns {string} 格式化后的内容
+ */
+ static _formatHistoryContent(content, operationType) {
+ // 不再添加操作类型前缀,直接返回内容
+ return content;
+ }
+
+ /**
+ * 创建并显示tooltip
+ * @param {HTMLElement} target 目标元素
+ * @param {string} text tooltip文本
+ */
+ static _showTooltip(target, text) {
+ // 移除已存在的tooltip
+ this._hideTooltip();
+
+ // 创建tooltip元素
+ const tooltip = document.createElement('div');
+ tooltip.className = 'tag_tooltip';
+ tooltip.textContent = text;
+ document.body.appendChild(tooltip);
+
+ // 获取目标元素的位置和尺寸
+ const rect = target.getBoundingClientRect();
+
+ // 计算tooltip位置
+ const tooltipRect = tooltip.getBoundingClientRect();
+ const left = rect.left + (rect.width - tooltipRect.width) / 2;
+ const top = rect.top - tooltipRect.height - 8; // 8px的间距
+
+ // 设置tooltip位置
+ tooltip.style.left = `${left}px`;
+ tooltip.style.top = `${top}px`;
+
+ // 保存当前tooltip引用
+ this.activeTooltip = tooltip;
+ }
+
+ /**
+ * 隐藏tooltip
+ */
+ static _hideTooltip() {
+ if (this.activeTooltip) {
+ this.activeTooltip.remove();
+ this.activeTooltip = null;
+ }
+ }
+
+ /**
+ * 创建历史记录弹窗
+ * @param {Object} options
+ * @returns {HTMLElement} 弹窗元素
+ */
+ static _createHistoryPopup({ historyList, nodeId }) {
+ const popup = document.createElement('div');
+ popup.className = 'popup_container';
+
+ // 创建标题栏
+ const titleBar = document.createElement('div');
+ titleBar.className = 'popup_title_bar';
+
+ const title = document.createElement('div');
+ title.className = 'popup_title';
+ title.textContent = '历史记录';
+
+ const actions = document.createElement('div');
+ actions.className = 'popup_actions';
+
+ // 创建清除当前按钮
+ const clearCurrentBtn = document.createElement('button');
+ clearCurrentBtn.className = 'popup_action_btn';
+ clearCurrentBtn.textContent = '清除当前';
+
+ // 使用EventManager添加点击事件
+ const clearCurrentCleanup = EventManager.addDOMListener(clearCurrentBtn, 'click', async (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.hideHistoryPopup();
+ try {
+ // 获取当前输入框内容
+ const inputEl = window.PromptAssistantInputWidgetMap?.[`${nodeId}_${this.currentInputId}`]?.inputEl;
+ const currentContent = inputEl?.value || '';
+
+ // 清除当前节点的历史
+ await HistoryCacheService.clearNodeHistory(nodeId);
+
+ // 如果输入框有内容,添加到历史记录
+ if (currentContent.trim()) {
+ HistoryCacheService.addHistory({
+ workflow_id: '',
+ node_id: nodeId,
+ input_id: this.currentInputId,
+ content: currentContent,
+ operation_type: 'input',
+ timestamp: Date.now()
+ });
+ logger.debug(`历史记录 | 清除后保存当前内容 | 节点:${nodeId} | 输入框:${this.currentInputId}`);
+ }
+
+ // 更新按钮状态
+ if (window.PromptAssistantInputWidgetMap?.[`${nodeId}_${this.currentInputId}`]?.widget) {
+ const widget = window.PromptAssistantInputWidgetMap[`${nodeId}_${this.currentInputId}`].widget;
+ UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService);
+ }
+
+ logger.debug(`历史记录 | 清除当前节点历史 | 节点:${nodeId}`);
+ } catch (error) {
+ logger.error(`历史记录 | 清除失败 | 错误:${error.message}`);
+ }
+ });
+ this.eventCleanups.push(clearCurrentCleanup);
+
+ // 创建清除所有按钮
+ const clearAllBtn = document.createElement('button');
+ clearAllBtn.className = 'popup_action_btn danger';
+ clearAllBtn.textContent = '清除所有';
+
+ // 使用EventManager添加点击事件
+ const clearAllCleanup = EventManager.addDOMListener(clearAllBtn, 'click', async (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.hideHistoryPopup();
+ try {
+ // 获取当前输入框内容
+ const inputEl = window.PromptAssistantInputWidgetMap?.[`${nodeId}_${this.currentInputId}`]?.inputEl;
+ const currentContent = inputEl?.value || '';
+
+ // 清除所有历史
+ await HistoryCacheService.clearAllHistory();
+
+ // 如果输入框有内容,添加到历史记录
+ if (currentContent.trim()) {
+ HistoryCacheService.addHistory({
+ workflow_id: '',
+ node_id: nodeId,
+ input_id: this.currentInputId,
+ content: currentContent,
+ operation_type: 'input',
+ timestamp: Date.now()
+ });
+ logger.debug(`历史记录 | 清除后保存当前内容 | 节点:${nodeId} | 输入框:${this.currentInputId}`);
+ }
+
+ // 更新按钮状态
+ if (window.PromptAssistantInputWidgetMap?.[`${nodeId}_${this.currentInputId}`]?.widget) {
+ const widget = window.PromptAssistantInputWidgetMap[`${nodeId}_${this.currentInputId}`].widget;
+ UIToolkit.updateUndoRedoButtonState(widget, HistoryCacheService);
+ }
+
+ logger.debug('历史记录 | 清除所有历史');
+ } catch (error) {
+ logger.error(`历史记录 | 清除失败 | 错误:${error.message}`);
+ }
+ });
+ this.eventCleanups.push(clearAllCleanup);
+
+ const closeBtn = document.createElement('button');
+ closeBtn.className = 'popup_btn';
+ const closeIcon = ResourceManager.getIcon('icon-close.svg');
+ closeIcon.className = 'popup_btn_icon';
+ closeBtn.appendChild(closeIcon);
+
+ // 使用EventManager添加点击事件
+ const closeCleanup = EventManager.addDOMListener(closeBtn, 'click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.hideHistoryPopup();
+ });
+ this.eventCleanups.push(closeCleanup);
+
+ // 添加按钮到操作区域
+ actions.appendChild(clearCurrentBtn);
+ actions.appendChild(clearAllBtn);
+ actions.appendChild(closeBtn);
+ titleBar.appendChild(title);
+ titleBar.appendChild(actions);
+
+ // 将历史记录按节点分组并排序
+ const { orderedNodeIds, nodeGroups } = this._groupAndSortHistoryByNode(historyList, nodeId);
+
+ // 创建tabs容器
+ const tabsContainer = document.createElement('div');
+ tabsContainer.className = 'popup_tabs_container';
+
+ // 创建可滚动区域
+ const tabsScroll = document.createElement('div');
+ tabsScroll.className = 'popup_tabs_scroll';
+
+ // 创建tabs
+ const tabs = document.createElement('div');
+ tabs.className = 'popup_tabs';
+
+ // 创建左右滚动指示器
+ const leftIndicator = document.createElement('div');
+ leftIndicator.className = 'tabs_scroll_indicator left';
+
+ // 使用缓存的图标 - 左箭头(向上图标逆时针旋转90度)
+ const leftIconImg = ResourceManager.getIcon('icon-moveup.svg');
+ if (leftIconImg) {
+ leftIconImg.className = 'scroll_indicator_icon rotate_left';
+ leftIndicator.appendChild(leftIconImg);
+ }
+ leftIndicator.style.display = 'none'; // 初始隐藏
+
+ const rightIndicator = document.createElement('div');
+ rightIndicator.className = 'tabs_scroll_indicator right';
+
+ // 使用缓存的图标 - 右箭头(向下图标逆时针旋转90度)
+ const rightIconImg = ResourceManager.getIcon('icon-movedown.svg');
+ if (rightIconImg) {
+ rightIconImg.className = 'scroll_indicator_icon rotate_right';
+ rightIndicator.appendChild(rightIconImg);
+ }
+ rightIndicator.style.display = 'none'; // 初始隐藏
+
+ // 添加指示器点击事件 - 改进滚动逻辑
+ const leftScrollCleanup = EventManager.addDOMListener(leftIndicator, 'click', () => {
+ // 获取可视区域宽度
+ const visibleWidth = tabsScroll.clientWidth;
+
+ // 计算滚动距离,PrimeVue风格是滚动一个较大的距离
+ const scrollDistance = visibleWidth * 0.75;
+
+ // 平滑滚动
+ tabsScroll.scrollBy({
+ left: -scrollDistance,
+ behavior: 'smooth'
+ });
+ });
+ this.eventCleanups.push(leftScrollCleanup);
+
+ const rightScrollCleanup = EventManager.addDOMListener(rightIndicator, 'click', () => {
+ // 获取可视区域宽度
+ const visibleWidth = tabsScroll.clientWidth;
+
+ // 计算滚动距离,PrimeVue风格是滚动一个较大的距离
+ const scrollDistance = visibleWidth * 0.75;
+
+ // 平滑滚动
+ tabsScroll.scrollBy({
+ left: scrollDistance,
+ behavior: 'smooth'
+ });
+ });
+ this.eventCleanups.push(rightScrollCleanup);
+
+ // 监听滚动事件,显示/隐藏滚动指示器
+ const scrollCleanup = EventManager.addDOMListener(tabsScroll, 'scroll', () => {
+ // 检查是否需要滚动
+ const canScroll = tabsScroll.scrollWidth > tabsScroll.clientWidth;
+
+ if (!canScroll) {
+ // 如果不需要滚动,隐藏两个指示器
+ leftIndicator.style.display = 'none';
+ rightIndicator.style.display = 'none';
+ return;
+ }
+
+ // 显示/隐藏左右滚动指示器
+ leftIndicator.style.display = tabsScroll.scrollLeft > 0 ? 'flex' : 'none';
+ rightIndicator.style.display =
+ tabsScroll.scrollLeft < (tabsScroll.scrollWidth - tabsScroll.clientWidth - 2) ? 'flex' : 'none';
+ });
+ this.eventCleanups.push(scrollCleanup);
+
+ // 初始检测是否需要滚动指示器
+ setTimeout(() => {
+ // 检查是否需要滚动
+ const canScroll = tabsScroll.scrollWidth > tabsScroll.clientWidth;
+
+ if (canScroll) {
+ // 如果需要滚动,显示右侧指示器
+ rightIndicator.style.display = 'flex';
+ }
+ }, 100);
+
+ // 创建内容区域
+ const tabContentsContainer = document.createElement('div');
+ tabContentsContainer.className = 'popup_content';
+
+ // 记录tab和内容元素的映射
+ const tabContentPairs = [];
+
+ // 遍历已排序的节点ID数组,确保当前节点在最前面,其他节点按序号排序
+ orderedNodeIds.forEach((groupNodeId) => {
+ const nodeItems = nodeGroups[groupNodeId];
+
+ // 如果没有数据,跳过
+ if (!nodeItems || nodeItems.length === 0) return;
+
+ // 创建tab
+ const tab = document.createElement('div');
+ tab.className = 'popup_tab';
+
+ // 设置tab标题
+ if (groupNodeId === nodeId) {
+ tab.textContent = '当前节点';
+ tab.classList.add('current_node');
+
+ // 当前节点tab默认选中并激活
+ tab.classList.add('active');
+ } else {
+ // 提取节点编号(假设格式为数字+可能的字母)
+ const nodeNumMatch = String(groupNodeId).match(/\d+/);
+ const nodeNum = nodeNumMatch ? nodeNumMatch[0] : groupNodeId;
+ tab.textContent = `节点 ${nodeNum}`;
+ }
+
+ // 创建内容容器
+ const tabContent = document.createElement('div');
+ tabContent.className = 'popup_tab_content';
+
+ // 如果该tab默认选中,显示其内容
+ if (tab.classList.contains('active')) {
+ tabContent.classList.add('active');
+ }
+
+ // 渲染该节点的历史记录
+ if (nodeItems.length === 0) {
+ const empty = document.createElement('div');
+ empty.className = 'popup_empty';
+ empty.textContent = '暂无历史记录';
+ tabContent.appendChild(empty);
+ } else {
+ nodeItems.forEach((item) => {
+ const itemDiv = document.createElement('div');
+ itemDiv.className = 'popup_list_item';
+
+ // 添加元数据容器
+ const metaDiv = document.createElement('div');
+ metaDiv.className = 'history_meta';
+
+ // 添加输入框ID(始终显示)
+ const inputIdSpan = document.createElement('span');
+ inputIdSpan.className = 'input_id';
+ inputIdSpan.textContent = `${item.input_id}`;
+ metaDiv.appendChild(inputIdSpan);
+
+ // 根据操作类型添加对应标签
+ if (item.operation_type) {
+ const operationSpan = document.createElement('span');
+
+ switch (item.operation_type) {
+ case 'translate':
+ operationSpan.className = 'history_operation translated';
+ operationSpan.textContent = '翻译';
+ break;
+ case 'expand':
+ operationSpan.className = 'history_operation expanded';
+ operationSpan.textContent = '扩写';
+ break;
+ case 'caption':
+ operationSpan.className = 'history_operation caption';
+ operationSpan.textContent = '提示词反推';
+ break;
+ // 其他操作类型可以在这里添加
+ }
+
+ if (operationSpan.textContent) {
+ metaDiv.appendChild(operationSpan);
+ }
+ }
+
+ // 创建内容容器
+ const contentDiv = document.createElement('div');
+ contentDiv.className = 'history_content';
+
+ // 设置显示内容 - 使用格式化方法
+ const displayContent = this._formatHistoryContent(item.content, item.operation_type);
+ contentDiv.textContent = displayContent;
+
+ // 组装历史记录项
+ itemDiv.appendChild(metaDiv);
+ itemDiv.appendChild(contentDiv);
+
+ // 添加鼠标悬浮事件,显示完整内容的tooltip
+ const mouseEnterCleanup = EventManager.addDOMListener(itemDiv, 'mouseenter', () => {
+ this._showTooltip(itemDiv, displayContent);
+ });
+
+ const mouseLeaveCleanup = EventManager.addDOMListener(itemDiv, 'mouseleave', () => {
+ this._hideTooltip();
+ });
+
+ this.eventCleanups.push(mouseEnterCleanup, mouseLeaveCleanup);
+
+ // 使用EventManager添加点击事件
+ const itemCleanup = EventManager.addDOMListener(itemDiv, 'click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ // 主动关闭 tooltip
+ this._hideTooltip();
+
+ // 特殊处理图像节点的历史记录
+ if (item.input_id === "image") {
+ // 复制内容到剪贴板
+ try {
+ navigator.clipboard.writeText(item.content).then(() => {
+ // 显示成功提示
+ app.extensionManager.toast.add({
+ severity: "success",
+ summary: "复制成功",
+ detail: "图像提示词已复制到剪贴板",
+ life: 3000
+ });
+
+ // 隐藏历史弹窗
+ this.hideHistoryPopup();
+ });
+ } catch (error) {
+ logger.error(`复制到剪贴板失败: ${error.message}`);
+
+ // 显示错误提示
+ app.extensionManager.toast.add({
+ severity: "error",
+ summary: "复制失败",
+ detail: error.message,
+ life: 3000
+ });
+ }
+ return;
+ }
+
+ // 使用UIToolkit写入内容到输入框
+ const success = UIToolkit.writeToInput(item.content, this.currentNodeId, this.currentInputId, {
+ highlight: true,
+ focus: true
+ });
+
+ if (success) {
+ // 如果内容来自其他输入框,使用新方法添加历史并更新撤销状态
+ if (item.node_id !== this.currentNodeId || item.input_id !== this.currentInputId) {
+ HistoryCacheService.addHistoryAndUpdateUndoState(
+ this.currentNodeId,
+ this.currentInputId,
+ item.content,
+ 'input'
+ );
+ }
+
+ // 隐藏历史弹窗
+ this.hideHistoryPopup();
+ }
+ });
+ this.eventCleanups.push(itemCleanup);
+
+ tabContent.appendChild(itemDiv);
+ });
+ }
+
+ // 添加到容器
+ tabs.appendChild(tab);
+ tabContentsContainer.appendChild(tabContent);
+
+ // 保存tab和内容的映射关系,用于点击tab时切换内容
+ tabContentPairs.push({ tab, content: tabContent });
+
+ // 添加tab点击事件
+ const tabCleanup = EventManager.addDOMListener(tab, 'click', () => {
+ // 获取当前激活的标签
+ const currentActiveTab = tabs.querySelector('.popup_tab.active');
+
+ // 如果点击的是当前激活的标签,不做任何处理
+ if (currentActiveTab === tab) return;
+
+ // 为当前激活的标签添加退出动画
+ if (currentActiveTab) {
+ currentActiveTab.classList.add('exiting');
+ // 监听动画结束
+ const animationEndHandler = () => {
+ currentActiveTab.classList.remove('active', 'exiting');
+ currentActiveTab.removeEventListener('transitionend', animationEndHandler);
+ };
+ currentActiveTab.addEventListener('transitionend', animationEndHandler);
+ }
+
+ // 移除所有内容的active类
+ tabContentPairs.forEach(pair => {
+ pair.content.classList.remove('active');
+ });
+
+ // 激活当前点击的tab
+ tab.classList.add('active');
+ tabContent.classList.add('active');
+
+ // 滚动到可见区域
+ tab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
+ });
+ this.eventCleanups.push(tabCleanup);
+ });
+
+ // 组装tabs容器
+ tabsScroll.appendChild(tabs);
+ tabsContainer.appendChild(tabsScroll);
+ tabsContainer.appendChild(leftIndicator);
+ tabsContainer.appendChild(rightIndicator);
+
+ // 组装弹窗
+ popup.appendChild(titleBar);
+ popup.appendChild(tabsContainer);
+ popup.appendChild(tabContentsContainer);
+
+ return popup;
+ }
+
+ /**
+ * 按节点分组并排序历史记录
+ * @param {Array} historyList 历史记录列表
+ * @param {string} currentNodeId 当前节点ID
+ * @returns {Object} 包含排序后的节点ID数组和分组后的历史记录
+ */
+ static _groupAndSortHistoryByNode(historyList, currentNodeId) {
+ // 按节点ID分组
+ const groups = {};
+
+ // 收集所有节点ID,用于后续排序
+ const nodeIds = new Set();
+
+ // 分组历史记录
+ historyList.forEach(item => {
+ const nodeId = item.node_id;
+ if (!groups[nodeId]) {
+ groups[nodeId] = [];
+ nodeIds.add(nodeId);
+ }
+ groups[nodeId].push(item);
+ });
+
+ // 确保当前节点ID存在于节点列表中
+ if (currentNodeId && !nodeIds.has(currentNodeId)) {
+ nodeIds.add(currentNodeId);
+ groups[currentNodeId] = [];
+ }
+
+ // 将节点ID转换为数组并排序
+ // 排序规则:1. 当前节点最前 2. 其他节点按数字序号排序
+ const sortedNodeIds = Array.from(nodeIds).sort((a, b) => {
+ // 如果a是当前节点,排在最前面
+ if (a === currentNodeId) return -1;
+ // 如果b是当前节点,排在最前面
+ if (b === currentNodeId) return 1;
+
+ // 提取节点ID中的数字部分
+ const getNodeNumber = (id) => {
+ const match = String(id).match(/\d+/);
+ return match ? parseInt(match[0]) : 0;
+ };
+
+ // 按数字大小排序
+ return getNodeNumber(a) - getNodeNumber(b);
+ });
+
+ // 对每个组内的历史记录按时间戳排序(最新的在前)
+ Object.keys(groups).forEach(nodeId => {
+ groups[nodeId].sort((a, b) => b.timestamp - a.timestamp);
+ });
+
+ // 返回排序后的节点ID数组和分组后的历史记录
+ return {
+ orderedNodeIds: sortedNodeIds,
+ nodeGroups: groups
+ };
+ }
+}
+
+export { HistoryManager };
\ No newline at end of file
diff --git a/js/modules/imageCaption.js b/js/modules/imageCaption.js
new file mode 100644
index 0000000..6c64e44
--- /dev/null
+++ b/js/modules/imageCaption.js
@@ -0,0 +1,1574 @@
+/**
+ * 图像节点小助手类
+ * 用于检测和处理图像节点,提供图像反推功能
+ */
+
+import { app } from "../../../../scripts/app.js";
+import { logger } from '../utils/logger.js';
+import { ResourceManager } from "../utils/resourceManager.js";
+import { UIToolkit } from "../utils/UIToolkit.js";
+import { EventManager } from "../utils/eventManager.js";
+import { llmVisionService } from '../services/llm_v.js';
+import { HistoryCacheService } from '../services/cache.js';
+
+// 调试开关
+window.DEBUG_BOUNDS = false; // 显示边界框
+window.DEBUG_BOUNDS_INFO = false; // 显示边界框信息
+window.DEBUG_BOUNDS_LOG = false; // 显示调试日志
+
+class ImageCaption {
+ /** 存储所有小助手实例的Map集合 */
+ static instances = new Map();
+
+ constructor() {
+ this.initialized = false;
+ }
+
+ /**
+ * 初始化图像小助手
+ */
+ async initialize() {
+ if (this.initialized) {
+ return;
+ }
+
+ try {
+ // 检查总开关状态
+ if (!window.FEATURES || !window.FEATURES.enabled || !window.FEATURES.imageCaption) {
+ logger.log("图像小助手初始化跳过:功能已禁用");
+ return;
+ }
+
+ // 检查app.canvas是否可用
+ if (!app.canvas) {
+ throw new Error("画布未初始化");
+ }
+
+ // 初始化事件管理器
+ EventManager.init();
+ // 初始化资源管理器
+ ResourceManager.init();
+ // 注册节点选择事件监听
+ this.registerNodeSelectionListener();
+
+ this.initialized = true;
+ logger.log("图像小助手初始化完成");
+
+ // 如果有当前选中的节点,立即处理
+ if (app.canvas.selected_nodes && Object.keys(app.canvas.selected_nodes).length > 0) {
+ app.canvas._imageCaptionSelectionHandler(app.canvas.selected_nodes);
+ }
+ } catch (error) {
+ logger.error(`图像小助手初始化失败: ${error.message}`);
+ this.initialized = false;
+ this.cleanup();
+ }
+ }
+
+ /**
+ * 注册节点选择事件监听
+ */
+ registerNodeSelectionListener() {
+ if (!app.canvas) {
+ logger.error("画布未初始化,无法注册节点选择事件监听器");
+ return;
+ }
+
+ // 创建选择事件处理器
+ const selectionHandler = (selected_nodes) => {
+ // 当总开关或图像反推功能关闭时,跳过所有节点处理
+ if (!window.FEATURES || !window.FEATURES.enabled || !window.FEATURES.imageCaption) {
+ return;
+ }
+
+ // 处理空选择情况
+ if (!selected_nodes || Object.keys(selected_nodes).length === 0) {
+ return;
+ }
+
+ // 处理选中的节点
+ for (const nodeId in selected_nodes) {
+ const node = app.canvas.graph._nodes_by_id[nodeId];
+ if (!node || node.id === -1) continue;
+
+ // 移除初始化标记判断,每次选中都重新检测节点状态
+ this.checkAndSetupNode(node);
+ }
+ };
+
+ // 保存原始的选择事件处理器
+ if (app.canvas.onSelectionChange && app.canvas.onSelectionChange !== selectionHandler) {
+ app.canvas._originalImageCaptionSelectionChange = app.canvas.onSelectionChange;
+ }
+
+ // 设置新的选择事件处理器
+ app.canvas._imageCaptionSelectionHandler = selectionHandler;
+
+ // 不覆盖现有的 onSelectionChange,因为提示词小助手可能已经设置了它
+ // 而是在 index.js 中的 onSelected 方法中调用我们的处理器
+
+ // 添加到LiteGraph的事件系统
+ if (app.canvas.graph) {
+ // 不覆盖现有的 onNodeSelectionChange,因为提示词小助手可能已经设置了它
+ app.canvas.graph._imageCaptionNodeSelectionChange = selectionHandler;
+ }
+
+ // 初始检查当前选中的节点
+ if (app.canvas.selected_nodes && Object.keys(app.canvas.selected_nodes).length > 0) {
+ selectionHandler(app.canvas.selected_nodes);
+ }
+ }
+
+ /**
+ * 检查节点并设置小助手
+ */
+ checkAndSetupNode(node) {
+ // 检查总开关和图像反推功能开关状态
+ if (!window.FEATURES || !window.FEATURES.enabled || !window.FEATURES.imageCaption) {
+ return;
+ }
+
+ // 检查节点是否有效
+ if (!node) return;
+
+ // 检查节点是否被折叠
+ if (node.flags && node.flags.collapsed) {
+ // 如果节点被折叠,清理已存在的小助手实例
+ if (ImageCaption.hasInstance(node.id)) {
+ this.cleanup(node.id);
+ }
+ return;
+ }
+
+ // 检查是否处于低质量渲染状态
+ if (app.canvas) {
+ const threshold = app.canvas.low_quality_zoom_threshold || 0.6;
+ const scale = app.canvas.ds.scale;
+ // 当处于低质量渲染状态时,清理小助手
+ if (scale < threshold) {
+ if (ImageCaption.hasInstance(node.id)) {
+ this.cleanup(node.id);
+ }
+ return;
+ }
+ }
+
+ // 检查是否有有效图像
+ if (!this.hasValidImage(node)) {
+ // 如果节点没有有效图像,清理已存在的小助手实例
+ if (ImageCaption.hasInstance(node.id)) {
+ this.cleanup(node.id);
+ }
+ return;
+ }
+
+ // 检查实例是否已存在
+ if (ImageCaption.hasInstance(node.id)) {
+ // 显示已存在的实例
+ const instance = ImageCaption.getInstance(node.id);
+ if (instance) {
+ this.showAssistantUI(instance);
+ }
+ return;
+ }
+
+ // 创建新的小助手实例
+ const assistant = this.setupNodeAssistant(node);
+ if (assistant) {
+ logger.log(`创建图像小助手 | ID: ${node.id}`);
+ }
+ }
+
+ /**
+ * 检查节点是否包含有效图像
+ */
+ hasValidImage(node) {
+ if (!node) return false;
+
+ try {
+ // 检查节点是否被折叠
+ if (node.flags && node.flags.collapsed) {
+ return false;
+ }
+
+ // 检查是否处于低质量渲染状态
+ if (app.canvas) {
+ // 获取画布的low_quality_zoom_threshold(默认0.6)
+ const threshold = app.canvas.low_quality_zoom_threshold || 0.6;
+ // 当前缩放比例
+ const scale = app.canvas.ds.scale;
+ // 如果缩放比例小于阈值,说明处于低质量渲染状态
+ if (scale < threshold) {
+ return false;
+ }
+ }
+
+ // 检查节点类型
+ const imageNodeTypes = [
+ 'PreviewImage',
+ 'SaveImage',
+ 'LoadImage',
+ 'ImageUpload',
+ 'VHS_LoadImage',
+ 'IPAdapter_Preprocessor',
+ 'Image Preprocessor'
+ ];
+
+ // 检查图像相关属性
+ const imageProperties = {
+ // 标准图像属性
+ hasImgs: !!(node.imgs && Array.isArray(node.imgs) && node.imgs.length > 0),
+ hasImageIndex: typeof node.imageIndex === 'number',
+ hasOverIndex: typeof node.overIndex === 'number',
+ // 预览相关属性
+ hasPreview: !!node.preview,
+ hasPreviewImage: !!node.preview_image,
+ // 输出相关属性(优化:只有同时满足输出类型为image且有实际图像数据时才返回true)
+ hasRealImageOutput: false
+ };
+
+ // 检查是否有实际的图像输出
+ if (node.outputs && node.outputs.some(output => output && (output.type === 'image' || output.name === 'IMAGE'))) {
+ // 只有当节点同时有图像数据时才认为有真实图像输出
+ imageProperties.hasRealImageOutput = imageProperties.hasImgs || imageProperties.hasPreview || imageProperties.hasPreviewImage;
+ }
+
+ // 对于PreviewImage和SaveImage类型的节点,只检查是否有实际的图像数据
+ if (['PreviewImage', 'SaveImage'].includes(node.type)) {
+ // 检查节点是否有图像数据
+ if (imageProperties.hasImgs) {
+ // 如果有imageIndex,检查对应索引的图像是否存在
+ if (imageProperties.hasImageIndex) {
+ return !!node.imgs[node.imageIndex];
+ }
+ // 如果没有imageIndex但有图像数组,说明有图像
+ return true;
+ }
+ return false;
+ }
+
+ // 其他已知的图像节点类型
+ if (imageNodeTypes.includes(node.type)) {
+ return true;
+ }
+
+ // 检查标准图像属性
+ if (imageProperties.hasImgs) {
+ if (imageProperties.hasImageIndex && node.imgs[node.imageIndex]) {
+ return true;
+ }
+ if (imageProperties.hasOverIndex && node.imgs[node.overIndex]) {
+ return true;
+ }
+ }
+
+ // 检查预览图像
+ if (imageProperties.hasPreview || imageProperties.hasPreviewImage) {
+ return true;
+ }
+
+ // 检查图像输出(优化:必须有实际图像数据)
+ if (imageProperties.hasRealImageOutput) {
+ return true;
+ }
+
+ return false;
+ } catch (error) {
+ logger.error(`检查节点图像失败 | ID: ${node.id} | ${error.message}`);
+ return false;
+ }
+ }
+
+ /**
+ * 为节点设置小助手
+ */
+ setupNodeAssistant(node) {
+ if (!node) return null;
+
+ try {
+ // 创建小助手实例
+ const assistant = this.createAssistant(node);
+ if (assistant) {
+ // 初始化显示状态
+ this.showAssistantUI(assistant);
+ return assistant;
+ }
+ return null;
+ } catch (error) {
+ logger.error(`创建图像小助手失败 | ID: ${node.id} | ${error.message}`);
+ return null;
+ }
+ }
+
+ /**
+ * 创建小助手实例
+ */
+ createAssistant(node) {
+ if (!node) return null;
+
+ const nodeId = node.id;
+
+ // 检查是否已存在实例
+ if (ImageCaption.hasInstance(nodeId)) {
+ return ImageCaption.getInstance(nodeId);
+ }
+
+ // 创建小助手对象
+ const assistant = {
+ type: "image_caption",
+ nodeId,
+ buttons: {},
+ node,
+ isTransitioning: false,
+ isFirstCreate: true, // 标记是否为首次创建
+ isMouseOver: false, // 鼠标悬停状态
+ isActive: false // 激活状态
+ };
+
+ // 创建UI并添加到实例集合
+ this.createAssistantUI(assistant);
+ ImageCaption.addInstance(nodeId, assistant);
+
+ // 设置初始显示状态
+ assistant.isMouseOver = true;
+
+ // 2秒后恢复正常的鼠标检测行为
+ setTimeout(() => {
+ if (assistant) {
+ assistant.isFirstCreate = false;
+ assistant.isMouseOver = false;
+ this.updateAssistantVisibility(assistant);
+ }
+ }, 2000);
+
+ return assistant;
+ }
+
+ /**
+ * 创建小助手UI
+ */
+ createAssistantUI(assistant) {
+ if (!assistant?.node) return null;
+
+ try {
+ // 创建内部内容容器
+ const innerContentDiv = document.createElement('div');
+ innerContentDiv.className = 'prompt-assistant-inner';
+
+ // 创建主容器
+ const containerDiv = document.createElement('div');
+ containerDiv.className = 'prompt-assistant-container';
+ containerDiv.dataset.nodeId = assistant.nodeId;
+
+ // 添加内容容器到主容器
+ containerDiv.appendChild(innerContentDiv);
+
+ // 保存引用
+ assistant.element = containerDiv;
+ assistant.innerContent = innerContentDiv;
+ assistant.buttons = {};
+
+ // 初始化UI组件和事件
+ this.addFunctionButtons(assistant);
+
+ // 默认隐藏状态
+ containerDiv.style.display = 'none';
+
+ // 使用固定定位样式
+ containerDiv.style.position = 'fixed';
+ containerDiv.style.zIndex = '999';
+ document.body.appendChild(containerDiv);
+
+ // 新增:添加鼠标事件监听
+ this._setupMouseEvents(assistant);
+
+ // 新增:延迟设置位置,确保canvas已经初始化
+ requestAnimationFrame(() => {
+ this._setupUIPosition(assistant);
+ });
+
+ return containerDiv;
+ } catch (error) {
+ logger.error(`图像小助手UI创建失败 | ID: ${assistant.nodeId} | ${error.message}`);
+ return null;
+ }
+ }
+
+ /**
+ * 添加功能按钮
+ */
+ addFunctionButtons(assistant) {
+ if (!assistant?.element) return;
+
+ // 创建反推按钮(中文)
+ const buttonZh = this.addButtonWithIcon(assistant, {
+ id: 'caption_zh',
+ title: '反推提示词(中文)',
+ icon: 'icon-caption-zh.svg',
+ onClick: async (e, assistant) => {
+ e.preventDefault();
+ e.stopPropagation();
+ await this.handleImageAnalysis(assistant, 'zh');
+ }
+ });
+
+ // 创建分割线
+ const divider = document.createElement('div');
+ divider.className = 'prompt-assistant-divider';
+
+ // 创建反推按钮(英文)
+ const buttonEn = this.addButtonWithIcon(assistant, {
+ id: 'caption_en',
+ title: '反推提示词(英文)',
+ icon: 'icon-caption-en.svg',
+ onClick: async (e, assistant) => {
+ e.preventDefault();
+ e.stopPropagation();
+ await this.handleImageAnalysis(assistant, 'en');
+ }
+ });
+
+ // 按顺序添加元素:中文按钮 -> 分割线 -> 英文按钮
+ if (buttonZh) {
+ assistant.innerContent.appendChild(buttonZh);
+ }
+ assistant.innerContent.appendChild(divider);
+ if (buttonEn) {
+ assistant.innerContent.appendChild(buttonEn);
+ }
+ }
+
+ /**
+ * 处理图像分析
+ * @param {Object} assistant - 小助手实例
+ * @param {string} lang - 语言类型
+ */
+ async handleImageAnalysis(assistant, lang) {
+ try {
+ const node = assistant.node;
+ if (!node || !node.imgs || node.imgs.length === 0) {
+ throw new Error('未找到有效的图像');
+ }
+
+ // 获取当前选中的图片
+ const currentImage = node.imgs[node.imageIndex || 0];
+ if (!currentImage) {
+ throw new Error('未找到有效的图像');
+ }
+
+ // 获取按钮元素
+ const buttonId = lang === 'en' ? 'caption_en' : 'caption_zh';
+ const buttonElement = assistant.buttons[buttonId];
+ if (!buttonElement) {
+ throw new Error('未找到按钮元素');
+ }
+
+ // 设置当前按钮为处理中状态
+ this._setButtonState(assistant, buttonId, 'processing', true);
+
+ // 禁用其他按钮
+ Object.keys(assistant.buttons).forEach(id => {
+ if (id !== buttonId) {
+ this._setButtonState(assistant, id, 'disabled', true);
+ }
+ });
+
+ // 更新小助手状态为激活状态
+ this._updateAssistantActiveState(assistant, true);
+
+ // 显示加载状态提示
+ const tipMessage = lang === 'en' ? "反推提示词...(英文)" : "反推提示词...(中文)";
+ UIToolkit.showStatusTip(
+ buttonElement,
+ 'loading',
+ tipMessage,
+ { x: buttonElement.getBoundingClientRect().left + buttonElement.offsetWidth / 2, y: buttonElement.getBoundingClientRect().top }
+ );
+
+ // 生成请求ID
+ const request_id = `${node.id}_${Date.now()}`;
+
+ // 调用图像分析服务,传入语言参数
+ const result = await llmVisionService.analyzeImage(currentImage, request_id, lang);
+
+ if (!result.success) {
+ throw new Error(result.error);
+ }
+
+ // 获取描述文本
+ const description = result.data.description;
+
+ // 尝试复制到剪贴板
+ try {
+ // 创建一个临时的textarea元素
+ const textarea = document.createElement('textarea');
+ textarea.value = description;
+ textarea.style.position = 'fixed';
+ textarea.style.opacity = '0';
+ document.body.appendChild(textarea);
+
+ // 选中文本
+ textarea.select();
+ textarea.setSelectionRange(0, 99999);
+
+ // 尝试复制
+ let copySuccess = false;
+ try {
+ copySuccess = document.execCommand('copy');
+ } catch (err) {
+ logger.warn(`execCommand复制失败,尝试使用clipboard API: ${err.message}`);
+ }
+
+ // 如果execCommand失败,尝试使用clipboard API
+ if (!copySuccess) {
+ await navigator.clipboard.writeText(description);
+ }
+
+ // 移除临时元素
+ document.body.removeChild(textarea);
+ logger.debug('文本复制成功');
+ } catch (copyError) {
+ logger.warn(`复制到剪贴板失败: ${copyError.message}`);
+ // 即使复制失败也继续执行,不抛出错误
+ }
+
+ // 为当前图像节点创建历史记录
+ HistoryCacheService.addHistory({
+ node_id: node.id,
+ input_id: "image",
+ content: description,
+ operation_type: 'caption',
+ timestamp: Date.now(),
+ request_id: request_id
+ });
+
+ // 显示成功提示
+ const successMessage = lang === 'en'
+ ? "反推完成,已复制到剪贴板"
+ : "反推完成,已复制到剪贴板";
+
+ UIToolkit.showStatusTip(
+ buttonElement,
+ 'success',
+ successMessage,
+ { x: buttonElement.getBoundingClientRect().left + buttonElement.offsetWidth / 2, y: buttonElement.getBoundingClientRect().top }
+ );
+
+ // 限制Toast显示的文本长度
+ const maxLength = 40;
+ const truncatedDescription = description.length > maxLength
+ ? description.substring(0, maxLength) + '...'
+ : description;
+
+ // 显示Toast提示
+ app.extensionManager.toast.add({
+ severity: "success",
+ summary: lang === 'en' ? "图像反推完成(英文),请使用 ctrl+v 粘贴" : "图像反推完成(中文),请使用 ctrl+v 粘贴",
+ detail: truncatedDescription,
+ life: 5000
+ });
+
+ } catch (error) {
+ logger.error(`图像分析失败: ${error.message}`);
+
+ // 获取按钮元素
+ const buttonId = lang === 'en' ? 'caption_en' : 'caption_zh';
+ const buttonElement = assistant.buttons[buttonId];
+
+ if (buttonElement) {
+ // 显示错误提示
+ UIToolkit.showStatusTip(
+ buttonElement,
+ 'error',
+ error.message,
+ { x: buttonElement.getBoundingClientRect().left + buttonElement.offsetWidth / 2, y: buttonElement.getBoundingClientRect().top }
+ );
+ }
+
+ app.extensionManager.toast.add({
+ severity: "error",
+ summary: lang === 'en' ? "Error" : "错误",
+ detail: error.message,
+ life: 3000
+ });
+ } finally {
+ // 获取按钮ID
+ const buttonId = lang === 'en' ? 'caption_en' : 'caption_zh';
+
+ // 重置当前按钮状态
+ this._setButtonState(assistant, buttonId, 'processing', false);
+
+ // 恢复其他按钮状态
+ Object.keys(assistant.buttons).forEach(id => {
+ if (id !== buttonId) {
+ this._setButtonState(assistant, id, 'disabled', false);
+ }
+ });
+
+ // 更新小助手状态为非激活状态
+ this._updateAssistantActiveState(assistant, false);
+ }
+ }
+
+ /**
+ * 设置按钮状态
+ * @private
+ * @param {Object} assistant - 小助手实例
+ * @param {string} buttonId - 按钮ID
+ * @param {string} stateType - 状态类型
+ * @param {boolean} value - 状态值
+ */
+ _setButtonState(assistant, buttonId, stateType, value = true) {
+ try {
+ const button = assistant.buttons[buttonId];
+ if (!button) return;
+
+ const stateClass = `button-${stateType}`;
+
+ if (value) {
+ button.classList.add(stateClass);
+ // 如果是禁用状态,添加disabled属性
+ if (stateType === 'disabled') {
+ button.setAttribute('disabled', 'disabled');
+ }
+ } else {
+ button.classList.remove(stateClass);
+ // 如果取消禁用状态,移除disabled属性
+ if (stateType === 'disabled') {
+ button.removeAttribute('disabled');
+ }
+ }
+
+ // 更新按钮可点击状态
+ this._updateButtonClickability(button);
+
+ } catch (error) {
+ logger.error(`按钮状态 | 设置失败 | 按钮:${buttonId} | 状态:${stateType} | 错误:${error.message}`);
+ }
+ }
+
+ /**
+ * 更新按钮可点击状态
+ * @private
+ * @param {HTMLElement} button - 按钮元素
+ */
+ _updateButtonClickability(button) {
+ // 检查按钮是否处于禁用或处理中状态
+ const isDisabled = button.classList.contains('button-disabled');
+ const isProcessing = button.classList.contains('button-processing');
+
+ if (isDisabled || isProcessing) {
+ // 如果按钮被禁用或正在处理中,阻止点击事件
+ button.style.pointerEvents = 'none';
+ } else {
+ // 恢复点击事件
+ button.style.pointerEvents = 'auto';
+ }
+ }
+
+ /**
+ * 检查小助手是否有按钮处于激活状态
+ * @private
+ * @param {Object} assistant - 小助手实例
+ * @returns {boolean} 是否有激活的按钮
+ */
+ _checkAssistantActiveState(assistant) {
+ if (!assistant || !assistant.buttons) return false;
+
+ // 遍历所有按钮,检查是否有按钮处于active或processing状态
+ for (const buttonId in assistant.buttons) {
+ const button = assistant.buttons[buttonId];
+ if (button.classList.contains('button-active') ||
+ button.classList.contains('button-processing')) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 更新小助手激活状态
+ * @private
+ * @param {Object} assistant - 小助手实例
+ * @param {boolean} isActive - 是否激活
+ */
+ _updateAssistantActiveState(assistant, isActive) {
+ if (!assistant) return;
+
+ // 更新激活状态
+ assistant.isActive = isActive;
+
+ // 如果激活,强制显示小助手
+ if (isActive) {
+ this.showAssistantUI(assistant);
+ } else {
+ // 如果不再激活,根据鼠标悬停状态决定是否显示
+ this.updateAssistantVisibility(assistant);
+ }
+ }
+
+ /**
+ * 添加带图标的按钮
+ */
+ addButtonWithIcon(assistant, config) {
+ if (!assistant?.element || !assistant?.innerContent) return null;
+
+ const { id, title, icon, onClick } = config;
+
+ // 创建按钮
+ const button = document.createElement('button');
+ button.className = 'prompt-assistant-button';
+ button.title = title || '';
+ button.dataset.id = id || `btn_${Date.now()}`;
+ button.dataset.icon = icon?.replace('.svg', '');
+
+ // 添加图标
+ if (icon) {
+ UIToolkit.addIconToButton(button, icon, title || id);
+ }
+
+ // 添加事件
+ if (typeof onClick === 'function') {
+ button.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (button.classList.contains('button-processing')) {
+ // 如果按钮正在处理中,显示"处理中"提示
+ const tipMessage = id.includes('en') ? "反推提示词...(英文)" : "反推提示词...(中文)";
+ UIToolkit.showStatusTip(
+ button,
+ 'loading',
+ tipMessage,
+ { x: button.getBoundingClientRect().left + button.offsetWidth / 2, y: button.getBoundingClientRect().top }
+ );
+ return;
+ }
+
+ // 如果按钮被禁用,不执行操作
+ if (button.classList.contains('button-disabled')) {
+ return;
+ }
+
+ // 执行点击回调
+ onClick(e, assistant);
+ });
+ }
+
+ // 保存引用
+ if (id) {
+ assistant.buttons[id] = button;
+ }
+
+ return button;
+ }
+
+ /**
+ * 设置UI位置
+ */
+ _setupUIPosition(assistant) {
+ if (!assistant?.element || !assistant?.node) return;
+
+ const containerDiv = assistant.element;
+
+ // 更新位置的函数
+ const updatePosition = () => {
+ if (!assistant.element || !assistant.node) return;
+
+ try {
+ const canvas = app.canvas;
+ // 如果canvas未初始化,延迟重试
+ if (!canvas) {
+ requestAnimationFrame(() => updatePosition());
+ return;
+ }
+
+ // 获取节点边界
+ const [nodeX, nodeY, nodeWidth, nodeHeight] = assistant.node.getBounding();
+
+ // 计算内部偏移量(用于将小助手放在节点内部)
+ const INNER_OFFSET_X = 10; // 水平偏移量
+ const INNER_OFFSET_Y = 18; // 垂直偏移量
+
+ // 计算定位点位置(节点右下角)
+ const anchorX = nodeX + nodeWidth - INNER_OFFSET_X;
+ const anchorY = nodeY + nodeHeight - INNER_OFFSET_Y;
+
+ // 获取画布元素的边界
+ const rect = canvas.canvas.getBoundingClientRect();
+
+ // 使用LGraphCanvas的坐标转换方法
+ // 将定位点位置转换为屏幕坐标
+ const canvasPoint = canvas.convertOffsetToCanvas([anchorX, anchorY]);
+
+ if (!canvasPoint) return;
+
+ // 计算最终的屏幕坐标(考虑画布元素的位置)
+ const screenX = canvasPoint[0] + rect.left;
+ const screenY = canvasPoint[1] + rect.top;
+
+ // 设置容器位置,使其右下角与定位点对齐
+ containerDiv.style.right = `${window.innerWidth - screenX}px`;
+ containerDiv.style.bottom = `${window.innerHeight - screenY}px`;
+ containerDiv.style.left = 'auto';
+ containerDiv.style.top = 'auto';
+
+ } catch (error) {
+ logger.error(`更新小助手位置失败: ${error.message}`);
+ }
+ };
+
+ // 初始更新位置
+ updatePosition();
+
+ // 使用防抖函数优化位置更新
+ const debouncedUpdatePosition = EventManager.debounce(updatePosition, 16); // 降低到16ms提高流畅度
+
+ // 添加窗口resize事件监听
+ assistant._eventCleanupFunctions = assistant._eventCleanupFunctions || [];
+ const removeResizeListener = EventManager.addDOMListener(window, 'resize', debouncedUpdatePosition);
+ assistant._eventCleanupFunctions.push(removeResizeListener);
+
+ // 监听画布变化
+ if (app.canvas) {
+ // 监听画布重绘
+ const originalDrawBackground = app.canvas.onDrawBackground;
+ app.canvas.onDrawBackground = function () {
+ const ret = originalDrawBackground?.apply(this, arguments);
+ // 直接调用updatePosition而不是防抖版本,保证重绘时位置准确
+ updatePosition();
+ return ret;
+ };
+
+ // 添加画布重绘清理函数
+ assistant._eventCleanupFunctions.push(() => {
+ if (originalDrawBackground) {
+ app.canvas.onDrawBackground = originalDrawBackground;
+ }
+ });
+
+ // 监听节点移动
+ if (assistant.node) {
+ // 使用LiteGraph提供的onNodeMoved事件
+ const originalOnNodeMoved = app.canvas.onNodeMoved;
+ app.canvas.onNodeMoved = function(node_dragged) {
+ if (originalOnNodeMoved) {
+ originalOnNodeMoved.apply(this, arguments);
+ }
+
+ // 仅当移动的是当前节点时更新位置
+ if (node_dragged && node_dragged.id === assistant.node.id) {
+ // 直接调用updatePosition而不是防抖版本,确保拖动时UI跟随节点
+ updatePosition();
+ }
+ };
+
+ // 添加节点移动清理函数
+ assistant._eventCleanupFunctions.push(() => {
+ if (app.canvas) {
+ app.canvas.onNodeMoved = originalOnNodeMoved;
+ }
+ });
+
+ // 为节点本身添加移动监听(兼容性处理)
+ const nodeOriginalOnNodeMoved = assistant.node.onNodeMoved;
+ assistant.node.onNodeMoved = function() {
+ const ret = nodeOriginalOnNodeMoved?.apply(this, arguments);
+ // 直接调用updatePosition而不是防抖版本
+ updatePosition();
+ return ret;
+ };
+
+ // 添加节点自身移动清理函数
+ assistant._eventCleanupFunctions.push(() => {
+ if (assistant.node && nodeOriginalOnNodeMoved) {
+ assistant.node.onNodeMoved = nodeOriginalOnNodeMoved;
+ }
+ });
+ }
+
+ // 监听画布缩放
+ const originalDSModified = app.canvas.ds.onModified;
+ app.canvas.ds.onModified = function (...args) {
+ if (originalDSModified) {
+ originalDSModified.apply(this, args);
+ }
+ // 缩放时直接更新,不使用防抖
+ updatePosition();
+ };
+
+ // 添加画布缩放清理函数
+ assistant._eventCleanupFunctions.push(() => {
+ if (app.canvas?.ds) {
+ app.canvas.ds.onModified = originalDSModified;
+ }
+ });
+ }
+
+ // 添加DOM元素清理函数
+ assistant._eventCleanupFunctions.push(() => {
+ if (containerDiv && document.body.contains(containerDiv)) {
+ document.body.removeChild(containerDiv);
+ }
+ });
+ }
+
+ /**
+ * 设置鼠标事件监听
+ * @private
+ */
+ _setupMouseEvents(assistant) {
+ if (!assistant?.node) return;
+
+ const checkMouseInNodeArea = (e) => {
+ // 检查总开关和图像反推功能开关状态
+ if (!window.FEATURES || !window.FEATURES.enabled || !window.FEATURES.imageCaption) {
+ return false;
+ }
+
+ const node = assistant.node;
+ if (!node) return false;
+
+ // 检查节点是否已被删除(关键修复)
+ if (!app.canvas || !app.canvas.graph || !app.canvas.graph._nodes_by_id[node.id]) {
+ return false;
+ }
+
+ const canvas = app.canvas;
+ if (!canvas) return false;
+
+ try {
+ // 获取鼠标在画布中的位置(使用LGraphCanvas的转换方法)
+ const canvasPos = canvas.convertEventToCanvasOffset(e);
+ if (!canvasPos) return false;
+
+ const [canvasX, canvasY] = canvasPos;
+
+ // 获取节点边界 - 使用LiteGraph内置方法
+ const [nodeX, nodeY, nodeWidth, nodeHeight] = node.getBounding();
+
+ // 计算标题栏高度 - 根据ComfyUI的节点样式
+ const titleHeight = node.flags?.collapsed ? 0 : (node.title_height || 30);
+
+ // 计算内容区域(排除标题栏)
+ const contentY = nodeY + titleHeight;
+ const contentHeight = nodeHeight - titleHeight;
+
+ // 检查鼠标是否在内容区域内(使用画布坐标系)
+ const isInside = canvasX >= nodeX && canvasX <= nodeX + nodeWidth &&
+ canvasY >= contentY && canvasY <= contentY + contentHeight;
+
+ // 检查鼠标是否在小助手UI上
+ const isOverUI = assistant.element &&
+ assistant.element.style.display !== 'none' &&
+ EventManager.isMouseOverElement(assistant.element);
+
+ return isInside || isOverUI;
+ } catch (error) {
+ logger.error(`检查节点区域失败: ${error.message}`);
+ return false;
+ }
+ };
+
+ // 使用 requestAnimationFrame 优化更新
+ let rafId = null;
+ let lastMouseEvent = null;
+
+ const updateMouseState = () => {
+ // 检查总开关和图像反推功能开关状态
+ if (!window.FEATURES || !window.FEATURES.enabled || !window.FEATURES.imageCaption) {
+ if (assistant.isMouseOver) {
+ assistant.isMouseOver = false;
+ this.updateAssistantVisibility(assistant);
+ }
+ rafId = null;
+ return;
+ }
+
+ // 检查节点是否已被删除(关键修复)
+ if (!assistant.node || !app.canvas || !app.canvas.graph || !app.canvas.graph._nodes_by_id[assistant.node.id]) {
+ if (assistant.isMouseOver) {
+ assistant.isMouseOver = false;
+ this.updateAssistantVisibility(assistant);
+ }
+ rafId = null;
+ return;
+ }
+
+ // 跳过首次创建的检查(首次创建时已经设置为显示)
+ if (assistant.isFirstCreate) {
+ rafId = null;
+ return;
+ }
+
+ // 如果小助手处于激活状态,保持鼠标悬停状态为true
+ if (assistant.isActive || this._checkAssistantActiveState(assistant)) {
+ if (!assistant.isMouseOver) {
+ assistant.isMouseOver = true;
+ this.updateAssistantVisibility(assistant);
+ }
+ rafId = null;
+ return;
+ }
+
+ // 只有在有最后鼠标事件的情况下才检查
+ if (lastMouseEvent) {
+ const isOver = checkMouseInNodeArea(lastMouseEvent);
+
+ // 只在状态变化时更新可见性,避免不必要的重绘
+ if (isOver !== assistant.isMouseOver) {
+ assistant.isMouseOver = isOver;
+ this.updateAssistantVisibility(assistant);
+ }
+ }
+
+ rafId = null;
+ };
+
+ // 处理鼠标移动
+ const handleMouseMove = (e) => {
+ // 检查总开关和图像反推功能开关状态
+ if (!window.FEATURES || !window.FEATURES.enabled || !window.FEATURES.imageCaption) {
+ return;
+ }
+
+ lastMouseEvent = e;
+ if (!rafId) {
+ rafId = requestAnimationFrame(updateMouseState);
+ }
+ };
+
+ // 处理画布变换
+ const handleCanvasTransform = () => {
+ // 检查总开关和图像反推功能开关状态
+ if (!window.FEATURES || !window.FEATURES.enabled || !window.FEATURES.imageCaption) {
+ return;
+ }
+
+ if (lastMouseEvent) {
+ if (!rafId) {
+ rafId = requestAnimationFrame(updateMouseState);
+ }
+ }
+ };
+
+ // 添加事件监听
+ if (app.canvas.canvas) {
+ // 鼠标移动事件
+ app.canvas.canvas.addEventListener('mousemove', handleMouseMove);
+
+ // 画布变换事件
+ const canvas = app.canvas;
+ const originalDSModified = canvas.ds.onModified;
+ canvas.ds.onModified = function (...args) {
+ if (originalDSModified) {
+ originalDSModified.apply(this, args);
+ }
+ handleCanvasTransform();
+ };
+
+ // 保存清理函数
+ assistant._eventCleanupFunctions = assistant._eventCleanupFunctions || [];
+ assistant._eventCleanupFunctions.push(() => {
+ app.canvas.canvas.removeEventListener('mousemove', handleMouseMove);
+ if (rafId) {
+ cancelAnimationFrame(rafId);
+ rafId = null;
+ }
+ // 恢复原始的 onModified 处理器
+ if (canvas.ds) {
+ canvas.ds.onModified = originalDSModified;
+ }
+ });
+ }
+ }
+
+ /**
+ * 更新小助手可见性
+ */
+ updateAssistantVisibility(assistant) {
+ if (!assistant) return;
+
+ // 检查总开关和图像反推功能开关状态
+ if (!window.FEATURES || !window.FEATURES.enabled || !window.FEATURES.imageCaption) {
+ this.hideAssistantUI(assistant);
+ return;
+ }
+
+ // 检查节点是否已被删除(关键修复)
+ if (assistant.node && (!app.canvas || !app.canvas.graph || !app.canvas.graph._nodes_by_id[assistant.node.id])) {
+ this.hideAssistantUI(assistant);
+ // 清理已删除节点的实例
+ this.cleanup(assistant.node.id);
+ return;
+ }
+
+ // 检查是否处于低质量渲染状态
+ if (app.canvas) {
+ const threshold = app.canvas.low_quality_zoom_threshold || 0.6;
+ const scale = app.canvas.ds.scale;
+ // 当处于低质量渲染状态时,隐藏小助手
+ if (scale < threshold) {
+ this.hideAssistantUI(assistant);
+ return;
+ }
+ }
+
+ // 跳过正在过渡的实例,避免动画中断
+ if (assistant.isTransitioning) {
+ return;
+ }
+
+ // 检查是否有按钮处于激活状态
+ const hasActiveButtons = this._checkAssistantActiveState(assistant);
+
+ // 如果有激活的按钮,强制显示小助手
+ if (assistant.isActive || hasActiveButtons) {
+ this.showAssistantUI(assistant);
+ return;
+ }
+
+ // 根据鼠标悬停状态或首次创建标记决定显示或隐藏
+ if (assistant.isFirstCreate || assistant.isMouseOver) {
+ // 使用requestAnimationFrame确保在下一帧渲染,提高流畅性
+ requestAnimationFrame(() => {
+ this.showAssistantUI(assistant);
+ });
+ } else {
+ // 使用requestAnimationFrame确保在下一帧渲染,提高流畅性
+ requestAnimationFrame(() => {
+ this.hideAssistantUI(assistant);
+ });
+ }
+ }
+
+ /**
+ * 显示小助手UI
+ */
+ showAssistantUI(assistant, forceAnimation = false) {
+ if (!assistant?.element) return;
+
+ // 避免重复显示
+ if (assistant.element.classList.contains('assistant-show') && !forceAnimation) {
+ assistant.element.style.display = 'flex';
+ assistant.element.style.opacity = '1';
+ return;
+ }
+
+ // 取消任何隐藏计时器
+ if (assistant.hideTimeout) {
+ clearTimeout(assistant.hideTimeout);
+ assistant.hideTimeout = null;
+ }
+
+ // 设置过渡状态
+ assistant.isTransitioning = true;
+
+ // 优化渲染性能
+ assistant.element.style.willChange = 'auto';
+ assistant.element.style.transform = 'translateZ(0)';
+ assistant.element.style.opacity = '1';
+
+ // 显示元素并应用动画类
+ assistant.element.style.display = 'flex';
+ void assistant.element.offsetWidth; // 触发回流
+ assistant.element.classList.remove('assistant-hide');
+ assistant.element.classList.add('assistant-show');
+
+ // 动画结束后重置过渡状态
+ setTimeout(() => {
+ assistant.isTransitioning = false;
+ // 动画结束后检查鼠标状态(关键修复)
+ setTimeout(() => this.forceUpdateMouseState(assistant), 10);
+ }, 300);
+ }
+
+ /**
+ * 隐藏小助手UI
+ */
+ hideAssistantUI(assistant) {
+ if (!assistant?.element) return;
+
+ // 避免重复隐藏
+ if (!assistant.element.classList.contains('assistant-show')) return;
+
+ // 如果小助手处于激活状态且功能开关开启,不隐藏
+ if (window.FEATURES && window.FEATURES.enabled && window.FEATURES.imageCaption &&
+ (assistant.isActive || this._checkAssistantActiveState(assistant))) {
+ return;
+ }
+
+ // 设置过渡状态
+ assistant.isTransitioning = true;
+
+ // 应用隐藏动画类
+ assistant.element.classList.add('assistant-hide');
+ assistant.element.classList.remove('assistant-show');
+
+ // 触发回流确保动画生效
+ void assistant.element.offsetWidth;
+
+ // 动画结束后隐藏元素
+ assistant.hideTimeout = setTimeout(() => {
+ if (assistant.element) {
+ assistant.element.style.display = 'none';
+ }
+ assistant.isTransitioning = false;
+ // 动画结束后检查鼠标状态(关键修复)
+ setTimeout(() => this.forceUpdateMouseState(assistant), 10);
+ }, 300);
+ }
+
+ /**
+ * 隐藏所有小助手
+ */
+ hideAllAssistants() {
+ ImageCaption.instances.forEach(assistant => {
+ this.hideAssistantUI(assistant);
+ });
+ }
+
+ /**
+ * 清理资源
+ * @param {string} [nodeId] - 可选的节点ID,如果提供则只清理该节点的实例
+ * @param {boolean} [silent=false] - 是否静默清理(不输出日志)
+ */
+ cleanup(nodeId = null, silent = false) {
+ try {
+ if (nodeId) {
+ // 清理特定节点
+ const instance = ImageCaption.getInstance(nodeId);
+ if (instance) {
+ // 记录删除前的实例数量
+ const beforeCount = ImageCaption.instances.size;
+ logger.debug(`[删除前] 图像小助手实例数量: ${beforeCount}`);
+
+ // 清理实例内部资源
+ this._cleanupInstance(instance, nodeId);
+
+ // 从实例集合中移除
+ ImageCaption.instances.delete(String(nodeId));
+
+ // 记录删除后的实例数量
+ const afterCount = ImageCaption.instances.size;
+
+ if (!silent) {
+ // 获取当前剩余的统计信息
+ logger.log(`清理图像小助手 | 节点ID: ${nodeId}`);
+ logger.log(`[剩余统计] 图像小助手实例: ${afterCount}个 | 删除前: ${beforeCount}个`);
+ }
+ } else {
+ // 检查是否有以该nodeId开头的实例(关键修复)
+ const keysToDelete = Array.from(ImageCaption.instances.keys())
+ .filter(key => key.startsWith(`${nodeId}_`));
+
+ if (keysToDelete.length > 0) {
+ const beforeCount = ImageCaption.instances.size;
+
+ // 清理所有匹配的实例
+ keysToDelete.forEach(key => {
+ const instance = ImageCaption.getInstance(key);
+ if (instance) {
+ this._cleanupInstance(instance, key);
+ ImageCaption.instances.delete(key);
+ }
+ });
+
+ const afterCount = ImageCaption.instances.size;
+
+ if (!silent) {
+ logger.log(`清理图像小助手 | 节点ID: ${nodeId} | 关联实例: ${keysToDelete.length}个`);
+ logger.log(`[剩余统计] 图像小助手实例: ${afterCount}个 | 删除前: ${beforeCount}个`);
+ }
+ }
+ }
+ return;
+ }
+
+ // 清理所有实例
+ const instanceCount = ImageCaption.instances.size;
+ if (instanceCount > 0) {
+ logger.debug(`[全部删除前] 图像小助手实例数量: ${instanceCount}`);
+
+ ImageCaption.instances.forEach((instance, key) => {
+ this._cleanupInstance(instance, key);
+ });
+ ImageCaption.instances.clear();
+
+ if (!silent) {
+ logger.log(`清理所有图像小助手 | 实例数: ${instanceCount}`);
+ logger.log(`[剩余统计] 图像小助手实例: 0个`);
+ }
+ }
+ } catch (error) {
+ logger.error(`清理资源失败: ${error.message}`);
+ }
+ }
+
+ /**
+ * 清理单个实例
+ * @param {Object} instance - 要清理的实例
+ * @param {string} instanceKey - 实例的键
+ */
+ _cleanupInstance(instance, instanceKey) {
+ try {
+ if (!instance) return;
+
+ // 1. 重置所有按钮状态
+ if (instance.buttons) {
+ Object.keys(instance.buttons).forEach(buttonId => {
+ try {
+ const button = instance.buttons[buttonId];
+ if (button) {
+ // 移除所有状态类
+ button.classList.remove('button-active', 'button-processing', 'button-disabled');
+ // 移除所有事件监听器
+ button.replaceWith(button.cloneNode(true));
+ }
+ } catch (err) {
+ logger.debug(`按钮清理 | 按钮:${buttonId} | 错误:${err.message}`);
+ }
+ });
+ // 清空按钮引用
+ instance.buttons = {};
+ }
+
+ // 2. 清理事件监听器
+ if (instance._eventCleanupFunctions) {
+ instance._eventCleanupFunctions.forEach(cleanup => {
+ if (typeof cleanup === 'function') {
+ try {
+ cleanup();
+ } catch (err) {
+ logger.error(`事件清理失败: ${err.message}`);
+ }
+ }
+ });
+ instance._eventCleanupFunctions = [];
+ }
+
+ // 3. 从DOM中移除元素
+ if (instance.element && instance.element.parentNode) {
+ // 确保在移除元素前清理所有子元素的事件
+ const allButtons = instance.element.querySelectorAll('button');
+ allButtons.forEach(button => {
+ button.replaceWith(button.cloneNode(true));
+ });
+ instance.element.parentNode.removeChild(instance.element);
+ }
+
+ // 4. 清理节点引用(关键修复)
+ if (instance.node) {
+ delete instance.node;
+ }
+
+ // 5. 清理实例属性
+ Object.keys(instance).forEach(key => {
+ try {
+ delete instance[key];
+ } catch (err) {
+ logger.error(`属性清理失败: ${err.message}`);
+ }
+ });
+
+ logger.debug(`图像小助手实例清理 | 结果:成功 | 实例:${instanceKey}`);
+ } catch (error) {
+ logger.error(`图像小助手实例清理失败: ${error.message}`);
+ }
+ }
+
+ // ---静态方法---
+ /**
+ * 添加实例到管理器
+ */
+ static addInstance(nodeId, assistant) {
+ if (nodeId != null && assistant != null) {
+ this.instances.set(String(nodeId), assistant);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 获取实例
+ */
+ static getInstance(nodeId) {
+ if (nodeId == null) return null;
+
+ // 确保nodeId是字符串类型
+ const key = String(nodeId);
+ const instance = this.instances.get(key);
+
+ // 添加调试日志
+ if (instance) {
+ logger.debug(`获取图像小助手实例 | 节点ID: ${key} | 结果: 成功`);
+ } else {
+ logger.debug(`获取图像小助手实例 | 节点ID: ${key} | 结果: 未找到`);
+ }
+
+ return instance;
+ }
+
+ /**
+ * 检查实例是否存在
+ */
+ static hasInstance(nodeId) {
+ if (nodeId == null) return false;
+ // 确保nodeId是字符串类型
+ return this.instances.has(String(nodeId));
+ }
+
+ /**
+ * 统一控制总开关功能
+ * 集中管理所有受总开关控制的服务功能
+ * @param {boolean} enable - 是否启用总开关
+ * @param {boolean} force - 是否强制执行(跳过状态判断)
+ */
+ async toggleGlobalFeature(enable, force = false) {
+ try {
+ // 更新状态
+ const oldValue = window.FEATURES.imageCaption;
+
+ // 状态未变化时不执行操作,除非force为true
+ if (!force && oldValue === enable) {
+ return;
+ }
+
+ logger.log(`图像小助手功能开关 | 动作:${enable ? "启用" : "禁用"}`);
+
+ if (enable) {
+ // === 启用图像小助手 ===
+
+ // 1. 初始化图像小助手(如果尚未初始化)
+ if (!this.initialized) {
+ await this.initialize();
+ }
+
+ // 2. 重置节点初始化标记,准备重新检测
+ if (app.canvas && app.canvas.graph) {
+ const nodes = app.canvas.graph._nodes || [];
+ nodes.forEach(node => {
+ if (node) {
+ node._imageCaptionInitialized = false;
+ }
+ });
+ }
+
+ // 3. 设置或恢复节点选择事件监听
+ this.registerNodeSelectionListener();
+
+ // 4. 如果有当前选中的节点,立即处理
+ if (app.canvas && app.canvas.selected_nodes && Object.keys(app.canvas.selected_nodes).length > 0) {
+ app.canvas._imageCaptionSelectionHandler(app.canvas.selected_nodes);
+ }
+ } else {
+ // === 禁用图像小助手 ===
+
+ // 清理所有图像小助手实例
+ this.cleanup(null, true);
+ }
+ } catch (error) {
+ logger.error(`图像小助手功能开关操作失败 | 错误: ${error.message}`);
+ }
+ }
+
+ /**
+ * 强制更新鼠标悬停状态
+ */
+ forceUpdateMouseState(assistant) {
+ if (!assistant || !assistant.node || !assistant.element) return;
+
+ // 总开关关闭时不处理鼠标状态更新
+ if (!window.FEATURES || !window.FEATURES.enabled || !window.FEATURES.imageCaption) {
+ return;
+ }
+
+ // 检查节点是否已被删除(关键修复)
+ if (!app.canvas || !app.canvas.graph || !app.canvas.graph._nodes_by_id[assistant.node.id]) {
+ assistant.isMouseOver = false;
+ this.updateAssistantVisibility(assistant);
+ return;
+ }
+
+ try {
+ // 获取鼠标在画布中的位置
+ const mousePos = EventManager.getMousePosition();
+ const canvas = app.canvas;
+
+ if (!canvas) return;
+
+ // 将屏幕坐标转换为画布坐标
+ const canvasPos = canvas.convertEventToCanvasOffset({
+ clientX: mousePos.x,
+ clientY: mousePos.y
+ });
+
+ if (!canvasPos) return;
+
+ const [canvasX, canvasY] = canvasPos;
+
+ // 获取节点边界
+ const [nodeX, nodeY, nodeWidth, nodeHeight] = assistant.node.getBounding();
+
+ // 计算标题栏高度 - 根据ComfyUI的节点样式
+ const titleHeight = assistant.node.flags?.collapsed ? 0 : (assistant.node.title_height || 30);
+
+ // 计算内容区域(排除标题栏)
+ const contentY = nodeY + titleHeight;
+ const contentHeight = nodeHeight - titleHeight;
+
+ // 检查鼠标是否在内容区域内
+ const isOverNode = canvasX >= nodeX && canvasX <= nodeX + nodeWidth &&
+ canvasY >= contentY && canvasY <= contentY + contentHeight;
+
+ // 检查鼠标是否在小助手UI上
+ const isOverUI = assistant.element.style.display !== 'none' &&
+ this.isMouseOverElement(assistant.element);
+
+ // 更新状态
+ const oldState = assistant.isMouseOver;
+ assistant.isMouseOver = isOverNode || isOverUI;
+
+ // 如果状态变化,更新可见性
+ if (oldState !== assistant.isMouseOver) {
+ logger.debug(`鼠标状态 | 手动更新 | 节点:${assistant.node.id} | 原状态:${oldState} | 新状态:${assistant.isMouseOver}`);
+ this.updateAssistantVisibility(assistant);
+ }
+
+ return assistant.isMouseOver;
+ } catch (error) {
+ logger.error(`强制更新鼠标状态失败: ${error.message}`);
+ return false;
+ }
+ }
+
+ /**
+ * 判断鼠标是否在元素上方
+ */
+ isMouseOverElement(element) {
+ if (!element) return false;
+
+ try {
+ // 获取鼠标位置
+ const mousePos = EventManager.getMousePosition();
+ const rect = element.getBoundingClientRect();
+
+ return (
+ mousePos.x >= rect.left &&
+ mousePos.x <= rect.right &&
+ mousePos.y >= rect.top &&
+ mousePos.y <= rect.bottom
+ );
+ } catch (error) {
+ logger.error(`鼠标位置检测失败 | 错误: ${error.message}`);
+ return false;
+ }
+ }
+}
+
+// 创建单例实例
+const imageCaption = new ImageCaption();
+
+// 导出
+export { imageCaption, ImageCaption };
\ No newline at end of file
diff --git a/js/modules/settings.js b/js/modules/settings.js
new file mode 100644
index 0000000..2eba4c7
--- /dev/null
+++ b/js/modules/settings.js
@@ -0,0 +1,682 @@
+/**
+ * 小助手设置服务
+ * 负责管理小助手的设置选项,提供开关控制功能
+ */
+
+import { app } from "../../../../scripts/app.js";
+import { logger } from '../utils/logger.js';
+import { PromptAssistant } from "./PromptAssistant.js";
+import { EventManager } from "../utils/eventManager.js";
+import { ResourceManager } from "../utils/resourceManager.js";
+import { HistoryCacheService, TagCacheService, TranslateCacheService, CACHE_CONFIG } from "../services/cache.js";
+import { UIToolkit } from "../utils/UIToolkit.js";
+import { FEATURES, handleFeatureChange } from "../config/features.js";
+
+// 标记是否是首次加载页面
+let isFirstLoad = true;
+
+// ====================== 设置管理 ======================
+
+/**
+ * 创建加载按钮
+ * @param {string} text - 按钮文本
+ * @param {Function} onClick - 点击回调函数
+ * @returns {HTMLButtonElement} 按钮元素
+ */
+function createLoadingButton(text, onClick) {
+ const button = document.createElement('button');
+ button.className = 'p-button p-component p-button-primary w-52';
+ button.innerHTML = `${text}`;
+
+ button.addEventListener('click', async () => {
+ if (button.disabled) return;
+
+ // 开始加载状态
+ button.disabled = true;
+ button.classList.add('p-disabled');
+
+ try {
+ await onClick();
+
+ // 显示成功提示
+ app.extensionManager.toast.add({
+ severity: "success",
+ summary: "清理已清理完成",
+ life: 3000
+ });
+
+ } catch (error) {
+ // 显示错误提示
+ app.extensionManager.toast.add({
+ severity: "error",
+ summary: "清理失败",
+ detail: error.message || "缓存清理过程中发生错误",
+ life: 3000
+ });
+ logger.error(`按钮操作失败: ${error.message}`);
+ } finally {
+ // 恢复按钮状态
+ button.disabled = false;
+ button.classList.remove('p-disabled');
+ }
+ });
+
+ return button;
+}
+
+/**
+ * 注册设置选项
+ * 将设置选项添加到ComfyUI设置面板
+ */
+export function registerSettings() {
+ try {
+ app.registerExtension({
+ name: "PromptAssistant.Settings",
+ settings: [
+ // 总开关 - 独立控制小助手系统级功能
+ {
+ id: "PromptAssistant.Features.Enabled",
+ name: "启用小助手",
+ category: ["✨提示词小助手", " 小助手开关", "总开关"],
+ type: "boolean",
+ defaultValue: true,
+ tooltip: "关闭后,提示词小助手所有功能将禁用",
+ onChange: async (value) => {
+ try {
+ // 获取当前状态,用于判断是否是初始化
+ const currentState = window.FEATURES.enabled;
+
+ // 更新全局状态
+ window.FEATURES.enabled = value;
+ logger.log(`总开关状态变更 | 状态:${value ? "启用" : "禁用"}`);
+
+ // 从全局 app 对象获取 promptAssistant 实例
+ const promptAssistantInstance = app.promptAssistant;
+ const imageCaptionInstance = app.imageCaption;
+
+ if (!promptAssistantInstance) {
+ logger.error("总开关切换失败 | 错误:未找到PromptAssistant实例");
+ return;
+ }
+
+ // 根据开关状态执行相应操作
+ if (value) {
+ // 启用功能
+ await promptAssistantInstance.toggleGlobalFeature(true, true);
+ if (imageCaptionInstance) {
+ await imageCaptionInstance.toggleGlobalFeature(true, true);
+ }
+ logger.log("功能启用完成");
+ // 只在状态发生变化且不是首次加载时显示提示
+ if (currentState !== value && !isFirstLoad) {
+ app.extensionManager.toast.add({
+ severity: "info",
+ summary: "提示词小助手已启用",
+ life: 3000
+ });
+ }
+ } else {
+ // 禁用功能
+ await promptAssistantInstance.toggleGlobalFeature(false, true);
+ if (imageCaptionInstance) {
+ await imageCaptionInstance.toggleGlobalFeature(false, true);
+ }
+ logger.log("功能禁用完成");
+ // 只在状态发生变化且不是首次加载时显示提示
+ if (currentState !== value && !isFirstLoad) {
+ app.extensionManager.toast.add({
+ severity: "warn",
+ summary: "提示词小助手已禁用",
+ life: 3000
+ });
+ }
+ }
+
+ // 设置首次加载标志为 false,表示已经完成首次加载
+ isFirstLoad = false;
+ } catch (error) {
+ logger.error(`总开关切换异常 | 错误:${error.message}`);
+ }
+ }
+ },
+
+ // 历史功能(包含历史、撤销、重做按钮)
+ {
+ id: "PromptAssistant.Features.History",
+ name: "启用历史功能",
+ category: ["✨提示词小助手", "功能开关", "历史功能"],
+ type: "boolean",
+ defaultValue: true,
+ tooltip: "开启或关闭历史、撤销、重做功能",
+ onChange: (value) => {
+ const oldValue = FEATURES.history;
+ FEATURES.history = value;
+ handleFeatureChange('历史功能', value, oldValue);
+ logger.log(`历史功能 - 已${value ? "启用" : "禁用"}`);
+ }
+ },
+
+ // 标签工具
+ {
+ id: "PromptAssistant.Features.Tag",
+ name: "标签工具",
+ category: ["✨提示词小助手", "功能开关", "标签功能"],
+ type: "boolean",
+ defaultValue: true,
+ tooltip: "开启或关闭标签工具功能",
+ onChange: (value) => {
+ const oldValue = FEATURES.tag;
+ FEATURES.tag = value;
+ handleFeatureChange('标签工具', value, oldValue);
+ logger.log(`标签工具 - 已${value ? "启用" : "禁用"}`);
+ }
+ },
+
+ // 扩写功能
+ {
+ id: "PromptAssistant.Features.Expand",
+ name: "扩写功能",
+ category: ["✨提示词小助手", "功能开关", "扩写功能"],
+ type: "boolean",
+ defaultValue: true,
+ tooltip: "开启或关闭扩写功能",
+ onChange: (value) => {
+ const oldValue = FEATURES.expand;
+ FEATURES.expand = value;
+ handleFeatureChange('扩写功能', value, oldValue);
+ logger.log(`扩写功能 - 已${value ? "启用" : "禁用"}`);
+ }
+ },
+
+ // 翻译功能
+ {
+ id: "PromptAssistant.Features.Translate",
+ name: "翻译功能",
+ category: ["✨提示词小助手", "功能开关", "翻译功能"],
+ type: "boolean",
+ defaultValue: true,
+ tooltip: "开启或关闭翻译功能",
+ onChange: (value) => {
+ const oldValue = FEATURES.translate;
+ FEATURES.translate = value;
+ handleFeatureChange('翻译功能', value, oldValue);
+ logger.log(`翻译功能 - 已${value ? "启用" : "禁用"}`);
+ }
+ },
+
+ // 图像反推功能
+ {
+ id: "PromptAssistant.Features.ImageCaption",
+ name: "图像反推功能",
+ category: ["✨提示词小助手", "功能开关", "图像反推"],
+ type: "boolean",
+ defaultValue: true,
+ tooltip: "开启或关闭图像反推提示词功能",
+ onChange: (value) => {
+ const oldValue = FEATURES.imageCaption;
+ FEATURES.imageCaption = value;
+ handleFeatureChange('图像反推', value, oldValue);
+ logger.log(`图像反推功能 - 已${value ? "启用" : "禁用"}`);
+ }
+ },
+
+ // 系统设置
+ {
+ id: "PromptAssistant.Settings.LogLevel",
+ name: "日志级别",
+ category: ["✨提示词小助手", "系统设置", "日志级别"],
+ type: "combo",
+ defaultValue: "0",
+ options: [
+ { text: "错误日志", value: "0" },
+ { text: "基础日志", value: "1" },
+ { text: "详细日志", value: "2" }
+ ],
+ tooltip: "设置日志输出级别:错误日志(仅错误)、基础日志(错误+基础信息)、详细日志(错误+基础信息+调试信息)",
+ onChange: (value) => {
+ const oldValue = window.FEATURES.logLevel;
+ window.FEATURES.logLevel = parseInt(value);
+ logger.setLevel(window.FEATURES.logLevel);
+ logger.log(`日志级别已更新 | 原级别:${oldValue} | 新级别:${value}`);
+ }
+ },
+
+ {
+ id: "PromptAssistant.Settings.ClearCache",
+ name: "清理缓存",
+ category: ["✨提示词小助手", "系统设置", "清理缓存"],
+ type: () => {
+ const row = document.createElement("tr");
+ row.className = "promptwidget-settings-row";
+
+ const labelCell = document.createElement("td");
+ labelCell.className = "comfy-menu-label";
+ row.appendChild(labelCell);
+
+ const buttonCell = document.createElement("td");
+ const button = createLoadingButton("清理所有缓存", async () => {
+ try {
+ // 获取清理前的缓存统计
+ const beforeStats = {
+ history: HistoryCacheService.getHistoryStats(),
+ tags: 0,
+ translate: TranslateCacheService.getTranslateCacheStats()
+ };
+
+ // 统计所有标签数量
+ const tagCacheKeys = Object.keys(localStorage)
+ .filter(key => key.startsWith(CACHE_CONFIG.TAG_KEY_PREFIX));
+
+ // 计算所有缓存中的标签总数
+ tagCacheKeys.forEach(key => {
+ try {
+ const cacheData = JSON.parse(localStorage.getItem(key));
+ if (cacheData && typeof cacheData === 'object') {
+ // 获取缓存中的标签数量
+ const tagCount = Object.keys(cacheData).length;
+ beforeStats.tags += tagCount;
+ }
+ } catch (e) {
+ // 移除错误日志,静默处理解析错误
+ }
+ });
+
+ // 执行历史记录清理操作
+ HistoryCacheService.clearAllHistory();
+
+ // 清理所有标签缓存
+ tagCacheKeys.forEach(key => {
+ localStorage.removeItem(key);
+ });
+
+ // 清理翻译缓存
+ TranslateCacheService.clearAllTranslateCache();
+
+ // 获取清理后的缓存统计
+ const afterStats = {
+ history: HistoryCacheService.getHistoryStats(),
+ tags: 0, // 清理后标签数应该为0
+ translate: TranslateCacheService.getTranslateCacheStats()
+ };
+
+ // 计算清理数量
+ const clearedHistory = beforeStats.history.total - afterStats.history.total;
+ const clearedTags = beforeStats.tags;
+ const clearedTranslate = beforeStats.translate.total - afterStats.translate.total;
+
+ // 只输出最终统计结果
+ logger.log(`缓存清理完成 | 历史记录: ${clearedHistory}条 | 标签: ${clearedTags}个 | 翻译: ${clearedTranslate}条`);
+
+ // 更新所有实例的撤销/重做按钮状态
+ PromptAssistant.instances.forEach((instance) => {
+ if (instance && instance.nodeId && instance.inputId) {
+ UIToolkit.updateUndoRedoButtonState(instance, HistoryCacheService);
+ }
+ });
+
+ } catch (error) {
+ // 简化错误日志
+ logger.error(`缓存清理失败`);
+ throw error;
+ }
+ });
+
+ buttonCell.appendChild(button);
+ row.appendChild(buttonCell);
+ return row;
+ }
+ },
+
+ // 百度翻译 API 配置
+ {
+ id: "PromptAssistant.Settings.TranslateType",
+ name: "翻译方法",
+ category: ["✨提示词小助手", "翻译和扩写配置", "翻译方法"],
+ type: "combo",
+ defaultValue: "baidu",
+ options: [
+ { text: "百度翻译", value: "baidu" },
+ { text: "LLM翻译", value: "llm" }
+ ],
+ tooltip: "可选百度机翻或者大模型翻译,注意:大模型翻译速度比较慢,格式可能会发生变化。",
+ onChange: (value) => {
+ localStorage.setItem("PromptAssistant_Settings_translate_type", value);
+ logger.debug("翻译方式已更新:" + value);
+ }
+ },
+
+ {
+ id: "PromptAssistant.Settings.BaiduTranslate",
+ name: "百度翻译配置",
+ category: ["✨提示词小助手", "翻译和扩写配置", "百度翻译"],
+ tooltip: "百度翻译的App_id和密钥申请方法,请查看右上角插件介绍",
+ type: () => {
+ const row = document.createElement("tr");
+ row.className = "promptwidget-settings-row";
+
+ // 输入框容器单元格
+ const inputCell = document.createElement("td");
+ inputCell.style.display = "flex";
+ inputCell.style.gap = "10px";
+ inputCell.style.alignItems = "center";
+
+ // AppID 输入框
+ const appIdInput = document.createElement("input");
+ appIdInput.type = "text";
+ appIdInput.className = "p-inputtext";
+ appIdInput.placeholder = "AppID";
+ appIdInput.value = localStorage.getItem("PromptAssistant_Settings_baidu_translate_appid") || "";
+ appIdInput.title = "请输入百度翻译API的AppID";
+ appIdInput.style.flex = "1";
+ appIdInput.style.minWidth = "120px";
+ appIdInput.addEventListener("change", (e) => {
+ localStorage.setItem("PromptAssistant_Settings_baidu_translate_appid", e.target.value);
+ logger.debug("百度翻译AppID已更新");
+ if (window.app && app.extensionManager && app.extensionManager.toast) {
+ app.extensionManager.toast.add({
+ severity: "success",
+ summary: "百度翻译 APP_ID 已更新",
+ life: 3000
+ });
+ }
+ });
+
+ // 密钥输入框容器
+ const secretContainer = document.createElement("div");
+ secretContainer.className = "p-password p-component p-inputwrapper";
+ secretContainer.style.flex = "1";
+ secretContainer.style.minWidth = "120px";
+ secretContainer.style.position = "relative";
+ secretContainer.style.display = "inline-flex";
+ secretContainer.style.alignItems = "center";
+
+ // 密钥输入框
+ const secretInput = document.createElement("input");
+ secretInput.type = "password";
+ secretInput.className = "p-password-input p-inputtext";
+ secretInput.placeholder = "密钥";
+ secretInput.value = localStorage.getItem("PromptAssistant_Settings_baidu_translate_secret") || "";
+ secretInput.title = "请输入百度翻译API的密钥";
+ secretInput.style.width = "100%";
+ secretInput.style.paddingRight = "2.5rem";
+ secretInput.addEventListener("change", (e) => {
+ localStorage.setItem("PromptAssistant_Settings_baidu_translate_secret", e.target.value);
+ logger.debug("百度翻译密钥已更新");
+ if (window.app && app.extensionManager && app.extensionManager.toast) {
+ app.extensionManager.toast.add({
+ severity: "success",
+ summary: "百度翻译密钥已更新",
+ life: 3000
+ });
+ }
+ });
+
+ // 添加密码显示切换图标
+ const toggleIcon = document.createElement("i");
+ toggleIcon.className = "p-password-icon";
+ toggleIcon.style.cursor = "pointer";
+ toggleIcon.style.position = "absolute";
+ toggleIcon.style.right = "0.5rem";
+ toggleIcon.style.top = "50%";
+ toggleIcon.style.transform = "translateY(-50%)";
+ toggleIcon.style.display = "flex";
+ toggleIcon.style.alignItems = "center";
+ toggleIcon.style.justifyContent = "center";
+ toggleIcon.style.width = "2rem";
+ toggleIcon.style.height = "100%";
+ toggleIcon.style.color = "var(--input-text)";
+ toggleIcon.style.userSelect = "none";
+ toggleIcon.style.webkitUserSelect = "none";
+ toggleIcon.style.msUserSelect = "none";
+ toggleIcon.innerHTML = `
+
+ `;
+
+ // 添加切换密码显示的功能
+ toggleIcon.addEventListener("click", (e) => {
+ // 阻止事件冒泡和默认行为
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (secretInput.type === "password") {
+ secretInput.type = "text";
+ toggleIcon.innerHTML = `
+
+ `;
+ } else {
+ secretInput.type = "password";
+ toggleIcon.innerHTML = `
+
+ `;
+ }
+ });
+
+ // 阻止鼠标按下事件的默认行为
+ toggleIcon.addEventListener("mousedown", (e) => {
+ e.preventDefault();
+ });
+
+ secretContainer.appendChild(secretInput);
+ secretContainer.appendChild(toggleIcon);
+
+ inputCell.appendChild(appIdInput);
+ inputCell.appendChild(secretContainer);
+ row.appendChild(inputCell);
+
+ return row;
+ }
+ },
+
+ // LLM API 配置
+ {
+ id: "PromptAssistant.Settings.LLM.ApiKey",
+ name: "LLM配置",
+ category: ["✨提示词小助手", "翻译和扩写配置", "LLM"],
+ tooltip: "大模型API的密钥申请方法,请查看右上角插件介绍",
+ type: () => {
+ const row = document.createElement("tr");
+ row.className = "promptwidget-settings-row";
+
+ // 输入框容器单元格
+ const inputCell = document.createElement("td");
+ inputCell.style.display = "flex";
+ inputCell.style.alignItems = "center";
+
+ // 密钥输入框容器
+ const secretContainer = document.createElement("div");
+ secretContainer.className = "p-password p-component p-inputwrapper";
+ secretContainer.style.flex = "1";
+ secretContainer.style.position = "relative";
+ secretContainer.style.display = "inline-flex";
+ secretContainer.style.alignItems = "center";
+
+ // 密钥输入框
+ const secretInput = document.createElement("input");
+ secretInput.type = "password";
+ secretInput.className = "p-password-input p-inputtext";
+ secretInput.placeholder = "API Key";
+ secretInput.value = localStorage.getItem("PromptAssistant_Settings_llm_api_key") || "";
+ secretInput.title = "请输入LLM API的密钥";
+ secretInput.style.width = "406px";
+ secretInput.style.paddingRight = "2.5rem";
+ secretInput.addEventListener("change", (e) => {
+ localStorage.setItem("PromptAssistant_Settings_llm_api_key", e.target.value);
+ logger.debug("LLM API密钥已更新");
+ if (window.app && app.extensionManager && app.extensionManager.toast) {
+ app.extensionManager.toast.add({
+ severity: "success",
+ summary: "LLM API密钥已更新",
+ life: 3000
+ });
+ }
+ });
+
+ // 添加密码显示切换图标
+ const toggleIcon = document.createElement("i");
+ toggleIcon.className = "p-password-icon";
+ toggleIcon.style.cursor = "pointer";
+ toggleIcon.style.position = "absolute";
+ toggleIcon.style.right = "0.5rem";
+ toggleIcon.style.top = "50%";
+ toggleIcon.style.transform = "translateY(-50%)";
+ toggleIcon.style.display = "flex";
+ toggleIcon.style.alignItems = "center";
+ toggleIcon.style.justifyContent = "center";
+ toggleIcon.style.width = "2rem";
+ toggleIcon.style.height = "100%";
+ toggleIcon.style.color = "var(--input-text)";
+ toggleIcon.style.userSelect = "none";
+ toggleIcon.style.webkitUserSelect = "none";
+ toggleIcon.style.msUserSelect = "none";
+ toggleIcon.innerHTML = `
+
+ `;
+
+ // 添加切换密码显示的功能
+ toggleIcon.addEventListener("click", (e) => {
+ // 阻止事件冒泡和默认行为
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (secretInput.type === "password") {
+ secretInput.type = "text";
+ toggleIcon.innerHTML = `
+
+ `;
+ } else {
+ secretInput.type = "password";
+ toggleIcon.innerHTML = `
+
+ `;
+ }
+ });
+
+ // 阻止鼠标按下事件的默认行为
+ toggleIcon.addEventListener("mousedown", (e) => {
+ e.preventDefault();
+ });
+
+ secretContainer.appendChild(secretInput);
+ secretContainer.appendChild(toggleIcon);
+ inputCell.appendChild(secretContainer);
+ row.appendChild(inputCell);
+
+ return row;
+ }
+ },
+
+ // 关于插件信息
+ {
+ id: "PromptAssistant.Settings.About",
+ name: "关于",
+ category: ["✨提示词小助手", " ✨提示词小助手"],
+ type: () => {
+ const row = document.createElement("tr");
+ row.className = "promptwidget-settings-row";
+ const cell = document.createElement("td");
+ cell.colSpan = 2;
+ cell.style.display = "flex";
+ cell.style.alignItems = "center";
+ cell.style.gap = "12px";
+ // 版本徽标
+ const versionBadge = document.createElement("img");
+ versionBadge.alt = "Version";
+ versionBadge.style.display = "block";
+ versionBadge.style.height = "20px";
+
+ // 从全局变量获取版本号
+ if (!window.PromptAssistant_Version) {
+ logger.error("未找到版本号,徽标将无法正确显示");
+ versionBadge.src = `https://img.shields.io/badge/版本-未知-red?style=flat`;
+ } else {
+ const version = window.PromptAssistant_Version;
+ versionBadge.src = `https://img.shields.io/badge/版本-${version}-green?style=flat`;
+ logger.debug(`版本号徽标已更新: ${version}`);
+ }
+
+ cell.appendChild(versionBadge);
+
+
+
+ // GitHub 徽标
+ const authorTag = document.createElement("a");
+ authorTag.href = "https://github.com/yawiii/comfyui_prompt_widget";
+ authorTag.target = "_blank";
+ authorTag.style.textDecoration = "none";
+ authorTag.style.display = "flex";
+ authorTag.style.alignItems = "center";
+ const authorBadge = document.createElement("img");
+ authorBadge.alt = "Static Badge";
+ authorBadge.src = "https://img.shields.io/badge/Github-Yawiii-blue?style=flat&logo=github&logoColor=black&labelColor=%23E1E1E2&color=%2307A3D7";
+ authorBadge.style.display = "block";
+ authorBadge.style.height = "20px";
+ authorTag.appendChild(authorBadge);
+ cell.appendChild(authorTag);
+ // 添加分隔符
+ const separator = document.createElement("div");
+ separator.style.width = "20px"; // 设置间距宽度
+ cell.appendChild(separator);
+
+ // 插件介绍文本
+ const introText = document.createElement("span");
+ introText.textContent = "插件介绍:";
+ introText.style.marginRight = "10px"; // 与右侧徽标保持间距
+ introText.style.fontWeight = "500"; // 稍微加粗
+ introText.style.color = "var(--p-text-muted-color)"; // 使用系统文本颜色
+ cell.appendChild(introText);
+ // B站徽标
+ const biliTag = document.createElement("a");
+ biliTag.href = "https://space.bilibili.com/3493083790025838";
+ biliTag.target = "_blank";
+ biliTag.style.textDecoration = "none";
+ biliTag.style.display = "flex";
+ biliTag.style.alignItems = "center";
+ const biliBadge = document.createElement("img");
+ biliBadge.alt = "Bilibili";
+ biliBadge.src = "https://img.shields.io/badge/b%E7%AB%99-%23E1E1E2?style=flat&logo=bilibili&logoColor=%2307A3D7";
+ biliBadge.style.display = "block";
+ biliBadge.style.height = "20px";
+ biliTag.appendChild(biliBadge);
+ cell.appendChild(biliTag);
+
+ // 抖音徽标
+ const douyinTag = document.createElement("a");
+ douyinTag.href = "https://v.douyin.com/iFhYw6e/";
+ douyinTag.target = "_blank";
+ douyinTag.style.textDecoration = "none";
+ douyinTag.style.display = "flex";
+ douyinTag.style.alignItems = "center";
+ const douyinBadge = document.createElement("img");
+ douyinBadge.alt = "Douyin";
+ douyinBadge.src = "https://img.shields.io/badge/%E6%8A%96%E9%9F%B3-%23E1E1E2?style=flat&logo=TikTok&logoColor=%23161823";
+ douyinBadge.style.display = "block";
+ douyinBadge.style.height = "20px";
+ douyinTag.appendChild(douyinBadge);
+ cell.appendChild(douyinTag);
+
+ row.appendChild(cell);
+ return row;
+ }
+ }
+ ]
+ });
+
+ logger.log("小助手设置注册成功");
+ return true;
+ } catch (error) {
+ logger.error(`小助手设置注册失败: ${error.message}`);
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/js/modules/tag.js b/js/modules/tag.js
new file mode 100644
index 0000000..951ffe6
--- /dev/null
+++ b/js/modules/tag.js
@@ -0,0 +1,1495 @@
+/**
+ * 标签管理器
+ * 负责管理标签的显示和操作
+ */
+
+import { logger } from '../utils/logger.js';
+import { TagCacheService } from "../services/cache.js";
+import { UIToolkit } from "../utils/UIToolkit.js";
+import { PopupManager } from "../utils/popupManager.js";
+import { ResourceManager } from "../utils/resourceManager.js";
+import { EventManager } from "../utils/eventManager.js";
+import { PromptFormatter } from "../utils/promptFormatter.js";
+
+/**
+ * 标签管理器类
+ * 管理标签弹窗和标签选择
+ */
+class TagManager {
+ static popupInstance = null;
+ static onCloseCallback = null; // 添加关闭回调存储
+ static eventCleanups = []; // 事件清理函数数组
+ static searchTimeout = null; // 搜索延迟定时器
+ static currentNodeId = null;
+ static currentInputId = null;
+ static activeTooltip = null;
+ static usedTags = new Map(); // 存储已使用标签的Map: key为标签值,value为对应的DOM元素
+
+ /**
+ * 检查标签是否已插入到输入框中
+ * @param {string} tagValue 标签值
+ * @param {string} nodeId 节点ID
+ * @param {string} inputId 输入框ID
+ * @returns {boolean} 是否已插入
+ */
+ static isTagUsed(tagValue, nodeId, inputId) {
+ const mappingKey = `${nodeId}_${inputId}`;
+ const mapping = window.PromptAssistantInputWidgetMap?.[mappingKey];
+ if (!mapping || !mapping.inputEl) return false;
+
+ // 检查输入框内容是否包含标签的任一格式
+ const inputValue = mapping.inputEl.value;
+ return TagCacheService.isTagInInput(nodeId, inputId, tagValue, inputValue);
+ }
+
+ /**
+ * 更新标签状态
+ * @param {HTMLElement} tagElement 标签元素
+ * @param {boolean} isUsed 是否使用中
+ */
+ static updateTagState(tagElement, isUsed) {
+ if (isUsed) {
+ tagElement.classList.add('used');
+ } else {
+ tagElement.classList.remove('used');
+ }
+ }
+
+ /**
+ * 处理标签点击
+ * @param {HTMLElement} tagElement 标签元素
+ * @param {string} tagName 标签名称
+ * @param {string} tagValue 标签值
+ * @param {Event} e 事件对象
+ */
+ static handleTagClick(tagElement, tagName, tagValue, e) {
+ // 阻止事件冒泡,确保弹窗不会关闭
+ e.stopPropagation();
+
+ // 获取输入框信息
+ const mappingKey = `${this.currentNodeId}_${this.currentInputId}`;
+ const mapping = window.PromptAssistantInputWidgetMap?.[mappingKey];
+ if (!mapping || !mapping.inputEl) return;
+
+ const inputEl = mapping.inputEl;
+ const inputValue = inputEl.value;
+
+ // 判断标签是否已使用
+ const isUsed = this.isTagUsed(tagValue, this.currentNodeId, this.currentInputId);
+
+ try {
+ if (isUsed) {
+ // 标签已使用,移除
+ this.removeTag(tagValue, this.currentNodeId, this.currentInputId, true);
+ this.updateTagState(tagElement, false);
+ this.usedTags.delete(tagValue);
+ logger.debug(`标签操作 | 动作:移除 | 标签:"${tagName}" | 原始值:"${tagValue}"`);
+ } else {
+ // 标签未使用,插入
+ // 获取光标位置前后的文本
+ const cursorPos = inputEl.selectionStart;
+ const beforeText = inputValue.substring(0, cursorPos);
+ const afterText = inputValue.substring(cursorPos);
+
+ // 确定使用哪种格式
+ const formatType = PromptFormatter.determineFormatType(beforeText, afterText);
+
+ // 获取或创建标签格式
+ let formats;
+ const existingFormats = TagCacheService.getTagFormats(this.currentNodeId, this.currentInputId, tagValue);
+ if (existingFormats) {
+ // 如果缓存中已有该标签的格式,直接使用缓存的格式
+ formats = existingFormats;
+ } else {
+ // 如果缓存中没有,创建新的格式
+ formats = PromptFormatter.formatTag(tagValue);
+ }
+
+ // 根据formatType选择要插入的格式
+ let insertFormat;
+ switch (formatType) {
+ case 1:
+ insertFormat = formats.format1;
+ break;
+ case 2:
+ insertFormat = formats.format2;
+ break;
+ case 3:
+ insertFormat = formats.format3;
+ break;
+ case 4:
+ insertFormat = formats.format4;
+ break;
+ default:
+ insertFormat = formats.format2; // 默认使用格式2
+ }
+
+ // 如果是新创建的格式,添加到缓存
+ if (!existingFormats) {
+ formats.insertedFormat = insertFormat;
+ TagCacheService.addTag(this.currentNodeId, this.currentInputId, tagValue, formats);
+ } else {
+ // 如果是已存在的格式,更新insertedFormat
+ TagCacheService.updateInsertedFormat(this.currentNodeId, this.currentInputId, tagValue, insertFormat);
+ }
+
+ // 插入到光标位置
+ UIToolkit.insertAtCursor(insertFormat, this.currentNodeId, this.currentInputId, {
+ highlight: true,
+ keepFocus: true
+ });
+
+ // 更新光标位置到插入内容之后
+ setTimeout(() => {
+ if (inputEl === document.activeElement) {
+ const newPos = cursorPos + insertFormat.length;
+ inputEl.setSelectionRange(newPos, newPos);
+ inputEl.focus();
+ }
+ }, 0);
+
+ // 更新标签状态
+ this.updateTagState(tagElement, true);
+ this.usedTags.set(tagValue, tagElement);
+
+ logger.debug(`标签操作 | 动作:插入 | 标签:"${tagName}" | 原始值:"${tagValue}" | 格式类型:${formatType} | 插入格式:"${insertFormat}"`);
+ }
+ } catch (error) {
+ logger.error(`标签操作失败 | 标签:"${tagName}" | 错误:${error.message}`);
+ }
+ }
+
+ /**
+ * 从输入框中移除标签
+ * @param {string} tagValue 标签值
+ * @param {string} nodeId 节点ID
+ * @param {string} inputId 输入框ID
+ * @param {boolean} keepFocus 是否保持焦点,默认为true
+ */
+ static removeTag(tagValue, nodeId, inputId, keepFocus = true) {
+ const mappingKey = `${nodeId}_${inputId}`;
+ const mapping = window.PromptAssistantInputWidgetMap?.[mappingKey];
+
+ if (mapping && mapping.inputEl) {
+ const inputEl = mapping.inputEl;
+ const currentValue = inputEl.value;
+
+ // 获取标签的所有格式
+ const formatInfo = TagCacheService.getTagFormats(nodeId, inputId, tagValue);
+ if (!formatInfo) return false;
+
+ // 优先使用insertedFormat进行精确匹配
+ if (formatInfo.insertedFormat) {
+ const tagIndex = currentValue.indexOf(formatInfo.insertedFormat);
+ if (tagIndex !== -1) {
+ // 直接使用精确替换,不进行额外的清理
+ const newValue = currentValue.substring(0, tagIndex) +
+ currentValue.substring(tagIndex + formatInfo.insertedFormat.length);
+
+ // 更新输入框值
+ inputEl.value = newValue;
+
+ // 触发事件
+ inputEl.dispatchEvent(new Event('input', { bubbles: true }));
+ inputEl.dispatchEvent(new Event('change', { bubbles: true }));
+
+ if (keepFocus) {
+ inputEl.focus();
+ }
+
+ logger.debug(`标签移除 | 方式:精确匹配 | 标签:${tagValue} | 格式:"${formatInfo.insertedFormat}"`);
+ return true;
+ }
+ }
+
+ // 如果insertedFormat不存在或未找到,按优先级尝试其他格式
+ const removeOrder = ['format4', 'format3', 'format2', 'format1'];
+
+ for (const formatKey of removeOrder) {
+ const format = formatInfo[formatKey];
+ if (!format) continue;
+
+ const tagIndex = currentValue.indexOf(format);
+ if (tagIndex !== -1) {
+ // 检查是否是独立的标签(前后是空格或标点)
+ const isValidRemoval = this._isValidTagRemoval(currentValue, tagIndex, format);
+ if (isValidRemoval) {
+ // 直接使用精确替换,不进行额外的清理
+ const newValue = currentValue.substring(0, tagIndex) +
+ currentValue.substring(tagIndex + format.length);
+
+ // 更新输入框值
+ inputEl.value = newValue;
+
+ // 触发事件
+ inputEl.dispatchEvent(new Event('input', { bubbles: true }));
+ inputEl.dispatchEvent(new Event('change', { bubbles: true }));
+
+ if (keepFocus) {
+ inputEl.focus();
+ }
+
+ logger.debug(`标签移除 | 方式:格式匹配 | 标签:${tagValue} | 格式:${formatKey}`);
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 检查标签移除是否有效
+ * @param {string} value 当前输入框的值
+ * @param {number} index 标签开始位置
+ * @param {string} format 标签格式
+ * @returns {boolean} 是否是有效的移除
+ * @private
+ */
+ static _isValidTagRemoval(value, index, format) {
+ // 获取标签前后的字符
+ const beforeChar = index > 0 ? value[index - 1] : '';
+ const afterChar = index + format.length < value.length ? value[index + format.length] : '';
+
+ // 检查前后字符是否是空格或标点
+ const isValidChar = char => !char || char === ' ' || char === ',' || char === '.' || char === ';';
+
+ return isValidChar(beforeChar) && isValidChar(afterChar);
+ }
+
+ /**
+ * 清理标签移除后的文本
+ * @param {string} text 要清理的文本
+ * @param {number} removePosition 标签移除的位置
+ * @param {number} removeLength 移除的长度
+ * @returns {string} 清理后的文本
+ */
+ static _cleanupAfterRemoval(text, removePosition, removeLength) {
+ // 获取移除位置前后的一小段文本进行清理
+ const cleanRange = 10; // 清理范围(前后各10个字符)
+ const startClean = Math.max(0, removePosition - cleanRange);
+ const endClean = Math.min(text.length, removePosition + cleanRange);
+
+ // 分割文本为三部分:前段、清理段、后段
+ const beforeText = text.substring(0, startClean);
+ let cleanText = text.substring(startClean, endClean);
+ const afterText = text.substring(endClean);
+
+ // 只清理中间部分
+ cleanText = cleanText
+ // 移除连续的逗号
+ .replace(/,\s*,/g, ',')
+ // 确保逗号后有一个空格
+ .replace(/,(\S)/g, ', $1')
+ // 移除多余的空格
+ .replace(/\s+/g, ' ')
+ .trim();
+
+ // 重新组合文本
+ let result = beforeText + cleanText + afterText;
+
+ // 处理首尾
+ if (removePosition === 0) {
+ result = result.replace(/^\s*,\s*/, ''); // 如果标签在开头,移除开头的逗号和空格
+ }
+ if (removePosition + removeLength >= text.length) {
+ result = result.replace(/\s*,\s*$/, ''); // 如果标签在结尾,移除结尾的逗号和空格
+ }
+
+ return result;
+ }
+
+ /**
+ * 创建并显示tooltip
+ * @param {HTMLElement} target 目标元素
+ * @param {string} text tooltip文本
+ */
+ static _showTooltip(target, text) {
+ // 移除已存在的tooltip
+ this._hideTooltip();
+
+ // 创建tooltip元素
+ const tooltip = document.createElement('div');
+ tooltip.className = 'tag_tooltip';
+ tooltip.innerHTML = text; // 使用 innerHTML 以支持 HTML 内容
+ document.body.appendChild(tooltip);
+
+ // 获取目标元素的位置和尺寸
+ const rect = target.getBoundingClientRect();
+
+ // 计算tooltip位置
+ const tooltipRect = tooltip.getBoundingClientRect();
+ const left = rect.left + (rect.width - tooltipRect.width) / 2;
+ const top = rect.top - tooltipRect.height - 8; // 8px的间距
+
+ // 设置tooltip位置
+ tooltip.style.left = `${left}px`;
+ tooltip.style.top = `${top}px`;
+
+ // 保存当前tooltip引用
+ this.activeTooltip = tooltip;
+ }
+
+ /**
+ * 隐藏tooltip
+ */
+ static _hideTooltip() {
+ if (this.activeTooltip) {
+ this.activeTooltip.remove();
+ this.activeTooltip = null;
+ }
+ }
+
+ /**
+ * 递归创建标签结构
+ * @param {Object} data 当前层级的数据
+ * @param {string} level 当前层级(用于样式调整)
+ * @returns {HTMLElement} 创建的DOM元素
+ */
+ static _createAccordionContent(data, level = '0') {
+ // 如果是顶级(一级分类),则创建标签页结构
+ if (level === '0') {
+ // 创建外层容器
+ const container = document.createElement('div');
+ container.style.display = 'flex';
+ container.style.flexDirection = 'column';
+ container.style.height = '100%';
+ container.style.overflow = 'hidden';
+
+ // 创建标签页容器
+ const tabsContainer = document.createElement('div');
+ tabsContainer.className = 'popup_tabs_container';
+
+ // 创建标签滚动区域
+ const tabsScroll = document.createElement('div');
+ tabsScroll.className = 'popup_tabs_scroll';
+
+ // 创建标签栏
+ const tabs = document.createElement('div');
+ tabs.className = 'popup_tabs';
+
+ // 创建内容区域
+ const tabContents = document.createElement('div');
+ tabContents.className = 'tag_category_container';
+ tabContents.style.overflow = 'hidden';
+ tabContents.style.display = 'flex';
+ tabContents.style.flexDirection = 'column';
+
+ // 获取所有一级分类
+ const categories = Object.keys(data);
+
+ // 如果没有分类,返回空容器
+ if (categories.length === 0) {
+ const emptyContainer = document.createElement('div');
+ emptyContainer.className = 'tag_category_container';
+ return emptyContainer;
+ }
+
+ // 创建左右滚动指示器
+ const leftIndicator = document.createElement('div');
+ leftIndicator.className = 'tabs_scroll_indicator left';
+
+ // 使用缓存的图标 - 左箭头(向上图标逆时针旋转90度)
+ const leftIconImg = ResourceManager.getIcon('icon-moveup.svg');
+ if (leftIconImg) {
+ leftIconImg.className = 'scroll_indicator_icon rotate_left';
+ leftIndicator.appendChild(leftIconImg);
+ } else {
+ // 备用文本图标
+ leftIndicator.innerHTML = '←';
+ }
+
+ leftIndicator.style.display = 'none'; // 初始隐藏
+
+ const rightIndicator = document.createElement('div');
+ rightIndicator.className = 'tabs_scroll_indicator right';
+
+ // 使用缓存的图标 - 右箭头(向下图标逆时针旋转90度)
+ const rightIconImg = ResourceManager.getIcon('icon-movedown.svg');
+ if (rightIconImg) {
+ rightIconImg.className = 'scroll_indicator_icon rotate_right';
+ rightIndicator.appendChild(rightIconImg);
+ } else {
+ // 备用文本图标
+ rightIndicator.innerHTML = '→';
+ }
+
+ rightIndicator.style.display = 'none'; // 初始隐藏
+
+ // 添加指示器点击事件 - 改进滚动逻辑
+ const leftClickCleanup = EventManager.addDOMListener(leftIndicator, 'click', () => {
+ // 获取可视区域宽度
+ const visibleWidth = tabsScroll.clientWidth;
+
+ // 计算滚动距离,PrimeVue风格是滚动一个较大的距离
+ const scrollDistance = visibleWidth * 0.75;
+
+ // 平滑滚动
+ tabsScroll.scrollBy({
+ left: -scrollDistance,
+ behavior: 'smooth'
+ });
+ });
+
+ const rightClickCleanup = EventManager.addDOMListener(rightIndicator, 'click', () => {
+ // 获取可视区域宽度
+ const visibleWidth = tabsScroll.clientWidth;
+
+ // 计算滚动距离,PrimeVue风格是滚动一个较大的距离
+ const scrollDistance = visibleWidth * 0.75;
+
+ // 平滑滚动
+ tabsScroll.scrollBy({
+ left: scrollDistance,
+ behavior: 'smooth'
+ });
+ });
+
+ // 监听滚动事件,显示/隐藏滚动指示器
+ const scrollCleanup = EventManager.addDOMListener(tabsScroll, 'scroll', () => {
+ // 检查是否需要滚动
+ const canScroll = tabsScroll.scrollWidth > tabsScroll.clientWidth;
+
+ if (!canScroll) {
+ // 如果不需要滚动,隐藏两个指示器
+ leftIndicator.style.display = 'none';
+ rightIndicator.style.display = 'none';
+ return;
+ }
+
+ // 显示/隐藏左右滚动指示器
+ leftIndicator.style.display = tabsScroll.scrollLeft > 0 ? 'flex' : 'none';
+ rightIndicator.style.display =
+ tabsScroll.scrollLeft < (tabsScroll.scrollWidth - tabsScroll.clientWidth - 2) ? 'flex' : 'none';
+ });
+
+ this.eventCleanups.push(leftClickCleanup, rightClickCleanup, scrollCleanup);
+
+ // 初始检测是否需要滚动指示器
+ setTimeout(() => {
+ // 检查是否需要滚动
+ const canScroll = tabsScroll.scrollWidth > tabsScroll.clientWidth;
+
+ if (canScroll) {
+ // 如果需要滚动,显示右侧指示器
+ rightIndicator.style.display = 'flex';
+ }
+ }, 100);
+
+ // 为每个一级分类创建标签和内容
+ categories.forEach((category, index) => {
+ // 创建标签
+ const tab = document.createElement('div');
+ tab.className = 'popup_tab';
+ tab.textContent = category;
+ tab.setAttribute('data-category', category);
+
+ // 第一个标签默认激活
+ if (index === 0) {
+ tab.classList.add('active');
+ }
+
+ // 添加标签点击事件
+ const tabClickCleanup = EventManager.addDOMListener(tab, 'click', (e) => {
+ // 获取当前激活的标签
+ const currentActiveTab = tabs.querySelector('.popup_tab.active');
+
+ // 如果点击的是当前激活的标签,不做任何处理
+ if (currentActiveTab === tab) return;
+
+ // 为当前激活的标签添加退出动画
+ if (currentActiveTab) {
+ currentActiveTab.classList.add('exiting');
+ // 监听动画结束
+ const animationEndHandler = () => {
+ currentActiveTab.classList.remove('active', 'exiting');
+ currentActiveTab.removeEventListener('transitionend', animationEndHandler);
+ };
+ currentActiveTab.addEventListener('transitionend', animationEndHandler);
+ }
+
+ // 移除所有内容的active类
+ tabContents.querySelectorAll('.popup_tab_content').forEach(c => {
+ c.classList.remove('active');
+ c.style.display = 'none';
+ });
+
+ // 添加当前标签的active类
+ tab.classList.add('active');
+
+ // 添加对应内容的active类
+ const contentId = tab.getAttribute('data-category');
+ const content = tabContents.querySelector(`.popup_tab_content[data-category="${contentId}"]`);
+ if (content) {
+ content.classList.add('active');
+ content.style.display = 'block';
+ }
+
+ // 改进滚动逻辑:确保选中的标签完全可见
+ const tabRect = tab.getBoundingClientRect();
+ const scrollRect = tabsScroll.getBoundingClientRect();
+
+ // 检查标签是否完全在可视区域内
+ const isFullyVisible =
+ tabRect.left >= scrollRect.left &&
+ tabRect.right <= scrollRect.right;
+
+ if (!isFullyVisible) {
+ // 如果标签在左侧不完全可见
+ if (tabRect.left < scrollRect.left) {
+ tab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
+ }
+ // 如果标签在右侧不完全可见
+ else if (tabRect.right > scrollRect.right) {
+ tab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'end' });
+ }
+ }
+ });
+
+ this.eventCleanups.push(tabClickCleanup);
+ tabs.appendChild(tab);
+
+ // 创建对应的内容区域
+ const content = document.createElement('div');
+ content.className = 'popup_tab_content';
+ content.setAttribute('data-category', category);
+ content.style.flex = '1';
+ content.style.display = 'none';
+
+ // 第一个内容默认显示
+ if (index === 0) {
+ content.classList.add('active');
+ content.style.display = 'block';
+ }
+
+ // 递归创建子内容(二级分类开始使用手风琴)
+ if (typeof data[category] === 'object' && data[category] !== null) {
+ const innerContent = this._createInnerAccordion(data[category], '1');
+ content.appendChild(innerContent);
+ }
+
+ tabContents.appendChild(content);
+ });
+
+ // 组装标签页结构
+ tabsScroll.appendChild(tabs);
+ tabsContainer.appendChild(leftIndicator);
+ tabsContainer.appendChild(tabsScroll);
+ tabsContainer.appendChild(rightIndicator);
+
+ // 组装容器
+ container.appendChild(tabsContainer);
+ container.appendChild(tabContents);
+
+ return container;
+ } else {
+ // 非顶级分类使用普通容器
+ const container = document.createElement('div');
+ container.className = 'tag_category_container';
+
+ // 跟踪当前层级的第一个手风琴
+ let isFirstAccordionInLevel = true;
+
+ for (const [key, value] of Object.entries(data)) {
+ // 如果值是字符串,说明是标签
+ if (typeof value === 'string') {
+ const tagItem = document.createElement('div');
+ tagItem.className = 'tag_item';
+ tagItem.setAttribute('data-name', key);
+ tagItem.setAttribute('data-value', value);
+
+ // 检查标签是否已使用
+ if (this.isTagUsed(value, this.currentNodeId, this.currentInputId)) {
+ tagItem.classList.add('used');
+ this.usedTags.set(value, tagItem);
+ }
+
+ const tagText = document.createElement('span');
+ tagText.className = 'tag_item_text';
+ tagText.textContent = key;
+
+ tagItem.appendChild(tagText);
+
+ // 添加鼠标事件监听
+ const mouseEnterCleanup = EventManager.addDOMListener(tagItem, 'mouseenter', () => {
+ this._showTooltip(tagItem, value);
+ });
+
+ const mouseLeaveCleanup = EventManager.addDOMListener(tagItem, 'mouseleave', () => {
+ this._hideTooltip();
+ });
+
+ // 添加点击事件
+ const tagClickCleanup = EventManager.addDOMListener(tagItem, 'click', (e) => {
+ this.handleTagClick(tagItem, key, value, e);
+ });
+
+ this.eventCleanups.push(mouseEnterCleanup, mouseLeaveCleanup, tagClickCleanup);
+
+ container.appendChild(tagItem);
+ }
+ // 递归处理下一级
+ else if (typeof value === 'object' && value !== null) {
+ const accordion = document.createElement('div');
+ accordion.className = 'tag_accordion';
+ accordion.setAttribute('data-category', key);
+ accordion.setAttribute('data-level', level);
+
+ const header = document.createElement('div');
+ header.className = 'tag_accordion_header';
+
+ const headerTitle = document.createElement('div');
+ headerTitle.className = 'tag_accordion_title';
+ headerTitle.textContent = key;
+
+ const headerIcon = document.createElement('div');
+ headerIcon.className = 'tag_accordion_icon';
+
+ // 只用一个图标
+ const iconImg = ResourceManager.getIcon('icon-movedown.svg');
+ if (iconImg) {
+ iconImg.classList.add('accordion_arrow_icon');
+ headerIcon.appendChild(iconImg);
+ }
+
+ header.appendChild(headerTitle);
+ header.appendChild(headerIcon);
+
+ const content = document.createElement('div');
+ content.className = 'tag_accordion_content';
+
+ // 递归创建子内容
+ const childContent = this._createAccordionContent(value, (parseInt(level) + 1).toString());
+ content.appendChild(childContent);
+
+ // 如果是当前层级的第一个手风琴,默认展开
+ if (isFirstAccordionInLevel) {
+ header.classList.add('active');
+ content.classList.add('active');
+ if (iconImg) {
+ iconImg.classList.add('rotate-180');
+ }
+ isFirstAccordionInLevel = false;
+ }
+
+ // 添加手风琴切换事件
+ const accordionCleanup = EventManager.addDOMListener(header, 'click', (e) => {
+ e.stopPropagation();
+
+ // 获取当前手风琴的层级
+ const currentLevel = accordion.getAttribute('data-level');
+ // 获取当前标签页下的所有同级手风琴
+ const parentTab = header.closest('.popup_tab_content');
+ if (parentTab) {
+ // 只关闭同级别的其他手风琴
+ const siblingAccordions = parentTab.querySelectorAll(`.tag_accordion[data-level="${currentLevel}"] .tag_accordion_header.active`);
+ if (!header.classList.contains('active')) {
+ siblingAccordions.forEach(otherHeader => {
+ if (otherHeader !== header) {
+ // 获取父级手风琴
+ const parentAccordion = otherHeader.closest('.tag_accordion');
+ // 确保是同级的手风琴
+ if (parentAccordion && parentAccordion.getAttribute('data-level') === currentLevel) {
+ otherHeader.classList.remove('active');
+ const otherContent = otherHeader.nextElementSibling;
+ if (otherContent && otherContent.classList.contains('active')) {
+ otherContent.classList.remove('active');
+ }
+ const otherIcon = otherHeader.querySelector('.accordion_arrow_icon');
+ if (otherIcon) {
+ otherIcon.classList.remove('rotate-180');
+ }
+ }
+ }
+ });
+ }
+ }
+
+ // 切换当前手风琴状态
+ header.classList.toggle('active');
+ content.classList.toggle('active');
+ // 图标旋转
+ const arrowIcon = headerIcon.querySelector('.accordion_arrow_icon');
+ if (arrowIcon) {
+ arrowIcon.classList.toggle('rotate-180');
+ }
+ });
+
+ this.eventCleanups.push(accordionCleanup);
+
+ accordion.appendChild(header);
+ accordion.appendChild(content);
+ container.appendChild(accordion);
+ }
+ }
+
+ return container;
+ }
+ }
+
+ /**
+ * 为二级分类创建手风琴元素
+ * @param {string} key 分类名称
+ * @param {Object} value 分类内容
+ * @param {string} level 当前层级
+ * @returns {HTMLElement} 手风琴元素
+ */
+ static _createAccordionElement(key, value, level) {
+ const accordion = document.createElement('div');
+ accordion.className = 'tag_accordion';
+
+ accordion.setAttribute('data-category', key);
+
+ const header = document.createElement('div');
+ header.className = 'tag_accordion_header';
+
+ const headerTitle = document.createElement('div');
+ headerTitle.className = 'tag_accordion_title';
+ headerTitle.textContent = key;
+
+ const headerIcon = document.createElement('div');
+ headerIcon.className = 'tag_accordion_icon';
+
+ // 只用一个图标
+ const iconImg = ResourceManager.getIcon('icon-movedown.svg');
+ if (iconImg) {
+ iconImg.classList.add('accordion_arrow_icon');
+ headerIcon.appendChild(iconImg);
+ }
+
+ header.appendChild(headerTitle);
+ header.appendChild(headerIcon);
+
+ const content = document.createElement('div');
+ content.className = 'tag_accordion_content';
+
+ // 递归创建子内容
+ const childContent = this._createAccordionContent(value, (parseInt(level) + 1).toString());
+ content.appendChild(childContent);
+
+ // 添加手风琴切换事件,包含关闭其他手风琴的逻辑
+ const accordionCleanup = EventManager.addDOMListener(header, 'click', (e) => {
+ e.stopPropagation();
+ // 获取当前标签页下的所有手风琴
+ const parentTab = header.closest('.popup_tab_content');
+ if (parentTab) {
+ if (!header.classList.contains('active')) {
+ const otherAccordions = parentTab.querySelectorAll('.tag_accordion_header.active');
+ otherAccordions.forEach(otherHeader => {
+ if (otherHeader !== header) {
+ otherHeader.classList.remove('active');
+ const otherContent = otherHeader.nextElementSibling;
+ if (otherContent && otherContent.classList.contains('active')) {
+ otherContent.classList.remove('active');
+ }
+ const otherIcon = otherHeader.querySelector('.accordion_arrow_icon');
+ if (otherIcon) {
+ otherIcon.classList.add('rotate-180');
+ }
+ }
+ });
+ }
+ }
+ // 切换当前手风琴状态
+ header.classList.toggle('active');
+ content.classList.toggle('active');
+ // 图标旋转
+ const arrowIcon = headerIcon.querySelector('.accordion_arrow_icon');
+ if (arrowIcon) {
+ if (header.classList.contains('active')) {
+ arrowIcon.classList.add('rotate-180');
+ } else {
+ arrowIcon.classList.remove('rotate-180');
+ }
+ }
+ });
+ this.eventCleanups.push(accordionCleanup);
+
+ accordion.appendChild(header);
+ accordion.appendChild(content);
+
+ // 默认展开第一个手风琴(根据父元素的位置)
+ if (accordion.parentElement && accordion.parentElement.firstChild === accordion) {
+ header.classList.add('active');
+ content.classList.add('active');
+ }
+
+ return accordion;
+ }
+
+ /**
+ * 为二级分类创建内容
+ * @param {Object} data 分类数据
+ * @param {string} level 当前层级
+ * @returns {HTMLElement} 内容元素
+ */
+ static _createInnerAccordion(data, level) {
+ const container = document.createElement('div');
+ container.className = 'tag_category_container';
+ container.style.flex = '1';
+ container.style.overflow = 'auto';
+ container.style.maxHeight = 'none';
+
+ // 跟踪是否为第一个手风琴
+ let isFirstAccordion = true;
+
+ for (const [key, value] of Object.entries(data)) {
+ // 如果值是对象,创建新的手风琴
+ if (typeof value === 'object' && value !== null) {
+ const accordion = document.createElement('div');
+ accordion.className = 'tag_accordion';
+ accordion.setAttribute('data-category', key);
+ const header = document.createElement('div');
+ header.className = 'tag_accordion_header';
+
+ const headerTitle = document.createElement('div');
+ headerTitle.className = 'tag_accordion_title';
+ headerTitle.textContent = key;
+
+ const headerIcon = document.createElement('div');
+ headerIcon.className = 'tag_accordion_icon';
+
+ // 只用一个图标
+ const iconImg = ResourceManager.getIcon('icon-movedown.svg');
+ if (iconImg) {
+ iconImg.classList.add('accordion_arrow_icon');
+ headerIcon.appendChild(iconImg);
+ }
+
+ header.appendChild(headerTitle);
+ header.appendChild(headerIcon);
+
+ const content = document.createElement('div');
+ content.className = 'tag_accordion_content';
+
+ // 递归创建子内容
+ const childContent = this._createAccordionContent(value, (parseInt(level) + 1).toString());
+ childContent.style.maxHeight = '160px'; // 限制嵌套内容高度
+ content.appendChild(childContent);
+
+ // 如果是第一个手风琴,默认展开
+ if (isFirstAccordion) {
+ header.classList.add('active');
+ content.classList.add('active');
+ if (iconImg) {
+ iconImg.classList.add('rotate-180');
+ }
+ isFirstAccordion = false;
+ }
+
+ // 添加手风琴切换事件,包含关闭其他手风琴的逻辑
+ const accordionCleanup = EventManager.addDOMListener(header, 'click', (e) => {
+ e.stopPropagation();
+ // 获取当前标签页下的所有手风琴
+ const parentTab = header.closest('.popup_tab_content');
+ if (parentTab) {
+ if (!header.classList.contains('active')) {
+ const otherAccordions = parentTab.querySelectorAll('.tag_accordion_header.active');
+ otherAccordions.forEach(otherHeader => {
+ if (otherHeader !== header) {
+ otherHeader.classList.remove('active');
+ const otherContent = otherHeader.nextElementSibling;
+ if (otherContent && otherContent.classList.contains('active')) {
+ otherContent.classList.remove('active');
+ }
+ const otherIcon = otherHeader.querySelector('.accordion_arrow_icon');
+ if (otherIcon) {
+ otherIcon.classList.remove('rotate-180');
+ }
+ }
+ });
+ }
+ }
+ // 切换当前手风琴状态
+ header.classList.toggle('active');
+ content.classList.toggle('active');
+ // 图标旋转
+ const arrowIcon = headerIcon.querySelector('.accordion_arrow_icon');
+ if (arrowIcon) {
+ arrowIcon.classList.toggle('rotate-180');
+ }
+ });
+
+ this.eventCleanups.push(accordionCleanup);
+
+ accordion.appendChild(header);
+ accordion.appendChild(content);
+ container.appendChild(accordion);
+ } else if (typeof value === 'string') {
+ // 如果值是字符串,创建标签项(保持原有的标签创建逻辑)
+ const tagItem = document.createElement('div');
+ tagItem.className = 'tag_item';
+ tagItem.setAttribute('data-name', key);
+ tagItem.setAttribute('data-value', value);
+
+ // 检查标签是否已使用
+ if (this.isTagUsed(value, this.currentNodeId, this.currentInputId)) {
+ tagItem.classList.add('used');
+ this.usedTags.set(value, tagItem);
+ }
+
+ const tagText = document.createElement('span');
+ tagText.className = 'tag_item_text';
+ tagText.textContent = key;
+
+ tagItem.appendChild(tagText);
+
+ // 添加鼠标事件监听
+ const mouseEnterCleanup = EventManager.addDOMListener(tagItem, 'mouseenter', () => {
+ this._showTooltip(tagItem, value);
+ });
+
+ const mouseLeaveCleanup = EventManager.addDOMListener(tagItem, 'mouseleave', () => {
+ this._hideTooltip();
+ });
+
+ // 添加点击事件
+ const tagClickCleanup = EventManager.addDOMListener(tagItem, 'click', (e) => {
+ this.handleTagClick(tagItem, key, value, e);
+ });
+
+ this.eventCleanups.push(mouseEnterCleanup, mouseLeaveCleanup, tagClickCleanup);
+
+ container.appendChild(tagItem);
+ }
+ }
+
+ return container;
+ }
+
+ /**
+ * 比对输入框内容与标签缓存,更新标签状态
+ * @param {string} nodeId 节点ID
+ * @param {string} inputId 输入框ID
+ * @param {boolean} updateUI 是否更新UI状态
+ * @returns {Object} 比对结果,包含缓存数量和匹配数量
+ */
+ static compareInputWithCache(nodeId, inputId, updateUI = true) {
+ const startTime = performance.now();
+
+ // 获取输入框值
+ const mappingKey = `${nodeId}_${inputId}`;
+ const mapping = window.PromptAssistantInputWidgetMap?.[mappingKey];
+ if (!mapping || !mapping.inputEl) return { cacheCount: 0, matchedCount: 0 };
+
+ const inputValue = mapping.inputEl.value;
+
+ // 获取缓存
+ const cache = TagCacheService.getTagCache(nodeId, inputId);
+ const cacheCount = cache ? cache.size : 0;
+ let matchedCount = 0;
+
+ if (updateUI) {
+ // 清空已使用标签映射
+ this.usedTags.clear();
+
+ // 获取所有标签元素
+ const tagItems = document.querySelectorAll('.tag_item');
+
+ // 遍历所有标签元素,更新状态
+ tagItems.forEach(tagItem => {
+ const tagValue = tagItem.getAttribute('data-value');
+ if (!tagValue) return;
+
+ // 检查是否在输入框中
+ const isUsed = TagCacheService.isTagInInput(nodeId, inputId, tagValue, inputValue);
+
+ if (isUsed) {
+ matchedCount++;
+ this.usedTags.set(tagValue, tagItem);
+ }
+
+ // 更新标签显示状态
+ this.updateTagState(tagItem, isUsed);
+ });
+ } else {
+ // 只统计匹配数量,不更新UI
+ cache?.forEach((_, rawTag) => {
+ if (TagCacheService.isTagInInput(nodeId, inputId, rawTag, inputValue)) {
+ matchedCount++;
+ }
+ });
+ }
+
+ const endTime = performance.now();
+ logger.debug(`标签状态 | 动作:比对 | 节点:${nodeId} | 输入框:${inputId} | 缓存数量:${cacheCount} | 匹配数量:${matchedCount} | 耗时:${(endTime - startTime).toFixed(2)}ms`);
+
+ return { cacheCount, matchedCount };
+ }
+
+ /**
+ * 显示标签弹窗
+ * @param {Object} options 配置选项
+ */
+ static async showTagPopup(options) {
+ const {
+ anchorButton,
+ nodeId,
+ inputId,
+ onClose,
+ buttonInfo,
+ onTagSelect,
+ refresh = false
+ } = options;
+
+ // 保存当前节点和输入框ID
+ this.currentNodeId = nodeId;
+ this.currentInputId = inputId;
+ this.onTagSelectCallback = onTagSelect;
+
+ // 清理现有事件监听
+ this._cleanupEvents();
+
+ try {
+ // 创建弹窗容器
+ const popup = document.createElement('div');
+ popup.className = 'popup_container popup-up';
+ popup.style.display = 'flex';
+ popup.style.flexDirection = 'column';
+
+ // 创建标题栏
+ const titleBar = document.createElement('div');
+ titleBar.className = 'popup_title_bar';
+
+ const title = document.createElement('div');
+ title.className = 'popup_title';
+ title.textContent = '标签工具';
+
+ // 创建搜索框容器
+ const searchContainer = document.createElement('div');
+ searchContainer.className = 'popup_search_container';
+
+ const searchInput = document.createElement('input');
+ searchInput.className = 'popup_search_input';
+ searchInput.type = 'text';
+ searchInput.placeholder = '搜索标签...';
+
+ // 创建清除按钮
+ const clearBtn = document.createElement('button');
+ clearBtn.className = 'popup_btn';
+ const clearIcon = ResourceManager.getIcon('icon-close.svg');
+ clearIcon.className = 'popup_btn_icon';
+ clearBtn.appendChild(clearIcon);
+ clearBtn.title = '清除搜索';
+ clearBtn.style.display = 'none';
+
+ // 添加清除按钮点击事件
+ const clearBtnCleanup = EventManager.addDOMListener(clearBtn, 'click', (e) => {
+ e.stopPropagation();
+ searchInput.value = '';
+ clearBtn.style.display = 'none';
+ searchInput.blur(); // 让搜索框失去焦点
+ this._handleSearch('');
+ });
+ this.eventCleanups.push(clearBtnCleanup);
+
+ searchContainer.appendChild(searchInput);
+ searchContainer.appendChild(clearBtn);
+
+ // 创建操作按钮容器
+ const actions = document.createElement('div');
+ actions.className = 'popup_actions';
+
+ // 添加刷新按钮
+ const refreshBtn = document.createElement('button');
+ refreshBtn.className = 'popup_btn';
+ const refreshIcon = ResourceManager.getIcon('icon-refresh.svg');
+ refreshIcon.className = 'popup_btn_icon';
+ refreshBtn.appendChild(refreshIcon);
+ refreshBtn.title = '刷新标签状态';
+
+ // 添加关闭按钮
+ const closeBtn = document.createElement('button');
+ closeBtn.className = 'popup_btn';
+ const closeIcon = ResourceManager.getIcon('icon-close.svg');
+ closeIcon.className = 'popup_btn_icon';
+ closeBtn.appendChild(closeIcon);
+
+ // 添加刷新事件
+ const refreshCleanup = EventManager.addDOMListener(refreshBtn, 'click', () => {
+ try {
+ refreshBtn.style.transform = 'rotate(360deg)';
+ refreshBtn.style.transition = 'transform 0.5s';
+
+ // 执行比对和更新
+ this.compareInputWithCache(nodeId, inputId, true);
+
+ } catch (error) {
+ logger.error(`标签状态刷新失败: ${error.message}`);
+ } finally {
+ setTimeout(() => {
+ refreshBtn.style.transform = '';
+ refreshBtn.style.transition = '';
+ }, 500);
+ }
+ });
+ this.eventCleanups.push(refreshCleanup);
+
+ // 添加关闭事件
+ const closeCleanup = EventManager.addDOMListener(closeBtn, 'click', () => {
+ PopupManager.closeAllPopups();
+ });
+ this.eventCleanups.push(closeCleanup);
+
+ // 添加搜索事件
+ const searchCleanup = EventManager.addDOMListener(searchInput, 'input', (e) => {
+ const value = e.target.value;
+ clearBtn.style.display = value.length > 0 ? 'flex' : 'none';
+
+ if (this.searchTimeout) {
+ clearTimeout(this.searchTimeout);
+ }
+ this.searchTimeout = setTimeout(() => {
+ this._handleSearch(value.toLowerCase());
+ }, 300);
+ });
+ this.eventCleanups.push(searchCleanup);
+
+ // 添加搜索框聚焦和点击事件,防止弹窗关闭
+ const searchFocusCleanup = EventManager.addDOMListener(searchInput, 'focus', (e) => {
+ e.stopPropagation();
+ });
+ this.eventCleanups.push(searchFocusCleanup);
+
+ const searchClickCleanup = EventManager.addDOMListener(searchInput, 'click', (e) => {
+ e.stopPropagation();
+ });
+ this.eventCleanups.push(searchClickCleanup);
+
+ // 组装标题栏 - 修改组装顺序
+ titleBar.appendChild(title);
+ titleBar.appendChild(searchContainer);
+ titleBar.appendChild(actions);
+ actions.appendChild(refreshBtn);
+ actions.appendChild(closeBtn);
+
+ // 创建内容容器
+ const contentContainer = document.createElement('div');
+ contentContainer.style.display = 'flex';
+ contentContainer.style.flexDirection = 'column';
+ contentContainer.style.flex = '1';
+ contentContainer.style.overflow = 'hidden';
+
+ // 获取标签数据并创建内容
+ const tagData = await ResourceManager.getTagData(refresh);
+ const content = this._createAccordionContent(tagData);
+ content.style.flex = '1';
+ content.style.overflow = 'hidden';
+
+ // 统计缓存和匹配情况
+ this.compareInputWithCache(nodeId, inputId, true);
+
+ // 组装内容区域
+ contentContainer.appendChild(content);
+
+ // 组装弹窗
+ popup.appendChild(titleBar);
+ popup.appendChild(contentContainer);
+
+ // 显示弹窗
+ await PopupManager.showPopup({
+ popup: popup,
+ anchorButton: anchorButton,
+ buttonInfo: buttonInfo,
+ preventCloseOnElementTypes: ['tag_item', 'tag_item_text', 'tag_search_input'], // 阻止标签和搜索框关闭弹窗
+ onClose: () => {
+ this._cleanupEvents();
+ if (typeof onClose === 'function') {
+ onClose();
+ }
+ }
+ });
+
+ // 初始化滚动指示器
+ setTimeout(() => {
+ const tabsScroll = popup.querySelector('.popup_tabs_scroll');
+ if (tabsScroll) {
+ // 检查是否需要滚动
+ const canScroll = tabsScroll.scrollWidth > tabsScroll.clientWidth;
+
+ // 获取滚动指示器
+ const leftIndicator = document.querySelector('.tabs_scroll_indicator.left');
+ const rightIndicator = document.querySelector('.tabs_scroll_indicator.right');
+
+ if (canScroll) {
+ // 如果需要滚动,更新左右指示器状态
+ if (leftIndicator) {
+ leftIndicator.style.display = tabsScroll.scrollLeft > 0 ? 'flex' : 'none';
+ }
+ if (rightIndicator) {
+ rightIndicator.style.display =
+ tabsScroll.scrollLeft < (tabsScroll.scrollWidth - tabsScroll.clientWidth - 2) ? 'flex' : 'none';
+ }
+ } else {
+ // 如果不需要滚动,隐藏两个指示器
+ if (leftIndicator) leftIndicator.style.display = 'none';
+ if (rightIndicator) rightIndicator.style.display = 'none';
+ }
+
+ // 找到当前激活的标签
+ const activeTab = popup.querySelector('.popup_tab.active');
+ if (activeTab) {
+ // 滚动到激活的标签,确保它在视图中居中
+ activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
+
+ // 再次触发滚动事件,确保滚动指示器状态更新
+ setTimeout(() => {
+ tabsScroll.dispatchEvent(new Event('scroll'));
+ }, 100);
+ }
+ }
+ }, 100);
+
+ } catch (error) {
+ logger.error(`标签弹窗创建失败: ${error.message}`);
+ this._cleanupAll();
+ }
+ }
+
+ /**
+ * 处理搜索
+ * @param {string} searchText 搜索文本
+ */
+ static _handleSearch(searchText) {
+ const tagItems = document.querySelectorAll('.tag_item');
+ const accordions = document.querySelectorAll('.tag_accordion');
+ const tabs = document.querySelectorAll('.popup_tab');
+ const tabContents = document.querySelectorAll('.popup_tab_content');
+ const popup = document.querySelector('.popup_container');
+ const tabsContainer = document.querySelector('.popup_tabs_container');
+
+ // 搜索结果容器class
+ const SEARCH_RESULT_CLASS = 'tag_search_result_list';
+ let searchResultList = document.querySelector('.' + SEARCH_RESULT_CLASS);
+
+ if (!searchText) {
+ // 恢复原有内容
+ if (searchResultList) searchResultList.remove();
+ if (tabsContainer) tabsContainer.style.display = '';
+ tagItems.forEach(item => item.style.display = '');
+ accordions.forEach(accordion => accordion.style.display = '');
+ tabs.forEach(tab => tab.style.display = '');
+ tabContents.forEach(content => {
+ if (content.classList.contains('active')) {
+ content.style.display = 'block';
+ } else {
+ content.style.display = 'none';
+ }
+ });
+
+ // 重置滚动条
+ const tabsScroll = document.querySelector('.popup_tabs_scroll');
+ if (tabsScroll) {
+ const activeTab = document.querySelector('.popup_tab.active');
+ if (activeTab) {
+ activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
+ } else {
+ tabsScroll.scrollLeft = 0;
+ }
+ setTimeout(() => {
+ tabsScroll.dispatchEvent(new Event('scroll'));
+ }, 100);
+ }
+ return;
+ }
+
+ // 有搜索内容时,隐藏原有内容和tabs栏
+ if (tabsContainer) tabsContainer.style.display = 'none';
+ tabContents.forEach(content => content.style.display = 'none');
+ tabs.forEach(tab => tab.style.display = 'none');
+ accordions.forEach(accordion => accordion.style.display = 'none');
+
+ // 移除旧的搜索结果容器
+ if (searchResultList) searchResultList.remove();
+
+ // 创建新的搜索结果容器
+ searchResultList = document.createElement('div');
+ searchResultList.className = SEARCH_RESULT_CLASS;
+
+ // 收集所有匹配标签及其分类
+ let matchCount = 0;
+ tagItems.forEach(item => {
+ const name = item.getAttribute('data-name') || '';
+ const value = item.getAttribute('data-value') || '';
+ const matches = name.toLowerCase().includes(searchText) || value.toLowerCase().includes(searchText);
+ if (!matches) return;
+
+ // 获取完整的分类路径
+ const categoryPath = [];
+
+ // 获取一级分类(标签页)
+ const tabContent = item.closest('.popup_tab_content');
+ if (tabContent) {
+ const category = tabContent.getAttribute('data-category');
+ if (category) categoryPath.push(category);
+ }
+
+ // 获取所有父级手风琴的标题
+ let parent = item.closest('.tag_accordion');
+ while (parent) {
+ const title = parent.querySelector('.tag_accordion_title');
+ if (title) {
+ categoryPath.unshift(title.textContent);
+ }
+ parent = parent.parentElement.closest('.tag_accordion');
+ }
+
+ // 克隆标签节点,保留交互
+ const tagClone = item.cloneNode(true);
+ tagClone.style.display = '';
+ tagClone.style.margin = '4px 4px';
+ tagClone.style.cursor = 'pointer';
+ tagClone.classList.add('search_result_tag_item');
+
+ // 添加鼠标事件监听
+ const mouseEnterCleanup = EventManager.addDOMListener(tagClone, 'mouseenter', () => {
+ // 显示包含值和完整分类路径的tooltip
+ const tooltipContent = `${value}\n类别: ${categoryPath.join(' > ')}`;
+ this._showTooltip(tagClone, tooltipContent);
+ });
+
+ const mouseLeaveCleanup = EventManager.addDOMListener(tagClone, 'mouseleave', () => {
+ this._hideTooltip();
+ });
+
+ // 重新绑定点击事件
+ tagClone.onclick = (e) => {
+ this.handleTagClick(tagClone, name, value, e);
+ };
+
+ // 检查标签是否已使用
+ if (this.isTagUsed(value, this.currentNodeId, this.currentInputId)) {
+ tagClone.classList.add('used');
+ }
+
+ searchResultList.appendChild(tagClone);
+ matchCount++;
+
+ this.eventCleanups.push(mouseEnterCleanup, mouseLeaveCleanup);
+ });
+
+ // 无结果提示
+ if (matchCount === 0) {
+ const empty = document.createElement('div');
+ empty.textContent = '无匹配标签';
+ empty.style.textAlign = 'center';
+ empty.style.color = '#888';
+ empty.style.padding = '32px 0';
+ searchResultList.appendChild(empty);
+ }
+
+ // 插入到内容区(搜索框下方)
+ const contentContainer = popup.querySelector('div[style*="flex-direction: column"]:not(.popup_title_bar)');
+ if (contentContainer) {
+ contentContainer.appendChild(searchResultList);
+ }
+ }
+
+ /**
+ * 隐藏标签弹窗
+ */
+ static hideTagPopup() {
+ // 清理事件监听
+ this._cleanupEvents();
+
+ // 使用PopupManager关闭所有弹窗
+ PopupManager.closeAllPopups();
+ }
+
+ /**
+ * 清理所有相关资源
+ */
+ static _cleanupAll() {
+ // 清理事件监听
+ this._cleanupEvents();
+
+ // 使用PopupManager关闭所有弹窗
+ PopupManager.closeAllPopups();
+ }
+
+ /**
+ * 清理所有事件监听
+ */
+ static _cleanupEvents() {
+ // 执行并清空所有事件清理函数
+ if (this.eventCleanups.length > 0) {
+ this.eventCleanups.forEach(cleanup => {
+ if (typeof cleanup === 'function') {
+ cleanup();
+ }
+ });
+ this.eventCleanups = [];
+ }
+ }
+
+ /**
+ * 更新所有标签的状态
+ * @param {string} nodeId 节点ID
+ * @param {string} inputId 输入框ID
+ */
+ static updateAllTagsState(nodeId, inputId) {
+ const mappingKey = `${nodeId}_${inputId}`;
+ const mapping = window.PromptAssistantInputWidgetMap?.[mappingKey];
+ if (!mapping || !mapping.inputEl) return;
+
+ const inputValue = mapping.inputEl.value;
+ const startTime = performance.now();
+
+ // 清空已使用标签映射
+ this.usedTags.clear();
+
+ // 获取所有标签元素,包括所有标签页中的标签
+ const tagItems = document.querySelectorAll('.tag_item');
+ let checkedCount = 0;
+ let matchedCount = 0;
+
+ // 检查每个标签是否在输入框中
+ tagItems.forEach(tagItem => {
+ const tagValue = tagItem.getAttribute('data-value');
+ if (!tagValue) return;
+
+ checkedCount++;
+
+ // 检查是否在输入框中
+ const isUsed = TagCacheService.isTagInInput(nodeId, inputId, tagValue, inputValue);
+
+ if (isUsed) {
+ matchedCount++;
+ }
+
+ // 更新状态
+ this.updateTagState(tagItem, isUsed);
+
+ // 如果已使用,添加到映射
+ if (isUsed) {
+ this.usedTags.set(tagValue, tagItem);
+ }
+ });
+
+ const endTime = performance.now();
+ logger.debug(`标签状态 | 动作:更新所有 | 节点:${nodeId} | 输入框:${inputId} | 检查数量:${checkedCount} | 匹配数量:${matchedCount} | 耗时:${(endTime - startTime).toFixed(2)}ms`);
+ }
+
+ /**
+ * 添加辅助方法:统计标签总数
+ * @param {Object} tagData 标签数据
+ * @returns {number} 标签总数
+ */
+ static _countTags(tagData) {
+ let count = 0;
+ const countTagsInObject = (obj) => {
+ for (const key in obj) {
+ if (typeof obj[key] === 'string') {
+ count++;
+ } else if (typeof obj[key] === 'object' && obj[key] !== null) {
+ countTagsInObject(obj[key]);
+ }
+ }
+ };
+ countTagsInObject(tagData);
+ return count;
+ }
+}
+
+export { TagManager };
\ No newline at end of file
diff --git a/js/services/baidu.js b/js/services/baidu.js
new file mode 100644
index 0000000..9d58085
--- /dev/null
+++ b/js/services/baidu.js
@@ -0,0 +1,258 @@
+/**
+ * 百度翻译 API 服务
+ * 提供与百度翻译 API 的交互功能
+ */
+
+import { logger } from '../utils/logger.js';
+import { MD5 } from "../utils/md5.js";
+
+class BaiduTranslateService {
+ // API 配置
+ static API_CONFIG = {
+ BASE_URL: 'https://fanyi-api.baidu.com/api/trans/vip/translate',
+ FROM_LANG: 'auto', // 源语言,auto为自动检测
+ TO_LANG: 'zh', // 目标语言,默认翻译为中文
+ };
+
+ // 错误码映射
+ static ERROR_CODES = {
+ '52001': '请求超时,请重试',
+ '52002': '系统错误,请重试',
+ '52003': '未授权用户,请检查appid是否正确或服务是否开通',
+ '54000': '必填参数为空,请检查是否少传参数',
+ '54001': '签名错误,请检查您的签名生成方法',
+ '54003': '访问频率受限,请降低您的调用频率,或进行身份认证后切换为高级版/尊享版',
+ '54004': '账户余额不足,请前往管理控制台充值',
+ '54005': '长query请求频繁,请降低长query的发送频率,3s后再试',
+ '58000': '客户端IP非法,检查个人资料里填写的IP地址是否正确,可前往开发者信息-基本信息修改',
+ '58001': '译文语言方向不支持,检查译文语言是否在语言列表里',
+ '58002': '服务当前已关闭,请前往百度管理控制台开启服务',
+ '58003': '此IP已被封禁',
+ '90107': '认证未通过或未生效,请前往我的认证查看认证进度',
+ '20003': '请求内容存在安全风险',
+ };
+
+ /**
+ * 获取错误信息
+ * @param {string} code 错误码
+ * @returns {string} 错误信息
+ */
+ static getErrorMessage(code) {
+ return this.ERROR_CODES[code] || `未知错误(错误码:${code})`;
+ }
+
+ /**
+ * 获取 APP_ID
+ * @returns {string} APP_ID
+ */
+ static getAppId() {
+ return localStorage.getItem("PromptAssistant_Settings_baidu_translate_appid") || '';
+ }
+
+ /**
+ * 获取 SECRET_KEY
+ * @returns {string} SECRET_KEY
+ */
+ static getSecretKey() {
+ return localStorage.getItem("PromptAssistant_Settings_baidu_translate_secret") || '';
+ }
+
+ /**
+ * 生成签名
+ * @param {string} query 待翻译文本
+ * @param {string} salt 随机数
+ * @returns {string} 签名
+ */
+ static generateSign(query, salt) {
+ try {
+ // 拼接字符串
+ const str = this.getAppId() + query + salt + this.getSecretKey();
+ // 计算 MD5
+ return MD5(str);
+ } catch (error) {
+ logger.error(`签名生成失败 | 错误: ${error.message}`);
+ return '';
+ }
+ }
+
+ /**
+ * 使用 JSONP 方式发送请求
+ * @param {Object} params 请求参数
+ * @returns {Promise} Promise对象
+ */
+ static jsonp(params) {
+ return new Promise((resolve, reject) => {
+ // 创建唯一的回调函数名
+ const callbackName = 'baiduTranslateCallback_' + Date.now();
+
+ // 创建script标签
+ const script = document.createElement('script');
+
+ // 构建URL
+ const queryString = Object.keys(params)
+ .map(key => `${key}=${encodeURIComponent(params[key])}`)
+ .join('&');
+
+ // 设置全局回调函数
+ window[callbackName] = (response) => {
+ // 清理工作
+ delete window[callbackName];
+ document.body.removeChild(script);
+
+ if (response.error_code) {
+ const errorMessage = this.getErrorMessage(response.error_code);
+ reject(new Error(`错误:${errorMessage}`));
+ } else {
+ resolve(response);
+ }
+ };
+
+ // 设置script标签的src
+ script.src = `${this.API_CONFIG.BASE_URL}?${queryString}&callback=${callbackName}`;
+
+ // 处理加载错误
+ script.onerror = () => {
+ delete window[callbackName];
+ document.body.removeChild(script);
+ reject(new Error('错误:网络请求失败'));
+ };
+
+ // 添加script标签到页面
+ document.body.appendChild(script);
+ });
+ }
+
+ /**
+ * 发送翻译请求
+ * @param {string} text 待翻译文本
+ * @param {string} from 源语言,默认自动检测
+ * @param {string} to 目标语言,默认中文
+ * @param {string} request_id 外部传入的唯一请求ID
+ * @returns {Promise