fix(workflow): accept non-string widget values and support GlobalSeed node in gen-params (#1026)

This commit is contained in:
Will Miao
2026-07-18 22:57:20 +08:00
parent 74a7d12ca4
commit d15a8aa9a2
4 changed files with 93 additions and 8 deletions

View File

@@ -3471,7 +3471,7 @@ class NodeRegistryHandler:
status=400,
)
if not isinstance(value, str) or not value:
if value is None or (isinstance(value, str) and not value):
return web.json_response(
{"success": False, "error": "Missing value parameter"}, status=400
)
@@ -3578,7 +3578,7 @@ class NodeRegistryHandler:
status=400,
)
if not isinstance(value, str) or not value:
if value is None or (isinstance(value, str) and not value):
return web.json_response(
{"success": False, "error": "Missing value parameter"}, status=400
)

View File

@@ -141,6 +141,20 @@ const PARAM_TO_WIDGET_CANDIDATES = {
scheduler: ['scheduler'],
};
// ---------------------------------------------------------------------------
// Node-type-specific widget name overrides.
// Keys are ComfyUI node class names (e.g. "GlobalSeed //Inspire").
// Values are partial PARAM_TO_WIDGET_CANDIDATES maps; the per-node candidates
// are tried *before* the global ones. Only the params listed here are
// overridden — every other param still uses the global candidates.
// ---------------------------------------------------------------------------
const NODE_TYPE_WIDGET_OVERRIDES = {
// Inspire Pack — Global Seed node stores the seed in a widget named "value"
'GlobalSeed //Inspire': {
seed: ['value'],
},
};
// ---------------------------------------------------------------------------
// Parse a combined sampler+scheduler value (space-separated or underscore)
// e.g., "Euler a Karras", "DPM++ 2M beta", "er_sde_beta"
@@ -235,7 +249,7 @@ function resolveSamplerScheduler(rawValue) {
// Find which gen params can be sent to a given node, matching by widget names
// Returns array of { widgetName, value } objects
// ---------------------------------------------------------------------------
function findMatchingWidgets(nodeWidgetNames, resolvedParams) {
function findMatchingWidgets(nodeWidgetNames, resolvedParams, nodeType) {
if (!nodeWidgetNames || !Array.isArray(nodeWidgetNames) || nodeWidgetNames.length === 0) {
return [];
}
@@ -243,6 +257,26 @@ function findMatchingWidgets(nodeWidgetNames, resolvedParams) {
const widgetSet = new Set(nodeWidgetNames.map(w => String(w).toLowerCase()));
const updates = [];
// Resolve node-type-specific overrides (if any)
const typeOverrides =
nodeType && typeof nodeType === 'string'
? (NODE_TYPE_WIDGET_OVERRIDES[nodeType] || {})
: {};
/**
* Build the effective candidate list for a parameter:
* type-specific overrides (if any) come first, then the global candidates.
*/
function getCandidates(key) {
const global = PARAM_TO_WIDGET_CANDIDATES[key] || [key];
const extra = typeOverrides[key];
if (extra && Array.isArray(extra) && extra.length > 0) {
// Prepend type-specific candidates; keep global as fallback
return [...extra, ...global];
}
return global;
}
// Simple numeric/string params: seed, steps, cfg
const simpleParams = [
{ key: 'seed', value: resolvedParams.seed },
@@ -251,7 +285,7 @@ function findMatchingWidgets(nodeWidgetNames, resolvedParams) {
];
for (const { key, value } of simpleParams) {
if (value === undefined || value === null || value === '') continue;
const candidates = PARAM_TO_WIDGET_CANDIDATES[key] || [key];
const candidates = getCandidates(key);
for (const candidate of candidates) {
if (widgetSet.has(candidate.toLowerCase())) {
updates.push({ widgetName: candidate, value });
@@ -262,7 +296,7 @@ function findMatchingWidgets(nodeWidgetNames, resolvedParams) {
// Sampler
if (resolvedParams.sampler) {
const candidates = PARAM_TO_WIDGET_CANDIDATES.sampler;
const candidates = getCandidates('sampler');
for (const candidate of candidates) {
if (widgetSet.has(candidate.toLowerCase())) {
updates.push({ widgetName: candidate, value: resolvedParams.sampler });
@@ -273,7 +307,7 @@ function findMatchingWidgets(nodeWidgetNames, resolvedParams) {
// Scheduler
if (resolvedParams.scheduler) {
const candidates = PARAM_TO_WIDGET_CANDIDATES.scheduler;
const candidates = getCandidates('scheduler');
for (const candidate of candidates) {
if (widgetSet.has(candidate.toLowerCase())) {
updates.push({ widgetName: candidate, value: resolvedParams.scheduler });
@@ -290,6 +324,7 @@ export {
SCHEDULER_SUFFIXES,
SCHEDULER_ONLY_VALUES,
PARAM_TO_WIDGET_CANDIDATES,
NODE_TYPE_WIDGET_OVERRIDES,
parseCombinedSamplerName,
resolveSamplerScheduler,
findMatchingWidgets,

View File

@@ -1144,8 +1144,8 @@ export async function sendGenParamsToWorkflow(genParams) {
const node = targetNodes[nodeKey];
if (!node) continue;
const widgetNames = node.widget_names || [];
const updates = findMatchingWidgets(widgetNames, raw);
const widgetNames = getWidgetNames(node);
const updates = findMatchingWidgets(widgetNames, raw, node.type_name);
if (updates.length === 0) {
showToast(`Node "${node.title || node.type}" has no matching widgets for these parameters`, {}, 'warning');

View File

@@ -9,6 +9,7 @@ import {
parseCombinedSamplerName,
resolveSamplerScheduler,
findMatchingWidgets,
NODE_TYPE_WIDGET_OVERRIDES,
} from '../../../static/js/utils/genParamsMapper.js';
// ---------------------------------------------------------------------------
@@ -243,4 +244,53 @@ describe('findMatchingWidgets', () => {
const updates = findMatchingWidgets(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler'], resolved);
expect(updates.map(u => u.widgetName)).toEqual(['seed', 'steps', 'cfg', 'sampler_name', 'scheduler']);
});
// --- node-type-specific overrides ---
it('matches GlobalSeed //Inspire value widget for seed param', () => {
const updates = findMatchingWidgets(
['value', 'mode', 'action', 'last_seed'],
{ seed: 42 },
'GlobalSeed //Inspire'
);
expect(updates).toHaveLength(1);
expect(updates[0]).toEqual({ widgetName: 'value', value: 42 });
});
it('ignores nodeType when it does not match any override entry', () => {
const updates = findMatchingWidgets(
['value', 'mode', 'action', 'last_seed'],
{ seed: 42 },
'SomeOtherNode'
);
expect(updates).toEqual([]);
});
it('still falls back to global candidates when override candidates do not match', () => {
// GlobalSeed override does not include steps — should use global candidate "steps"
const updates = findMatchingWidgets(
['steps', 'cfg', 'sampler_name'],
{ steps: 20 },
'GlobalSeed //Inspire'
);
expect(updates).toHaveLength(1);
expect(updates[0]).toEqual({ widgetName: 'steps', value: 20 });
});
it('prefers overrides when both override and global candidates match', () => {
// If a hypothetical node has both "value" and "seed" widgets AND a
// GlobalSeed override, the override candidate "value" should take precedence
const updates = findMatchingWidgets(
['seed', 'noise_seed', 'value', 'mode'],
{ seed: 99 },
'GlobalSeed //Inspire'
);
expect(updates).toHaveLength(1);
expect(updates[0].widgetName).toBe('value');
});
it('omits nodeType argument and still matches via global candidates', () => {
const updates = findMatchingWidgets(['seed', 'steps', 'cfg'], { seed: 7 });
expect(updates).toHaveLength(1);
expect(updates[0]).toEqual({ widgetName: 'seed', value: 7 });
});
});