Compare commits
2 Commits
f17aa7db8a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c8c31e4d0 | |||
| 151df9712e |
@@ -126,4 +126,3 @@ output/
|
|||||||
models/
|
models/
|
||||||
input/
|
input/
|
||||||
test/
|
test/
|
||||||
tests/
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ include LICENSE
|
|||||||
include README.md
|
include README.md
|
||||||
|
|
||||||
recursive-include src *.py
|
recursive-include src *.py
|
||||||
recursive-include web *.js *.css
|
recursive-include web *.js *.mjs *.css
|
||||||
recursive-include locales *.json
|
recursive-include locales *.json
|
||||||
|
|
||||||
recursive-exclude * __pycache__
|
recursive-exclude * __pycache__
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
|
|
||||||
模板变量默认折叠,展开后点击变量即可插入当前规则输入框。
|
模板变量默认折叠,展开后点击变量即可插入当前规则输入框。
|
||||||
|
|
||||||
|
## 节点预览
|
||||||
|

|
||||||
|
|
||||||
## 模板变量
|
## 模板变量
|
||||||
|
|
||||||
- 时间:`%date:yyyy-MM-dd%`、`%year%`、`%month%`、`%day%`、`%hour%`、`%minute%`、`%second%`
|
- 时间:`%date:yyyy-MM-dd%`、`%year%`、`%month%`、`%day%`、`%hour%`、`%minute%`、`%second%`
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
+2
-2
@@ -1,5 +1,6 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
|
import { buildReadOnlyPrompt } from "./read_only_prompt.mjs";
|
||||||
|
|
||||||
const NODE_NAME = "SmartSaveImage";
|
const NODE_NAME = "SmartSaveImage";
|
||||||
const CSS_HREF = "extensions/SmartSaveImage/smart_save.css";
|
const CSS_HREF = "extensions/SmartSaveImage/smart_save.css";
|
||||||
@@ -289,9 +290,8 @@ function buildPanel(node) {
|
|||||||
statusLine.textContent = "正在计算…";
|
statusLine.textContent = "正在计算…";
|
||||||
statusLine.className = "ssi-status";
|
statusLine.className = "ssi-status";
|
||||||
try {
|
try {
|
||||||
const graph = await app.graphToPrompt();
|
|
||||||
const payload = {
|
const payload = {
|
||||||
prompt: graph?.output || {},
|
prompt: buildReadOnlyPrompt(app.graph, node),
|
||||||
root_mode: widgetValue(node, "root_mode", "output"),
|
root_mode: widgetValue(node, "root_mode", "output"),
|
||||||
custom_root: widgetValue(node, "custom_root", ""),
|
custom_root: widgetValue(node, "custom_root", ""),
|
||||||
folder_template: widgetValue(node, "folder_template", ""),
|
folder_template: widgetValue(node, "folder_template", ""),
|
||||||
|
|||||||
Reference in New Issue
Block a user