dsh-image-gen 0.2.4 → 0.2.5

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/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
- import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
2
+ import * as dshSettings from "@deepseek-ai/dsh-settings";
3
3
  import { defineTool } from "@deepseek-ai/dsh-tools";
4
4
  import z from "@deepseek-ai/schemastery";
5
5
  import { mkdir, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
@@ -26,11 +26,58 @@ const DEFAULT_SEEDREAM_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
26
26
  const DEFAULT_DASHSCOPE_ENDPOINT = "https://dashscope.aliyuncs.com/api/v1";
27
27
  const DEFAULT_COMFYUI_BASE_URL = "http://127.0.0.1:8188";
28
28
  const DEFAULT_COMFYUI_TIMEOUT_MS = 3e5;
29
+ const DEFAULT_COMFYUI_WORKFLOW_LABEL = "API workflow";
29
30
  /** Default model names. */
30
31
  const DEFAULT_GOOGLE_MODEL = "gemini-3.1-flash-image";
31
32
  const DEFAULT_OPENAI_MODEL = "gpt-image-2";
32
33
  const DEFAULT_SEEDREAM_MODEL = "doubao-seedream-5-0-260128";
33
34
  const DEFAULT_DASHSCOPE_MODEL = "qwen-image-3.0";
35
+ /** Named workflows, falling back to the legacy single-workflow fields when the list is empty. */
36
+ function resolveComfyUIWorkflows(source) {
37
+ const named = [];
38
+ for (const entry of source.comfyuiWorkflows ?? []) {
39
+ const name = typeof entry?.name === "string" ? entry.name.trim() : "";
40
+ const json = typeof entry?.json === "string" ? entry.json : "";
41
+ if (name.length > 0 && json.trim().length > 0) {
42
+ const presetPrompt = typeof entry.presetPrompt === "string" ? entry.presetPrompt.trim() : "";
43
+ named.push(presetPrompt.length > 0 ? {
44
+ name,
45
+ json,
46
+ presetPrompt
47
+ } : {
48
+ name,
49
+ json
50
+ });
51
+ }
52
+ }
53
+ if (named.length > 0) return named;
54
+ const legacyJson = typeof source.comfyuiWorkflowJson === "string" ? source.comfyuiWorkflowJson : "";
55
+ if (legacyJson.trim().length === 0) return [];
56
+ const legacyName = typeof source.comfyuiWorkflowName === "string" ? source.comfyuiWorkflowName.trim() : "";
57
+ return [{
58
+ name: legacyName.length > 0 ? legacyName : DEFAULT_COMFYUI_WORKFLOW_LABEL,
59
+ json: legacyJson
60
+ }];
61
+ }
62
+ /** The workflow ComfyUI calls use by default: the configured active name, else the first entry. */
63
+ function activeComfyUIWorkflow(source) {
64
+ const workflows = resolveComfyUIWorkflows(source);
65
+ if (workflows.length === 0) return void 0;
66
+ const activeName = typeof source.comfyuiActiveWorkflow === "string" ? source.comfyuiActiveWorkflow.trim() : "";
67
+ return workflows.find((workflow) => workflow.name === activeName) ?? workflows[0];
68
+ }
69
+ /**
70
+ * Combine a workflow's preset with the user prompt: preset first, user second,
71
+ * joined by one comma — never doubled when the preset already ends in a
72
+ * separator, and reduced to the non-empty side when the other is blank.
73
+ */
74
+ function mergeComfyUIPrompt(preset, user) {
75
+ const presetText = typeof preset === "string" ? preset.trim().replace(/[,;\s]+$/, "") : "";
76
+ const userText = user.trim();
77
+ if (presetText.length === 0) return userText;
78
+ if (userText.length === 0) return presetText;
79
+ return `${presetText}, ${userText}`;
80
+ }
34
81
  //#endregion
35
82
  //#region lib/types/config.js
36
83
  /** User-facing configuration for supported image providers. */
@@ -56,6 +103,12 @@ const Config = z.object({
56
103
  dashscopeEndpoint: z.string().default(DEFAULT_DASHSCOPE_ENDPOINT),
57
104
  dashscopeModel: z.string().default(DEFAULT_DASHSCOPE_MODEL),
58
105
  comfyuiBaseURL: z.string().default(DEFAULT_COMFYUI_BASE_URL),
106
+ comfyuiWorkflows: z.array(z.object({
107
+ name: z.string(),
108
+ json: z.string(),
109
+ presetPrompt: z.string().default("")
110
+ })).default([]),
111
+ comfyuiActiveWorkflow: z.string().default(""),
59
112
  comfyuiWorkflowJson: z.string().default(""),
60
113
  comfyuiWorkflowName: z.string().default(""),
61
114
  comfyuiTimeoutMs: z.number().min(1e3).max(36e5).default(DEFAULT_COMFYUI_TIMEOUT_MS),
@@ -86,13 +139,17 @@ function resolveProvider(config) {
86
139
  endpoint: config.dashscopeEndpoint ?? "https://dashscope.aliyuncs.com/api/v1",
87
140
  imageSize: "1024*1024"
88
141
  };
89
- case "comfyui": return {
90
- provider: "comfyui",
91
- baseURL: config.comfyuiBaseURL ?? "http://127.0.0.1:8188",
92
- workflowJson: config.comfyuiWorkflowJson ?? "",
93
- workflowName: config.comfyuiWorkflowName?.trim() || "API workflow",
94
- timeoutMs: config.comfyuiTimeoutMs ?? 3e5
95
- };
142
+ case "comfyui": {
143
+ const workflows = resolveComfyUIWorkflows(config);
144
+ const workflow = activeComfyUIWorkflow(config);
145
+ return {
146
+ provider: "comfyui",
147
+ baseURL: config.comfyuiBaseURL ?? "http://127.0.0.1:8188",
148
+ workflows,
149
+ ...workflow === void 0 ? {} : { workflow },
150
+ timeoutMs: config.comfyuiTimeoutMs ?? 3e5
151
+ };
152
+ }
96
153
  case "google": return {
97
154
  provider: "google",
98
155
  apiKeyEnv: GOOGLE_API_KEY_ENV,
@@ -103,43 +160,82 @@ function resolveProvider(config) {
103
160
  };
104
161
  }
105
162
  }
163
+ /** The workflow a ComfyUI call runs: the requested name when given, else the active one. */
164
+ function selectComfyUIWorkflow(active, requested) {
165
+ if (active.workflow === void 0) throw new Error("ComfyUI image generation requires an imported workflow; import one in Settings > Plugins > Image generation.");
166
+ if (typeof requested !== "string" || requested.trim().length === 0) return active.workflow;
167
+ const name = requested.trim();
168
+ const workflow = active.workflows.find((candidate) => candidate.name === name);
169
+ if (workflow === void 0) throw new Error(`No ComfyUI workflow named "${name}" is configured. Available workflows: ${active.workflows.map((entry) => entry.name).join(", ")}.`);
170
+ return workflow;
171
+ }
106
172
  //#endregion
107
173
  //#region lib/types/comfyui-workflow.js
108
174
  /** Pure ComfyUI API-workflow validation and placeholder injection. */
109
175
  const COMFYUI_PROMPT_PLACEHOLDER = "{{prompt}}";
110
176
  const COMFYUI_SEED_PLACEHOLDER = "{{seed}}";
111
- /** Parse, clone, and inject one prompt plus an optional randomized seed. */
112
- function prepareComfyUIWorkflow(workflowJson, prompt, seed = randomSeed()) {
177
+ const COMFYUI_IMAGE_PLACEHOLDER = "{{image}}";
178
+ /** Legacy single-percent placeholders from early releases, still accepted. */
179
+ const LEGACY_PROMPT_PLACEHOLDER = "%prompt%";
180
+ const LEGACY_SEED_PLACEHOLDER = "%seed%";
181
+ const LEGACY_IMAGE_PLACEHOLDER = "%image%";
182
+ /** LoadImage-style input key that receives the uploaded source image name. */
183
+ const IMAGE_INPUT_KEY = "image";
184
+ /**
185
+ * Parse, clone, and inject one prompt plus an optional randomized seed.
186
+ *
187
+ * Prompt and seed placeholders are replaced inside any string so existing
188
+ * workflows that embed them in longer text keep working. The image
189
+ * placeholder is stricter: it only matches a dedicated `inputs.image` field
190
+ * whose value is exactly the placeholder, and at most one may exist, because
191
+ * the field must carry a single uploaded file name.
192
+ */
193
+ function prepareComfyUIWorkflow(workflowJson, prompt, seed = randomSeed(), image) {
113
194
  const workflow = parseWorkflow(workflowJson);
114
195
  let promptReplacements = 0;
115
196
  const inject = (value) => {
116
197
  if (typeof value === "string") {
117
- if (value === "{{prompt}}") {
118
- promptReplacements += 1;
119
- return prompt;
120
- }
121
- if (value === "{{seed}}") return seed;
122
- if (value.includes("{{prompt}}")) {
198
+ let replaced = value;
199
+ if (replaced.includes("{{prompt}}") || replaced.includes(LEGACY_PROMPT_PLACEHOLDER)) {
123
200
  promptReplacements += 1;
124
- return value.replaceAll(COMFYUI_PROMPT_PLACEHOLDER, prompt);
201
+ replaced = replaced.replaceAll(COMFYUI_PROMPT_PLACEHOLDER, prompt).replaceAll(LEGACY_PROMPT_PLACEHOLDER, prompt);
125
202
  }
126
- return value.includes("{{seed}}") ? value.replaceAll(COMFYUI_SEED_PLACEHOLDER, String(seed)) : value;
203
+ if (replaced === "{{seed}}" || replaced === LEGACY_SEED_PLACEHOLDER) return seed;
204
+ if (replaced.includes("{{seed}}") || replaced.includes(LEGACY_SEED_PLACEHOLDER)) replaced = replaced.replaceAll(COMFYUI_SEED_PLACEHOLDER, String(seed)).replaceAll(LEGACY_SEED_PLACEHOLDER, String(seed));
205
+ return replaced;
127
206
  }
128
207
  if (Array.isArray(value)) return value.map(inject);
129
208
  if (!isRecord(value)) return value;
130
209
  return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, inject(child)]));
131
210
  };
211
+ let imageInputs = 0;
132
212
  const prepared = Object.fromEntries(Object.entries(workflow).map(([nodeId, value]) => {
133
213
  const node = record$4(value);
134
214
  const inputs = node === void 0 ? void 0 : record$4(node.inputs);
135
- return inputs === void 0 ? [nodeId, value] : [nodeId, {
215
+ if (inputs === void 0) return [nodeId, value];
216
+ const nextInputs = inject(inputs);
217
+ if (isImagePlaceholder(nextInputs[IMAGE_INPUT_KEY])) {
218
+ imageInputs += 1;
219
+ if (image !== void 0) nextInputs[IMAGE_INPUT_KEY] = image;
220
+ }
221
+ return [nodeId, {
136
222
  ...node,
137
- inputs: inject(inputs)
223
+ inputs: nextInputs
138
224
  }];
139
225
  }));
140
226
  if (promptReplacements === 0) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} in a text input`);
227
+ if (image === void 0) {
228
+ if (imageInputs > 0) throw new Error(`ComfyUI workflow contains an ${COMFYUI_IMAGE_PLACEHOLDER} image input, which requires edit_image with a source image`);
229
+ return prepared;
230
+ }
231
+ if (imageInputs === 0) throw new Error(`ComfyUI workflow must contain exactly one ${COMFYUI_IMAGE_PLACEHOLDER} image input to edit images`);
232
+ if (imageInputs > 1) throw new Error(`ComfyUI workflow must contain exactly one ${COMFYUI_IMAGE_PLACEHOLDER} image input; found ${String(imageInputs)}`);
141
233
  return prepared;
142
234
  }
235
+ /** Random seed in the 32-bit range ComfyUI samplers accept. */
236
+ function randomSeed() {
237
+ return Math.floor(Math.random() * 4294967296);
238
+ }
143
239
  function parseWorkflow(workflowJson) {
144
240
  if (workflowJson.trim().length === 0) throw new Error("Import a ComfyUI API workflow JSON file in Settings before generating");
145
241
  if (new TextEncoder().encode(workflowJson).byteLength > 5242880) throw new Error("ComfyUI workflow file must be no larger than 5 MB");
@@ -153,11 +249,14 @@ function parseWorkflow(workflowJson) {
153
249
  if (!Object.values(value).some((node) => {
154
250
  const inputs = record$4(node)?.inputs;
155
251
  return inputs !== void 0 && containsPromptPlaceholder(inputs);
156
- })) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} in a text input`);
252
+ })) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} (or ${LEGACY_PROMPT_PLACEHOLDER}) in a text input`);
157
253
  return value;
158
254
  }
255
+ function isImagePlaceholder(value) {
256
+ return value === "{{image}}" || value === LEGACY_IMAGE_PLACEHOLDER;
257
+ }
159
258
  function containsPromptPlaceholder(value) {
160
- if (typeof value === "string") return value.includes(COMFYUI_PROMPT_PLACEHOLDER);
259
+ if (typeof value === "string") return value.includes("{{prompt}}") || value.includes(LEGACY_PROMPT_PLACEHOLDER);
161
260
  if (Array.isArray(value)) return value.some(containsPromptPlaceholder);
162
261
  return isRecord(value) && Object.values(value).some(containsPromptPlaceholder);
163
262
  }
@@ -167,19 +266,37 @@ function isRecord(value) {
167
266
  function record$4(value) {
168
267
  return isRecord(value) ? value : void 0;
169
268
  }
170
- function randomSeed() {
171
- return Math.floor(Math.random() * 4294967296);
172
- }
173
269
  //#endregion
174
270
  //#region lib/types/comfyui.js
175
271
  const ERROR_LIMIT$3 = 4096;
176
272
  const POLL_INTERVAL_MS = 500;
177
273
  const MAX_HISTORY_BYTES = 16777216;
178
- /** Run one ComfyUI workflow and return its first final image. */
274
+ /** Run one ComfyUI text-to-image workflow and return its first final image. */
179
275
  async function generateComfyUIImage(input) {
276
+ const seed = randomSeed();
277
+ const workflow = prepareComfyUIWorkflow(input.workflowJson, input.prompt, seed);
278
+ return {
279
+ ...await runJob(input, async (baseURL, signal) => {
280
+ return await downloadOutput(baseURL, await waitForOutput(baseURL, await submitWorkflow(baseURL, workflow, signal), signal), input.maxBytes, signal);
281
+ }),
282
+ seed
283
+ };
284
+ }
285
+ /** Upload one source image, run the workflow, and return its first final image. */
286
+ async function editComfyUIImage(input) {
287
+ const seed = randomSeed();
288
+ return {
289
+ ...await runJob(input, async (baseURL, signal) => {
290
+ const imageName = await uploadSourceImage(baseURL, input.sourceImage, signal);
291
+ return await downloadOutput(baseURL, await waitForOutput(baseURL, await submitWorkflow(baseURL, prepareComfyUIWorkflow(input.workflowJson, input.prompt, seed, imageName), signal), signal), input.maxBytes, signal);
292
+ }),
293
+ seed
294
+ };
295
+ }
296
+ /** Share abort forwarding, the single timeout, and error normalization. */
297
+ async function runJob(input, run) {
180
298
  input.signal.throwIfAborted();
181
299
  const baseURL = comfyUIBaseURL(input.baseURL);
182
- const workflow = prepareComfyUIWorkflow(input.workflowJson, input.prompt);
183
300
  const controller = new AbortController();
184
301
  const forwardAbort = () => {
185
302
  controller.abort(input.signal.reason);
@@ -189,7 +306,7 @@ async function generateComfyUIImage(input) {
189
306
  controller.abort(/* @__PURE__ */ new Error("ComfyUI generation timed out"));
190
307
  }, input.timeoutMs);
191
308
  try {
192
- return await downloadOutput(baseURL, await waitForOutput(baseURL, await submitWorkflow(baseURL, workflow, controller.signal), controller.signal), input.maxBytes, controller.signal);
309
+ return await run(baseURL, controller.signal);
193
310
  } catch (error) {
194
311
  input.signal.throwIfAborted();
195
312
  if (controller.signal.aborted) throw new Error(`ComfyUI generation timed out after ${String(input.timeoutMs)} ms`);
@@ -200,6 +317,33 @@ async function generateComfyUIImage(input) {
200
317
  input.signal.removeEventListener("abort", forwardAbort);
201
318
  }
202
319
  }
320
+ /** Upload the source image and return the LoadImage-compatible name ComfyUI stored it under. */
321
+ async function uploadSourceImage(baseURL, image, signal) {
322
+ const filename = uploadFilename(image.mediaType);
323
+ const bytes = new Uint8Array(image.data.byteLength);
324
+ bytes.set(image.data);
325
+ const form = new FormData();
326
+ form.append("image", new Blob([bytes], { type: image.mediaType }), filename);
327
+ const response = await fetch(endpoint(baseURL, "upload/image"), {
328
+ method: "POST",
329
+ redirect: "error",
330
+ signal,
331
+ headers: { accept: "application/json" },
332
+ body: form
333
+ });
334
+ const text = await readBoundedText$3(response, ERROR_LIMIT$3);
335
+ if (!response.ok) throw new Error(`ComfyUI image upload failed (${response.status}): ${text}`);
336
+ const payload = parseJsonRecord(text, "ComfyUI /upload/image returned invalid JSON");
337
+ if (typeof payload.name !== "string" || payload.name.length === 0) throw new Error(`ComfyUI /upload/image returned no name: ${text}`);
338
+ const subfolder = typeof payload.subfolder === "string" ? payload.subfolder : "";
339
+ return subfolder.length > 0 ? `${subfolder}/${payload.name}` : payload.name;
340
+ }
341
+ /** LoadImage-style inputs need a file name with a decodable extension. */
342
+ function uploadFilename(mediaType) {
343
+ const extension = mediaType === "image/png" ? "png" : mediaType === "image/jpeg" ? "jpg" : mediaType === "image/webp" ? "webp" : void 0;
344
+ if (extension === void 0) throw new Error(`ComfyUI edit_image accepts PNG, JPEG, or WebP source images; got ${mediaType}`);
345
+ return `dsh-image-gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.${extension}`;
346
+ }
203
347
  async function submitWorkflow(baseURL, workflow, signal) {
204
348
  const response = await fetch(endpoint(baseURL, "prompt"), {
205
349
  method: "POST",
@@ -1242,7 +1386,7 @@ const inject = [
1242
1386
  ];
1243
1387
  function apply(ctx, config = {}) {
1244
1388
  let current = () => config;
1245
- installSettingsSection(ctx, settingsNamespace(IMAGE_GENERATION_NAMESPACE), Config, config, {
1389
+ installImageSettings(ctx, config, {
1246
1390
  setSource: (source) => {
1247
1391
  current = source;
1248
1392
  },
@@ -1287,19 +1431,26 @@ function apply(ctx, config = {}) {
1287
1431
  size: {
1288
1432
  type: "string",
1289
1433
  description: "Optional dimensions or size tier for OpenAI, Seedream, or DashScope."
1434
+ },
1435
+ workflow: {
1436
+ type: "string",
1437
+ description: "Optional name of the ComfyUI workflow to run; omit to use the active workflow from settings. Only meaningful when the ComfyUI provider is selected."
1290
1438
  }
1291
1439
  },
1292
1440
  output: imageOutput("Generated"),
1293
1441
  async execute(args, exec) {
1294
1442
  const active = resolveProvider(current());
1295
- if (active.provider === "comfyui") return saveGenerated(ctx, await generateComfyUIImage({
1296
- baseURL: active.baseURL,
1297
- workflowJson: active.workflowJson,
1298
- prompt: args.prompt,
1299
- timeoutMs: active.timeoutMs,
1300
- maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1301
- signal: exec.signal
1302
- }), active.provider, active.workflowName, "API workflow", current(), exec);
1443
+ if (active.provider === "comfyui") {
1444
+ const workflow = selectComfyUIWorkflow(active, args.workflow);
1445
+ return saveGenerated(ctx, await generateComfyUIImage({
1446
+ baseURL: active.baseURL,
1447
+ workflowJson: workflow.json,
1448
+ prompt: mergeComfyUIPrompt(workflow.presetPrompt, args.prompt),
1449
+ timeoutMs: active.timeoutMs,
1450
+ maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1451
+ signal: exec.signal
1452
+ }), active.provider, workflow.name, "API workflow", current(), exec);
1453
+ }
1303
1454
  const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
1304
1455
  if (credential === void 0 || credential.value.length === 0) throw new Error(`generate_image requires the ${active.apiKeyEnv} credential; configure it in Settings > Plugins > Image generation.`);
1305
1456
  if (active.provider === "google") {
@@ -1394,14 +1545,15 @@ function apply(ctx, config = {}) {
1394
1545
  size: {
1395
1546
  type: "string",
1396
1547
  description: "Optional output size for OpenAI, Seedream, or DashScope."
1548
+ },
1549
+ workflow: {
1550
+ type: "string",
1551
+ description: "Optional name of the ComfyUI workflow to run; omit to use the active workflow from settings. Only meaningful when the ComfyUI provider is selected."
1397
1552
  }
1398
1553
  },
1399
1554
  output: imageOutput("Edited"),
1400
1555
  async execute(args, exec) {
1401
1556
  const active = resolveProvider(current());
1402
- if (active.provider === "comfyui") throw new Error("edit_image is not yet supported by the ComfyUI provider; switch providers or use generate_image");
1403
- const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
1404
- if (credential === void 0 || credential.value.length === 0) throw new Error(`edit_image requires the ${active.apiKeyEnv} credential; configure it in Settings > Plugins > Image generation.`);
1405
1557
  const sourceImages = await resolveReferenceImages({
1406
1558
  ...exec.agent === void 0 ? {} : { agent: exec.agent },
1407
1559
  attachments: ctx.attachments,
@@ -1412,6 +1564,26 @@ function apply(ctx, config = {}) {
1412
1564
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1413
1565
  signal: exec.signal
1414
1566
  });
1567
+ if (active.provider === "comfyui") {
1568
+ if (sourceImages.length > 1) throw new Error(`ComfyUI edit_image supports exactly one source image per call; this call resolved ${String(sourceImages.length)} images. Call edit_image again with source_attachment_id set to the single attachment ID of the image to edit.`);
1569
+ const sourceImage = sourceImages[0];
1570
+ if (sourceImage === void 0) throw new Error("edit_image requires a reference image");
1571
+ const workflow = selectComfyUIWorkflow(active, args.workflow);
1572
+ return saveGenerated(ctx, await editComfyUIImage({
1573
+ baseURL: active.baseURL,
1574
+ workflowJson: workflow.json,
1575
+ prompt: mergeComfyUIPrompt(workflow.presetPrompt, args.prompt),
1576
+ sourceImage: {
1577
+ data: sourceImage.data,
1578
+ mediaType: sourceImage.mediaType
1579
+ },
1580
+ timeoutMs: active.timeoutMs,
1581
+ maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1582
+ signal: exec.signal
1583
+ }), active.provider, workflow.name, "API workflow", current(), exec);
1584
+ }
1585
+ const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
1586
+ if (credential === void 0 || credential.value.length === 0) throw new Error(`edit_image requires the ${active.apiKeyEnv} credential; configure it in Settings > Plugins > Image generation.`);
1415
1587
  if (active.provider === "google") {
1416
1588
  const aspectRatio = args.aspect_ratio ?? active.aspectRatio;
1417
1589
  const imageSize = args.image_size ?? active.imageSize;
@@ -1545,6 +1717,7 @@ function imageOutput(verb) {
1545
1717
  output: value.output,
1546
1718
  ...verb === "Edited" ? { operation: "edit" } : {},
1547
1719
  ...typeof value.savedTo === "string" ? { savedTo: value.savedTo } : {},
1720
+ ...typeof value.seed === "number" ? { seed: value.seed } : {},
1548
1721
  prompt: args.prompt
1549
1722
  })
1550
1723
  };
@@ -1574,7 +1747,8 @@ async function saveGenerated(ctx, generated, provider, model, output, config, ex
1574
1747
  attachment,
1575
1748
  provider,
1576
1749
  model,
1577
- output
1750
+ output,
1751
+ ...typeof generated.seed === "number" ? { seed: generated.seed } : {}
1578
1752
  };
1579
1753
  if (config.saveToWorkspace === false) return value;
1580
1754
  const workspaceRoot = exec.agent?.session.header.cwd;
@@ -1606,5 +1780,28 @@ function imagePresentation(result) {
1606
1780
  }]
1607
1781
  };
1608
1782
  }
1783
+ /**
1784
+ * Wire the settings namespace across both dsh-settings API generations.
1785
+ * A namespace import keeps module loading safe on either version; the branch
1786
+ * picks the service method (0.1.2+) or the legacy top-level relay (<= rc.2),
1787
+ * and falls back to the composition entry with a warning when neither exists
1788
+ * so an incompatible host degrades the settings UI instead of failing boot.
1789
+ */
1790
+ function installImageSettings(ctx, config, hooks) {
1791
+ const namespace = dshSettings;
1792
+ if (typeof namespace.SettingsProvider?.prototype?.installSection === "function") {
1793
+ ctx.inject(["settings"], (settingsCtx) => {
1794
+ settingsCtx.settings.installSection(ctx, IMAGE_GENERATION_NAMESPACE, Config, config, hooks);
1795
+ });
1796
+ return;
1797
+ }
1798
+ const legacyInstall = namespace.installSettingsSection;
1799
+ const legacyNamespace = namespace.settingsNamespace;
1800
+ if (typeof legacyInstall === "function" && typeof legacyNamespace === "function") {
1801
+ legacyInstall(ctx, legacyNamespace(IMAGE_GENERATION_NAMESPACE), Config, config, hooks);
1802
+ return;
1803
+ }
1804
+ ctx.logger.warn("dsh-image-gen: this DSH exposes neither settings API generation; settings UI stays on the composition entry");
1805
+ }
1609
1806
  //#endregion
1610
1807
  export { Config, IMAGE_ROUTE, apply, imageAttachmentFromMeta, inject, name };
@@ -15,6 +15,8 @@ export interface GalleryItem {
15
15
  aspectRatio?: string;
16
16
  imageSize?: string;
17
17
  output?: string;
18
+ /** Workflow seed reported by the ComfyUI provider. Optional so records written by older versions stay readable; the object store is schemaless, so no IndexedDB version bump is needed. */
19
+ seed?: number;
18
20
  }
19
21
  type GalleryListener = () => void;
20
22
  /**
@@ -18,6 +18,7 @@ declare const DICT: {
18
18
  readonly filterOpenAI: "OpenAI / 中转站";
19
19
  readonly filterSeedream: "字节 Seedream";
20
20
  readonly filterDashScope: "阿里 DashScope";
21
+ readonly filterComfyUI: "本地 ComfyUI";
21
22
  readonly emptyTitle: "暂无生图记录";
22
23
  readonly emptyDesc: "在对话中让 Agent 生图后,生成的图片会自动收录到这里。";
23
24
  readonly noMatchTitle: "未找到匹配结果";
@@ -44,6 +45,7 @@ declare const DICT: {
44
45
  readonly filterOpenAI: "OpenAI / Relay";
45
46
  readonly filterSeedream: "ByteDance Seedream";
46
47
  readonly filterDashScope: "Aliyun DashScope";
48
+ readonly filterComfyUI: "Local ComfyUI";
47
49
  readonly emptyTitle: "No images generated yet";
48
50
  readonly emptyDesc: "Images generated during conversations will automatically appear here.";
49
51
  readonly noMatchTitle: "No matching images";
@@ -8,6 +8,8 @@ export interface ImageResultPresentation {
8
8
  readonly model: string;
9
9
  readonly output: string;
10
10
  readonly savedTo?: string;
11
+ /** Workflow seed reported by the ComfyUI provider, when available. */
12
+ readonly seed?: number;
11
13
  }
12
14
  interface ImageResultState {
13
15
  readonly turn: number;
@@ -46,8 +48,23 @@ interface ConversationNodeDefinitionLike<State> {
46
48
  }, match: MatchLike): State;
47
49
  buildViewNode(context: ContextLike<State>): Record<string, unknown> | null;
48
50
  }
49
- /** One image result row per Turn, re-anchored to Turn end after the final answer settles. */
51
+ /** Options controlling where the artifact anchors as its Turn progresses. */
52
+ export interface ImageResultNodeOptions {
53
+ /**
54
+ * Whether the host transcript view folds completed-Turn process content
55
+ * (Compact mode). Omission or a thrown/unavailable read falls back to
56
+ * Compact-safe anchoring, which keeps the artifact always visible.
57
+ */
58
+ readonly isCompactTranscript?: () => boolean;
59
+ }
60
+ /** Default definition: unknown transcript mode falls back to Compact-safe anchoring. */
50
61
  export declare const imageResultDefinition: ConversationNodeDefinitionLike<ImageResultState>;
62
+ /**
63
+ * One image result row per Turn, anchored at its own tool/result position while
64
+ * the Turn runs, then optionally re-anchored beside the final answer once the
65
+ * Turn closes (only needed under the folding Compact transcript view).
66
+ */
67
+ export declare function createImageResultDefinition(options?: ImageResultNodeOptions): ConversationNodeDefinitionLike<ImageResultState>;
51
68
  /** Parse the plugin-owned durable presentation metadata from a Tool result event. */
52
69
  export declare function imageResultFromMeta(value: unknown): ImageResultPresentation | undefined;
53
70
  export {};
@@ -1,7 +1,7 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import type { SettingsScope } from '@deepseek-ai/dsh-client-runtime/client';
3
3
  import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
4
- import { type ImageProvider } from '../shared.js';
4
+ import { type ComfyUIWorkflowEntry, type ImageProvider } from '../shared.js';
5
5
  import { type LocaleService } from './gallery-view.js';
6
6
  import { type ImageResultPresentation } from './image-result-node.js';
7
7
  type Provider = ImageProvider;
@@ -16,6 +16,8 @@ interface ImageSettings {
16
16
  dashscopeEndpoint?: string;
17
17
  dashscopeModel?: string;
18
18
  comfyuiBaseURL?: string;
19
+ comfyuiWorkflows?: ComfyUIWorkflowEntry[];
20
+ comfyuiActiveWorkflow?: string;
19
21
  comfyuiWorkflowJson?: string;
20
22
  comfyuiWorkflowName?: string;
21
23
  comfyuiTimeoutMs?: number;
@@ -1,8 +1,19 @@
1
1
  export declare const COMFYUI_PROMPT_PLACEHOLDER = "{{prompt}}";
2
2
  export declare const COMFYUI_SEED_PLACEHOLDER = "{{seed}}";
3
+ export declare const COMFYUI_IMAGE_PLACEHOLDER = "{{image}}";
3
4
  type JsonRecord = Record<string, unknown>;
4
5
  /** Validate imported JSON without exposing workflow graph details to callers. */
5
6
  export declare function validateComfyUIWorkflowJson(workflowJson: string): void;
6
- /** Parse, clone, and inject one prompt plus an optional randomized seed. */
7
- export declare function prepareComfyUIWorkflow(workflowJson: string, prompt: string, seed?: number): JsonRecord;
7
+ /**
8
+ * Parse, clone, and inject one prompt plus an optional randomized seed.
9
+ *
10
+ * Prompt and seed placeholders are replaced inside any string so existing
11
+ * workflows that embed them in longer text keep working. The image
12
+ * placeholder is stricter: it only matches a dedicated `inputs.image` field
13
+ * whose value is exactly the placeholder, and at most one may exist, because
14
+ * the field must carry a single uploaded file name.
15
+ */
16
+ export declare function prepareComfyUIWorkflow(workflowJson: string, prompt: string, seed?: number, image?: string): JsonRecord;
17
+ /** Random seed in the 32-bit range ComfyUI samplers accept. */
18
+ export declare function randomSeed(): number;
8
19
  export {};
@@ -1,15 +1,32 @@
1
- /** ComfyUI text-to-image adapter using an imported API-format workflow. */
1
+ /** ComfyUI text-to-image and image-to-image adapters using an imported API-format workflow. */
2
2
  import type { ImageMediaType } from '@deepseek-ai/dsh-attachment';
3
3
  export interface GeneratedComfyUIImage {
4
4
  data: Uint8Array;
5
5
  mediaType: ImageMediaType;
6
+ /** Concrete seed injected into the workflow, surfaced for provenance metadata. */
7
+ seed: number;
6
8
  }
7
- /** Run one ComfyUI workflow and return its first final image. */
8
- export declare function generateComfyUIImage(input: {
9
+ /** Source image bytes a ComfyUI workflow accepts through its image input. */
10
+ export interface ComfyUISourceImage {
11
+ data: Uint8Array;
12
+ mediaType: ImageMediaType;
13
+ }
14
+ interface ComfyUIJobInput {
9
15
  baseURL: string;
16
+ timeoutMs: number;
17
+ signal: AbortSignal;
18
+ }
19
+ /** Run one ComfyUI text-to-image workflow and return its first final image. */
20
+ export declare function generateComfyUIImage(input: ComfyUIJobInput & {
10
21
  workflowJson: string;
11
22
  prompt: string;
12
- timeoutMs: number;
13
23
  maxBytes: number;
14
- signal: AbortSignal;
15
24
  }): Promise<GeneratedComfyUIImage>;
25
+ /** Upload one source image, run the workflow, and return its first final image. */
26
+ export declare function editComfyUIImage(input: ComfyUIJobInput & {
27
+ workflowJson: string;
28
+ prompt: string;
29
+ sourceImage: ComfyUISourceImage;
30
+ maxBytes: number;
31
+ }): Promise<GeneratedComfyUIImage>;
32
+ export {};
@@ -1,7 +1,7 @@
1
1
  /** User-facing configuration for supported image providers. */
2
2
  import z from '@deepseek-ai/schemastery';
3
- import { DEFAULT_DASHSCOPE_ENDPOINT, DEFAULT_DASHSCOPE_MODEL, DEFAULT_COMFYUI_BASE_URL, DEFAULT_COMFYUI_TIMEOUT_MS, DEFAULT_COMFYUI_WORKFLOW_LABEL, DEFAULT_GOOGLE_ENDPOINT, DEFAULT_GOOGLE_MODEL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_SEEDREAM_BASE_URL, DEFAULT_SEEDREAM_MODEL, IMAGE_PROVIDERS, type ImageProvider } from './shared.js';
4
- export { DEFAULT_DASHSCOPE_ENDPOINT, DEFAULT_DASHSCOPE_MODEL, DEFAULT_COMFYUI_BASE_URL, DEFAULT_COMFYUI_TIMEOUT_MS, DEFAULT_COMFYUI_WORKFLOW_LABEL, DEFAULT_GOOGLE_ENDPOINT, DEFAULT_GOOGLE_MODEL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_SEEDREAM_BASE_URL, DEFAULT_SEEDREAM_MODEL, IMAGE_PROVIDERS, type ImageProvider, };
3
+ import { DEFAULT_DASHSCOPE_ENDPOINT, DEFAULT_DASHSCOPE_MODEL, DEFAULT_COMFYUI_BASE_URL, DEFAULT_COMFYUI_TIMEOUT_MS, DEFAULT_COMFYUI_WORKFLOW_LABEL, DEFAULT_GOOGLE_ENDPOINT, DEFAULT_GOOGLE_MODEL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_SEEDREAM_BASE_URL, DEFAULT_SEEDREAM_MODEL, IMAGE_PROVIDERS, activeComfyUIWorkflow, resolveComfyUIWorkflows, type ComfyUIWorkflowEntry, type ImageProvider } from './shared.js';
4
+ export { DEFAULT_DASHSCOPE_ENDPOINT, DEFAULT_DASHSCOPE_MODEL, DEFAULT_COMFYUI_BASE_URL, DEFAULT_COMFYUI_TIMEOUT_MS, DEFAULT_COMFYUI_WORKFLOW_LABEL, DEFAULT_GOOGLE_ENDPOINT, DEFAULT_GOOGLE_MODEL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_SEEDREAM_BASE_URL, DEFAULT_SEEDREAM_MODEL, IMAGE_PROVIDERS, activeComfyUIWorkflow, resolveComfyUIWorkflows, type ComfyUIWorkflowEntry, type ImageProvider, };
5
5
  /** Default workspace subfolder that receives generated image files. */
6
6
  export declare const DEFAULT_WORKSPACE_FOLDER = "dsh-image-gen";
7
7
  /** Google API credential reference. */
@@ -29,9 +29,13 @@ export interface Config {
29
29
  dashscopeEndpoint?: string;
30
30
  dashscopeModel?: string;
31
31
  comfyuiBaseURL?: string;
32
- /** ComfyUI API-format workflow JSON imported through the Web settings page. */
32
+ /** Named ComfyUI workflows managed by the Web settings page. */
33
+ comfyuiWorkflows?: ComfyUIWorkflowEntry[];
34
+ /** Name of the workflow ComfyUI calls use by default. */
35
+ comfyuiActiveWorkflow?: string;
36
+ /** Legacy single-workflow storage; synced to the active entry for downgrades. */
33
37
  comfyuiWorkflowJson?: string;
34
- /** Original imported file name, used as a human-readable result label. */
38
+ /** Original imported file name of the legacy single workflow. */
35
39
  comfyuiWorkflowName?: string;
36
40
  comfyuiTimeoutMs?: number;
37
41
  /** Also write every generated image as a file under the session workspace. */
@@ -70,7 +74,12 @@ export declare function resolveProvider(config: Config): {
70
74
  } | {
71
75
  provider: 'comfyui';
72
76
  baseURL: string;
73
- workflowJson: string;
74
- workflowName: string;
77
+ workflows: ComfyUIWorkflowEntry[];
78
+ workflow?: ComfyUIWorkflowEntry;
75
79
  timeoutMs: number;
76
80
  };
81
+ /** The workflow a ComfyUI call runs: the requested name when given, else the active one. */
82
+ export declare function selectComfyUIWorkflow(active: {
83
+ workflows: ComfyUIWorkflowEntry[];
84
+ workflow?: ComfyUIWorkflowEntry;
85
+ }, requested?: string): ComfyUIWorkflowEntry;
@@ -20,5 +20,36 @@ export declare const DEFAULT_GOOGLE_MODEL = "gemini-3.1-flash-image";
20
20
  export declare const DEFAULT_OPENAI_MODEL = "gpt-image-2";
21
21
  export declare const DEFAULT_SEEDREAM_MODEL = "doubao-seedream-5-0-260128";
22
22
  export declare const DEFAULT_DASHSCOPE_MODEL = "qwen-image-3.0";
23
+ /** One named ComfyUI API-format workflow imported through settings. */
24
+ export interface ComfyUIWorkflowEntry {
25
+ /** Unique human-readable label; used as the result model and by tool calls. */
26
+ name: string;
27
+ /** API-format workflow JSON with {{prompt}} / {{seed}} and optional {{image}} placeholders. */
28
+ json: string;
29
+ /** Optional preset prepended to the user prompt on every call of this workflow. */
30
+ presetPrompt?: string;
31
+ }
32
+ /** Raw ComfyUI workflow fields as persisted through DSH Settings. */
33
+ export interface ComfyUIWorkflowSource {
34
+ /** Named workflows managed by the Web settings page. */
35
+ comfyuiWorkflows?: readonly ComfyUIWorkflowEntry[];
36
+ /** Name of the entry ComfyUI calls use by default. */
37
+ comfyuiActiveWorkflow?: string;
38
+ /** Legacy single-workflow storage; synced to the active entry for downgrades. */
39
+ comfyuiWorkflowJson?: string;
40
+ comfyuiWorkflowName?: string;
41
+ }
42
+ /** Named workflows, falling back to the legacy single-workflow fields when the list is empty. */
43
+ export declare function resolveComfyUIWorkflows(source: ComfyUIWorkflowSource): ComfyUIWorkflowEntry[];
44
+ /** The workflow ComfyUI calls use by default: the configured active name, else the first entry. */
45
+ export declare function activeComfyUIWorkflow(source: ComfyUIWorkflowSource): ComfyUIWorkflowEntry | undefined;
46
+ /** Derive a workflow label that does not collide with the given existing names. */
47
+ export declare function uniqueComfyUIWorkflowName(name: string, existing: readonly string[]): string;
48
+ /**
49
+ * Combine a workflow's preset with the user prompt: preset first, user second,
50
+ * joined by one comma — never doubled when the preset already ends in a
51
+ * separator, and reduced to the non-empty side when the other is blank.
52
+ */
53
+ export declare function mergeComfyUIPrompt(preset: string | undefined, user: string): string;
23
54
  export declare const DEFAULT_MODELS: Record<ImageProvider, string>;
24
55
  export declare const DEFAULT_BASE_URLS: Record<ImageProvider, string>;