From 984e4c44cc9b6748de8ea3045e12959b56dba4b1 Mon Sep 17 00:00:00 2001
From: wjluoxiao <331030818@qq.com>
Date: Fri, 16 Oct 2026 03:33:08 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=8A=82=E7=82=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 4 +
README_zh.md | 3 +
js/xb_qwen_prompt_preset.js | 46 ++++++++--
js/xb_shenbi.js | 166 ++++++++++++++++++++++++++++++------
locales/en/nodeDefs.json | 3 +
locales/zh/nodeDefs.json | 3 +
nodes_qwen_prompt_preset.py | 115 +++++++++++++++----------
7 files changed, 259 insertions(+), 81 deletions(-)
diff --git a/README.md b/README.md
index 7f5ce44..ec8473c 100644
--- a/README.md
+++ b/README.md
@@ -109,6 +109,10 @@ Both a **local model** and an **online API** backend are supported; API keys liv
- **🖼️ Image Prompt Preset Pro** — the same, plus built-in LLM prompt enhancement / reverse.
- **🖼️ Qwen2.1 Prompt Preset** — preset + element panel, wired to the official Load CLIP + Generate Text chain.
- **🖌️ ShenBi** — an input-less paint board (create / edit modes) that outputs a strictly on-resolution image.
+ > The preview window reads files from **this server's** `input` dir (boards live in `input/xb_shenbi/`).
+ > A workflow only stores the file name, so after copying it to another machine (local ↔ cloud) the window
+ > falls back to a blank canvas and explains why in the badge / browser console. The window itself needs a
+ > recent ComfyUI frontend (DOM widgets).
#### 9. 🖼️ Model-specific Encoders (3)
diff --git a/README_zh.md b/README_zh.md
index 86e5afb..44b5d1f 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -105,6 +105,9 @@
- **🖼️ 生图提示词预设 Pro**:同上,并内置 LLM 提示词增强 / 反推。
- **🖼️ Qwen2.1 提示词预设**:预设 + 元素面板,接官方 Load CLIP + Generate Text 链路。
- **🖌️ 神笔**:无输入端、只输出 Image 的画板节点(创作 / 编辑双模式),输出严格对齐设定分辨率。
+ > 显示窗依赖**本机** input 目录里的文件(画板存在 `input/xb_shenbi/`):工作流只记文件名,
+ > 把工作流拷到别的机器(本地 ↔ 云机)后,除非那台机器上有同名文件,否则显示窗会回落成空白画布
+ > 并在角标 / 浏览器控制台说明原因。显示窗本身需要较新的 ComfyUI 前端(支持 DOM 部件)。
#### 9. 🖼️ 专用编码器(3 个)
diff --git a/js/xb_qwen_prompt_preset.js b/js/xb_qwen_prompt_preset.js
index 0f597db..abc8db5 100644
--- a/js/xb_qwen_prompt_preset.js
+++ b/js/xb_qwen_prompt_preset.js
@@ -3249,6 +3249,8 @@ function setupNode(node) {
* + 提示词框标题行(右侧「📋 复制」)+ 参数设定显示(只读摘要 5 行)
* + 「📝 提示词」端口接线 → 提示词框锁定(二选一)
* 弹窗:导演台同款二级弹窗(底部:自动保存 + 字号 + 界面缩放 + 取消/💾 保存)
+ * 🤖 LLM设置 顶部 = LLM 增强总开关(单选必选:启用LLM增强 / 关闭LLM增强,默认启用)
+ * → 节点 widget `llm_enable`(BOOLEAN,节点表面隐藏);关闭 = 后端不加载文本编码器、不生成文本
* 配置:manager_settings.qwen = { run.seed_control, extra_settings }(Generate Text 的全量参数走节点 widget,由弹窗读写)
* 说明:本块所有标识符统一 xbr/XBR 前缀,避免与生图预设面板既有函数/常量重名。
* ========================================================================== */
@@ -3258,12 +3260,15 @@ const XBR_SEED_MODES = ["randomize", "fixed", "increment", "decrement"];const XB
const XBR_IO_MODES = ["自动", "文生图", "图生图"];
const XBR_SAMPLING_MODES = ["on", "off"];
const XBR_MTP_MODES = ["auto", "off", "2", "3", "4", "5"];
+/** LLM 增强总开关(🤖 LLM设置 弹窗顶部单选,必选;默认「启用LLM增强」)*/
+const XBR_LLM_MODES = ["启用LLM增强", "关闭LLM增强"];
const XBR_PANEL_BUTTONS = [
- { id: "llm", label: "🤖 LLM设置", title: "🤖 LLM设置", subtitle: "模型选择 = 官方 Load CLIP;参数设置 = 官方 Generate Text 全量参数。执行节点即生成,没有开关。" },
+ { id: "llm", label: "🤖 LLM设置", title: "🤖 LLM设置", subtitle: "顶部 = LLM 增强总开关(默认启用);模型选择 = 官方 Load CLIP;参数设置 = 官方 Generate Text 全量参数。" },
{ id: "preset", label: "✨ 预设参数", title: "✨ 预设参数", subtitle: "输出语言 / 空latent类型 / 预设模式 / 设定词 / SKILL。提示词正文在节点上的提示词框里编辑。" },
];
/** 全部收进弹窗的节点 widget(节点表面只留画幅参数 / 生成数量) */
const XBR_QWEN_WIDGETS = [
+ "llm_enable",
"clip_name", "clip_type", "device",
"max_length", "sampling_mode", "temperature", "top_k", "top_p", "min_p",
"repetition_penalty", "presence_penalty", "seed", "control_after_generate",
@@ -3497,7 +3502,18 @@ function xbrDialog({ label, title, subtitle, width = 820, autoSaveRef = null, on
function xbrRenderLlm(body, node, draft, ctx) {
const s = draft.settings.qwen;
- body.append(makeSectionTitle("模型选择(官方 Load CLIP)"));
+ // ── 顶部:LLM 增强总开关(单选必选,默认「启用LLM增强」)──
+ body.append(makeSectionTitle("LLM 增强"));
+ body.append(field("LLM 增强", radioRow(XBR_LLM_MODES, draft.llm ? XBR_LLM_MODES[0] : XBR_LLM_MODES[1], (v) => {
+ draft.llm = (v === XBR_LLM_MODES[0]);
+ ctx.rerender(); // 关闭 → 下面的区块标成不生效
+ })));
+ body.append(xbrHint(draft.llm
+ ? "启用(默认):执行节点时加载文本编码器,把正文生成成完整提示词(下面的模型与参数全部生效)。"
+ : "关闭:执行节点只输出拼装好的提示词 + 空latent —— 不加载文本编码器、不生成文本(下面的模型与参数都不生效)。"));
+
+ const offMark = draft.llm ? "" : " — 已关闭 LLM 增强,不生效";
+ body.append(makeSectionTitle("模型选择(官方 Load CLIP)" + offMark));
const clips = xbrWidgetOptions(node, "clip_name");
if (clips.length) body.append(field("模型", selectControl(clips, draft.clip_name, (v) => { draft.clip_name = v; })));
else body.append(field("模型", xbrTextControl(draft.clip_name, "models/text_encoders 里没有模型", (v) => { draft.clip_name = v; })));
@@ -3505,7 +3521,7 @@ function xbrRenderLlm(body, node, draft, ctx) {
body.append(field("设备", selectControl(xbrWidgetOptions(node, "device"), draft.device, (v) => { draft.device = v; })));
body.append(xbrHint("与官方 Load CLIP 完全一致:模型来自 ComfyUI/models/text_encoders;Qwen-Image 系文本编码器请选 类型 = qwen_image。"));
- body.append(makeSectionTitle("参数设置(官方 Generate Text 全量参数)"));
+ body.append(makeSectionTitle("参数设置(官方 Generate Text 全量参数)" + offMark));
const t = draft.text;
body.append(field("最大长度 max_length", xbrNumberControl(t.max_length, { min: 1, max: 32768, step: 1 }, (v) => { t.max_length = Math.round(v); })));
body.append(field("采样模式 sampling_mode", selectControl(XBR_SAMPLING_MODES, t.sampling_mode, (v) => { t.sampling_mode = v; ctx.rerender(); })));
@@ -3527,7 +3543,10 @@ function xbrRenderLlm(body, node, draft, ctx) {
body.append(field("使用内置模板 use_default_template", checkboxControl(t.use_default_template, "使用模型自带的系统提示词 / 对话模板", (v) => { t.use_default_template = v; })));
body.append(field("mtp", selectControl(XBR_MTP_MODES, t.mtp, (v) => { t.mtp = v; })));
body.append(xbrHint("mtp:多 token 预测投机解码,没有 MTP 权重时无效;与官方 Generate Text 同义。"));
- body.append(el("div", "font-size:11px;color:#888;line-height:1.6;margin:2px 0 8px;", "提示词正文在节点上的提示词框里编辑;「📝 提示词」端口接线后该框锁定。"));
+ body.append(el("div", "font-size:11px;color:" + (draft.llm ? "#888" : "#d9a441") + ";line-height:1.6;margin:2px 0 8px;",
+ draft.llm
+ ? "提示词正文在节点上的提示词框里编辑;「📝 提示词」端口接线后该框锁定。"
+ : "已关闭 LLM 增强:执行节点不会加载上面的模型、也不会用这些参数,输出 = 本档设定词置顶 + 提示词框正文(「📝 提示词」端口接线时用它)。"));
}
/* ── 弹窗内容:✨ 预设参数(生图预设 + 设定词 + SKILL)───────────── */
@@ -3685,7 +3704,8 @@ function xbrRenderHelp(body) {
" · 第 2 行按钮:🎨 风格 | 📐 视角 | 👤 主体 | 🕺 姿态 | 👗 装扮 | 🎒 道具 | 💡 光影 | 🏞️ 背景",
" · 画幅比例 / 宽度 / 高度 / 生成数量:按空latent类型的官方步长自动对齐",
" · 提示词框右上角 📋 复制;下面是参数设定显示,实时告诉你当前配置",
- " · 没有开关:执行节点就会用文本编码器生成",
+ " · 开关:🤖 LLM设置 顶部「启用LLM增强 / 关闭LLM增强」(默认启用);",
+ " 关闭后执行节点只用拼装好的提示词,不加载文本编码器、不生成文本",
"",
"【元素面板怎么用】",
" · 点选项行 = 加入提示词,再点一下 = 移除",
@@ -3695,6 +3715,10 @@ function xbrRenderHelp(body) {
" · 搜索框与「全部加入」只作用于当前子类",
"",
"【🤖 LLM设置】",
+ " · 顶部总开关(单选必选,默认「启用LLM增强」):",
+ " ◆ 启用LLM增强 = 执行节点时加载文本编码器,把正文生成成完整提示词",
+ " ◆ 关闭LLM增强 = 只输出拼装好的提示词 + 空latent(不加载文本编码器、不生成文本)",
+ " 下面的模型选择与参数全部不生效(面板里会标出「不生效」)",
" · 模型选择 = 官方 Load CLIP:模型 / 类型 / 设备,枚举与官方节点逐字一致",
" · 参数设置 = 官方 Generate Text 全量参数:max_length | sampling_mode | temperature |",
" top_k | top_p | min_p | repetition_penalty | presence_penalty | seed |",
@@ -3780,6 +3804,7 @@ function xbrRenderHelp(body) {
" (多 GPU 机器上核显常被枚举成 cuda:0)→ 启动前设 HIP_VISIBLE_DEVICES=1 让 ComfyUI 用独显再试",
" · 生成慢 / 显存不够:设备改 cpu,或把 max_length 调小",
" · 结果太发散:温度调低、top_p 调小;想复现同一结果就把生成后控制改成固定",
+ " · 只想拼提示词(不跑模型、省显存):🤖 LLM设置 顶部切成「关闭LLM增强」再执行",
" · 节点表面参数很少?故意的:选项都收在弹窗里",
" · 改完参数要重新执行节点才生效",
"",
@@ -3819,6 +3844,7 @@ async function xbrOpenModal(node, panelId) {
skill: String(xbrWidgetVal(node, "skill_name") ?? ""),
skillMode: (() => { const v = String(xbrWidgetVal(node, "skill_mode") ?? ""); return XBR_SKILL_MODES.includes(v) ? v : "自动"; })(),
// 官方 Load CLIP
+ llm: xbrFlag(xbrWidgetVal(node, "llm_enable"), true), // LLM 增强总开关(默认启用)
clip_name: String(xbrWidgetVal(node, "clip_name") ?? ""),
clip_type: String(xbrWidgetVal(node, "clip_type") ?? "qwen_image"),
device: String(xbrWidgetVal(node, "device") ?? "default"),
@@ -3855,6 +3881,7 @@ async function xbrOpenModal(node, panelId) {
node.__ippWritePreset?.(draft.mode, ioCommit, draft.lang, draft.presetText);
} catch (_) {}
// 官方 Load CLIP
+ xbrSetWidget(node, "llm_enable", draft.llm); // LLM 增强总开关
xbrSetWidget(node, "clip_name", draft.clip_name, true);
xbrSetWidget(node, "clip_type", draft.clip_type, true);
xbrSetWidget(node, "device", draft.device, true);
@@ -3931,11 +3958,14 @@ function xbrInfoLines(node, promptText) {
const model = xbrClean(xbrWidgetVal(node, "clip_name"));
const modelShort = String(model).split(/[\\/]/).pop() || model; // 只留文件名,不带目录
const line1 = `📊 空 latent:${xbrClean(xbrWidgetVal(node, "latent_kind"))} | ${xbrWidgetVal(node, "width")}x${xbrWidgetVal(node, "height")} | 数量 ${xbrWidgetVal(node, "batch_size")}`;
- const line2 = `🤖 LLM 模型:${modelShort || "未选择"}`;
- const line3 = `⚙️ Text 参数: 最大长度 ${xbrWidgetVal(node, "max_length")} 丨 思考模式 ${xbrFlag(xbrWidgetVal(node, "thinking"), false) ? "开" : "关"} 丨 温度 ${xbrWidgetVal(node, "temperature")}`;
+ const llmOn = xbrFlag(xbrWidgetVal(node, "llm_enable"), true);
+ const line2 = llmOn ? `🤖 LLM 增强:已启用 | 模型:${modelShort || "未选择"}`
+ : "🤖 LLM 增强:已关闭(只输出拼装好的提示词)";
+ const line3 = `⚙️ Text 参数: 最大长度 ${xbrWidgetVal(node, "max_length")} 丨 思考模式 ${xbrFlag(xbrWidgetVal(node, "thinking"), false) ? "开" : "关"} 丨 温度 ${xbrWidgetVal(node, "temperature")}`
+ + (llmOn ? "" : "(不生效)");
const ctlRaw = xbrClean(xbrWidgetVal(node, XBR_SEED_CTL_WIDGET)) || "randomize";
const line4 = `✨ 生成种子:${xbrWidgetVal(node, "seed")} | ${XBR_SEED_LABEL[ctlRaw] || ctlRaw}`;
- const line5 = `📝 文本数量:${genLen} 字`;
+ const line5 = llmOn ? `📝 文本数量:${genLen} 字` : "📝 文本数量:未生成(已关闭 LLM 增强)";
return [line1, line2, line3, line4, line5].join("\n");
}
diff --git a/js/xb_shenbi.js b/js/xb_shenbi.js
index d9ec9d5..0477d0f 100644
--- a/js/xb_shenbi.js
+++ b/js/xb_shenbi.js
@@ -9,6 +9,14 @@
* 创作模式 —— 没有画板存档时显示「设定分辨率 + 画布底色」的纯色画布(默认纯白);
* 编辑模式 —— 显示画板存档,没画过则显示源图;
* 画过之后两个模式都显示画板存档。
+ * ⚠️ 显示窗要显示「存档 / 源图」时必须满足两条**服务端本机**条件:
+ * ① 该文件存在于这台机器的 ComfyUI/input(画板在 input/xb_shenbi/);
+ * ② /api/view 能取到它。
+ * 工作流只带**文件名**,画板文件不会跟着工作流走 —— 从本地拷到云机、或云端 input 被重置时,
+ * 文件不存在(/view 404),旧版本就是一块「图裂」黑窗(用户报的「云机上看不到画布」)。
+ * 现在前端会把加载失败的 URL 记下来,回落成空白画布 / 源图,并在角标 + 控制台说明原因。
+ * ⚠️ 显示窗是 DOM 部件(LGraphNode.addDOMWidget):ComfyUI 前端太旧(无该 API)时整块窗口都不出现,
+ * 此时会在控制台报错并弹提示(提示升级前端)。
* · 显示窗只做显示,不做点击编辑(避免和操作按钮重复成两个编辑入口);
* 官方 image_upload 自带的 `upload` 按钮按模式显隐,`$$canvas-image-preview` 面板一律隐藏,
* 保证「显示窗」全节点只有一个。
@@ -185,6 +193,11 @@ function isPainting(value) {
return PAINT_EXT_RE.test(text) && !/[\n\r]/.test(text);
}
+/** 存档值的短标签:老工作流里的内嵌 base64 太长,提示里只写"内嵌画像存档" */
+function archiveLabel(value) {
+ return String(value || "").startsWith("data:") ? "内嵌画像存档" : String(value || "");
+}
+
/** 把画布 PNG 传到 input/xb_shenbi/ 下,返回节点里要存的文件名("子目录/文件名") */
async function uploadPainting(blob, filename) {
const body = new FormData();
@@ -263,27 +276,36 @@ async function openBrushEditor(cfg) {
let width = 0;
let height = 0;
let bitmap = null;
- try {
- if (cfg.paintedUrl) {
- bitmap = await loadBitmap(cfg.paintedUrl);
- width = bitmap.naturalWidth;
- height = bitmap.naturalHeight;
- } else if (cfg.imageUrl) {
- bitmap = await loadBitmap(cfg.imageUrl);
- const sw = bitmap.naturalWidth;
- const sh = bitmap.naturalHeight;
- // 编辑模式:大于设定分辨率才缩小裁切,小于则保持原尺寸
- if (cfg.targetW && cfg.targetH && (sw > cfg.targetW || sh > cfg.targetH)) {
- width = cfg.targetW;
- height = cfg.targetH;
+ // 底图候选:画板存档 → 源图。⚠️ 两者都是**服务端本机**的文件,工作流从别的机器
+ // (如本地 → 云机)拷过来时常常读不到:读不到就退到下一个候选,都读不到按设定分辨率的
+ // 空白画布打开 —— 绝不因为"存档不在本机"就把用户堵在"载入失败"上(后端也是这个兜底口径)。
+ const candidates = [];
+ if (cfg.paintedUrl) candidates.push({ url: cfg.paintedUrl, painted: true });
+ if (cfg.imageUrl) candidates.push({ url: cfg.imageUrl, painted: false });
+ for (const cand of candidates) {
+ try {
+ bitmap = await loadBitmap(cand.url);
+ if (cand.painted) {
+ width = bitmap.naturalWidth;
+ height = bitmap.naturalHeight;
} else {
- width = sw;
- height = sh;
+ const sw = bitmap.naturalWidth;
+ const sh = bitmap.naturalHeight;
+ // 编辑模式:大于设定分辨率才缩小裁切,小于则保持原尺寸
+ if (cfg.targetW && cfg.targetH && (sw > cfg.targetW || sh > cfg.targetH)) {
+ width = cfg.targetW;
+ height = cfg.targetH;
+ } else {
+ width = sw;
+ height = sh;
+ }
}
+ break;
+ } catch (err) {
+ bitmap = null;
+ notify((cand.painted ? "画板存档" : "源图") + "在本机读不到,已跳过(文件不在服务端 input 目录):"
+ + ((err && err.message) || err), "warn");
}
- } catch (err) {
- notify("图片载入失败:" + ((err && err.message) || err), "error");
- return null;
}
if (!width || !height) { width = cfg.width || 1024; height = cfg.height || 1024; }
@@ -756,7 +778,11 @@ function setupNode(node) {
let panelImg = null;
let panelHint = null;
let panelCaption = null;
+ let panelNote = null;
let panelBox = null;
+ /** 本机加载失败的图 URL(存档 / 源图文件不在服务端本机时最常见)——
+ 记住它,显示窗就不再拿它去撞墙,改为回落显示 + 说明 */
+ const badSrcs = new Set();
/* ── 按钮:打开画板(放在显示窗上面) ─────────────────────── */
const openBoard = async () => {
@@ -881,12 +907,42 @@ function setupNode(node) {
panelCaption = document.createElement("span");
panelCaption.style.cssText = "position:absolute;left:10px;top:10px;padding:4px 11px;border-radius:999px;" +
"background:rgba(20,20,25,.78);color:#cfd5df;font-size:12px;line-height:1;pointer-events:none;display:none;";
- box.append(panelImg, panelHint, panelCaption);
+ // 存档读不到时的说明角标(压在左下角:纯白/纯黑画布上都看得清)
+ panelNote = document.createElement("span");
+ panelNote.style.cssText = "position:absolute;left:10px;bottom:10px;max-width:calc(100% - 20px);padding:7px 12px;" +
+ "border-radius:8px;background:rgba(58,31,31,.94);border:1px solid #7d3b3b;color:#ffd9d9;" +
+ "font-size:12px;line-height:1.6;pointer-events:none;display:none;white-space:pre-wrap;text-align:left;" +
+ "box-sizing:border-box;overflow:hidden;";
+ box.append(panelImg, panelHint, panelCaption, panelNote);
panelBox = box;
panel = node.addDOMWidget("xb_sb_preview", "preview", box, { serialize: false, hideOnZoom: false });
} catch (err) {
panel = null;
- console.warn("[XB-BOX 神笔] 显示窗不可用:", err);
+ // 显示窗是 DOM 部件(addDOMWidget)。老版 ComfyUI 前端没有这个 API → 整个窗口都不会出现,
+ // 节点上只剩几个按钮。这是「云机上完全看不到画布窗口」的第一嫌疑人,必须说出来。
+ // 必需要求:ComfyUI 前端 ≥ 1.3(或任何带 addDOMWidget 的版本)。
+ console.error("[XB-BOX 神笔] 显示窗不可用:当前 ComfyUI 前端不支持 DOM 部件(LGraphNode.addDOMWidget)。"
+ + "请把 ComfyUI / ComfyUI_frontend 升级到较新版本后刷新页面。", err);
+ notify("显示窗不可用:当前 ComfyUI 前端太旧(缺 addDOMWidget),升级前端后刷新页面即可", "error");
+ }
+ if (panelImg) {
+ // 存档 / 源图都是**服务端本机**的文件:工作流从别的机器(如本地 → 云机)拷过来、
+ // 或云端 input 目录被重置时,这个文件不存在 →
只会烂成一块"图裂"空窗
+ // (用户看到的就是"画布显示不出来")。这里把它记进 badSrcs,refreshPanel 回落到
+ // 空白画布 / 源图,并给出明确说明,而不是留一块黑框让人猜。
+ panelImg.addEventListener("error", () => {
+ const src = panelImg.getAttribute("src");
+ if (!src || badSrcs.has(src)) return;
+ // img 的 error 也可能是"上一次请求被中断"的迟到事件 → 用独立探针复核,避免误判好图
+ const probe = new Image();
+ probe.onerror = () => {
+ if (badSrcs.size > 32) badSrcs.clear();
+ badSrcs.add(src);
+ reportBadSrc(src);
+ refreshPanel();
+ };
+ probe.src = src;
+ });
}
if (panel) {
// 只给 computeLayoutSize(弹性)——不能给 computeSize,否则会被当成固定高度部件
@@ -896,6 +952,34 @@ function setupNode(node) {
try { panel.serialize = false; } catch (_) { /* ignore */ }
}
+ /**
+ * 显示窗图片加载失败时的诊断输出。
+ * 「云机上看不到画布」最常见的一条就是:工作流里存的是画板**文件名**,而那个文件只存在于
+ * 另一台机器的 input 目录里 → /view 404 →
什么都不显示(旧版本连提示都没有,只剩一块黑框)。
+ */
+ const reportBadSrc = (src) => {
+ const create = isCreateMode();
+ const arch = create ? (paintValue(true) || transientPaint()) : paintValue(false);
+ const label = (arch && String(paintingSrc(arch)) === String(src))
+ ? `画板存档「${archiveLabel(arch)}」`
+ : `源图「${String(getVal(node, "image") || "")}」`;
+ let file = "?";
+ try {
+ const u = new URL(src, location.href);
+ const sub = u.searchParams.get("subfolder") || "";
+ file = `input/${sub ? sub + "/" : ""}${u.searchParams.get("filename") || ""}`;
+ } catch (_) { /* ignore */ }
+ console.warn("[XB-BOX 神笔] ❌ 显示窗图片加载失败 → 已回落显示\n"
+ + ` · 类型:${label}\n`
+ + ` · URL:${src}\n`
+ + ` · 服务端文件:${file}\n`
+ + " · 排查:① 把上面的 URL 贴到浏览器地址栏:404 = 文件不在那台机器上,500 = 后端读不了\n"
+ + " ② 到该机器 ComfyUI 的 input 目录看这个文件在不在\n"
+ + " ③ 工作流是从别的机器(本地 ↔ 云机)拷来的?画板只记文件名,文件不会跟着走\n"
+ + " · 处理:在本机重新点「🎨 创作画布」/「🖌️ 编辑图片」作画并保存,即可重建存档");
+ notify(`显示窗读不到${label}:该文件不在本机 input 目录,已回落显示(详情见浏览器控制台 [XB-BOX 神笔])`, "warn");
+ };
+
function refreshPanel() {
if (!panelImg) return;
const create = isCreateMode();
@@ -904,18 +988,39 @@ function setupNode(node) {
const [targetW, targetH] = resolveTarget(node);
const color = String(getVal(node, "canvas_color") || BG_WHITE);
+ // 画板存档:创作 = created_data(后端未重启时退到本次会话的临时存档);编辑 = painted_data
+ const archive = create ? (painted || transientPaint()) : painted;
+ const archiveUrl = archive ? paintingSrc(archive) : "";
+ const sourceUrl = source ? viewUrl(source) : "";
+
// 创作模式没画过 → 纯色空白画布:直接用 CSS 背景铺色,**完全不经过 canvas**。
// ⚠️ 原来这里是 blankCanvasUrl():建画布 → 填色 → toDataURL →
。
// 但画布渲染失效时不会报错(画出来是透明的),于是"创作模式加载不出空白画布";
// 而编辑模式走的是
,不碰 canvas,所以一直正常。
+ // ⚠️ 存档/源图在**服务端本机**读不到时(换机器、云端 input 被重置),后端是"忽略存档 →
+ // 输出空白画布",前端同样回落,绝不把一块"图裂"空窗丢给用户。
let url = "";
let solid = null;
- if (painted) url = paintingSrc(painted);
- else if (create) {
- const temp = transientPaint();
- if (temp) url = paintingSrc(temp);
- else solid = color === BG_BLACK ? BLACK : WHITE;
- } else if (source) url = viewUrl(source);
+ let note = "";
+ if (archiveUrl) {
+ if (badSrcs.has(archiveUrl)) {
+ note = `⚠️ 画板存档读不到:${archiveLabel(archive)}\n(服务端 input/${PAINT_SUBFOLDER} 里没有这个文件)`;
+ if (create) {
+ solid = color === BG_BLACK ? BLACK : WHITE;
+ note += `\n已按 ${targetW} × ${targetH} 显示空白画布;点「🎨 创作画布」重新作画即可`;
+ } else if (sourceUrl && !badSrcs.has(sourceUrl)) {
+ url = sourceUrl;
+ note += "\n已回落到源图显示";
+ }
+ } else {
+ url = archiveUrl;
+ }
+ } else if (create) {
+ solid = color === BG_BLACK ? BLACK : WHITE;
+ } else if (sourceUrl) {
+ if (badSrcs.has(sourceUrl)) note = `⚠️ 源图读不到:${source}\n(服务端 input 目录里没有这个文件)`;
+ else url = sourceUrl;
+ }
if (panelBox) {
panelBox.style.background = solid || "#141419";
@@ -934,7 +1039,14 @@ function setupNode(node) {
panelImg.removeAttribute("src");
panelImg.style.display = "none";
panelHint.style.display = "block";
- panelHint.textContent = "尚未作画\n请先在「图片」里选择或上传一张本地图片";
+ panelHint.textContent = note || "尚未作画\n请先在「图片」里选择或上传一张本地图片";
+ }
+
+ // 说明角标:有图 / 有纯色画布时用角标;都没有时上面那行提示文字已经说了,不用重复
+ if (panelNote) {
+ const badge = (url || solid) ? note : "";
+ panelNote.style.display = badge ? "block" : "none";
+ panelNote.textContent = badge;
}
// 标注两个模式都显示:切换分辨率时这里会立刻反映目标尺寸
diff --git a/locales/en/nodeDefs.json b/locales/en/nodeDefs.json
index 1fe73aa..9c735d5 100644
--- a/locales/en/nodeDefs.json
+++ b/locales/en/nodeDefs.json
@@ -408,6 +408,9 @@
"batch_size": {
"name": "Batch Size"
},
+ "llm_enable": {
+ "name": "LLM Enhance"
+ },
"clip_name": {
"name": "Model (Load CLIP)"
},
diff --git a/locales/zh/nodeDefs.json b/locales/zh/nodeDefs.json
index facf757..82ce6fb 100644
--- a/locales/zh/nodeDefs.json
+++ b/locales/zh/nodeDefs.json
@@ -5315,6 +5315,9 @@
"batch_size": {
"name": "生成数量"
},
+ "llm_enable": {
+ "name": "LLM 增强"
+ },
"clip_name": {
"name": "模型·Load CLIP"
},
diff --git a/nodes_qwen_prompt_preset.py b/nodes_qwen_prompt_preset.py
index e60d449..a53fe2f 100644
--- a/nodes_qwen_prompt_preset.py
+++ b/nodes_qwen_prompt_preset.py
@@ -10,7 +10,9 @@ XB-BOX - 🖼️ Qwen2.1提示词预设
· 画幅比例 / 宽度 / 高度 / 生成数量
· 元素面板提示词拼装(8 大类词表,前端 js/xb_qwen_prompt_preset.js)
-② 文本生成(官方两个节点的参数,逐字对齐;没有开关,执行即生成)
+② 文本生成(官方两个节点的参数,逐字对齐)
+ · 总开关 llm_enable(弹窗「🤖 LLM设置」顶部的单选「启用LLM增强 / 关闭LLM增强」,默认启用)
+ ⛔ 关闭 = 只输出拼装好的提示词 + 空latent:不加载文本编码器、不生成文本(模型与参数都不生效)
· 模型选择 = 官方 Load CLIP:clip_name(models/text_encoders)+ type(默认 qwen_image)+ device(默认 default)
· 参数设置 = 官方 Generate Text 全量参数:
max_length / sampling_mode(on·off)/ temperature / top_k / top_p / min_p /
@@ -711,7 +713,7 @@ _LANG_HINT = {
}
_SETTING_KEYS = ("latent_kind", "output_lang", "preset_mode", "io_mode", "three_view_text", "aspect_ratio",
- "width", "height", "batch_size", "clip_name", "clip_type", "device",
+ "width", "height", "batch_size", "llm_enable", "clip_name", "clip_type", "device",
"max_length", "sampling_mode", "temperature", "top_k", "top_p", "min_p",
"repetition_penalty", "presence_penalty", "seed", "thinking",
"use_default_template", "mtp", "skill_name", "skill_mode", "internal_prompt")
@@ -913,7 +915,15 @@ class XB_QwenPromptPreset:
"tooltip": "一次生成的图片数量(空 latent 的 batch 维度)",
}),
- # ══ ② 官方 Load CLIP(模型选择)══
+ # ══ ② LLM 增强总开关(弹窗「🤖 LLM设置」顶部的单选,默认启用)══
+ "llm_enable": ("BOOLEAN", {
+ "default": True,
+ "label_on": "✅ 启用 LLM 增强", "label_off": "⛔ 关闭 LLM 增强",
+ "tooltip": "启用(默认)= 用文本编码器把正文生成成完整提示词\n"
+ "关闭 = 只输出拼装好的提示词 + 空latent(不加载文本编码器、不生成文本)",
+ }),
+
+ # ══ ③ 官方 Load CLIP(模型选择)══
"clip_name": (text_encoder_list(), {
"tooltip": "文本编码器(models/text_encoders)。与官方 Load CLIP 同一个列表。",
}),
@@ -926,7 +936,7 @@ class XB_QwenPromptPreset:
"tooltip": "设备:与官方 Load CLIP 一致;default=自动,cpu=放内存",
}),
- # ══ ③ 官方 Generate Text(参数设置,全量)══
+ # ══ ④ 官方 Generate Text(参数设置,全量)══
"max_length": ("INT", {
"default": 512, "min": 1, "max": 32768,
"tooltip": "Generate Text · max_length:最大生成长度",
@@ -978,7 +988,7 @@ class XB_QwenPromptPreset:
"tooltip": "Generate Text · mtp:多 token 预测投机解码;没有 MTP 权重时无效",
}),
- # ══ ④ 内部字段(节点表面由前端面板隐藏)══
+ # ══ ⑤ 内部字段(节点表面由前端面板隐藏)══
"internal_prompt": ("STRING", {
"default": "", "multiline": True,
"tooltip": "节点内拼装/编辑的提示词正文(随工作流保存)",
@@ -1009,7 +1019,8 @@ class XB_QwenPromptPreset:
FUNCTION = "generate"
CATEGORY = "XB_ToolBox/Image_Params"
DESCRIPTION = ("Qwen2.1提示词预设:生图预设(空latent类型/输出语言/预设模式/画幅参数 + 8 大类元素面板拼装)"
- "+ 官方 Load CLIP 与官方 Generate Text(纯本地文本生成,无开关,执行即生成)。"
+ "+ 官方 Load CLIP 与官方 Generate Text(纯本地文本生成)。"
+ "「启用 / 关闭 LLM 增强」在弹窗「🤖 LLM设置」顶部(默认启用;关闭 = 只输出拼装好的提示词)。"
"输出:提示词 / 空latent。")
@classmethod
@@ -1024,6 +1035,7 @@ class XB_QwenPromptPreset:
clip_name, clip_type, device,
max_length, sampling_mode, temperature, top_k, top_p, min_p,
repetition_penalty, presence_penalty, seed, thinking, use_default_template, mtp,
+ llm_enable=True, # LLM 增强总开关(默认启用;关闭 = 不加载文本编码器、不生成文本)
internal_prompt="", manager_settings="", text="", images=None, unique_id=None,
skill_name=SKILL_NONE, skill_mode=SKILL_MODE_AUTO, io_mode=IO_MODE_AUTO):
# ① 画幅归一 + 空 latent(与「🖼️ 生图提示词预设」逐行一致)
@@ -1054,7 +1066,6 @@ class XB_QwenPromptPreset:
with_lang_hint=not bool(skill_text))
# ③ 官方 Load CLIP + 官方 Generate Text(参数与调用方式逐字对齐官方节点)
- clip = get_clip(clip_name, clip_type, device)
# 官方 image 输入:一批图 = N 张参考图(官方图像编辑最多 10 张,提示词可引用「第 N 张图」)
# ⚠️ 不能把多帧当 video:Qwen-Image-2.1 的文本编码器链里**没人读 video**
# (qwen_image21/qwen3vl/sd1_clip 全程不碰 video,全库只有 gemma4 那个 tokenizer 认),
@@ -1069,55 +1080,67 @@ class XB_QwenPromptPreset:
if mode_needs_image(preset_mode) and img_count == 0:
safe_print(f"[XB-Qwen提示词预设] ⚠️ 当前档位「{preset_mode}」属于图生图,但没有接参考图"
f"(🖼️ 图像,多张按批接即可)→ 本次按「文生图」设定词纯文字生成")
- try: # 日志留痕:看得见“到底送了什么给文本编码器”
- print(f"[XB-Qwen提示词预设] 送模型 prompt({len(model_prompt)} 字):"
- f"{model_prompt[:120].replace(chr(10), ' / ')}" + ("…" if len(model_prompt) > 120 else ""))
- except Exception:
- pass
- # SKILL 已在上方读入 skill_text,并在那里决定了要不要叠加语言提示
- tokens = clip.tokenize(
- model_prompt,
- image=image_arg,
- skip_template=not bool(use_default_template),
- min_length=1,
- thinking=bool(thinking),
- system_prompt=skill_text,
- )
- _mtp = False if mtp == "off" else (True if mtp == "auto" else int(mtp))
- generated_ids = clip.generate(
- tokens,
- do_sample=(sampling_mode == "on"),
- max_length=int(max_length),
- temperature=float(temperature),
- top_k=int(top_k),
- top_p=float(top_p),
- min_p=float(min_p),
- repetition_penalty=float(repetition_penalty),
- presence_penalty=float(presence_penalty),
- seed=int(seed),
- mtp=_mtp,
- )
- raw = clip.decode(generated_ids) or ""
- # 思考段分离(与官方 TextGenerate 完全一致:不闭合时整段都算正文)
- reasoning, _, gen_text = raw.partition("")
- if not reasoning.lstrip().startswith("") and not model_prompt.rstrip().endswith(""):
- gen_text = raw
- gen_text = gen_text.strip()
+ # ⛔「关闭 LLM 增强」(默认启用):整段跳过 —— 不加载文本编码器、不生成文本
+ gen_text = ""
+ if llm_enable:
+ clip = get_clip(clip_name, clip_type, device)
+ try: # 日志留痕:看得见“到底送了什么给文本编码器”
+ print(f"[XB-Qwen提示词预设] 送模型 prompt({len(model_prompt)} 字):"
+ f"{model_prompt[:120].replace(chr(10), ' / ')}" + ("…" if len(model_prompt) > 120 else ""))
+ except Exception:
+ pass
+ # SKILL 已在上方读入 skill_text,并在那里决定了要不要叠加语言提示
+ tokens = clip.tokenize(
+ model_prompt,
+ image=image_arg,
+ skip_template=not bool(use_default_template),
+ min_length=1,
+ thinking=bool(thinking),
+ system_prompt=skill_text,
+ )
+ _mtp = False if mtp == "off" else (True if mtp == "auto" else int(mtp))
+ generated_ids = clip.generate(
+ tokens,
+ do_sample=(sampling_mode == "on"),
+ max_length=int(max_length),
+ temperature=float(temperature),
+ top_k=int(top_k),
+ top_p=float(top_p),
+ min_p=float(min_p),
+ repetition_penalty=float(repetition_penalty),
+ presence_penalty=float(presence_penalty),
+ seed=int(seed),
+ mtp=_mtp,
+ )
+ raw = clip.decode(generated_ids) or ""
+
+ # 思考段分离(与官方 TextGenerate 完全一致:不闭合时整段都算正文)
+ reasoning, _, gen_text = raw.partition("")
+ if not reasoning.lstrip().startswith("") and not model_prompt.rstrip().endswith(""):
+ gen_text = raw
+ gen_text = gen_text.strip()
+ else:
+ safe_print("[XB-Qwen提示词预设] ⛔ 已关闭 LLM 增强 → 只输出拼装好的提示词"
+ "(不加载文本编码器、不生成文本)")
# ④ 设定词原封不动置顶(与未生成时的成句规则一致;t2i/i2i 模版条款同样进最终提示词)
- final_prompt = build_prompt(lang, preset_mode, three_view_text, gen_text,
+ # 关闭 LLM 增强时正文用「外接提示词优先」的那一份(与外接端口时的成句规则一致)
+ final_prompt = build_prompt(lang, preset_mode, three_view_text,
+ gen_text if llm_enable else body,
has_image=has_img, io_mode=io_mode)
try:
shape = tuple(latent.get("samples").shape)
except Exception:
shape = ()
+ llm_state = (f"丨CLIP {clip_name}({clip_type}·{device})"
+ f"丨Text 最大长度 {max_length}·采样 {sampling_mode}·思考 {'开' if thinking else '关'}"
+ f"丨SKILL {skill_used}({len(skill_text)} 字·{skill_mode_of(skill_mode)})"
+ ) if llm_enable else "丨LLM 增强 关闭(不加载 CLIP、不生成文本)"
print(f"[XB-Qwen提示词预设] {spec['label']} 丨 {w}x{h}({aspect_ratio}·步长 {spec['step']})"
f"丨数量 {shape[0] if shape else batch_size}丨空latent {shape}"
- f"丨CLIP {clip_name}({clip_type}·{device})"
- f"丨Text 最大长度 {max_length}·采样 {sampling_mode}·思考 {'开' if thinking else '关'}"
- f"丨SKILL {skill_used}({len(skill_text)} 字·{skill_mode_of(skill_mode)})"
+ f"{llm_state}"
f"丨模式 {preset_mode}丨模版 {io_eff}"
f"{'(自动' + (':检测到参考图' if has_img else ':未接参考图') + ')' if io_mode_of(io_mode) == IO_MODE_AUTO else ''}"
f"丨参考图 {img_count} 张"
@@ -1125,7 +1148,7 @@ class XB_QwenPromptPreset:
ui = {
"text": [final_prompt],
- "model": [str(clip_name or "-")],
+ "model": [str(clip_name or "-") if llm_enable else "- (已关闭 LLM 增强)"],
"gen_len": [len(gen_text)],
}
# 种子回写不在后端做:节点上的 seed widget 由 ComfyUI 原生的