From 151df9712ee27cb963785f89213c0ec92b1a4c77 Mon Sep 17 00:00:00 2001 From: kjqwer <2990346238@qq.com> Date: Thu, 23 Jul 2026 13:36:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D-1=20=E8=A2=AB=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - MANIFEST.in | 2 +- tests/test_read_only_prompt.mjs | 68 +++++++++++++++++++++++++++++++++ web/read_only_prompt.mjs | 61 +++++++++++++++++++++++++++++ web/smart_save.js | 4 +- 5 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 tests/test_read_only_prompt.mjs create mode 100644 web/read_only_prompt.mjs diff --git a/.gitignore b/.gitignore index f047fb2..d65d999 100644 --- a/.gitignore +++ b/.gitignore @@ -126,4 +126,3 @@ output/ models/ input/ test/ -tests/ diff --git a/MANIFEST.in b/MANIFEST.in index 98014c3..c067860 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,7 +2,7 @@ include LICENSE include README.md recursive-include src *.py -recursive-include web *.js *.css +recursive-include web *.js *.mjs *.css recursive-include locales *.json recursive-exclude * __pycache__ diff --git a/tests/test_read_only_prompt.mjs b/tests/test_read_only_prompt.mjs new file mode 100644 index 0000000..648b09b --- /dev/null +++ b/tests/test_read_only_prompt.mjs @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { buildReadOnlyPrompt } from "../web/read_only_prompt.mjs"; + +test("preview snapshot does not serialize or mutate random seed widgets", () => { + let serializeCalls = 0; + const seedWidget = { + name: "noise_seed", + value: -1, + serializeValue() { + serializeCalls += 1; + this.value = 123456; + return this.value; + }, + }; + const sampler = { + id: 1, + comfyClass: "KSampler Adv. (Efficient)", + inputs: [], + widgets: [seedWidget, { name: "steps", value: 20 }], + }; + const saver = { + id: 2, + comfyClass: "SmartSaveImage", + inputs: [{ name: "images", link: 10 }], + widgets: [], + }; + const unrelated = { + id: 3, + comfyClass: "KSampler", + inputs: [], + widgets: [{ name: "seed", value: 999 }], + }; + const graph = { + _nodes: [sampler, saver, unrelated], + links: { 10: { origin_id: 1, target_id: 2 } }, + getNodeById(id) { + return this._nodes.find((node) => node.id === id); + }, + }; + + const snapshot = buildReadOnlyPrompt(graph, saver); + + assert.equal(snapshot["1"].inputs.noise_seed, -1); + assert.equal(seedWidget.value, -1); + assert.equal(serializeCalls, 0); + assert.equal(snapshot["3"], undefined); +}); + +test("non-serializable controls are ignored without calling hooks", () => { + const node = { + id: 4, + type: "ExampleNode", + inputs: [], + widgets: [ + { name: "text", value: "hello" }, + { name: "button", value: "click", options: { serialize: false } }, + { name: "object", value: { nested: true } }, + ], + }; + const snapshot = buildReadOnlyPrompt({ _nodes: [node] }); + + assert.deepEqual(snapshot["4"], { + class_type: "ExampleNode", + inputs: { text: "hello" }, + }); +}); diff --git a/web/read_only_prompt.mjs b/web/read_only_prompt.mjs new file mode 100644 index 0000000..efffac1 --- /dev/null +++ b/web/read_only_prompt.mjs @@ -0,0 +1,61 @@ +function getLink(graph, linkId) { + const links = graph?.links; + if (!links || linkId == null) return null; + return typeof links.get === "function" ? links.get(linkId) : links[linkId]; +} + +function collectUpstreamNodes(graph, targetNode) { + if (!targetNode) return new Set(graph?._nodes || []); + + const found = new Set([targetNode]); + const pending = [targetNode]; + while (pending.length) { + const current = pending.pop(); + for (const input of current?.inputs || []) { + const link = getLink(graph, input?.link); + if (!link) continue; + const origin = graph?.getNodeById?.(link.origin_id); + if (!origin || found.has(origin)) continue; + found.add(origin); + pending.push(origin); + } + } + return found; +} + +function safeWidgetValue(value) { + if (value == null || ["string", "number", "boolean"].includes(typeof value)) { + return value; + } + if (typeof value === "bigint") return String(value); + if (Array.isArray(value)) { + const result = value.map(safeWidgetValue); + return result.some((item) => item === undefined) ? undefined : result; + } + return undefined; +} + +/** + * Build only the metadata subset needed by SmartSave's path preview. + * Reading widget.value directly is intentional: serializeValue and queue hooks + * may randomize seeds, upload files, or mutate third-party node state. + */ +export function buildReadOnlyPrompt(graph, targetNode = null) { + const included = collectUpstreamNodes(graph, targetNode); + const prompt = {}; + + for (const node of graph?._nodes || []) { + if (!included.has(node)) continue; + const inputs = {}; + for (const widget of node?.widgets || []) { + if (!widget?.name || widget?.options?.serialize === false) continue; + const value = safeWidgetValue(widget.value); + if (value !== undefined) inputs[widget.name] = value; + } + prompt[String(node.id)] = { + class_type: node.comfyClass || node.type || "", + inputs, + }; + } + return prompt; +} diff --git a/web/smart_save.js b/web/smart_save.js index af4ddd7..5dae6c1 100644 --- a/web/smart_save.js +++ b/web/smart_save.js @@ -1,5 +1,6 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; +import { buildReadOnlyPrompt } from "./read_only_prompt.mjs"; const NODE_NAME = "SmartSaveImage"; const CSS_HREF = "extensions/SmartSaveImage/smart_save.css"; @@ -289,9 +290,8 @@ function buildPanel(node) { statusLine.textContent = "正在计算…"; statusLine.className = "ssi-status"; try { - const graph = await app.graphToPrompt(); const payload = { - prompt: graph?.output || {}, + prompt: buildReadOnlyPrompt(app.graph, node), root_mode: widgetValue(node, "root_mode", "output"), custom_root: widgetValue(node, "custom_root", ""), folder_template: widgetValue(node, "folder_template", ""),