面向 Stable Diffusion / ComfyUI 的标签浏览、随机组合、AI润色、批量生图全功能 Web 工具。桌面/手机双端适配,数据通过 Flask 服务端同步。
| 层 | 技术 | 说明 |
|---|---|---|
| 后端 | Python 3 + Flask | 单文件 server.py,所有路由集中管理 |
| 前端 | 原生 JS(零框架) | 单文件 app.js,全局状态对象 S 统一管理 |
| 样式 | CSS 变量 + 媒体查询 | 桌面暗色主题,@media (max-width: 768px) 手机适配 |
| 存储 | JSON 文件 + localStorage | 服务端存 user_data/,客户端存 localStorage,双向同步 |
| AI 润色 | OpenAI 兼容 API | 支持 LM Studio / Ollama / 自定义 |
| 生图 | ComfyUI REST API | 通过 Flask 代理转发 |
超级无敌魔导书/
├── server.py # Flask 后端,所有 API 路由
├── launch.py # 启动器,自动打开浏览器
├── start.bat # 双击启动
├── data/
│ ├── tags.json # 短语标签库(14大类,2553条)
│ └── tags_single.json # 单一标签库(14大类,2547条)
├── user_data/ # 用户数据(自动创建)
│ ├── custom_tags.json # 自定义标签
│ ├── presets/ # 用户保存的预设组合(JSON文件)
│ ├── history/ # 提示词复制历史
│ ├── comfyui_config.json # ComfyUI 连接地址
│ ├── llm_config.json # LLM API 配置(桌面/手机共享)
│ ├── llm_presets.json # 润色预设列表
│ └── sync_data.json # 统一同步数据(收藏/上限/模式/相册/隐藏等)
├── workflows/ # ComfyUI API 格式工作流 JSON
├── screenshots/ # 界面截图
├── static/
│ ├── index.html # 前端 HTML(所有弹窗、模态框)
│ ├── app.js # 前端逻辑(约 900 行单文件)
│ └── style.css # 样式(约 360 行)
└── PROJECT_SUMMARY.md # 本文档
所有状态集中在一个全局对象 S 中,方便追踪和调试:
var S = {
// 标签库
allData: null, // 当前加载的标签库数据
activeCat: null, // 当前选中的大类名
activeSc: null, // 当前选中的子类名
isSearching: false, // 是否在搜索模式
// 选中的标签
posTags: [], // 正面标签 [{en,zh,weight,category,subcategory,locked}]
negTags: [], // 负面标签
autoSortPos: true, // 正面自动排序
autoSortNeg: true, // 负面自动排序
activeTab: 'positive', // 当前面板标签页
// 开关
useQuality: false, // 基础质量词
useWeights: false, // 权重语法
allowNsfw: false, // NSFW 开关
randWeight: false, // 随机权重
spaceMode: 0, // 空格转换(0关/1空格→_/2_→空格)
// 模板
template: '', // 提示词模板文本
// 收藏和限制
favs: {}, // {en: true} 收藏的标签
catLimits: {}, // {大类名: 上限} 随机数量限制
scLimits: {}, // {"大类|子类": 上限}
catModes: {}, // {"大类名": "cat"/"sc"} 随机模式
hidden: {}, // 隐藏的分类和子类
// ComfyUI
comfyuiQueue: [], // 任务队列 [{idx,body,done}]
comfyuiRunning: false, // 是否正在运行
comfyuiStopped: false, // 是否已终止
wfSettings: {}, // 工作流参数 {文件名: {steps,cfg,...}}
clipMaps: {}, // CLIP绑定 {文件名: {pos:"节点ID", neg:"节点ID"}}
comfyuiLang: 'en', // ComfyUI 语言
// LLM 润色
llmRunning: false, // LLM 是否运行中
llmHistory: [], // 润色记录
// 负面提示词预设
negTemplate: '', // 预设文本
negTemplateAuto: false, // 是否自动发送
// 加载图片
loadImage: null, // {node_id, filename}
// 其他
undoStack: [], // 撤销栈
undoIdx: -1,
tagMode: 'phrase', // 标签模式 'phrase'/'single'
};| 键名 | 内容 |
|---|---|
grimoire2_favs |
收藏列表 {en: true} |
grimoire2_catLimits |
大类随机上限 |
grimoire2_scLimits |
子类随机上限 |
grimoire2_catModes |
大类随机模式 |
grimoire2_allowNsfw |
NSFW 开关布尔值 |
grimoire2_randWeight |
随机权重布尔值 |
grimoire2_spaceMode |
空格模式 |
grimoire2_ui |
UI 状态(宽高/张数/种子/模板/工作流) |
grimoire2_llm |
LLM 配置 |
grimoire2_llmhist |
润色记录 |
grimoire2_llm_presets |
润色预设 |
grimoire2_templates |
提示词模板列表 |
grimoire2_wfsettings |
工作流参数设置 |
grimoire2_clipmaps |
CLIP 正负绑定 |
grimoire2_cuiUrl |
ComfyUI 地址 |
grimoire2_modelsel |
模型选择 |
grimoire2_state |
页面状态(展开的分类/移动Tab) |
grimoire2_tagMode |
标签模式 |
grimoire2_locked |
锁定标签列表 |
grimoire2_gallery |
返图记录 |
grimoire2_negtpl |
负面提示词预设 |
grimoire2_negtpl_list |
负面预设列表 |
grimoire2_loadimg |
加载的图片信息 |
grimoire2_albums |
相册数据 [{id,name,images[{url,filename,prompt}]}] |
grimoire2_active_album |
当前活跃相册 ID |
grimoire2_bp |
绑定路径列表 |
grimoire2_bottom_h |
底部区域高度 |
grimoire2_hidden |
隐藏的分类/子分类 |
grimoire2_ui_glass |
全局透明度 (0-100) |
grimoire2_ui_blur |
模糊程度 (0-40px) |
grimoire2_ui_left |
左侧栏透明度 (0-100) |
grimoire2_ui_center |
中间区域透明度 (0-100) |
grimoire2_ui_right |
右侧面板透明度 (0-100) |
grimoire2_ui_theme |
主题 "dark" / "light" |
grimoire2_ui_bg |
自定义背景图 (base64) |
grimoire2_ui_bg_x |
背景 X 偏移 (-50~50) |
grimoire2_ui_bg_y |
背景 Y 偏移 (-50~50) |
grimoire2_ui_bg_size |
背景缩放模式 |
grimoire2_ui_ripple1 |
水面涟漪特效开关 |
grimoire2_ui_vanta_birds |
3D 群鸟特效开关 |
grimoire2_ui_vanta_waves |
3D 波浪特效开关 |
grimoire2_ui_wave_color |
波浪颜色 Hex |
| 路由 | 方法 | 用途 |
|---|---|---|
/api/tags?mode=phrase|single |
GET | 获取标签库 |
/api/search?q=&mode= |
GET | 搜索标签 |
/api/custom-tags |
GET | 获取自定义标签 |
/api/custom-tags/save |
POST | 覆盖自定义标签 |
/api/custom-tags/add |
POST | 添加单条标签 |
/api/custom-tags/delete |
POST | 删除标签 |
/api/custom-tags/edit |
POST | 编辑标签 |
/api/custom-tags/add-category |
POST | 新建大类 |
/api/custom-tags/delete-category |
POST | 删除大类 |
/api/custom-tags/add-subcategory |
POST | 新建子类 |
/api/custom-tags/delete-subcategory |
POST | 删除子类 |
| 路由 | 方法 | 用途 |
|---|---|---|
/api/presets |
GET | 获取内置+用户预设 |
/api/presets/save |
POST | 保存预设 {name, tags[], weights{}, ...} |
/api/presets/delete/<name> |
DELETE | 删除用户预设 |
/api/presets/export/<name> |
GET | 导出预设 |
/api/presets/import |
POST | 导入预设 |
| 路由 | 方法 | 用途 |
|---|---|---|
/api/comfyui/status |
GET | 在线状态 |
/api/comfyui/test-conn |
POST | 测试连接 |
/api/comfyui/set-url |
POST | 设置地址 |
/api/comfyui/workflows |
GET | 工作流列表 |
/api/comfyui/workflow-size?file= |
GET | 读取宽高 |
/api/comfyui/workflow-info?file= |
GET | 解析模型节点类型 |
/api/comfyui/loadimage-nodes?file= |
GET | 获取 LoadImage 节点列表 |
/api/comfyui/models |
GET | 获取可用模型 |
/api/comfyui/generate |
POST | 提交生成任务 |
/api/comfyui/result/<id> |
GET | 轮询结果 |
/api/comfyui/proxy-image |
GET | 代理图片(手机能访问) |
/api/comfyui/upload-image |
POST | 上传图片到 ComfyUI |
| 路由 | 方法 | 用途 |
|---|---|---|
/api/llm/translate |
POST | 润色翻译 |
/api/llm/config |
GET | 获取 LLM 配置 |
/api/llm/config/save |
POST | 保存 LLM 配置 |
/api/llm/presets |
GET | 获取润色预设 |
/api/llm/presets/save |
POST | 保存润色预设 |
| 路由 | 方法 | 用途 |
|---|---|---|
/api/user/sync |
GET | 获取同步数据 |
/api/user/sync |
POST | 保存同步数据(深度合并) |
/api/bind/scan |
POST | 扫描 output 目录图片 |
/api/bind/img |
GET | 提供绑定目录的图片文件 |
/api/bind/delete |
POST | 文件移入回收站 |
/api/bind/delete-by-filename |
POST | 按文件名搜索删除到回收站 |
/api/bind/meta |
POST | 读取本地 PNG 文件的 tEXt 块,提取 CLIP 提示词(不依赖 ComfyUI) |
/api/comfyui/image-meta |
GET | 从 PNG 元数据读取生成参数(需 ComfyUI) |
/api/bind/meta |
POST | 读取本地 PNG 文件的 tEXt 块,提取 CLIP 提示词(不依赖 ComfyUI) |
/api/update/changelog |
GET | 返回 CHANGELOG.md 最新 3 个版本内容 |
用户点击标签
→ addTag(en, zh, panel)
→ S.posTags.push({en,zh,weight,...})
→ refreshPanel(panel) # 更新右侧面板
→ updatePreview() # 更新提示词预览
→ renderGrid() # 刷新标签网格
→ saveLocked() # 持久化锁定的标签
用户点击 🚀 生图
→ getCuiPrompt() # 读取 preview 文本
→ _queueWithRefine(prompt, wf, w, h, count)
→ 如果开启自动润色:
→ _buildRawPrompt() # 取正面部分
→ _callLlm(posOnly) # 发给大模型
→ 返回后拼回 "--neg 负面标签"
→ _queueCuiPrompt(prompt, wf, w, h, count)
→ body = {prompt, workflow, width, height, overrides,
rand_seed, wf_settings, clip_mapping,
neg_template, load_image}
→ S.comfyuiQueue.push(...)
→ _execNextQueue() → api('/api/comfyui/generate')
服务端 server.py:
→ 加载工作流 JSON
→ 注入正面提示词到 CLIP 节点
→ 注入负面提示词(负面标签 + 负面预设)
→ 注入 loaded_image 到 LoadImage 节点
→ 应用宽高/参数/种子/模型覆盖
→ 发送到 ComfyUI
任何数据修改
→ 对应的 saveXxx() 函数
→ localStorage.setItem(...) # 本地保存
→ _syncSave({key:1}) # 推送到服务器
页面加载
→ loadXxx() # 从 localStorage 读
→ _syncLoad(callback) # 从服务器拉取
→ 深度合并 → 覆盖 localStorage → 更新 UI
服务端 user_data/sync_data.json:
{favs:{...}, catLimits:{...}, ...}
POST 写入时做深度合并,不覆盖已有字段
桌面电脑:
ComfyUI 在 127.0.0.1:8188
Flask 在 0.0.0.0:5802
手机访问: 192.168.2.100:5802
返图 URL: /api/comfyui/proxy-image?filename=...
→ Flask 转发到 ComfyUI → 返回图片流
| 函数 | 用途 |
|---|---|
init() |
初始化入口,加载所有数据和状态 |
renderTree() |
渲染桌面端左侧分类树 |
renderGrid() |
渲染中间标签网格 |
refreshPanel(panel) |
渲染右侧选中面板(正面/负面) |
updatePreview() |
更新提示词预览 |
addTag(en,zh,panel) |
添加标签到面板 |
removeTag(en,panel) |
从面板移除标签 |
_genRandomTags(lockedPos) |
随机生成标签(遵守防冲突规则) |
genPrompt(tags) |
标签数组 → 提示词字符串 |
_queueCuiPrompt(p,wf,w,h,count) |
将任务加入 ComfyUI 队列 |
_execNextQueue() |
执行队列中的下一个任务 |
_pollComfyUI(promptId,start,qi) |
轮询 ComfyUI 生成结果(含步数进度估算) |
_connectCuiWS(clientId,promptId,start,qi,url) |
WebSocket 连接 ComfyUI 获取实时进度 |
_playNotifSound() |
生图完成播放提示音(Web Audio API) |
_showDesktopNotification(title,body) |
浏览器桌面通知 |
_openResolutionPicker() |
打开常用分辨率选择弹窗 |
_resSwitchTab(tab) |
切换分辨率面板(竖屏/方图/横屏) |
_sortAlbumImages(arr,mode) |
相册图片排序(8 种模式) |
_loadAllDims(images,cb) |
异步加载图片真实尺寸(缓存后复用) |
_callLlm(prompt,callback) |
调用 LLM API |
_buildRawPrompt() |
构建完整原始提示词(含 --neg) |
_tagsToPrompt(pk) |
标签数组 → 提示词(含模板) |
_updateMobileSelected() |
更新手机端已选标签栏 |
_buildMobileDrawer() |
构建手机端抽屉分类树 |
_syncSave(keys) |
推送数据到服务器 |
_syncLoad(callback) |
从服务器拉取并合并数据 |
| 函数 | 用途 |
|---|---|
merge_tags() |
合并内置标签 + 用户自定义标签 |
_load_tags(mode) |
按模式加载标签库 |
read_json(path, def) / write_json(path, data) |
JSON 文件读写 |
_load_workflow_raw(name) |
加载工作流文件 |
_getClipMapping() |
前端调用,获取 CLIP 绑定 |
api_comfyui_generate() |
核心生成逻辑 |
api_comfyui_result() |
轮询生成结果 |
api_user_sync_save() |
同步数据保存(深度合并) |
api_bind_meta() |
本地 PNG 元数据读取,解析 workflow JSON 提取 CLIP 文本 |
var _isMobile = window.innerWidth <= 768;- 桌面三栏 → 手机单栏 + 底部三 Tab
- 左侧分类树 → 左侧滑出抽屉(📁 分类按钮打开)
- 右侧面板 → 手机提示词 Tab
- ComfyUI 区 → 手机生图 Tab
- 免责声明 → 工具栏 ⚠ 按钮 → 弹窗
#m-tabbar— 底部导航栏#m-drawer— 左侧抽屉#m-overlay— 抽屉遮罩#m-selected-bar— 标签网格下方已选标签区#disclaimer-modal— 手机端免责弹窗#comfyui-gallery— 画廊容器(生图 Tab 移动到提示词面板底部)#btn-gallery-album— 相册管理按钮<div id="m-tabbar" style="display:none">— 桌面端隐藏
写入 data/tags.json 和 data/tags_single.json 的子类配置:
| 配置字段 | 作用 | 示例 |
|---|---|---|
"type": "single" |
子类级互斥,随机只抽 1 个 | 发型、发色、表情 |
"randomMode": "singlePool" |
大类级,163 个标签中全局只抽 1 个 | 艺术风格 |
"poolGroup": "scene" |
子类跨组共享,几个子类共享 1 个抽取名额 | 自然场景/城市建筑/室内场景 |
_genRandomTags() 函数实现三层防护,保证生成的提示词不自相矛盾。
→ 检查 ComfyUI 地址是否配置正确(手机不能访问 127.0.0.1)
→ 图片 URL 是否走了 /api/comfyui/proxy-image 代理
→ 检查 HTML 中 #comfyui-gallery 容器是否存在
→ 检查手机端切换到「生图」Tab 后 gallery 是否正确移到 prompt-panel
→ 通过 📂 output 扫描的 PNG 图片从本地读取元数据(无需 ComfyUI)
→ 点击预览时自动调用 /api/bind/meta 解析 PNG tEXt 块
→ 若文件无元数据(非生图软件生成),则显示"(无)"
→ 预览后提示词自动缓存到 localStorage
→ 检查对应 saveXxx() 是否调用了 _syncSave()
→ 检查 _syncLoad() 是否处理了对应数据键
→ 检查服务器 sync_data.json 深度合并是否正确
→ 检查 app.js 是否有语法错误(打开浏览器控制台 F12)
→ 常见:括号不匹配、if 缺 }、引号未闭合
→ 用 python -c "compile(open('static/app.js').read(),'app.js','exec')" 快速检查语法
→ 检查工作流是否为 API 格式(在 ComfyUI 中 Export → API) → 检查 CLIP 节点是否正负绑定正确(🔗 正负绑定按钮) → 检查 ComfyUI 是否运行中(🌐 端口按钮测试连接)
- 状态变量:需要在
S对象中声明 - 持久化:添加
loadXxx()/saveXxx()函数 - 服务器同步:在
_syncSave()添加add('key', value) - 服务器恢复:在
_syncLoad()添加对应处理 - 手机端:宽度检测
_isMobile,在_initMobile()中添加逻辑 - 版本号:每次修改 JS/CSS 后更新
index.html中的?v=XX