comfyui-mcp 0.52.119 → 0.52.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -4724,6 +4724,7 @@ const ANIMA_REGIONAL_WIRED_PROMPT_INPUT = {
|
|
|
4724
4724
|
};
|
|
4725
4725
|
const DASIWA_STACK_WIDGET = "stack_data";
|
|
4726
4726
|
const DASIWA_LTX2_LORA_LOADER = "DaSiWa_LTX2LoraLoader";
|
|
4727
|
+
const COMFY_DYNAMICCOMBO_V3 = "COMFY_DYNAMICCOMBO_V3";
|
|
4727
4728
|
function isAnimaRegionalPromptWidget(widget) {
|
|
4728
4729
|
return ANIMA_REGIONAL_PROMPT_WIDGETS.has(widget);
|
|
4729
4730
|
}
|
|
@@ -4870,6 +4871,144 @@ function animaRegionalPromptRefusal(type, widget) {
|
|
|
4870
4871
|
`APPLY / savePrompts() copies the textarea back over widget.value, so a success here would be a lie. ` +
|
|
4871
4872
|
`${route} Do not retry panel_set_widget on this widget.`);
|
|
4872
4873
|
}
|
|
4874
|
+
/** Strictly parse one detail row for the dynamic-combo child gate. A compact
|
|
4875
|
+
* response is intentionally not enough: this guard needs the live input names
|
|
4876
|
+
* and types, not just the node type. */
|
|
4877
|
+
function parseVerifiedQueriedNodeDetail(payload) {
|
|
4878
|
+
if (!payload)
|
|
4879
|
+
return null;
|
|
4880
|
+
if (Object.prototype.hasOwnProperty.call(payload, "truncated") &&
|
|
4881
|
+
payload.truncated !== false) {
|
|
4882
|
+
return null;
|
|
4883
|
+
}
|
|
4884
|
+
let row;
|
|
4885
|
+
if (Object.prototype.hasOwnProperty.call(payload, "nodes")) {
|
|
4886
|
+
if (!Array.isArray(payload.nodes) || payload.nodes.length !== 1)
|
|
4887
|
+
return null;
|
|
4888
|
+
row = payload.nodes[0];
|
|
4889
|
+
}
|
|
4890
|
+
else if (typeof payload.text === "string") {
|
|
4891
|
+
let headerSeen = false;
|
|
4892
|
+
for (const line of payload.text.split(/\r?\n/)) {
|
|
4893
|
+
const trimmed = line.trim();
|
|
4894
|
+
if (!trimmed)
|
|
4895
|
+
continue;
|
|
4896
|
+
if (!headerSeen && /^\d+ match\(es\) of \d+ in scope/.test(trimmed)) {
|
|
4897
|
+
headerSeen = true;
|
|
4898
|
+
continue;
|
|
4899
|
+
}
|
|
4900
|
+
if (!trimmed.startsWith("{")) {
|
|
4901
|
+
// Detail replies may carry a bounded clipping/truncation footer after
|
|
4902
|
+
// the one JSON row. It cannot establish a second node identity.
|
|
4903
|
+
if (row !== undefined)
|
|
4904
|
+
continue;
|
|
4905
|
+
return null;
|
|
4906
|
+
}
|
|
4907
|
+
if (row !== undefined)
|
|
4908
|
+
return null;
|
|
4909
|
+
try {
|
|
4910
|
+
row = JSON.parse(trimmed);
|
|
4911
|
+
}
|
|
4912
|
+
catch {
|
|
4913
|
+
return null;
|
|
4914
|
+
}
|
|
4915
|
+
headerSeen = true;
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
4919
|
+
return null;
|
|
4920
|
+
const record = row;
|
|
4921
|
+
const identity = parseQueriedNodeIdentityRow(record);
|
|
4922
|
+
if (!identity || !Array.isArray(record.inputs))
|
|
4923
|
+
return null;
|
|
4924
|
+
const widgets = record.widgets && typeof record.widgets === "object" && !Array.isArray(record.widgets)
|
|
4925
|
+
? record.widgets
|
|
4926
|
+
: null;
|
|
4927
|
+
return { ...identity, inputs: record.inputs, widgets };
|
|
4928
|
+
}
|
|
4929
|
+
/** The ONLY dynamic-combo child type this refuses.
|
|
4930
|
+
*
|
|
4931
|
+
* #2299 measured exactly one reverting child — `model.prompt`, a STRING — and a
|
|
4932
|
+
* STRING child is the only one with a durable escape: it is exposed as a real
|
|
4933
|
+
* input socket, so a PrimitiveStringMultiline link survives the frontend's
|
|
4934
|
+
* re-serialize pass and wins at execution.
|
|
4935
|
+
*
|
|
4936
|
+
* Refusing the parent's whole child set instead would be a capability removal.
|
|
4937
|
+
* `src/services/api-nodes.ts` classifies a v3 dynamic combo's children as
|
|
4938
|
+
* INT | FLOAT | STRING | BOOLEAN | COMBO, and the Nano Banana 2 shape fixtured in
|
|
4939
|
+
* `src/__tests__/services/api-nodes.test.ts` reveals `model.aspect_ratio`,
|
|
4940
|
+
* `model.resolution` and `model.thinking_level` — all COMBO, and all REQUIRED:
|
|
4941
|
+
* the server 400s with `required_input_missing` when a dotted child is absent.
|
|
4942
|
+
* A refusal there would leave a required input with NO route, while naming a
|
|
4943
|
+
* remedy that cannot be carried out (a STRING output does not reach a COMBO
|
|
4944
|
+
* input). No non-STRING child has been measured reverting; if one ever is, it
|
|
4945
|
+
* needs an honest receipt ("written, not confirmed persistent"), not a refusal.
|
|
4946
|
+
*
|
|
4947
|
+
* So: prove STRING, or keep the normal setter path. */
|
|
4948
|
+
const DYNAMIC_COMBO_REFUSED_CHILD_TYPE = "STRING";
|
|
4949
|
+
function dynamicComboSubWidgetRefusal(nodeType, parentInput, widget) {
|
|
4950
|
+
return fail(`panel_set_widget cannot set dynamic-combo sub-widget "${widget}" on ${nodeType}. ` +
|
|
4951
|
+
`The parent input "${parentInput}" is ${COMFY_DYNAMICCOMBO_V3} and "${widget}" is a ` +
|
|
4952
|
+
`${DYNAMIC_COMBO_REFUSED_CHILD_TYPE} child; its frontend ` +
|
|
4953
|
+
`serializer can re-materialize the combo and overwrite widget.value after a ` +
|
|
4954
|
+
`successful write and immediate readback, so panel_run could queue an empty or ` +
|
|
4955
|
+
`stale value. Drive the child input by link instead: add a ` +
|
|
4956
|
+
`PrimitiveStringMultiline, set its STRING widget, then panel_connect its ` +
|
|
4957
|
+
`STRING output to this node's "${widget}" input. Set the parent "${parentInput}" ` +
|
|
4958
|
+
`only to choose the combo option. No graph_set_widget was dispatched; do not ` +
|
|
4959
|
+
`retry this dotted child write.`);
|
|
4960
|
+
}
|
|
4961
|
+
/** Refuse a dotted widget only when the live detail row proves BOTH that its
|
|
4962
|
+
* prefix is a COMFY_DYNAMICCOMBO_V3 input and that the addressed child is a
|
|
4963
|
+
* STRING (see {@link DYNAMIC_COMBO_REFUSED_CHILD_TYPE} for why the child type is
|
|
4964
|
+
* load-bearing rather than the parent type alone).
|
|
4965
|
+
* Ordinary composite widgets (for example rgthree `lora_1.on`) and ordinary
|
|
4966
|
+
* dotted STRING names remain on the normal setter path. An unreadable detail
|
|
4967
|
+
* probe is not evidence of a dynamic combo, so it falls through to the panel's
|
|
4968
|
+
* existing validation rather than broadening this refusal. */
|
|
4969
|
+
async function refuseDynamicComboSubWidgetWrite(ctx, nodeId, widget) {
|
|
4970
|
+
const dot = widget.indexOf(".");
|
|
4971
|
+
if (dot <= 0 || dot === widget.length - 1)
|
|
4972
|
+
return null;
|
|
4973
|
+
const probe = await ctx.call({
|
|
4974
|
+
cmd: "graph_query",
|
|
4975
|
+
ids: [nodeId],
|
|
4976
|
+
fields: "detail",
|
|
4977
|
+
limit: 1,
|
|
4978
|
+
});
|
|
4979
|
+
if (probe.isError)
|
|
4980
|
+
return null;
|
|
4981
|
+
const detail = parseVerifiedQueriedNodeDetail(parseToolResultJson(probe));
|
|
4982
|
+
const requestedId = canonicalQueriedNodeId(nodeId);
|
|
4983
|
+
if (!detail || !requestedId || detail.id !== requestedId)
|
|
4984
|
+
return null;
|
|
4985
|
+
const parentInput = widget.slice(0, dot);
|
|
4986
|
+
const childInput = detail.inputs.find((input) => input &&
|
|
4987
|
+
typeof input === "object" &&
|
|
4988
|
+
!Array.isArray(input) &&
|
|
4989
|
+
input.name === widget);
|
|
4990
|
+
const childExists = childInput !== undefined ||
|
|
4991
|
+
(detail.widgets !== null && Object.prototype.hasOwnProperty.call(detail.widgets, widget));
|
|
4992
|
+
if (!childExists)
|
|
4993
|
+
return null;
|
|
4994
|
+
const parent = detail.inputs.find((input) => input &&
|
|
4995
|
+
typeof input === "object" &&
|
|
4996
|
+
!Array.isArray(input) &&
|
|
4997
|
+
input.name === parentInput);
|
|
4998
|
+
if (!parent || typeof parent !== "object" || Array.isArray(parent))
|
|
4999
|
+
return null;
|
|
5000
|
+
if (parent.type !== COMFY_DYNAMICCOMBO_V3)
|
|
5001
|
+
return null;
|
|
5002
|
+
// A dynamic-combo parent is not on its own the #2299 shape. Only a child the
|
|
5003
|
+
// probe PROVES is STRING is refused; a COMBO/INT/FLOAT/BOOLEAN child, or one
|
|
5004
|
+
// whose declared type this row does not carry, keeps the normal setter path.
|
|
5005
|
+
const childType = childInput && typeof childInput === "object" && !Array.isArray(childInput)
|
|
5006
|
+
? childInput.type
|
|
5007
|
+
: undefined;
|
|
5008
|
+
if (childType !== DYNAMIC_COMBO_REFUSED_CHILD_TYPE)
|
|
5009
|
+
return null;
|
|
5010
|
+
return dynamicComboSubWidgetRefusal(detail.type, parentInput, widget);
|
|
5011
|
+
}
|
|
4873
5012
|
/** Refuse the known LC123 regional-canvas prompt widgets. Identity is read
|
|
4874
5013
|
* from a one-node `graph_query`; anything else (query failed, type unreadable,
|
|
4875
5014
|
* some other node that happens to own `negative_prompt`) is fail-open. */
|
|
@@ -12686,7 +12825,17 @@ function withRetryToken(d) {
|
|
|
12686
12825
|
*/
|
|
12687
12826
|
export function buildPanelToolDefs() {
|
|
12688
12827
|
// Local helper so each def reads like the original `tool(...)` call.
|
|
12689
|
-
const def = (name, description, schema, handler) => ({
|
|
12828
|
+
const def = (name, description, schema, handler) => ({
|
|
12829
|
+
name,
|
|
12830
|
+
// Keep the long historical setter description readable at its call site;
|
|
12831
|
+
// #2299's dynamic-combo warning is appended here so both registrations
|
|
12832
|
+
// expose the same actionable workaround.
|
|
12833
|
+
description: name === "panel_set_widget"
|
|
12834
|
+
? `${description} A dotted STRING child of a live COMFY_DYNAMICCOMBO_V3 input (for example model.prompt) is refused when the live detail proves both halves of that shape (non-STRING children such as Nano Banana 2's model.resolution stay writable): the frontend can reserialize the combo after immediate readback and panel_run may then use an empty/stale child. Drive the corresponding STRING child input with a PrimitiveStringMultiline link instead; set the parent only to choose the combo option.`
|
|
12835
|
+
: description,
|
|
12836
|
+
schema,
|
|
12837
|
+
handler,
|
|
12838
|
+
});
|
|
12690
12839
|
const capturePanelKitchenTarget = (ctx) => {
|
|
12691
12840
|
const base = getComfyUIBaseUrl().replace(/\/+$/, "");
|
|
12692
12841
|
const observed = ctx.bridge.tabServerOrigin?.(ctx.tabId);
|
|
@@ -13584,6 +13733,9 @@ export function buildPanelToolDefs() {
|
|
|
13584
13733
|
const blocked = await refuseAnimaRegionalPromptWrite(ctx, args.node_id, args.widget);
|
|
13585
13734
|
if (blocked)
|
|
13586
13735
|
return blocked;
|
|
13736
|
+
const dynamicComboBlocked = await refuseDynamicComboSubWidgetWrite(ctx, args.node_id, args.widget);
|
|
13737
|
+
if (dynamicComboBlocked)
|
|
13738
|
+
return dynamicComboBlocked;
|
|
13587
13739
|
const daSiWaBlocked = await refuseDaSiWaStackWrite(ctx, args.node_id, args.widget);
|
|
13588
13740
|
if (daSiWaBlocked)
|
|
13589
13741
|
return daSiWaBlocked;
|
|
@@ -13791,6 +13943,29 @@ export function buildPanelToolDefs() {
|
|
|
13791
13943
|
}
|
|
13792
13944
|
innerExpectedNodeType = innerIdentity.type;
|
|
13793
13945
|
}
|
|
13946
|
+
// #2299 — every pre-write guard above probed the OUTER scope, but this retry
|
|
13947
|
+
// writes a DIFFERENT node: the promoted inner one. For a dynamic-combo child
|
|
13948
|
+
// that is exactly the false success the guard exists to stop. The outer probe
|
|
13949
|
+
// cannot see it and correctly falls open — the container exposes the promoted
|
|
13950
|
+
// child but not the `model` PARENT, so the COMFY_DYNAMICCOMBO_V3 half of the
|
|
13951
|
+
// shape is unprovable there. Only the inner node carries both halves.
|
|
13952
|
+
//
|
|
13953
|
+
// The stack_data fence above does re-query after entering, but it is gated on
|
|
13954
|
+
// `expectedNodeType`, which is set only when the addressed widget IS stack_data
|
|
13955
|
+
// — so it never runs for any other widget. This is the same re-probe for the
|
|
13956
|
+
// dotted-child case, and the canvas is already inside the subgraph here, so
|
|
13957
|
+
// `inner.innerNodeId` resolves.
|
|
13958
|
+
const innerDynamicComboBlocked = await refuseDynamicComboSubWidgetWrite(ctx, inner.innerNodeId, inner.widget);
|
|
13959
|
+
if (innerDynamicComboBlocked) {
|
|
13960
|
+
const exitedEarly = await ctx.call({ cmd: "graph_exit_subgraph" }, 15000);
|
|
13961
|
+
return appendToolResultText(first, `
|
|
13962
|
+
|
|
13963
|
+
(The promoted inner node ${inner.innerNodeId} owns "${inner.widget}" as a ` +
|
|
13964
|
+
`dynamic-combo child; ${textOfToolResult(innerDynamicComboBlocked)} ` +
|
|
13965
|
+
`No inner graph_set_widget was dispatched.${exitedEarly.isError
|
|
13966
|
+
? ` panel_exit_subgraph also FAILED: ${textOfToolResult(exitedEarly)}`
|
|
13967
|
+
: ""})`);
|
|
13968
|
+
}
|
|
13794
13969
|
const written = await write(inner.innerNodeId, inner.widget, innerExpectedNodeType);
|
|
13795
13970
|
const exited = await ctx.call({ cmd: "graph_exit_subgraph" }, 15000);
|
|
13796
13971
|
if (!written.isError) {
|