dsh-comfyui 0.5.1 → 0.5.2
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.
- package/client/client.js +120 -22
- package/client/client.js.map +1 -1
- package/lib/analyze.js +24 -2
- package/lib/comfyui.d.ts +3 -0
- package/lib/comfyui.js +60 -5
- package/lib/convert.d.ts +12 -0
- package/lib/convert.js +76 -10
- package/lib/index.js +3 -2
- package/lib/routes.js +14 -2
- package/lib/tools.js +20 -6
- package/package.json +1 -1
package/lib/analyze.js
CHANGED
|
@@ -44,14 +44,36 @@ export function analyzeGraph(graph) {
|
|
|
44
44
|
const isolated = active
|
|
45
45
|
.filter((node) => !linkedIds.has(node.id))
|
|
46
46
|
.map((node) => ({ id: node.id, type: node.type }));
|
|
47
|
+
// Bypassed nodes are pass-throughs at conversion time (resolveOrigin in
|
|
48
|
+
// convert.ts: the output follows the FIRST wired input). Mirror that rule
|
|
49
|
+
// here, or a chain `120 → [406 bypassed] → 119` splits into two components
|
|
50
|
+
// and "main flow" extraction ships 119 referencing a node it left out
|
|
51
|
+
// (Issue #8).
|
|
52
|
+
const linkById = new Map(links.map((link) => [link[0], link]));
|
|
53
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
54
|
+
const passThroughOrigin = (originId) => {
|
|
55
|
+
let current = originId;
|
|
56
|
+
const visited = new Set();
|
|
57
|
+
while (nodeById.get(current)?.mode === 4) {
|
|
58
|
+
if (visited.has(current))
|
|
59
|
+
return undefined;
|
|
60
|
+
visited.add(current);
|
|
61
|
+
const inputLink = (nodeById.get(current)?.inputs ?? []).find((entry) => entry.link !== null)?.link;
|
|
62
|
+
const upstream = inputLink === undefined || inputLink === null ? undefined : linkById.get(inputLink);
|
|
63
|
+
if (upstream === undefined)
|
|
64
|
+
return undefined;
|
|
65
|
+
current = upstream[1];
|
|
66
|
+
}
|
|
67
|
+
return current;
|
|
68
|
+
};
|
|
47
69
|
// Connected components over linked active nodes.
|
|
48
70
|
const adjacency = new Map();
|
|
49
71
|
for (const node of active)
|
|
50
72
|
adjacency.set(node.id, new Set());
|
|
51
73
|
for (const link of links) {
|
|
52
|
-
const a = link[1];
|
|
74
|
+
const a = passThroughOrigin(link[1]);
|
|
53
75
|
const b = link[3];
|
|
54
|
-
if (
|
|
76
|
+
if (a === undefined)
|
|
55
77
|
continue;
|
|
56
78
|
if (!adjacency.has(a) || !adjacency.has(b))
|
|
57
79
|
continue;
|
package/lib/comfyui.d.ts
CHANGED
|
@@ -109,7 +109,10 @@ export declare class ComfyUIClient {
|
|
|
109
109
|
private readonly connectTimeoutMs;
|
|
110
110
|
private readonly maxMediaBytes;
|
|
111
111
|
constructor(baseUrl: string, apiKey: string | undefined, connectTimeoutMs: number, maxMediaBytes: number);
|
|
112
|
+
private base;
|
|
112
113
|
private endpoint;
|
|
114
|
+
/** fetch() one route, retrying a 404 under /api until the prefix is known. */
|
|
115
|
+
private fetchRoute;
|
|
113
116
|
private request;
|
|
114
117
|
/** Best-effort GET of an arbitrary JSON endpoint on the ComfyUI server.
|
|
115
118
|
*
|
package/lib/comfyui.js
CHANGED
|
@@ -62,6 +62,14 @@ export function guessContentType(filename) {
|
|
|
62
62
|
}
|
|
63
63
|
/** The per-process client id ComfyUI uses to correlate queued prompts. */
|
|
64
64
|
export const CLIENT_ID = randomUUID();
|
|
65
|
+
/**
|
|
66
|
+
* Route prefix learned per base URL: '' when bare routes answer, '/api' when
|
|
67
|
+
* only the /api-prefixed mirror does. ComfyUI serves every route under both,
|
|
68
|
+
* but API-only reverse proxies (comfy-api-proxy, Issue #6) forward just
|
|
69
|
+
* `/api/*`, so the bare /system_stats probe 404s. Learned on the first
|
|
70
|
+
* request and kept for the process — clients are created per call.
|
|
71
|
+
*/
|
|
72
|
+
const routePrefix = new Map();
|
|
65
73
|
/** HTTP client over the ComfyUI REST API. */
|
|
66
74
|
export class ComfyUIClient {
|
|
67
75
|
baseUrl;
|
|
@@ -74,8 +82,34 @@ export class ComfyUIClient {
|
|
|
74
82
|
this.connectTimeoutMs = connectTimeoutMs;
|
|
75
83
|
this.maxMediaBytes = maxMediaBytes;
|
|
76
84
|
}
|
|
85
|
+
base() {
|
|
86
|
+
return this.baseUrl.replace(/\/+$/, '');
|
|
87
|
+
}
|
|
77
88
|
endpoint(path) {
|
|
78
|
-
|
|
89
|
+
if (path.startsWith('/api/'))
|
|
90
|
+
return `${this.base()}${path}`;
|
|
91
|
+
return `${this.base()}${routePrefix.get(this.base()) ?? ''}${path}`;
|
|
92
|
+
}
|
|
93
|
+
/** fetch() one route, retrying a 404 under /api until the prefix is known. */
|
|
94
|
+
async fetchRoute(path, init) {
|
|
95
|
+
const base = this.base();
|
|
96
|
+
const response = await fetch(this.endpoint(path), init);
|
|
97
|
+
if (path.startsWith('/api/') || routePrefix.has(base))
|
|
98
|
+
return response;
|
|
99
|
+
if (response.ok) {
|
|
100
|
+
routePrefix.set(base, '');
|
|
101
|
+
return response;
|
|
102
|
+
}
|
|
103
|
+
if (response.status !== 404)
|
|
104
|
+
return response;
|
|
105
|
+
const retry = await fetch(`${base}/api${path}`, init);
|
|
106
|
+
if (!retry.ok) {
|
|
107
|
+
await retry.body?.cancel();
|
|
108
|
+
return response;
|
|
109
|
+
}
|
|
110
|
+
routePrefix.set(base, '/api');
|
|
111
|
+
await response.body?.cancel();
|
|
112
|
+
return retry;
|
|
79
113
|
}
|
|
80
114
|
async request(path, init = {}, timeoutMs) {
|
|
81
115
|
const controller = new AbortController();
|
|
@@ -84,7 +118,7 @@ export class ComfyUIClient {
|
|
|
84
118
|
const headers = { ...init.headers };
|
|
85
119
|
if (this.apiKey !== undefined)
|
|
86
120
|
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
87
|
-
const response = await
|
|
121
|
+
const response = await this.fetchRoute(path, { ...init, headers, signal: controller.signal });
|
|
88
122
|
if (!response.ok) {
|
|
89
123
|
const body = await response.text().catch(() => '');
|
|
90
124
|
throw new ComfyUIError(`ComfyUI ${path} failed: HTTP ${response.status}${body !== '' ? ` — ${body.slice(0, 300)}` : ''}`, response.status);
|
|
@@ -284,7 +318,28 @@ export class ComfyUIClient {
|
|
|
284
318
|
}
|
|
285
319
|
/** List one user-data subdirectory (e.g. 'workflows') on the ComfyUI server. */
|
|
286
320
|
async listUserData(subdir) {
|
|
287
|
-
|
|
321
|
+
let data;
|
|
322
|
+
try {
|
|
323
|
+
data = await this.request(`/v2/userdata?path=${encodeURIComponent(subdir)}`);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
// Some builds (e.g. the 0.35 desktop app, Issue #5) have no /v2/userdata;
|
|
327
|
+
// the v1 listing takes `dir` and returns paths relative to it.
|
|
328
|
+
if (!(error instanceof ComfyUIError) || error.status !== 404)
|
|
329
|
+
throw error;
|
|
330
|
+
const legacy = await this.request(`/userdata?dir=${encodeURIComponent(subdir)}&recurse=true&split=false&full_info=true`);
|
|
331
|
+
if (!Array.isArray(legacy))
|
|
332
|
+
return [];
|
|
333
|
+
return legacy.map((item) => {
|
|
334
|
+
const rel = (typeof item === 'string' ? item : item.path).replace(/\\/g, '/');
|
|
335
|
+
const entry = { name: rel, path: `${subdir}/${rel}`, type: 'file' };
|
|
336
|
+
if (typeof item !== 'string' && item.size !== undefined)
|
|
337
|
+
entry.size = item.size;
|
|
338
|
+
if (typeof item !== 'string' && item.modified !== undefined)
|
|
339
|
+
entry.modified = item.modified;
|
|
340
|
+
return entry;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
288
343
|
if (!Array.isArray(data))
|
|
289
344
|
return [];
|
|
290
345
|
if (typeof data[0] === 'string') {
|
|
@@ -319,7 +374,7 @@ export class ComfyUIClient {
|
|
|
319
374
|
const headers = {};
|
|
320
375
|
if (this.apiKey !== undefined)
|
|
321
376
|
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
322
|
-
const response = await
|
|
377
|
+
const response = await this.fetchRoute(`/view?${params.toString()}`, { headers, signal: controller.signal });
|
|
323
378
|
if (!response.ok) {
|
|
324
379
|
throw new ComfyUIError(`ComfyUI /view failed: HTTP ${response.status}`);
|
|
325
380
|
}
|
|
@@ -352,7 +407,7 @@ export class ComfyUIClient {
|
|
|
352
407
|
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
353
408
|
if (rangeHeader !== undefined)
|
|
354
409
|
headers['Range'] = rangeHeader;
|
|
355
|
-
const response = await
|
|
410
|
+
const response = await this.fetchRoute(`/view?${params.toString()}`, { headers, method, signal: controller.signal });
|
|
356
411
|
if (response.status !== 206 && response.status !== 416 && !response.ok) {
|
|
357
412
|
throw new ComfyUIError(`ComfyUI /view failed: HTTP ${response.status}`);
|
|
358
413
|
}
|
package/lib/convert.d.ts
CHANGED
|
@@ -12,6 +12,18 @@ export type ConvertResult = {
|
|
|
12
12
|
ok: false;
|
|
13
13
|
error: string;
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* Flatten DynamicCombo values that 0.2.0–0.5.1 extraction wrapped as
|
|
17
|
+
* `{ key, inputs: { sub: value } }` back into the flat API shape
|
|
18
|
+
* (`master: key`, `master.sub: value`, recursively). Library entries saved by
|
|
19
|
+
* those versions still carry the wrapped shape; normalizing at queue time
|
|
20
|
+
* heals them without asking users to re-extract (Issue #10). Returns the same
|
|
21
|
+
* object when nothing needed flattening.
|
|
22
|
+
*/
|
|
23
|
+
export declare function flattenDynamicCombos<W extends Record<string, {
|
|
24
|
+
class_type: string;
|
|
25
|
+
inputs: Record<string, unknown>;
|
|
26
|
+
}>>(workflow: W): W;
|
|
15
27
|
/**
|
|
16
28
|
* Convert a UI-graph workflow (or one extracted component of it) to API
|
|
17
29
|
* format using the live node definitions.
|
package/lib/convert.js
CHANGED
|
@@ -30,6 +30,51 @@ function inputSpec(objectInfo, classType, name) {
|
|
|
30
30
|
}
|
|
31
31
|
return undefined;
|
|
32
32
|
}
|
|
33
|
+
/** Option keys a COMFY_DYNAMICCOMBO_V3 spec declares, or undefined when unreadable. */
|
|
34
|
+
function dynamicComboKeys(spec) {
|
|
35
|
+
const options = isObject(spec[1]) ? spec[1].options : undefined;
|
|
36
|
+
if (!Array.isArray(options))
|
|
37
|
+
return undefined;
|
|
38
|
+
const keys = options
|
|
39
|
+
.map((option) => (isObject(option) ? option.key : undefined))
|
|
40
|
+
.filter((key) => typeof key === 'string');
|
|
41
|
+
return keys.length > 0 ? keys : undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Flatten DynamicCombo values that 0.2.0–0.5.1 extraction wrapped as
|
|
45
|
+
* `{ key, inputs: { sub: value } }` back into the flat API shape
|
|
46
|
+
* (`master: key`, `master.sub: value`, recursively). Library entries saved by
|
|
47
|
+
* those versions still carry the wrapped shape; normalizing at queue time
|
|
48
|
+
* heals them without asking users to re-extract (Issue #10). Returns the same
|
|
49
|
+
* object when nothing needed flattening.
|
|
50
|
+
*/
|
|
51
|
+
export function flattenDynamicCombos(workflow) {
|
|
52
|
+
const isWrapped = (value) => isObject(value) && typeof value.key === 'string' && isObject(value.inputs)
|
|
53
|
+
&& Object.keys(value).every((k) => k === 'key' || k === 'inputs');
|
|
54
|
+
const flatten = (prefix, value, into) => {
|
|
55
|
+
if (!isWrapped(value)) {
|
|
56
|
+
into[prefix] = value;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
into[prefix] = value.key;
|
|
60
|
+
for (const [sub, subValue] of Object.entries(value.inputs))
|
|
61
|
+
flatten(`${prefix}.${sub}`, subValue, into);
|
|
62
|
+
};
|
|
63
|
+
let changed = false;
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const [id, node] of Object.entries(workflow)) {
|
|
66
|
+
if (!Object.values(node.inputs).some(isWrapped)) {
|
|
67
|
+
out[id] = node;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
changed = true;
|
|
71
|
+
const inputs = {};
|
|
72
|
+
for (const [name, value] of Object.entries(node.inputs))
|
|
73
|
+
flatten(name, value, inputs);
|
|
74
|
+
out[id] = { ...node, inputs };
|
|
75
|
+
}
|
|
76
|
+
return changed ? out : workflow;
|
|
77
|
+
}
|
|
33
78
|
/**
|
|
34
79
|
* Derive the ordered widget-input names for one node. The graph's own input
|
|
35
80
|
* array is the source of truth — widgets_values is stored in UI widget order,
|
|
@@ -289,27 +334,48 @@ export function convertGraphToApi(graph, objectInfo, options) {
|
|
|
289
334
|
inputs[name] = value;
|
|
290
335
|
}
|
|
291
336
|
}
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
337
|
+
// DynamicCombo V3 stays FLAT in the API prompt: the master input holds the
|
|
338
|
+
// selected option key as a string and its sub-widgets sit beside it as
|
|
339
|
+
// `master.sub` keys. The server rebuilds the nested shape itself
|
|
340
|
+
// (DynamicCombo._expand_schema_for_dynamic matches the plain value against
|
|
341
|
+
// option keys); wrapping it as { key, inputs } never matches, the input is
|
|
342
|
+
// dropped, and the node dies at execute() — Issue #10 (SaveVideo 'codec').
|
|
343
|
+
// Validate the selected key here so a bad value fails at extraction rather
|
|
344
|
+
// than after a full generation.
|
|
295
345
|
for (const [name, value] of Object.entries(inputs)) {
|
|
296
346
|
const spec = inputSpec(objectInfo, node.type, name);
|
|
297
347
|
if (!Array.isArray(spec) || spec[0] !== 'COMFY_DYNAMICCOMBO_V3')
|
|
298
348
|
continue;
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
349
|
+
const keys = dynamicComboKeys(spec);
|
|
350
|
+
if (keys === undefined || Array.isArray(value))
|
|
351
|
+
continue;
|
|
352
|
+
if (typeof value !== 'string' || !keys.includes(value)) {
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
error: `节点 ${node.type}(id ${node.id})的输入 ${name} 取值 ${JSON.stringify(value)} 不是可选项之一(${keys.join(' / ')})——请在 ComfyUI 画布上重新选择后再提取`,
|
|
356
|
+
};
|
|
303
357
|
}
|
|
304
|
-
inputs[name] = { key: value, inputs: sub };
|
|
305
|
-
for (const subName of Object.keys(sub))
|
|
306
|
-
delete inputs[`${name}.${subName}`];
|
|
307
358
|
}
|
|
308
359
|
workflow[String(node.id)] = { class_type: node.type, inputs };
|
|
309
360
|
}
|
|
310
361
|
if (Object.keys(workflow).length === 0) {
|
|
311
362
|
return { ok: false, error: '转换结果为空(图中没有可执行的节点)' };
|
|
312
363
|
}
|
|
364
|
+
// Reference integrity: every link must point at a node of this same output.
|
|
365
|
+
// A component that leaves out an upstream node would otherwise extract as
|
|
366
|
+
// ok and only fail at run time (Issue #8).
|
|
367
|
+
for (const [id, node] of Object.entries(workflow)) {
|
|
368
|
+
for (const [name, value] of Object.entries(node.inputs)) {
|
|
369
|
+
if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== 'string' || typeof value[1] !== 'number')
|
|
370
|
+
continue;
|
|
371
|
+
if (workflow[value[0]] === undefined) {
|
|
372
|
+
return {
|
|
373
|
+
ok: false,
|
|
374
|
+
error: `节点 ${node.class_type}(id ${id})的输入 ${name} 引用了不在本次提取范围内的节点 ${value[0]}——请改用「整体提取」,或把这部分画布整理成独立的流程`,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
313
379
|
// Fail loudly on nodes whose required inputs are missing — the source graph
|
|
314
380
|
// itself is broken (inputs never wired), and the server would only echo a
|
|
315
381
|
// confusing per-node error at run time. Growable/lazy v3 inputs (template
|
package/lib/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Config } from './config.js';
|
|
|
4
4
|
import { ComfyUIClient, CLIENT_ID } from './comfyui.js';
|
|
5
5
|
import { ComfyUIStore } from './store.js';
|
|
6
6
|
import { QueueTracker } from './queue.js';
|
|
7
|
-
import { convertGraphToApi } from './convert.js';
|
|
7
|
+
import { convertGraphToApi, flattenDynamicCombos } from './convert.js';
|
|
8
8
|
import { analyzeGraph } from './analyze.js';
|
|
9
9
|
import { ProgressTracker } from './progress.js';
|
|
10
10
|
import { COMFYUI_SKILL } from './skill.js';
|
|
@@ -164,7 +164,8 @@ export async function apply(ctx, entryConfig) {
|
|
|
164
164
|
},
|
|
165
165
|
queue: async (workflow, meta) => {
|
|
166
166
|
const client = runtime.createClient(await resolveApiKey(ctx, resolved.apiKeyEnv));
|
|
167
|
-
|
|
167
|
+
// Heals library entries saved with the pre-0.5.2 wrapped DynamicCombo shape (Issue #10).
|
|
168
|
+
let prompt = flattenDynamicCombos(workflow);
|
|
168
169
|
if (meta.parameters !== undefined && meta.parameters.length > 0) {
|
|
169
170
|
const objectInfo = await objectInfoCached(client);
|
|
170
171
|
const slots = await store.loadSlots();
|
package/lib/routes.js
CHANGED
|
@@ -250,7 +250,14 @@ export function mountComfyUIRoutes(ctx, runtime) {
|
|
|
250
250
|
});
|
|
251
251
|
}
|
|
252
252
|
catch (error) {
|
|
253
|
-
|
|
253
|
+
// Name the probed address: a save that never persisted leaves the
|
|
254
|
+
// default 127.0.0.1:8188 in force, and a bare "fetch failed" reads
|
|
255
|
+
// like a network problem with the address the user typed (Issue #11).
|
|
256
|
+
sendJson(response, 200, {
|
|
257
|
+
ok: false,
|
|
258
|
+
error: `${errorMessage(error)}(探测地址 ${runtime.getConfig().baseUrl})`,
|
|
259
|
+
latencyMs: Date.now() - startedAt,
|
|
260
|
+
});
|
|
254
261
|
}
|
|
255
262
|
}),
|
|
256
263
|
}));
|
|
@@ -1428,7 +1435,12 @@ export function mountComfyUIRoutes(ctx, runtime) {
|
|
|
1428
1435
|
const client = runtime.createClient(await runtime.getApiKey());
|
|
1429
1436
|
const entry = await client.getHistory(promptId);
|
|
1430
1437
|
if (entry === undefined) {
|
|
1431
|
-
|
|
1438
|
+
// No history entry: still waiting in ComfyUI's queue, or evicted.
|
|
1439
|
+
// Tell the two apart so the background card keeps polling a long
|
|
1440
|
+
// queue instead of timing it out as "gone".
|
|
1441
|
+
const queue = await client.getQueue();
|
|
1442
|
+
const waiting = [...queue.queue_running, ...queue.queue_pending].some((item) => item.prompt_id === promptId);
|
|
1443
|
+
sendJson(response, 200, { ok: true, status: waiting ? 'queued' : 'unknown' });
|
|
1432
1444
|
return;
|
|
1433
1445
|
}
|
|
1434
1446
|
const statusStr = entry.status?.status_str;
|
package/lib/tools.js
CHANGED
|
@@ -263,7 +263,7 @@ function summarizeFields(fields, required) {
|
|
|
263
263
|
else if (typeof typeOrList === 'string') {
|
|
264
264
|
entry.type = typeOrList;
|
|
265
265
|
}
|
|
266
|
-
if (optionsRecord !== undefined &&
|
|
266
|
+
if (optionsRecord !== undefined && optionsRecord.default !== undefined) {
|
|
267
267
|
entry.default = optionsRecord.default;
|
|
268
268
|
}
|
|
269
269
|
out.push(entry);
|
|
@@ -307,16 +307,23 @@ function objectInfoDefinition(runtime) {
|
|
|
307
307
|
: entries.filter(([name]) => name.toLowerCase().includes(filter));
|
|
308
308
|
const nodes = filtered.slice(0, 60).map(([classType, def]) => ({
|
|
309
309
|
class_type: classType,
|
|
310
|
-
|
|
310
|
+
// Omitted rather than assigned undefined: a key holding undefined makes the
|
|
311
|
+
// whole result fail DSH's lossless-JSON validation. The render type already
|
|
312
|
+
// declares display_name optional.
|
|
313
|
+
...(def.display_name !== undefined ? { display_name: def.display_name } : {}),
|
|
311
314
|
description: (def.description ?? '').slice(0, 200),
|
|
312
315
|
required: summarizeFields(def.input?.required, true),
|
|
313
316
|
optional: summarizeFields(def.input?.optional, false),
|
|
314
317
|
}));
|
|
318
|
+
const hint = filtered.length > nodes.length
|
|
319
|
+
? `filter matched ${filtered.length} nodes, showing first ${nodes.length} — narrow the filter for more`
|
|
320
|
+
: undefined;
|
|
315
321
|
return {
|
|
316
322
|
total: entries.length,
|
|
317
323
|
shown: nodes.length,
|
|
318
324
|
filter: filter ?? null,
|
|
319
|
-
|
|
325
|
+
// Omitted rather than assigned undefined, same reason as display_name.
|
|
326
|
+
...(hint !== undefined ? { hint } : {}),
|
|
320
327
|
nodes,
|
|
321
328
|
};
|
|
322
329
|
},
|
|
@@ -409,12 +416,13 @@ function workflowDefinition(runtime, ctx) {
|
|
|
409
416
|
lines.push(` 技能包${skill.required ? '(运行前必读)' : ''}: ${skill.summary}${extra} — 运行前先 action: skill { id: "${workflow.id}" }`);
|
|
410
417
|
}
|
|
411
418
|
for (const param of workflow.parameters ?? []) {
|
|
412
|
-
const
|
|
419
|
+
const label = param.label ?? param.name;
|
|
420
|
+
const def = param.default === undefined ? '' : `,默认 ${typeof param.default === 'string' ? `"${param.default}"` : String(param.default)}`;
|
|
413
421
|
const options = Array.isArray(param.options) && param.options.length > 0 ? `,可选: ${param.options.join(' / ')}` : '';
|
|
414
422
|
const upload = param.upload !== undefined
|
|
415
423
|
? `,上传类型: ${param.upload}${param.upload === 'media' ? `(${param.subfolder ?? ''}/,空值=移除该参考位)` : ''}`
|
|
416
424
|
: '';
|
|
417
|
-
lines.push(` ${param.name}(${
|
|
425
|
+
lines.push(` ${param.name}(${label}${param.random === true ? ',随机' : ''}${def}${options}${upload})`);
|
|
418
426
|
}
|
|
419
427
|
}
|
|
420
428
|
const loadArea = data.loadArea;
|
|
@@ -506,7 +514,13 @@ function workflowDefinition(runtime, ctx) {
|
|
|
506
514
|
parameters: (parameters ?? []).map(({ name: pname, label, type, default: def, random, numberKind, options, upload }) => {
|
|
507
515
|
// DSH validates tool output as lossless JSON: JSON.stringify drops
|
|
508
516
|
// undefined keys, so omit optional fields instead of passing undefined.
|
|
509
|
-
const entry = { name: pname
|
|
517
|
+
const entry = { name: pname };
|
|
518
|
+
if (label !== undefined)
|
|
519
|
+
entry.label = label;
|
|
520
|
+
if (type !== undefined)
|
|
521
|
+
entry.type = type;
|
|
522
|
+
if (def !== undefined)
|
|
523
|
+
entry.default = def;
|
|
510
524
|
if (random !== undefined)
|
|
511
525
|
entry.random = random;
|
|
512
526
|
// Tells the model whether decimals are accepted; absent means the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-comfyui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Let the DeepSeek Harness agent smartly drive a local or remote ComfyUI to generate anything, with workflow and asset management panels, per-workflow skill packs, a companion skill and a same-origin media proxy. / 让 DeepSeek Harness 的 Agent 智能驱动本地或远程 ComfyUI 生成任何内容。附带工作流、资产管理面板与技能包管理挂载。配套 skill 与同源媒体代理。",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|