dsh-comfyui 0.3.0-beta.0

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.
@@ -0,0 +1,4 @@
1
+ # dsh-comfyui bundle patch: inserts the plugin into a profile's layer stack.
2
+ - insert:
3
+ - id: comfyui
4
+ name: 'dsh-comfyui'
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Canvas analysis for ComfyUI UI-graph workflows. A saved canvas is often a
3
+ * test bench holding several independent flows at once: `groups` are visual
4
+ * rectangles only, and the executable unit is a connected component over the
5
+ * graph links (with bypassed and dangling nodes excluded). The analysis feeds
6
+ * the extract (拆分) choices in the panel and the agent-facing skill.
7
+ */
8
+ export interface GraphNodeLike {
9
+ id: number;
10
+ type: string;
11
+ mode?: number;
12
+ inputs?: Array<{
13
+ link: number | null;
14
+ }>;
15
+ pos?: [number, number] | number[];
16
+ }
17
+ export interface GraphGroupLike {
18
+ title?: string;
19
+ bounding: [number, number, number, number] | number[];
20
+ }
21
+ export interface GraphLike {
22
+ nodes: GraphNodeLike[];
23
+ links: Array<[number, number, number, number, number, string] | number[]>;
24
+ groups?: GraphGroupLike[];
25
+ }
26
+ export interface IsolatedNode {
27
+ id: number;
28
+ type: string;
29
+ }
30
+ export interface ComponentInfo {
31
+ /** 1-based index, ordered largest first. */
32
+ index: number;
33
+ nodeIds: number[];
34
+ size: number;
35
+ /** Group titles that contain at least one node of the component. */
36
+ groups: string[];
37
+ /** Distinct node class types in the component (preview). */
38
+ nodeTypes: string[];
39
+ }
40
+ export interface GraphAnalysis {
41
+ ok: true;
42
+ /** Executable components, largest first. */
43
+ components: ComponentInfo[];
44
+ /** Dangling nodes ignored by extraction (Markdown, UI-only, unused primitives). */
45
+ isolated: IsolatedNode[];
46
+ /** Count of bypassed (mode 4) nodes skipped by extraction. */
47
+ bypassedCount: number;
48
+ /** 'single' when at most one component exists; 'multi' when several. */
49
+ mode: 'single' | 'multi';
50
+ }
51
+ /**
52
+ * Analyze a saved ComfyUI graph: split its active nodes into connected
53
+ * components, associate group titles, and list the dangling nodes that
54
+ * extraction ignores.
55
+ * @param graph - parsed ComfyUI UI graph (v0.4 format).
56
+ * @returns the analysis, or an error object when the graph is not readable.
57
+ */
58
+ export declare function analyzeGraph(graph: unknown): GraphAnalysis | {
59
+ ok: false;
60
+ error: string;
61
+ };
package/lib/analyze.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Canvas analysis for ComfyUI UI-graph workflows. A saved canvas is often a
3
+ * test bench holding several independent flows at once: `groups` are visual
4
+ * rectangles only, and the executable unit is a connected component over the
5
+ * graph links (with bypassed and dangling nodes excluded). The analysis feeds
6
+ * the extract (拆分) choices in the panel and the agent-facing skill.
7
+ */
8
+ function isObject(value) {
9
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
10
+ }
11
+ function nodeInGroup(node, group) {
12
+ const [gx, gy, gw, gh] = group.bounding;
13
+ const pos = node.pos ?? [0, 0];
14
+ const nx = pos[0] ?? 0;
15
+ const ny = pos[1] ?? 0;
16
+ return nx >= gx && ny >= gy && nx <= gx + gw && ny <= gy + gh;
17
+ }
18
+ /**
19
+ * Analyze a saved ComfyUI graph: split its active nodes into connected
20
+ * components, associate group titles, and list the dangling nodes that
21
+ * extraction ignores.
22
+ * @param graph - parsed ComfyUI UI graph (v0.4 format).
23
+ * @returns the analysis, or an error object when the graph is not readable.
24
+ */
25
+ export function analyzeGraph(graph) {
26
+ if (!isObject(graph) || !Array.isArray(graph.nodes) || !Array.isArray(graph.links)) {
27
+ return { ok: false, error: '无法解析图文件(缺少 nodes/links)' };
28
+ }
29
+ const nodes = graph.nodes;
30
+ const links = graph.links;
31
+ const groups = Array.isArray(graph.groups) ? graph.groups : [];
32
+ const active = nodes.filter((node) => node.mode !== 4);
33
+ const activeById = new Map(active.map((node) => [node.id, node]));
34
+ const bypassedCount = nodes.length - active.length;
35
+ // Dangling nodes: active but touching no link at all.
36
+ const linkedIds = new Set();
37
+ for (const link of links) {
38
+ if (typeof link[1] === 'number' && activeById.has(link[1]))
39
+ linkedIds.add(link[1]);
40
+ if (typeof link[3] === 'number' && activeById.has(link[3]))
41
+ linkedIds.add(link[3]);
42
+ }
43
+ const isolated = active
44
+ .filter((node) => !linkedIds.has(node.id))
45
+ .map((node) => ({ id: node.id, type: node.type }));
46
+ // Connected components over linked active nodes.
47
+ const adjacency = new Map();
48
+ for (const node of active)
49
+ adjacency.set(node.id, new Set());
50
+ for (const link of links) {
51
+ const a = link[1];
52
+ const b = link[3];
53
+ if (typeof a !== 'number' || typeof b !== 'number')
54
+ continue;
55
+ if (!adjacency.has(a) || !adjacency.has(b))
56
+ continue;
57
+ adjacency.get(a).add(b);
58
+ adjacency.get(b).add(a);
59
+ }
60
+ const seen = new Set();
61
+ const components = [];
62
+ for (const node of active) {
63
+ if (!linkedIds.has(node.id) || seen.has(node.id))
64
+ continue;
65
+ const stack = [node.id];
66
+ const memberIds = [];
67
+ while (stack.length > 0) {
68
+ const current = stack.pop();
69
+ if (seen.has(current))
70
+ continue;
71
+ seen.add(current);
72
+ memberIds.push(current);
73
+ for (const next of adjacency.get(current) ?? []) {
74
+ if (!seen.has(next))
75
+ stack.push(next);
76
+ }
77
+ }
78
+ const members = memberIds.map((id) => activeById.get(id));
79
+ const groupTitles = groups
80
+ .filter((group) => members.some((member) => nodeInGroup(member, group)))
81
+ .map((group) => group.title ?? '(未命名组)');
82
+ const nodeTypes = [...new Set(members.map((member) => member.type).filter((type) => type !== ''))];
83
+ components.push({ index: components.length + 1, nodeIds: memberIds, size: memberIds.length, groups: groupTitles, nodeTypes });
84
+ }
85
+ components.sort((a, b) => b.size - a.size);
86
+ components.forEach((component, position) => {
87
+ component.index = position + 1;
88
+ });
89
+ return {
90
+ ok: true,
91
+ components,
92
+ isolated,
93
+ bypassedCount,
94
+ mode: components.length > 1 ? 'multi' : 'single',
95
+ };
96
+ }
@@ -0,0 +1,195 @@
1
+ /** One media file produced by a workflow output node. */
2
+ export interface ComfyUIMediaRef {
3
+ filename: string;
4
+ subfolder: string;
5
+ type: string;
6
+ }
7
+ /** A media item with its locating information and proxy URL. */
8
+ export interface ComfyUIMediaItem extends ComfyUIMediaRef {
9
+ /** Output node id that produced the item. */
10
+ node: string;
11
+ /** Index within the node's media collection. */
12
+ index: number;
13
+ kind: 'image' | 'video' | 'audio' | 'other';
14
+ /** Same-origin proxy URL (web profile) or a descriptive placeholder. */
15
+ url: string;
16
+ }
17
+ /** One entry of the ComfyUI /history map. */
18
+ export interface ComfyUIHistoryEntry {
19
+ prompt?: unknown;
20
+ outputs?: Record<string, {
21
+ images?: ComfyUIMediaRef[];
22
+ videos?: ComfyUIMediaRef[];
23
+ gifs?: ComfyUIMediaRef[];
24
+ }>;
25
+ status?: {
26
+ status_str?: string;
27
+ completed?: boolean;
28
+ messages?: unknown[];
29
+ };
30
+ }
31
+ /** One entry of ComfyUI's /queue: server-side generation tasks. */
32
+ export interface ComfyUIQueueItem {
33
+ number: number;
34
+ prompt_id: string;
35
+ }
36
+ /** ComfyUI /queue response: the server-side generation queue. */
37
+ export interface ComfyUIQueueView {
38
+ queue_running: ComfyUIQueueItem[];
39
+ queue_pending: ComfyUIQueueItem[];
40
+ }
41
+ /** A unified job from ComfyUI /api/jobs (running + pending + history). */
42
+ export interface ComfyUIJob {
43
+ id: string;
44
+ status: 'pending' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
45
+ priority: number;
46
+ create_time: number | null;
47
+ execution_start_time?: number | null;
48
+ execution_end_time?: number | null;
49
+ execution_error?: {
50
+ message?: string;
51
+ exception_message?: string;
52
+ node_type?: string;
53
+ } | null;
54
+ outputs_count: number;
55
+ previewable_outputs_count: number;
56
+ preview_output?: {
57
+ filename?: string;
58
+ subfolder?: string;
59
+ type?: string;
60
+ mediaType?: string;
61
+ } | null;
62
+ workflow_id?: string | null;
63
+ outputs?: Record<string, unknown>;
64
+ workflow?: {
65
+ prompt: unknown;
66
+ extra_data?: Record<string, unknown>;
67
+ };
68
+ }
69
+ /** /api/jobs list response. */
70
+ export interface ComfyUIJobsResponse {
71
+ jobs: ComfyUIJob[];
72
+ pagination: {
73
+ offset: number;
74
+ limit: number | null;
75
+ total: number;
76
+ has_more: boolean;
77
+ };
78
+ }
79
+ /** One entry of a user-data directory listing (/v2/userdata). */
80
+ export interface ComfyUIUserDataEntry {
81
+ name: string;
82
+ path: string;
83
+ type: 'file' | 'directory';
84
+ size?: number;
85
+ modified?: number;
86
+ }
87
+ /** Failure talking to the ComfyUI server. */
88
+ export declare class ComfyUIError extends Error {
89
+ readonly status?: number | undefined;
90
+ constructor(message: string, status?: number | undefined);
91
+ }
92
+ /** The per-process client id ComfyUI uses to correlate queued prompts. */
93
+ export declare const CLIENT_ID: `${string}-${string}-${string}-${string}-${string}`;
94
+ /** HTTP client over the ComfyUI REST API. */
95
+ export declare class ComfyUIClient {
96
+ private readonly baseUrl;
97
+ private readonly apiKey;
98
+ private readonly connectTimeoutMs;
99
+ private readonly maxMediaBytes;
100
+ constructor(baseUrl: string, apiKey: string | undefined, connectTimeoutMs: number, maxMediaBytes: number);
101
+ private endpoint;
102
+ private request;
103
+ /** Upload a file (multipart body forwarded verbatim) into ComfyUI's input directory. */
104
+ uploadFile(body: Uint8Array, contentType: string): Promise<{
105
+ name?: string;
106
+ subfolder?: string;
107
+ type?: string;
108
+ }>;
109
+ /** Queue one API-format workflow and return its prompt id. */
110
+ queuePrompt(workflow: unknown, options?: {
111
+ promptId?: string;
112
+ front?: boolean;
113
+ extraData?: Record<string, unknown>;
114
+ }): Promise<string>;
115
+ /** Read one prompt's history entry; undefined while the prompt is unknown or evicted. */
116
+ getHistory(promptId: string): Promise<ComfyUIHistoryEntry | undefined>;
117
+ /** The server-side queue: running + pending prompts. */
118
+ getQueue(): Promise<ComfyUIQueueView>;
119
+ /** Unified job list with status filters, sorting, and pagination. */
120
+ getJobs(options?: {
121
+ status?: Array<ComfyUIJob['status']>;
122
+ limit?: number;
123
+ offset?: number;
124
+ sortBy?: 'created_at' | 'execution_duration';
125
+ sortOrder?: 'asc' | 'desc';
126
+ }): Promise<ComfyUIJobsResponse>;
127
+ /** One job by id, including its workflow prompt and outputs. */
128
+ getJob(jobId: string): Promise<ComfyUIJob | undefined>;
129
+ /** Remove specific prompts from the pending queue. */
130
+ deleteQueueItems(promptIds: string[]): Promise<void>;
131
+ /** Clear the entire pending queue (running job is unaffected). */
132
+ clearQueue(): Promise<void>;
133
+ /** Interrupt the running prompt; without an id, interrupt globally. */
134
+ interruptPrompt(promptId?: string): Promise<void>;
135
+ /** Cancel one job regardless of state (running → interrupt, pending → dequeue). */
136
+ cancelJob(jobId: string): Promise<{
137
+ cancelled: boolean;
138
+ }>;
139
+ /** Best-effort batch cancel; finished or unknown ids are no-ops. */
140
+ cancelJobs(jobIds: string[]): Promise<{
141
+ cancelled: boolean;
142
+ }>;
143
+ /** Clear or selectively delete history entries. */
144
+ clearHistory(): Promise<void>;
145
+ /** Delete specific history entries. */
146
+ deleteHistory(promptIds: string[]): Promise<void>;
147
+ /** Ask ComfyUI to unload models / free memory (per /free flags). */
148
+ freeMemory(options?: {
149
+ unloadModels?: boolean;
150
+ freeMemory?: boolean;
151
+ }): Promise<void>;
152
+ /** List one user-data subdirectory (e.g. 'workflows') on the ComfyUI server. */
153
+ listUserData(subdir: string): Promise<ComfyUIUserDataEntry[]>;
154
+ /** Read one user-data file (path relative to the user root, e.g. 'workflows/x.json'). */
155
+ getUserDataFile(relPath: string): Promise<unknown>;
156
+ /** Node definitions for workflow construction (comfyui_object_info). */
157
+ objectInfo(): Promise<Record<string, unknown>>;
158
+ /** Server health/version probe. */
159
+ systemStats(): Promise<{
160
+ system?: {
161
+ comfyui_version?: string;
162
+ };
163
+ }>;
164
+ /** Ask ComfyUI to interrupt the running prompt. */
165
+ interrupt(): Promise<void>;
166
+ /** Download one generated media file through GET /view. */
167
+ fetchView(ref: ComfyUIMediaRef): Promise<{
168
+ bytes: Uint8Array;
169
+ contentType: string;
170
+ }>;
171
+ /**
172
+ * Poll history until the prompt completes, fails, or the budget/signal ends.
173
+ * Interrupts the server when the signal aborts before throwing.
174
+ */
175
+ waitForCompletion(opts: {
176
+ promptId: string;
177
+ timeoutMs: number;
178
+ pollIntervalMs: number;
179
+ signal: AbortSignal;
180
+ }): Promise<ComfyUIHistoryEntry>;
181
+ }
182
+ export declare function hasMedia(entry: ComfyUIHistoryEntry): boolean;
183
+ /** Compose a readable failure message from history status messages. */
184
+ export declare function historyErrorMessage(promptId: string, entry: ComfyUIHistoryEntry): string;
185
+ /**
186
+ * Collect media items from a completed history entry, in node/output order,
187
+ * capped by maxItems. The URL is the same-origin proxy route when a web
188
+ * server is present, otherwise a ComfyUI /view URL (for headless hosts).
189
+ */
190
+ export declare function collectMedia(opts: {
191
+ promptId: string;
192
+ entry: ComfyUIHistoryEntry;
193
+ maxItems: number;
194
+ proxyBase: string | undefined;
195
+ }): ComfyUIMediaItem[];