dsh-image-gen 0.2.0 → 0.2.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/README.en.md +172 -171
- package/README.md +182 -187
- package/README.zh-CN.md +172 -171
- package/lib/client.js +186 -25
- package/lib/client.js.map +1 -1
- package/lib/index.js +385 -33
- package/lib/types/client/index.d.ts +22 -2
- package/lib/types/comfyui-workflow.d.ts +8 -0
- package/lib/types/comfyui.d.ts +15 -0
- package/lib/types/config.d.ts +14 -2
- package/lib/types/dashscope.d.ts +2 -2
- package/lib/types/google.d.ts +2 -2
- package/lib/types/openai-compatible.d.ts +1 -1
- package/lib/types/reference-image.d.ts +17 -0
- package/lib/types/seedream.d.ts +2 -2
- package/lib/types/shared.d.ts +5 -1
- package/package.json +133 -131
- package/skills/install-dsh-image-gen/SKILL.md +22 -22
package/lib/index.js
CHANGED
|
@@ -16,13 +16,16 @@ const IMAGE_PROVIDERS = [
|
|
|
16
16
|
"google",
|
|
17
17
|
"openai",
|
|
18
18
|
"seedream",
|
|
19
|
-
"dashscope"
|
|
19
|
+
"dashscope",
|
|
20
|
+
"comfyui"
|
|
20
21
|
];
|
|
21
22
|
/** Default endpoints and base URLs. */
|
|
22
23
|
const DEFAULT_GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/interactions";
|
|
23
24
|
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
24
25
|
const DEFAULT_SEEDREAM_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
|
|
25
26
|
const DEFAULT_DASHSCOPE_ENDPOINT = "https://dashscope.aliyuncs.com/api/v1";
|
|
27
|
+
const DEFAULT_COMFYUI_BASE_URL = "http://127.0.0.1:8188";
|
|
28
|
+
const DEFAULT_COMFYUI_TIMEOUT_MS = 3e5;
|
|
26
29
|
/** Default model names. */
|
|
27
30
|
const DEFAULT_GOOGLE_MODEL = "gemini-3.1-flash-image";
|
|
28
31
|
const DEFAULT_OPENAI_MODEL = "gpt-image-2";
|
|
@@ -52,6 +55,10 @@ const Config = z.object({
|
|
|
52
55
|
seedreamModel: z.string().default(DEFAULT_SEEDREAM_MODEL),
|
|
53
56
|
dashscopeEndpoint: z.string().default(DEFAULT_DASHSCOPE_ENDPOINT),
|
|
54
57
|
dashscopeModel: z.string().default(DEFAULT_DASHSCOPE_MODEL),
|
|
58
|
+
comfyuiBaseURL: z.string().default(DEFAULT_COMFYUI_BASE_URL),
|
|
59
|
+
comfyuiWorkflowJson: z.string().default(""),
|
|
60
|
+
comfyuiWorkflowName: z.string().default(""),
|
|
61
|
+
comfyuiTimeoutMs: z.number().min(1e3).max(36e5).default(DEFAULT_COMFYUI_TIMEOUT_MS),
|
|
55
62
|
saveToWorkspace: z.boolean().default(true),
|
|
56
63
|
workspaceFolder: z.string().default(DEFAULT_WORKSPACE_FOLDER)
|
|
57
64
|
});
|
|
@@ -79,6 +86,13 @@ function resolveProvider(config) {
|
|
|
79
86
|
endpoint: config.dashscopeEndpoint ?? "https://dashscope.aliyuncs.com/api/v1",
|
|
80
87
|
imageSize: "1024*1024"
|
|
81
88
|
};
|
|
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
|
+
};
|
|
82
96
|
case "google": return {
|
|
83
97
|
provider: "google",
|
|
84
98
|
apiKeyEnv: GOOGLE_API_KEY_ENV,
|
|
@@ -90,6 +104,256 @@ function resolveProvider(config) {
|
|
|
90
104
|
}
|
|
91
105
|
}
|
|
92
106
|
//#endregion
|
|
107
|
+
//#region lib/types/comfyui-workflow.js
|
|
108
|
+
/** Pure ComfyUI API-workflow validation and placeholder injection. */
|
|
109
|
+
const COMFYUI_PROMPT_PLACEHOLDER = "{{prompt}}";
|
|
110
|
+
const COMFYUI_SEED_PLACEHOLDER = "{{seed}}";
|
|
111
|
+
/** Parse, clone, and inject one prompt plus an optional randomized seed. */
|
|
112
|
+
function prepareComfyUIWorkflow(workflowJson, prompt, seed = randomSeed()) {
|
|
113
|
+
const workflow = parseWorkflow(workflowJson);
|
|
114
|
+
let promptReplacements = 0;
|
|
115
|
+
const inject = (value) => {
|
|
116
|
+
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}}")) {
|
|
123
|
+
promptReplacements += 1;
|
|
124
|
+
return value.replaceAll(COMFYUI_PROMPT_PLACEHOLDER, prompt);
|
|
125
|
+
}
|
|
126
|
+
return value.includes("{{seed}}") ? value.replaceAll(COMFYUI_SEED_PLACEHOLDER, String(seed)) : value;
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(value)) return value.map(inject);
|
|
129
|
+
if (!isRecord(value)) return value;
|
|
130
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, inject(child)]));
|
|
131
|
+
};
|
|
132
|
+
const prepared = Object.fromEntries(Object.entries(workflow).map(([nodeId, value]) => {
|
|
133
|
+
const node = record$4(value);
|
|
134
|
+
const inputs = node === void 0 ? void 0 : record$4(node.inputs);
|
|
135
|
+
return inputs === void 0 ? [nodeId, value] : [nodeId, {
|
|
136
|
+
...node,
|
|
137
|
+
inputs: inject(inputs)
|
|
138
|
+
}];
|
|
139
|
+
}));
|
|
140
|
+
if (promptReplacements === 0) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} in a text input`);
|
|
141
|
+
return prepared;
|
|
142
|
+
}
|
|
143
|
+
function parseWorkflow(workflowJson) {
|
|
144
|
+
if (workflowJson.trim().length === 0) throw new Error("Import a ComfyUI API workflow JSON file in Settings before generating");
|
|
145
|
+
if (new TextEncoder().encode(workflowJson).byteLength > 5242880) throw new Error("ComfyUI workflow file must be no larger than 5 MB");
|
|
146
|
+
let value;
|
|
147
|
+
try {
|
|
148
|
+
value = JSON.parse(workflowJson);
|
|
149
|
+
} catch {
|
|
150
|
+
throw new Error("ComfyUI workflow file is not valid JSON");
|
|
151
|
+
}
|
|
152
|
+
if (!isRecord(value) || Object.keys(value).length === 0) throw new Error("ComfyUI workflow must be a non-empty API-format JSON object");
|
|
153
|
+
if (!Object.values(value).some((node) => {
|
|
154
|
+
const inputs = record$4(node)?.inputs;
|
|
155
|
+
return inputs !== void 0 && containsPromptPlaceholder(inputs);
|
|
156
|
+
})) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} in a text input`);
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
function containsPromptPlaceholder(value) {
|
|
160
|
+
if (typeof value === "string") return value.includes(COMFYUI_PROMPT_PLACEHOLDER);
|
|
161
|
+
if (Array.isArray(value)) return value.some(containsPromptPlaceholder);
|
|
162
|
+
return isRecord(value) && Object.values(value).some(containsPromptPlaceholder);
|
|
163
|
+
}
|
|
164
|
+
function isRecord(value) {
|
|
165
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
166
|
+
}
|
|
167
|
+
function record$4(value) {
|
|
168
|
+
return isRecord(value) ? value : void 0;
|
|
169
|
+
}
|
|
170
|
+
function randomSeed() {
|
|
171
|
+
return Math.floor(Math.random() * 4294967296);
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region lib/types/comfyui.js
|
|
175
|
+
const ERROR_LIMIT$3 = 4096;
|
|
176
|
+
const POLL_INTERVAL_MS = 500;
|
|
177
|
+
const MAX_HISTORY_BYTES = 16777216;
|
|
178
|
+
/** Run one ComfyUI workflow and return its first final image. */
|
|
179
|
+
async function generateComfyUIImage(input) {
|
|
180
|
+
input.signal.throwIfAborted();
|
|
181
|
+
const baseURL = comfyUIBaseURL(input.baseURL);
|
|
182
|
+
const workflow = prepareComfyUIWorkflow(input.workflowJson, input.prompt);
|
|
183
|
+
const controller = new AbortController();
|
|
184
|
+
const forwardAbort = () => {
|
|
185
|
+
controller.abort(input.signal.reason);
|
|
186
|
+
};
|
|
187
|
+
input.signal.addEventListener("abort", forwardAbort, { once: true });
|
|
188
|
+
const timeout = setTimeout(() => {
|
|
189
|
+
controller.abort(/* @__PURE__ */ new Error("ComfyUI generation timed out"));
|
|
190
|
+
}, input.timeoutMs);
|
|
191
|
+
try {
|
|
192
|
+
return await downloadOutput(baseURL, await waitForOutput(baseURL, await submitWorkflow(baseURL, workflow, controller.signal), controller.signal), input.maxBytes, controller.signal);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
input.signal.throwIfAborted();
|
|
195
|
+
if (controller.signal.aborted) throw new Error(`ComfyUI generation timed out after ${String(input.timeoutMs)} ms`);
|
|
196
|
+
if (error instanceof TypeError) throw new Error(`Could not connect to ComfyUI at ${baseURL.origin}`);
|
|
197
|
+
throw error;
|
|
198
|
+
} finally {
|
|
199
|
+
clearTimeout(timeout);
|
|
200
|
+
input.signal.removeEventListener("abort", forwardAbort);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function submitWorkflow(baseURL, workflow, signal) {
|
|
204
|
+
const response = await fetch(endpoint(baseURL, "prompt"), {
|
|
205
|
+
method: "POST",
|
|
206
|
+
redirect: "error",
|
|
207
|
+
signal,
|
|
208
|
+
headers: {
|
|
209
|
+
"content-type": "application/json",
|
|
210
|
+
accept: "application/json"
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify({ prompt: workflow })
|
|
213
|
+
});
|
|
214
|
+
const text = await readBoundedText$3(response, ERROR_LIMIT$3);
|
|
215
|
+
if (!response.ok) throw new Error(`ComfyUI rejected the workflow (${response.status}): ${text}`);
|
|
216
|
+
const payload = parseJsonRecord(text, "ComfyUI /prompt returned invalid JSON");
|
|
217
|
+
if (typeof payload.prompt_id !== "string" || payload.prompt_id.length === 0) throw new Error(`ComfyUI /prompt returned no prompt_id: ${text}`);
|
|
218
|
+
return payload.prompt_id;
|
|
219
|
+
}
|
|
220
|
+
async function waitForOutput(baseURL, promptId, signal) {
|
|
221
|
+
for (;;) {
|
|
222
|
+
signal.throwIfAborted();
|
|
223
|
+
const response = await fetch(endpoint(baseURL, `history/${encodeURIComponent(promptId)}`), {
|
|
224
|
+
redirect: "error",
|
|
225
|
+
signal,
|
|
226
|
+
headers: { accept: "application/json" }
|
|
227
|
+
});
|
|
228
|
+
const text = await readBoundedText$3(response, MAX_HISTORY_BYTES);
|
|
229
|
+
if (!response.ok) throw new Error(`ComfyUI history request failed (${response.status}): ${text.slice(0, ERROR_LIMIT$3)}`);
|
|
230
|
+
const entry = record$3(parseJsonRecord(text, "ComfyUI history returned invalid JSON")[promptId]);
|
|
231
|
+
if (entry !== void 0) {
|
|
232
|
+
const status = record$3(entry.status);
|
|
233
|
+
if (status?.status_str === "error") throw new Error(`ComfyUI workflow failed: ${JSON.stringify(status.messages ?? status).slice(0, ERROR_LIMIT$3)}`);
|
|
234
|
+
const output = firstOutputImage(entry.outputs);
|
|
235
|
+
if (output !== void 0) return output;
|
|
236
|
+
if (status?.completed === true) throw new Error("ComfyUI workflow completed without an output image");
|
|
237
|
+
}
|
|
238
|
+
await delay(POLL_INTERVAL_MS, signal);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function firstOutputImage(value) {
|
|
242
|
+
const outputs = record$3(value);
|
|
243
|
+
if (outputs === void 0) return void 0;
|
|
244
|
+
for (const nodeOutput of Object.values(outputs)) {
|
|
245
|
+
const images = record$3(nodeOutput)?.images;
|
|
246
|
+
if (!Array.isArray(images)) continue;
|
|
247
|
+
for (const image of images) {
|
|
248
|
+
const item = record$3(image);
|
|
249
|
+
if (typeof item?.filename !== "string" || item.filename.length === 0) continue;
|
|
250
|
+
const output = {
|
|
251
|
+
filename: item.filename,
|
|
252
|
+
subfolder: typeof item.subfolder === "string" ? item.subfolder : "",
|
|
253
|
+
type: typeof item.type === "string" ? item.type : "output"
|
|
254
|
+
};
|
|
255
|
+
if (output.type === "output") return output;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function downloadOutput(baseURL, output, maxBytes, signal) {
|
|
260
|
+
const url = endpoint(baseURL, "view");
|
|
261
|
+
url.searchParams.set("filename", output.filename);
|
|
262
|
+
url.searchParams.set("subfolder", output.subfolder);
|
|
263
|
+
url.searchParams.set("type", output.type);
|
|
264
|
+
const response = await fetch(url, {
|
|
265
|
+
redirect: "error",
|
|
266
|
+
signal
|
|
267
|
+
});
|
|
268
|
+
if (!response.ok) throw new Error(`ComfyUI image download failed (${response.status})`);
|
|
269
|
+
const mediaType = imageMediaType$3(response.headers.get("content-type")) ?? imageMediaTypeFromName(output.filename);
|
|
270
|
+
if (mediaType === void 0) throw new Error("ComfyUI image download returned an unsupported content type");
|
|
271
|
+
return {
|
|
272
|
+
data: await readBoundedBytes$2(response, maxBytes),
|
|
273
|
+
mediaType
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function comfyUIBaseURL(value) {
|
|
277
|
+
let url;
|
|
278
|
+
try {
|
|
279
|
+
url = new URL(value.endsWith("/") ? value : `${value}/`);
|
|
280
|
+
} catch {
|
|
281
|
+
throw new Error("ComfyUI URL must be an absolute http:// or https:// URL");
|
|
282
|
+
}
|
|
283
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("ComfyUI URL must use http:// or https://");
|
|
284
|
+
return url;
|
|
285
|
+
}
|
|
286
|
+
function endpoint(baseURL, path) {
|
|
287
|
+
return new URL(path, baseURL);
|
|
288
|
+
}
|
|
289
|
+
function record$3(value) {
|
|
290
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
291
|
+
}
|
|
292
|
+
function parseJsonRecord(text, message) {
|
|
293
|
+
let value;
|
|
294
|
+
try {
|
|
295
|
+
value = JSON.parse(text);
|
|
296
|
+
} catch {
|
|
297
|
+
throw new Error(message);
|
|
298
|
+
}
|
|
299
|
+
const parsed = record$3(value);
|
|
300
|
+
if (parsed === void 0) throw new Error(message);
|
|
301
|
+
return parsed;
|
|
302
|
+
}
|
|
303
|
+
function imageMediaType$3(value) {
|
|
304
|
+
const mediaType = value?.split(";", 1)[0]?.trim().toLowerCase();
|
|
305
|
+
return mediaType === "image/png" || mediaType === "image/jpeg" || mediaType === "image/webp" || mediaType === "image/gif" ? mediaType : void 0;
|
|
306
|
+
}
|
|
307
|
+
function imageMediaTypeFromName(filename) {
|
|
308
|
+
const lower = filename.toLowerCase();
|
|
309
|
+
if (lower.endsWith(".png")) return "image/png";
|
|
310
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
311
|
+
if (lower.endsWith(".webp")) return "image/webp";
|
|
312
|
+
if (lower.endsWith(".gif")) return "image/gif";
|
|
313
|
+
}
|
|
314
|
+
async function delay(milliseconds, signal) {
|
|
315
|
+
await new Promise((resolve, reject) => {
|
|
316
|
+
const timer = setTimeout(done, milliseconds);
|
|
317
|
+
const abort = () => {
|
|
318
|
+
clearTimeout(timer);
|
|
319
|
+
signal.removeEventListener("abort", abort);
|
|
320
|
+
reject(signal.reason);
|
|
321
|
+
};
|
|
322
|
+
function done() {
|
|
323
|
+
signal.removeEventListener("abort", abort);
|
|
324
|
+
resolve();
|
|
325
|
+
}
|
|
326
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
async function readBoundedText$3(response, maxBytes) {
|
|
330
|
+
return new TextDecoder().decode(await readBoundedBytes$2(response, maxBytes));
|
|
331
|
+
}
|
|
332
|
+
async function readBoundedBytes$2(response, maxBytes) {
|
|
333
|
+
if (response.body === null) return /* @__PURE__ */ new Uint8Array();
|
|
334
|
+
const reader = response.body.getReader();
|
|
335
|
+
const chunks = [];
|
|
336
|
+
let bytes = 0;
|
|
337
|
+
try {
|
|
338
|
+
for (;;) {
|
|
339
|
+
const next = await reader.read();
|
|
340
|
+
if (next.done) break;
|
|
341
|
+
bytes += next.value.byteLength;
|
|
342
|
+
if (bytes > maxBytes) throw new Error(`ComfyUI response exceeded the ${String(maxBytes)} byte limit`);
|
|
343
|
+
chunks.push(next.value);
|
|
344
|
+
}
|
|
345
|
+
} finally {
|
|
346
|
+
reader.releaseLock();
|
|
347
|
+
}
|
|
348
|
+
const joined = new Uint8Array(bytes);
|
|
349
|
+
let offset = 0;
|
|
350
|
+
for (const chunk of chunks) {
|
|
351
|
+
joined.set(chunk, offset);
|
|
352
|
+
offset += chunk.byteLength;
|
|
353
|
+
}
|
|
354
|
+
return joined;
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
93
357
|
//#region lib/types/dashscope.js
|
|
94
358
|
async function generateDashScopeImage(options) {
|
|
95
359
|
assertQwenImageModel(options.model);
|
|
@@ -116,7 +380,7 @@ async function editDashScopeImage(options) {
|
|
|
116
380
|
model: options.model,
|
|
117
381
|
input: { messages: [{
|
|
118
382
|
role: "user",
|
|
119
|
-
content: [{ image: toDataUrl$1(
|
|
383
|
+
content: [...options.sourceImages.map((sourceImage) => ({ image: toDataUrl$1(sourceImage) })), { text: options.prompt }]
|
|
120
384
|
}] },
|
|
121
385
|
parameters: {
|
|
122
386
|
prompt_extend: true,
|
|
@@ -197,11 +461,11 @@ function editGoogleImage(input) {
|
|
|
197
461
|
interactionInput: [{
|
|
198
462
|
type: "text",
|
|
199
463
|
text: input.prompt
|
|
200
|
-
}, {
|
|
464
|
+
}, ...input.sourceImages.map((sourceImage) => ({
|
|
201
465
|
type: "image",
|
|
202
|
-
mime_type:
|
|
203
|
-
data: Buffer.from(
|
|
204
|
-
}]
|
|
466
|
+
mime_type: sourceImage.mediaType,
|
|
467
|
+
data: Buffer.from(sourceImage.data).toString("base64")
|
|
468
|
+
}))]
|
|
205
469
|
});
|
|
206
470
|
}
|
|
207
471
|
/** Shared Google request, response parsing, decoding, and size enforcement. */
|
|
@@ -309,30 +573,67 @@ async function readBoundedText$2(response, maxBytes, label) {
|
|
|
309
573
|
//#region lib/types/reference-image.js
|
|
310
574
|
/** Latest-DSH compatibility boundary for resolving conversation image references. */
|
|
311
575
|
/**
|
|
312
|
-
* Resolve
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
* messages or how nested model-facing content stores durable image refs.
|
|
576
|
+
* Resolve one or more edit references while keeping every DSH-specific detail
|
|
577
|
+
* behind this compatibility boundary. Explicit selectors preserve caller order;
|
|
578
|
+
* without selectors, all images in the newest image-bearing message are used.
|
|
316
579
|
*/
|
|
317
|
-
async function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
580
|
+
async function resolveReferenceImages(input) {
|
|
581
|
+
const sourceAttachmentIds = mergeSelectors({
|
|
582
|
+
single: input.sourceAttachmentId,
|
|
583
|
+
multiple: input.sourceAttachmentIds,
|
|
584
|
+
singleName: "source_attachment_id",
|
|
585
|
+
multipleName: "source_attachment_ids",
|
|
586
|
+
equal: attachmentIdsEqual
|
|
587
|
+
});
|
|
588
|
+
const sourcePaths = mergeSelectors({
|
|
589
|
+
single: input.sourcePath,
|
|
590
|
+
multiple: input.sourcePaths,
|
|
591
|
+
singleName: "source_path",
|
|
592
|
+
multipleName: "source_paths",
|
|
593
|
+
equal: (left, right) => left.trim() === right.trim()
|
|
594
|
+
});
|
|
595
|
+
if (sourceAttachmentIds !== void 0 && sourcePaths !== void 0) throw new Error("edit_image accepts only one of source_attachment_id, source_attachment_ids, source_path, or source_paths");
|
|
596
|
+
if (sourcePaths !== void 0) return Promise.all(sourcePaths.map((sourcePath) => readWorkspaceReferenceImage({
|
|
597
|
+
sourcePath,
|
|
321
598
|
...input.agent?.session.header?.cwd === void 0 ? {} : { workspaceRoot: input.agent.session.header.cwd },
|
|
322
599
|
...input.maxBytes === void 0 ? {} : { maxBytes: input.maxBytes },
|
|
323
600
|
signal: input.signal
|
|
324
|
-
});
|
|
601
|
+
})));
|
|
325
602
|
if (input.agent === void 0) throw new Error("edit_image requires an active DSH agent session to resolve a reference image");
|
|
326
|
-
const
|
|
327
|
-
if (
|
|
328
|
-
if (
|
|
603
|
+
const refs = findReferenceImages(input.agent.session.deriveMessages(), sourceAttachmentIds);
|
|
604
|
+
if (refs.length === 0) {
|
|
605
|
+
if (sourceAttachmentIds !== void 0) throw new Error(`edit_image could not find image attachment ${sourceAttachmentIds[0]} in the current conversation`);
|
|
329
606
|
throw new Error("edit_image requires an image in the current conversation; upload or generate an image first");
|
|
330
607
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
608
|
+
if (sourceAttachmentIds !== void 0 && refs.length !== sourceAttachmentIds.length) {
|
|
609
|
+
const missing = sourceAttachmentIds.find((id) => !refs.some((ref) => attachmentIdsEqual(String(ref.attachmentId), id)));
|
|
610
|
+
throw new Error(`edit_image could not find image attachment ${missing ?? "unknown"} in the current conversation`);
|
|
611
|
+
}
|
|
612
|
+
return Promise.all(refs.map(async (ref) => {
|
|
613
|
+
const stored = await input.attachments.readImage(ref, input.signal);
|
|
614
|
+
if (input.maxBytes !== void 0 && stored.data.byteLength > input.maxBytes) throw new Error(`edit_image source image is too large (${stored.data.byteLength} bytes; maximum ${input.maxBytes})`);
|
|
615
|
+
return {
|
|
616
|
+
data: stored.data,
|
|
617
|
+
mediaType: stored.ref.mediaType
|
|
618
|
+
};
|
|
619
|
+
}));
|
|
620
|
+
}
|
|
621
|
+
/** Find images in caller order, or every image in the newest image-bearing message. */
|
|
622
|
+
function findReferenceImages(messages, sourceAttachmentIds) {
|
|
623
|
+
if (sourceAttachmentIds !== void 0) return sourceAttachmentIds.flatMap((id) => {
|
|
624
|
+
const ref = findReferenceImage(messages, id);
|
|
625
|
+
return ref === void 0 ? [] : [ref];
|
|
626
|
+
});
|
|
627
|
+
const latestHumanMessage = [...messages].reverse().find((message) => message.source?.kind === "user");
|
|
628
|
+
if (latestHumanMessage !== void 0) {
|
|
629
|
+
const refs = collectInBlocks(latestHumanMessage.content);
|
|
630
|
+
if (refs.length > 0) return refs;
|
|
631
|
+
}
|
|
632
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
633
|
+
const refs = collectInBlocks(messages[index]?.content ?? []);
|
|
634
|
+
if (refs.length > 0) return refs;
|
|
635
|
+
}
|
|
636
|
+
return [];
|
|
336
637
|
}
|
|
337
638
|
/**
|
|
338
639
|
* Read an explicitly named workspace image without exposing filesystem or DSH
|
|
@@ -397,7 +698,7 @@ function findInBlocks(blocks, sourceAttachmentId) {
|
|
|
397
698
|
const block = blocks[index];
|
|
398
699
|
if (block === void 0) continue;
|
|
399
700
|
if (block.type === "image") {
|
|
400
|
-
if (sourceAttachmentId === void 0 || String(block.attachment.attachmentId)
|
|
701
|
+
if (sourceAttachmentId === void 0 || attachmentIdsEqual(String(block.attachment.attachmentId), sourceAttachmentId)) return block.attachment;
|
|
401
702
|
continue;
|
|
402
703
|
}
|
|
403
704
|
if (block.type === "tool-result") {
|
|
@@ -417,6 +718,33 @@ function parseDimensions(value) {
|
|
|
417
718
|
function imageMediaType$2(value) {
|
|
418
719
|
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
419
720
|
}
|
|
721
|
+
function collectInBlocks(blocks) {
|
|
722
|
+
const refs = [];
|
|
723
|
+
for (const block of blocks) {
|
|
724
|
+
if (block.type === "image") refs.push(block.attachment);
|
|
725
|
+
if (block.type === "tool-result") refs.push(...collectInBlocks(block.content));
|
|
726
|
+
}
|
|
727
|
+
return refs;
|
|
728
|
+
}
|
|
729
|
+
function attachmentIdsEqual(actual, requested) {
|
|
730
|
+
if (actual === requested) return true;
|
|
731
|
+
const actualDigest = sha256Digest(actual);
|
|
732
|
+
const requestedDigest = sha256Digest(requested);
|
|
733
|
+
return actualDigest !== void 0 && actualDigest === requestedDigest;
|
|
734
|
+
}
|
|
735
|
+
function sha256Digest(value) {
|
|
736
|
+
return /^(?:sha256:)?([0-9a-f]{64})$/i.exec(value.trim())?.[1]?.toLowerCase();
|
|
737
|
+
}
|
|
738
|
+
function mergeSelectors(input) {
|
|
739
|
+
if (input.multiple === void 0) return input.single === void 0 ? void 0 : [input.single];
|
|
740
|
+
if (input.multiple.length === 0) {
|
|
741
|
+
if (input.single !== void 0) return [input.single];
|
|
742
|
+
throw new Error(`edit_image ${input.multipleName} must not be empty`);
|
|
743
|
+
}
|
|
744
|
+
const single = input.single;
|
|
745
|
+
if (single !== void 0 && !input.multiple.some((value) => input.equal(single, value))) throw new Error(`edit_image ${input.singleName} must also appear in ${input.multipleName} when both are provided`);
|
|
746
|
+
return input.multiple;
|
|
747
|
+
}
|
|
420
748
|
function detectImageMediaType(data) {
|
|
421
749
|
if (startsWith(data, [
|
|
422
750
|
137,
|
|
@@ -538,8 +866,11 @@ async function generateOpenAICompatibleImage(input) {
|
|
|
538
866
|
}
|
|
539
867
|
async function editOpenAICompatibleImage(input) {
|
|
540
868
|
const form = new FormData();
|
|
541
|
-
const
|
|
542
|
-
|
|
869
|
+
const imageField = input.sourceImages.length === 1 ? "image" : "image[]";
|
|
870
|
+
input.sourceImages.forEach((sourceImage, index) => {
|
|
871
|
+
const uploadBytes = new Uint8Array(sourceImage.data);
|
|
872
|
+
form.append(imageField, new Blob([uploadBytes], { type: sourceImage.mediaType }), `reference-${index + 1}.${extensionOf(sourceImage.mediaType)}`);
|
|
873
|
+
});
|
|
543
874
|
form.append("prompt", input.prompt);
|
|
544
875
|
form.append("model", input.model);
|
|
545
876
|
if (input.size !== void 0 && input.size.length > 0) form.append("size", input.size);
|
|
@@ -684,7 +1015,7 @@ async function editSeedreamImage(input) {
|
|
|
684
1015
|
body: JSON.stringify({
|
|
685
1016
|
model: input.model,
|
|
686
1017
|
prompt: input.prompt,
|
|
687
|
-
image:
|
|
1018
|
+
image: input.sourceImages.map(toDataUrl),
|
|
688
1019
|
...input.size === void 0 || input.size.length === 0 ? {} : { size: input.size },
|
|
689
1020
|
response_format: "b64_json"
|
|
690
1021
|
})
|
|
@@ -961,6 +1292,14 @@ function apply(ctx, config = {}) {
|
|
|
961
1292
|
output: imageOutput("Generated"),
|
|
962
1293
|
async execute(args, exec) {
|
|
963
1294
|
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);
|
|
964
1303
|
const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
|
|
965
1304
|
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.`);
|
|
966
1305
|
if (active.provider === "google") {
|
|
@@ -1005,7 +1344,7 @@ function apply(ctx, config = {}) {
|
|
|
1005
1344
|
}));
|
|
1006
1345
|
ctx.tools.register(defineTool({
|
|
1007
1346
|
name: "edit_image",
|
|
1008
|
-
description: "Edit or restyle
|
|
1347
|
+
description: "Edit, combine, or restyle existing images with the configured provider. Images attached inline to the latest human message are already readable DSH attachments even when no workspace file exists. In that case, call edit_image immediately with prompt only; NEVER call read_image, glob, or shell to locate them, and NEVER invent @ paths. All inline images will be used in upload order. For specific older conversation images use source_attachment_id or source_attachment_ids; both canonical sha256: IDs and full bare SHA-256 digests are accepted. For files the user explicitly names in the workspace use source_path or source_paths. Provide exactly one selector field. Without a selector, images from the latest human message take priority; only when that message has no images does editing fall back to the newest conversation image.",
|
|
1009
1348
|
parameters: {
|
|
1010
1349
|
prompt: {
|
|
1011
1350
|
type: "string",
|
|
@@ -1016,10 +1355,20 @@ function apply(ctx, config = {}) {
|
|
|
1016
1355
|
type: "string",
|
|
1017
1356
|
description: "Optional attachment id of a specific image already present in the current conversation."
|
|
1018
1357
|
},
|
|
1358
|
+
source_attachment_ids: {
|
|
1359
|
+
type: "array",
|
|
1360
|
+
items: { type: "string" },
|
|
1361
|
+
description: "Optional ordered attachment ids of multiple images already present in the current conversation. Prompt references such as image 1 and image 2 follow this order."
|
|
1362
|
+
},
|
|
1019
1363
|
source_path: {
|
|
1020
1364
|
type: "string",
|
|
1021
1365
|
description: "Optional absolute or workspace-relative path of a specific image file inside the active session workspace. Prefer this when the user names a saved file."
|
|
1022
1366
|
},
|
|
1367
|
+
source_paths: {
|
|
1368
|
+
type: "array",
|
|
1369
|
+
items: { type: "string" },
|
|
1370
|
+
description: "Optional ordered absolute or workspace-relative paths of multiple image files inside the active session workspace."
|
|
1371
|
+
},
|
|
1023
1372
|
aspect_ratio: {
|
|
1024
1373
|
type: "string",
|
|
1025
1374
|
enum: [
|
|
@@ -1050,13 +1399,16 @@ function apply(ctx, config = {}) {
|
|
|
1050
1399
|
output: imageOutput("Edited"),
|
|
1051
1400
|
async execute(args, exec) {
|
|
1052
1401
|
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");
|
|
1053
1403
|
const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
|
|
1054
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.`);
|
|
1055
|
-
const
|
|
1405
|
+
const sourceImages = await resolveReferenceImages({
|
|
1056
1406
|
...exec.agent === void 0 ? {} : { agent: exec.agent },
|
|
1057
1407
|
attachments: ctx.attachments,
|
|
1058
1408
|
...typeof args.source_attachment_id === "string" ? { sourceAttachmentId: args.source_attachment_id } : {},
|
|
1409
|
+
...Array.isArray(args.source_attachment_ids) ? { sourceAttachmentIds: args.source_attachment_ids } : {},
|
|
1059
1410
|
...typeof args.source_path === "string" ? { sourcePath: args.source_path } : {},
|
|
1411
|
+
...Array.isArray(args.source_paths) ? { sourcePaths: args.source_paths } : {},
|
|
1060
1412
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1061
1413
|
signal: exec.signal
|
|
1062
1414
|
});
|
|
@@ -1068,7 +1420,7 @@ function apply(ctx, config = {}) {
|
|
|
1068
1420
|
endpoint: active.endpoint,
|
|
1069
1421
|
model: active.model,
|
|
1070
1422
|
prompt: args.prompt,
|
|
1071
|
-
|
|
1423
|
+
sourceImages,
|
|
1072
1424
|
aspectRatio,
|
|
1073
1425
|
imageSize,
|
|
1074
1426
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
@@ -1081,7 +1433,7 @@ function apply(ctx, config = {}) {
|
|
|
1081
1433
|
baseURL: active.baseURL,
|
|
1082
1434
|
model: active.model,
|
|
1083
1435
|
prompt: args.prompt,
|
|
1084
|
-
|
|
1436
|
+
sourceImages,
|
|
1085
1437
|
size,
|
|
1086
1438
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1087
1439
|
signal: exec.signal
|
|
@@ -1091,7 +1443,7 @@ function apply(ctx, config = {}) {
|
|
|
1091
1443
|
baseURL: active.baseURL,
|
|
1092
1444
|
model: active.model,
|
|
1093
1445
|
prompt: args.prompt,
|
|
1094
|
-
|
|
1446
|
+
sourceImages,
|
|
1095
1447
|
size,
|
|
1096
1448
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1097
1449
|
signal: exec.signal
|
|
@@ -1101,7 +1453,7 @@ function apply(ctx, config = {}) {
|
|
|
1101
1453
|
endpoint: active.endpoint,
|
|
1102
1454
|
model: active.model,
|
|
1103
1455
|
prompt: args.prompt,
|
|
1104
|
-
|
|
1456
|
+
sourceImages,
|
|
1105
1457
|
size,
|
|
1106
1458
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1107
1459
|
signal: exec.signal
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
-
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client';
|
|
3
2
|
import type { SettingsScope } from '@deepseek-ai/dsh-client-runtime/client';
|
|
4
3
|
import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
4
|
import { type ImageProvider } from '../shared.js';
|
|
@@ -15,12 +14,33 @@ interface ImageSettings {
|
|
|
15
14
|
seedreamModel?: string;
|
|
16
15
|
dashscopeEndpoint?: string;
|
|
17
16
|
dashscopeModel?: string;
|
|
17
|
+
comfyuiBaseURL?: string;
|
|
18
|
+
comfyuiWorkflowJson?: string;
|
|
19
|
+
comfyuiWorkflowName?: string;
|
|
20
|
+
comfyuiTimeoutMs?: number;
|
|
18
21
|
saveToWorkspace?: boolean;
|
|
19
22
|
workspaceFolder?: string;
|
|
20
23
|
}
|
|
24
|
+
interface CredentialInfo {
|
|
25
|
+
configured?: boolean;
|
|
26
|
+
}
|
|
27
|
+
interface CredentialResult {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
value?: Readonly<Record<string, CredentialInfo>>;
|
|
30
|
+
}
|
|
31
|
+
interface CredentialMutationResult {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
error?: {
|
|
34
|
+
message?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
interface CredentialsRemote {
|
|
38
|
+
describe(refs: string[]): Promise<CredentialResult>;
|
|
39
|
+
set(ref: string, value: string): Promise<CredentialMutationResult>;
|
|
40
|
+
}
|
|
21
41
|
interface SettingsFace {
|
|
22
42
|
scope: SettingsScope<ImageSettings>;
|
|
23
|
-
credentials:
|
|
43
|
+
credentials: CredentialsRemote;
|
|
24
44
|
locale?: LocaleService | undefined;
|
|
25
45
|
}
|
|
26
46
|
interface ImageCardFace {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const COMFYUI_PROMPT_PLACEHOLDER = "{{prompt}}";
|
|
2
|
+
export declare const COMFYUI_SEED_PLACEHOLDER = "{{seed}}";
|
|
3
|
+
type JsonRecord = Record<string, unknown>;
|
|
4
|
+
/** Validate imported JSON without exposing workflow graph details to callers. */
|
|
5
|
+
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;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** ComfyUI text-to-image adapter using an imported API-format workflow. */
|
|
2
|
+
import type { ImageMediaType } from '@deepseek-ai/dsh-attachment';
|
|
3
|
+
export interface GeneratedComfyUIImage {
|
|
4
|
+
data: Uint8Array;
|
|
5
|
+
mediaType: ImageMediaType;
|
|
6
|
+
}
|
|
7
|
+
/** Run one ComfyUI workflow and return its first final image. */
|
|
8
|
+
export declare function generateComfyUIImage(input: {
|
|
9
|
+
baseURL: string;
|
|
10
|
+
workflowJson: string;
|
|
11
|
+
prompt: string;
|
|
12
|
+
timeoutMs: number;
|
|
13
|
+
maxBytes: number;
|
|
14
|
+
signal: AbortSignal;
|
|
15
|
+
}): Promise<GeneratedComfyUIImage>;
|
package/lib/types/config.d.ts
CHANGED
|
@@ -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_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_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, 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, };
|
|
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. */
|
|
@@ -28,6 +28,12 @@ export interface Config {
|
|
|
28
28
|
seedreamModel?: string;
|
|
29
29
|
dashscopeEndpoint?: string;
|
|
30
30
|
dashscopeModel?: string;
|
|
31
|
+
comfyuiBaseURL?: string;
|
|
32
|
+
/** ComfyUI API-format workflow JSON imported through the Web settings page. */
|
|
33
|
+
comfyuiWorkflowJson?: string;
|
|
34
|
+
/** Original imported file name, used as a human-readable result label. */
|
|
35
|
+
comfyuiWorkflowName?: string;
|
|
36
|
+
comfyuiTimeoutMs?: number;
|
|
31
37
|
/** Also write every generated image as a file under the session workspace. */
|
|
32
38
|
saveToWorkspace?: boolean;
|
|
33
39
|
/** Workspace subfolder for generated images; empty means the workspace root. */
|
|
@@ -61,4 +67,10 @@ export declare function resolveProvider(config: Config): {
|
|
|
61
67
|
model: string;
|
|
62
68
|
endpoint: string;
|
|
63
69
|
imageSize: string;
|
|
70
|
+
} | {
|
|
71
|
+
provider: 'comfyui';
|
|
72
|
+
baseURL: string;
|
|
73
|
+
workflowJson: string;
|
|
74
|
+
workflowName: string;
|
|
75
|
+
timeoutMs: number;
|
|
64
76
|
};
|