dsh-image-gen 0.2.5 → 0.3.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.
- package/README.en.md +172 -172
- package/README.md +182 -182
- package/README.zh-CN.md +172 -172
- package/lib/client.js +4245 -620
- package/lib/client.js.map +1 -1
- package/lib/index.js +682 -66
- package/lib/types/client/browser-image-utils.d.ts +10 -0
- package/lib/types/client/conversation-image-revisions.d.ts +25 -0
- package/lib/types/client/conversation-regenerate.d.ts +11 -0
- package/lib/types/client/gallery-store.d.ts +39 -4
- package/lib/types/client/gallery-view.d.ts +129 -14
- package/lib/types/client/image-cache.d.ts +26 -0
- package/lib/types/client/studio-style.d.ts +1 -0
- package/lib/types/client/studio-view.d.ts +30 -0
- package/lib/types/image-route.d.ts +7 -1
- package/lib/types/index.d.ts +2 -1
- package/lib/types/shared.d.ts +73 -0
- package/lib/types/studio-route.d.ts +12 -0
- package/lib/types/studio.d.ts +8 -0
- package/lib/types/workspace-save.d.ts +24 -0
- package/package.json +11 -10
- package/skills/install-dsh-image-gen/SKILL.md +22 -22
package/lib/index.js
CHANGED
|
@@ -2,13 +2,16 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
|
2
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
|
-
import { mkdir, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
6
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { lstat, mkdir, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
//#region lib/types/shared.js
|
|
9
|
-
/** Values shared by the Host and browser Bundle faces. */
|
|
10
9
|
/** Browser route used by the generated-image card. */
|
|
11
10
|
const IMAGE_ROUTE = "/plugins/dsh-image-gen/image";
|
|
11
|
+
/** Browser route used for deleting generated images and workspace files. */
|
|
12
|
+
const DELETE_ROUTE = "/plugins/dsh-image-gen/delete";
|
|
13
|
+
/** Same-origin route used by the browser image workbench. */
|
|
14
|
+
const STUDIO_ROUTE = "/plugins/dsh-image-gen/studio";
|
|
12
15
|
/** Namespace persisted through DSH Settings. */
|
|
13
16
|
const IMAGE_GENERATION_NAMESPACE = "image-generation";
|
|
14
17
|
/** Supported providers. */
|
|
@@ -19,6 +22,13 @@ const IMAGE_PROVIDERS = [
|
|
|
19
22
|
"dashscope",
|
|
20
23
|
"comfyui"
|
|
21
24
|
];
|
|
25
|
+
/** Providers supported by the first browser workbench release. */
|
|
26
|
+
const CLOUD_IMAGE_PROVIDERS = [
|
|
27
|
+
"google",
|
|
28
|
+
"openai",
|
|
29
|
+
"seedream",
|
|
30
|
+
"dashscope"
|
|
31
|
+
];
|
|
22
32
|
/** Default endpoints and base URLs. */
|
|
23
33
|
const DEFAULT_GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/interactions";
|
|
24
34
|
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
@@ -91,6 +101,21 @@ const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
|
|
|
91
101
|
const SEEDREAM_API_KEY_ENV = "ARK_API_KEY";
|
|
92
102
|
/** DashScope credential reference. */
|
|
93
103
|
const DASHSCOPE_API_KEY_ENV = "DASHSCOPE_API_KEY";
|
|
104
|
+
/** Google tool-level controls. */
|
|
105
|
+
const ASPECT_RATIOS = [
|
|
106
|
+
"1:1",
|
|
107
|
+
"3:2",
|
|
108
|
+
"2:3",
|
|
109
|
+
"4:3",
|
|
110
|
+
"3:4",
|
|
111
|
+
"16:9",
|
|
112
|
+
"9:16"
|
|
113
|
+
];
|
|
114
|
+
const IMAGE_SIZES = [
|
|
115
|
+
"1K",
|
|
116
|
+
"2K",
|
|
117
|
+
"4K"
|
|
118
|
+
];
|
|
94
119
|
/** Cordis configuration schema. */
|
|
95
120
|
const Config = z.object({
|
|
96
121
|
provider: z.union(IMAGE_PROVIDERS).default("google"),
|
|
@@ -210,8 +235,8 @@ function prepareComfyUIWorkflow(workflowJson, prompt, seed = randomSeed(), image
|
|
|
210
235
|
};
|
|
211
236
|
let imageInputs = 0;
|
|
212
237
|
const prepared = Object.fromEntries(Object.entries(workflow).map(([nodeId, value]) => {
|
|
213
|
-
const node = record$
|
|
214
|
-
const inputs = node === void 0 ? void 0 : record$
|
|
238
|
+
const node = record$5(value);
|
|
239
|
+
const inputs = node === void 0 ? void 0 : record$5(node.inputs);
|
|
215
240
|
if (inputs === void 0) return [nodeId, value];
|
|
216
241
|
const nextInputs = inject(inputs);
|
|
217
242
|
if (isImagePlaceholder(nextInputs[IMAGE_INPUT_KEY])) {
|
|
@@ -247,7 +272,7 @@ function parseWorkflow(workflowJson) {
|
|
|
247
272
|
}
|
|
248
273
|
if (!isRecord(value) || Object.keys(value).length === 0) throw new Error("ComfyUI workflow must be a non-empty API-format JSON object");
|
|
249
274
|
if (!Object.values(value).some((node) => {
|
|
250
|
-
const inputs = record$
|
|
275
|
+
const inputs = record$5(node)?.inputs;
|
|
251
276
|
return inputs !== void 0 && containsPromptPlaceholder(inputs);
|
|
252
277
|
})) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} (or ${LEGACY_PROMPT_PLACEHOLDER}) in a text input`);
|
|
253
278
|
return value;
|
|
@@ -263,7 +288,7 @@ function containsPromptPlaceholder(value) {
|
|
|
263
288
|
function isRecord(value) {
|
|
264
289
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
265
290
|
}
|
|
266
|
-
function record$
|
|
291
|
+
function record$5(value) {
|
|
267
292
|
return isRecord(value) ? value : void 0;
|
|
268
293
|
}
|
|
269
294
|
//#endregion
|
|
@@ -371,9 +396,9 @@ async function waitForOutput(baseURL, promptId, signal) {
|
|
|
371
396
|
});
|
|
372
397
|
const text = await readBoundedText$3(response, MAX_HISTORY_BYTES);
|
|
373
398
|
if (!response.ok) throw new Error(`ComfyUI history request failed (${response.status}): ${text.slice(0, ERROR_LIMIT$3)}`);
|
|
374
|
-
const entry = record$
|
|
399
|
+
const entry = record$4(parseJsonRecord(text, "ComfyUI history returned invalid JSON")[promptId]);
|
|
375
400
|
if (entry !== void 0) {
|
|
376
|
-
const status = record$
|
|
401
|
+
const status = record$4(entry.status);
|
|
377
402
|
if (status?.status_str === "error") throw new Error(`ComfyUI workflow failed: ${JSON.stringify(status.messages ?? status).slice(0, ERROR_LIMIT$3)}`);
|
|
378
403
|
const output = firstOutputImage(entry.outputs);
|
|
379
404
|
if (output !== void 0) return output;
|
|
@@ -383,13 +408,13 @@ async function waitForOutput(baseURL, promptId, signal) {
|
|
|
383
408
|
}
|
|
384
409
|
}
|
|
385
410
|
function firstOutputImage(value) {
|
|
386
|
-
const outputs = record$
|
|
411
|
+
const outputs = record$4(value);
|
|
387
412
|
if (outputs === void 0) return void 0;
|
|
388
413
|
for (const nodeOutput of Object.values(outputs)) {
|
|
389
|
-
const images = record$
|
|
414
|
+
const images = record$4(nodeOutput)?.images;
|
|
390
415
|
if (!Array.isArray(images)) continue;
|
|
391
416
|
for (const image of images) {
|
|
392
|
-
const item = record$
|
|
417
|
+
const item = record$4(image);
|
|
393
418
|
if (typeof item?.filename !== "string" || item.filename.length === 0) continue;
|
|
394
419
|
const output = {
|
|
395
420
|
filename: item.filename,
|
|
@@ -410,7 +435,7 @@ async function downloadOutput(baseURL, output, maxBytes, signal) {
|
|
|
410
435
|
signal
|
|
411
436
|
});
|
|
412
437
|
if (!response.ok) throw new Error(`ComfyUI image download failed (${response.status})`);
|
|
413
|
-
const mediaType = imageMediaType$
|
|
438
|
+
const mediaType = imageMediaType$4(response.headers.get("content-type")) ?? imageMediaTypeFromName(output.filename);
|
|
414
439
|
if (mediaType === void 0) throw new Error("ComfyUI image download returned an unsupported content type");
|
|
415
440
|
return {
|
|
416
441
|
data: await readBoundedBytes$2(response, maxBytes),
|
|
@@ -430,7 +455,7 @@ function comfyUIBaseURL(value) {
|
|
|
430
455
|
function endpoint(baseURL, path) {
|
|
431
456
|
return new URL(path, baseURL);
|
|
432
457
|
}
|
|
433
|
-
function record$
|
|
458
|
+
function record$4(value) {
|
|
434
459
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
435
460
|
}
|
|
436
461
|
function parseJsonRecord(text, message) {
|
|
@@ -440,11 +465,11 @@ function parseJsonRecord(text, message) {
|
|
|
440
465
|
} catch {
|
|
441
466
|
throw new Error(message);
|
|
442
467
|
}
|
|
443
|
-
const parsed = record$
|
|
468
|
+
const parsed = record$4(value);
|
|
444
469
|
if (parsed === void 0) throw new Error(message);
|
|
445
470
|
return parsed;
|
|
446
471
|
}
|
|
447
|
-
function imageMediaType$
|
|
472
|
+
function imageMediaType$4(value) {
|
|
448
473
|
const mediaType = value?.split(";", 1)[0]?.trim().toLowerCase();
|
|
449
474
|
return mediaType === "image/png" || mediaType === "image/jpeg" || mediaType === "image/webp" || mediaType === "image/gif" ? mediaType : void 0;
|
|
450
475
|
}
|
|
@@ -599,17 +624,28 @@ function generateGoogleImage(input) {
|
|
|
599
624
|
}
|
|
600
625
|
/** Send one native Google image-editing request using already-resolved bytes. */
|
|
601
626
|
function editGoogleImage(input) {
|
|
627
|
+
const interactionInput = input.sourceImages.length === 1 ? [{
|
|
628
|
+
type: "image",
|
|
629
|
+
mime_type: input.sourceImages[0].mediaType,
|
|
630
|
+
data: Buffer.from(input.sourceImages[0].data).toString("base64")
|
|
631
|
+
}, {
|
|
632
|
+
type: "text",
|
|
633
|
+
text: input.prompt
|
|
634
|
+
}] : [...input.sourceImages.flatMap((sourceImage, index) => [{
|
|
635
|
+
type: "text",
|
|
636
|
+
text: `图 ${index + 1} (Image ${index + 1}):`
|
|
637
|
+
}, {
|
|
638
|
+
type: "image",
|
|
639
|
+
mime_type: sourceImage.mediaType,
|
|
640
|
+
data: Buffer.from(sourceImage.data).toString("base64")
|
|
641
|
+
}]), {
|
|
642
|
+
type: "text",
|
|
643
|
+
text: input.prompt
|
|
644
|
+
}];
|
|
602
645
|
return requestGoogleImage({
|
|
603
646
|
...input,
|
|
604
647
|
operation: "editing",
|
|
605
|
-
interactionInput
|
|
606
|
-
type: "text",
|
|
607
|
-
text: input.prompt
|
|
608
|
-
}, ...input.sourceImages.map((sourceImage) => ({
|
|
609
|
-
type: "image",
|
|
610
|
-
mime_type: sourceImage.mediaType,
|
|
611
|
-
data: Buffer.from(sourceImage.data).toString("base64")
|
|
612
|
-
}))]
|
|
648
|
+
interactionInput
|
|
613
649
|
});
|
|
614
650
|
}
|
|
615
651
|
/** Shared Google request, response parsing, decoding, and size enforcement. */
|
|
@@ -654,13 +690,13 @@ async function requestGoogleImage(input) {
|
|
|
654
690
|
};
|
|
655
691
|
}
|
|
656
692
|
function outputImage(value) {
|
|
657
|
-
const interaction = record$
|
|
693
|
+
const interaction = record$3(value);
|
|
658
694
|
if (interaction === void 0) return void 0;
|
|
659
695
|
const direct = imageContent(interaction.output_image, false);
|
|
660
696
|
if (direct !== void 0) return direct;
|
|
661
697
|
if (!Array.isArray(interaction.steps)) return void 0;
|
|
662
698
|
for (const step of interaction.steps) {
|
|
663
|
-
const modelOutput = record$
|
|
699
|
+
const modelOutput = record$3(step);
|
|
664
700
|
if (modelOutput?.type !== "model_output" || !Array.isArray(modelOutput.content)) continue;
|
|
665
701
|
for (const content of modelOutput.content) {
|
|
666
702
|
const image = imageContent(content, true);
|
|
@@ -669,14 +705,14 @@ function outputImage(value) {
|
|
|
669
705
|
}
|
|
670
706
|
}
|
|
671
707
|
function imageContent(value, requiresImageType) {
|
|
672
|
-
const image = record$
|
|
708
|
+
const image = record$3(value);
|
|
673
709
|
if (image === void 0 || requiresImageType && image.type !== "image" || typeof image.data !== "string") return void 0;
|
|
674
710
|
return {
|
|
675
711
|
data: image.data,
|
|
676
712
|
mime_type: image.mime_type
|
|
677
713
|
};
|
|
678
714
|
}
|
|
679
|
-
function record$
|
|
715
|
+
function record$3(value) {
|
|
680
716
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
681
717
|
}
|
|
682
718
|
function mediaTypeOf(value) {
|
|
@@ -820,9 +856,9 @@ function findReferenceImage(messages, sourceAttachmentId) {
|
|
|
820
856
|
}
|
|
821
857
|
/** Validate an untrusted serialized image reference at an HTTP/UI boundary. */
|
|
822
858
|
function parseImageAttachmentRef(value) {
|
|
823
|
-
const ref = record$
|
|
859
|
+
const ref = record$2(value);
|
|
824
860
|
if (ref === void 0) return void 0;
|
|
825
|
-
if (typeof ref.attachmentId !== "string" || !imageMediaType$
|
|
861
|
+
if (typeof ref.attachmentId !== "string" || !imageMediaType$3(ref.mediaType)) return void 0;
|
|
826
862
|
if (!nonNegativeInteger(ref.bytes) || !positiveInteger(ref.width) || !positiveInteger(ref.height)) return void 0;
|
|
827
863
|
if (ref.name !== void 0 && typeof ref.name !== "string") return void 0;
|
|
828
864
|
const originalDimensions = ref.originalDimensions === void 0 ? void 0 : parseDimensions(ref.originalDimensions);
|
|
@@ -852,14 +888,14 @@ function findInBlocks(blocks, sourceAttachmentId) {
|
|
|
852
888
|
}
|
|
853
889
|
}
|
|
854
890
|
function parseDimensions(value) {
|
|
855
|
-
const dimensions = record$
|
|
891
|
+
const dimensions = record$2(value);
|
|
856
892
|
if (dimensions === void 0 || !positiveInteger(dimensions.width) || !positiveInteger(dimensions.height)) return void 0;
|
|
857
893
|
return {
|
|
858
894
|
width: dimensions.width,
|
|
859
895
|
height: dimensions.height
|
|
860
896
|
};
|
|
861
897
|
}
|
|
862
|
-
function imageMediaType$
|
|
898
|
+
function imageMediaType$3(value) {
|
|
863
899
|
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
864
900
|
}
|
|
865
901
|
function collectInBlocks(blocks) {
|
|
@@ -924,27 +960,27 @@ function positiveInteger(value) {
|
|
|
924
960
|
function nonNegativeInteger(value) {
|
|
925
961
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
926
962
|
}
|
|
927
|
-
function record$
|
|
963
|
+
function record$2(value) {
|
|
928
964
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
929
965
|
}
|
|
930
966
|
//#endregion
|
|
931
967
|
//#region lib/types/image-route.js
|
|
932
|
-
const MAX_BODY_BYTES =
|
|
968
|
+
const MAX_BODY_BYTES = 65536;
|
|
933
969
|
/** Serve one verified durable image reference to a same-origin browser request. */
|
|
934
970
|
async function serveImage(req, res, deps) {
|
|
935
|
-
if (req.method !== "POST") return jsonError(res, 405, "method-not-allowed");
|
|
936
|
-
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError(res, 415, "json-required");
|
|
971
|
+
if (req.method !== "POST") return jsonError$1(res, 405, "method-not-allowed");
|
|
972
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$1(res, 415, "json-required");
|
|
937
973
|
const origin = req.headers.origin;
|
|
938
974
|
const host = req.headers.host;
|
|
939
|
-
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError(res, 403, "origin-rejected");
|
|
975
|
+
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError$1(res, 403, "origin-rejected");
|
|
940
976
|
let body;
|
|
941
977
|
try {
|
|
942
|
-
body = JSON.parse(await readBody(req));
|
|
978
|
+
body = JSON.parse(await readBody$1(req));
|
|
943
979
|
} catch {
|
|
944
|
-
return jsonError(res, 400, "invalid-request");
|
|
980
|
+
return jsonError$1(res, 400, "invalid-request");
|
|
945
981
|
}
|
|
946
982
|
const attachment = attachmentFromRequest(body);
|
|
947
|
-
if (attachment === void 0) return jsonError(res, 400, "invalid-attachment");
|
|
983
|
+
if (attachment === void 0) return jsonError$1(res, 400, "invalid-attachment");
|
|
948
984
|
try {
|
|
949
985
|
const stored = await deps.readImage(attachment);
|
|
950
986
|
res.writeHead(200, {
|
|
@@ -955,22 +991,61 @@ async function serveImage(req, res, deps) {
|
|
|
955
991
|
});
|
|
956
992
|
res.end(stored.data);
|
|
957
993
|
} catch {
|
|
958
|
-
jsonError(res, 404, "image-unavailable");
|
|
994
|
+
jsonError$1(res, 404, "image-unavailable");
|
|
959
995
|
}
|
|
960
996
|
}
|
|
997
|
+
/** Safely delete one or more generated image files from the workspace disk. */
|
|
998
|
+
async function serveDelete(req, res, deps) {
|
|
999
|
+
if (req.method !== "POST") return jsonError$1(res, 405, "method-not-allowed");
|
|
1000
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$1(res, 415, "json-required");
|
|
1001
|
+
const origin = req.headers.origin;
|
|
1002
|
+
const host = req.headers.host;
|
|
1003
|
+
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError$1(res, 403, "origin-rejected");
|
|
1004
|
+
let body;
|
|
1005
|
+
try {
|
|
1006
|
+
body = JSON.parse(await readBody$1(req));
|
|
1007
|
+
} catch {
|
|
1008
|
+
return jsonError$1(res, 400, "invalid-request");
|
|
1009
|
+
}
|
|
1010
|
+
const rawPaths = typeof body === "object" && body !== null && "paths" in body && Array.isArray(body.paths) ? body.paths : [];
|
|
1011
|
+
const deletedFiles = [];
|
|
1012
|
+
const failedFiles = [];
|
|
1013
|
+
for (const p of rawPaths) if (typeof p === "string" && p.trim()) try {
|
|
1014
|
+
if (await deps.deleteWorkspaceImage(p)) deletedFiles.push(p);
|
|
1015
|
+
else failedFiles.push({
|
|
1016
|
+
path: p,
|
|
1017
|
+
error: "File not found or rejected by safety checks"
|
|
1018
|
+
});
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
failedFiles.push({
|
|
1021
|
+
path: p,
|
|
1022
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
res.writeHead(200, {
|
|
1026
|
+
"content-type": "application/json",
|
|
1027
|
+
"cache-control": "no-store"
|
|
1028
|
+
});
|
|
1029
|
+
res.end(JSON.stringify({
|
|
1030
|
+
ok: failedFiles.length === 0,
|
|
1031
|
+
deletedCount: deletedFiles.length,
|
|
1032
|
+
deletedFiles,
|
|
1033
|
+
failedFiles
|
|
1034
|
+
}));
|
|
1035
|
+
}
|
|
961
1036
|
/** Validate the persisted reference carried by a tool presentation. */
|
|
962
1037
|
function imageAttachmentFromMeta(meta) {
|
|
963
|
-
const value = record(meta);
|
|
1038
|
+
const value = record$1(meta);
|
|
964
1039
|
if (value?.kind !== "dsh-image-gen") return void 0;
|
|
965
1040
|
return parseImageAttachmentRef(value.attachment);
|
|
966
1041
|
}
|
|
967
1042
|
function attachmentFromRequest(value) {
|
|
968
|
-
return parseImageAttachmentRef(record(value)?.attachment);
|
|
1043
|
+
return parseImageAttachmentRef(record$1(value)?.attachment);
|
|
969
1044
|
}
|
|
970
|
-
function record(value) {
|
|
1045
|
+
function record$1(value) {
|
|
971
1046
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
972
1047
|
}
|
|
973
|
-
async function readBody(req) {
|
|
1048
|
+
async function readBody$1(req) {
|
|
974
1049
|
const chunks = [];
|
|
975
1050
|
let bytes = 0;
|
|
976
1051
|
for await (const chunk of req) {
|
|
@@ -981,7 +1056,7 @@ async function readBody(req) {
|
|
|
981
1056
|
}
|
|
982
1057
|
return Buffer.concat(chunks).toString("utf8");
|
|
983
1058
|
}
|
|
984
|
-
function jsonError(res, status, code) {
|
|
1059
|
+
function jsonError$1(res, status, code) {
|
|
985
1060
|
res.writeHead(status, {
|
|
986
1061
|
"content-type": "application/json",
|
|
987
1062
|
"cache-control": "no-store"
|
|
@@ -1010,10 +1085,12 @@ async function generateOpenAICompatibleImage(input) {
|
|
|
1010
1085
|
}
|
|
1011
1086
|
async function editOpenAICompatibleImage(input) {
|
|
1012
1087
|
const form = new FormData();
|
|
1013
|
-
const imageField = input.sourceImages.length
|
|
1088
|
+
const imageField = input.sourceImages.length > 1 ? "image[]" : "image";
|
|
1014
1089
|
input.sourceImages.forEach((sourceImage, index) => {
|
|
1015
1090
|
const uploadBytes = new Uint8Array(sourceImage.data);
|
|
1016
|
-
|
|
1091
|
+
const blob = new Blob([uploadBytes], { type: sourceImage.mediaType });
|
|
1092
|
+
const filename = `reference-${index + 1}.${extensionOf(sourceImage.mediaType)}`;
|
|
1093
|
+
form.append(imageField, blob, filename);
|
|
1017
1094
|
});
|
|
1018
1095
|
form.append("prompt", input.prompt);
|
|
1019
1096
|
form.append("model", input.model);
|
|
@@ -1039,7 +1116,7 @@ async function parseImageResponse(response, provider, input) {
|
|
|
1039
1116
|
if (image === void 0) throw new Error(`${provider} image request returned no image: ${text.slice(0, ERROR_LIMIT$1)}`);
|
|
1040
1117
|
if (image.b64_json !== void 0) return {
|
|
1041
1118
|
data: decodeBase64$1(image.b64_json, provider),
|
|
1042
|
-
mediaType: imageMediaType$
|
|
1119
|
+
mediaType: imageMediaType$2(image.mime_type) ?? "image/png"
|
|
1043
1120
|
};
|
|
1044
1121
|
return downloadImage$1(image.url, provider, input);
|
|
1045
1122
|
}
|
|
@@ -1074,7 +1151,7 @@ async function downloadImage$1(url, provider, input) {
|
|
|
1074
1151
|
if (parsed === void 0) throw new Error(`${provider} image request returned invalid data URL`);
|
|
1075
1152
|
return {
|
|
1076
1153
|
data: decodeBase64$1(parsed.base64, provider),
|
|
1077
|
-
mediaType: imageMediaType$
|
|
1154
|
+
mediaType: imageMediaType$2(parsed.mediaType) ?? "image/png"
|
|
1078
1155
|
};
|
|
1079
1156
|
}
|
|
1080
1157
|
const response = await fetch(url, {
|
|
@@ -1083,7 +1160,7 @@ async function downloadImage$1(url, provider, input) {
|
|
|
1083
1160
|
...input.apiKey === void 0 ? {} : { headers: { authorization: `Bearer ${input.apiKey}` } }
|
|
1084
1161
|
});
|
|
1085
1162
|
if (!response.ok) throw new Error(`${provider} image download failed (${response.status})`);
|
|
1086
|
-
const mediaType = imageMediaType$
|
|
1163
|
+
const mediaType = imageMediaType$2(response.headers.get("content-type"));
|
|
1087
1164
|
if (mediaType === void 0) throw new Error(`${provider} image download returned unsupported content type`);
|
|
1088
1165
|
return {
|
|
1089
1166
|
data: await readBoundedBytes$1(response, input.maxBytes),
|
|
@@ -1112,7 +1189,7 @@ function decodeBase64$1(value, provider) {
|
|
|
1112
1189
|
if (decoded.length === 0) throw new Error(`${provider} image request returned invalid base64 image data`);
|
|
1113
1190
|
return new Uint8Array(decoded);
|
|
1114
1191
|
}
|
|
1115
|
-
function imageMediaType$
|
|
1192
|
+
function imageMediaType$2(value) {
|
|
1116
1193
|
const mediaType = value?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1117
1194
|
return mediaType === "image/png" || mediaType === "image/jpeg" || mediaType === "image/webp" || mediaType === "image/gif" ? mediaType : void 0;
|
|
1118
1195
|
}
|
|
@@ -1176,7 +1253,7 @@ async function editSeedreamImage(input) {
|
|
|
1176
1253
|
if (image === void 0) throw new Error(`seedream image editing returned no image: ${text.slice(0, ERROR_LIMIT)}`);
|
|
1177
1254
|
if (image.b64_json !== void 0) return {
|
|
1178
1255
|
data: decodeBase64(image.b64_json),
|
|
1179
|
-
mediaType: imageMediaType(image.mime_type) ?? "image/png"
|
|
1256
|
+
mediaType: imageMediaType$1(image.mime_type) ?? "image/png"
|
|
1180
1257
|
};
|
|
1181
1258
|
return downloadImage(image.url, input);
|
|
1182
1259
|
}
|
|
@@ -1214,7 +1291,7 @@ async function downloadImage(url, input) {
|
|
|
1214
1291
|
signal: input.signal
|
|
1215
1292
|
});
|
|
1216
1293
|
if (!response.ok) throw new Error(`seedream image download failed (${response.status})`);
|
|
1217
|
-
const mediaType = imageMediaType(response.headers.get("content-type"));
|
|
1294
|
+
const mediaType = imageMediaType$1(response.headers.get("content-type"));
|
|
1218
1295
|
if (mediaType === void 0) throw new Error("seedream image download returned unsupported content type");
|
|
1219
1296
|
return {
|
|
1220
1297
|
data: await readBoundedBytes(response, input.maxBytes),
|
|
@@ -1228,7 +1305,7 @@ function decodeBase64(value) {
|
|
|
1228
1305
|
if (decoded.length === 0) throw new Error("seedream image editing returned invalid base64 image data");
|
|
1229
1306
|
return new Uint8Array(decoded);
|
|
1230
1307
|
}
|
|
1231
|
-
function imageMediaType(value) {
|
|
1308
|
+
function imageMediaType$1(value) {
|
|
1232
1309
|
const mediaType = value?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1233
1310
|
return mediaType === "image/png" || mediaType === "image/jpeg" || mediaType === "image/webp" || mediaType === "image/gif" ? mediaType : void 0;
|
|
1234
1311
|
}
|
|
@@ -1375,6 +1452,517 @@ async function saveImageToWorkspace(options) {
|
|
|
1375
1452
|
}
|
|
1376
1453
|
return target;
|
|
1377
1454
|
}
|
|
1455
|
+
/** Regex pattern strictly matching generated image file names (e.g. image-01234567.png). */
|
|
1456
|
+
const GENERATED_IMAGE_NAME_REGEX = /^image-[0-9a-f]{8,}\.(png|jpg|jpeg|webp|gif)$/i;
|
|
1457
|
+
/** Discover detailed workspace records from DSH workspace.json storage. */
|
|
1458
|
+
async function getDshWorkspacesFull() {
|
|
1459
|
+
const list = [];
|
|
1460
|
+
try {
|
|
1461
|
+
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
1462
|
+
if (home) {
|
|
1463
|
+
const workspaceJsonPath = join(home, ".dsh", "storages", "workspace.json");
|
|
1464
|
+
const content = await readFile(workspaceJsonPath, "utf8");
|
|
1465
|
+
const workspaces = JSON.parse(content)?.tables?.workspaces;
|
|
1466
|
+
if (workspaces && typeof workspaces === "object") for (const [id, ws] of Object.entries(workspaces)) {
|
|
1467
|
+
const raw = ws;
|
|
1468
|
+
if (raw && typeof raw.path === "string") {
|
|
1469
|
+
const p = raw.path.trim();
|
|
1470
|
+
if (!p) continue;
|
|
1471
|
+
const title = typeof raw.title === "string" && raw.title.trim() ? raw.title.trim() : basename(p);
|
|
1472
|
+
const rawSessions = raw.sessionIds;
|
|
1473
|
+
const sessionIds = Array.isArray(rawSessions) ? rawSessions.filter((s) => typeof s === "string") : [];
|
|
1474
|
+
list.push({
|
|
1475
|
+
workspaceId: id,
|
|
1476
|
+
path: p,
|
|
1477
|
+
title,
|
|
1478
|
+
sessionIds
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
} catch {}
|
|
1484
|
+
return list;
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Safely delete a generated image file from workspace disk by its path.
|
|
1488
|
+
* Enforces strict safety gates:
|
|
1489
|
+
* 1. Base name must match generated image pattern (`image-<hex>.<ext>`) to avoid touching user files.
|
|
1490
|
+
* 2. Path must be a regular file and NOT a symbolic link.
|
|
1491
|
+
* 3. Real canonical path's basename must also match the pattern.
|
|
1492
|
+
* 4. Real canonical path must reside within an allowed workspace root (or process.cwd()).
|
|
1493
|
+
*/
|
|
1494
|
+
async function deleteImageFromWorkspace(filePath, allowedWorkspaceRoots) {
|
|
1495
|
+
const resolved = resolve(filePath);
|
|
1496
|
+
const base = resolved.split(/[/\\]/).pop() ?? "";
|
|
1497
|
+
if (!GENERATED_IMAGE_NAME_REGEX.test(base)) return false;
|
|
1498
|
+
try {
|
|
1499
|
+
const fileLstat = await lstat(resolved);
|
|
1500
|
+
if (fileLstat.isSymbolicLink() || !fileLstat.isFile()) return false;
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
if (err?.code === "ENOENT") return true;
|
|
1503
|
+
throw err;
|
|
1504
|
+
}
|
|
1505
|
+
try {
|
|
1506
|
+
const realFile = await realpath(resolved);
|
|
1507
|
+
const realBase = realFile.split(/[/\\]/).pop() ?? "";
|
|
1508
|
+
if (!GENERATED_IMAGE_NAME_REGEX.test(realBase)) return false;
|
|
1509
|
+
const rootsToCheck = /* @__PURE__ */ new Set();
|
|
1510
|
+
if (allowedWorkspaceRoots) {
|
|
1511
|
+
for (const r of allowedWorkspaceRoots) if (typeof r === "string" && r.trim()) rootsToCheck.add(resolve(r));
|
|
1512
|
+
}
|
|
1513
|
+
rootsToCheck.add(resolve(process.cwd()));
|
|
1514
|
+
let withinAllowed = false;
|
|
1515
|
+
for (const root of rootsToCheck) try {
|
|
1516
|
+
if (containsPath(await realpath(root), realFile)) {
|
|
1517
|
+
withinAllowed = true;
|
|
1518
|
+
break;
|
|
1519
|
+
}
|
|
1520
|
+
} catch {}
|
|
1521
|
+
if (!withinAllowed) return false;
|
|
1522
|
+
await unlink(realFile);
|
|
1523
|
+
return true;
|
|
1524
|
+
} catch (err) {
|
|
1525
|
+
if (err?.code === "ENOENT") return true;
|
|
1526
|
+
throw err;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
//#endregion
|
|
1530
|
+
//#region lib/types/studio.js
|
|
1531
|
+
const PROVIDER_LABELS = {
|
|
1532
|
+
google: "Google",
|
|
1533
|
+
openai: "OpenAI",
|
|
1534
|
+
seedream: "Seedream",
|
|
1535
|
+
dashscope: "DashScope"
|
|
1536
|
+
};
|
|
1537
|
+
const CREDENTIALS = {
|
|
1538
|
+
google: GOOGLE_API_KEY_ENV,
|
|
1539
|
+
openai: OPENAI_API_KEY_ENV,
|
|
1540
|
+
seedream: SEEDREAM_API_KEY_ENV,
|
|
1541
|
+
dashscope: DASHSCOPE_API_KEY_ENV
|
|
1542
|
+
};
|
|
1543
|
+
const RATIO_LABELS = {
|
|
1544
|
+
auto: "自动",
|
|
1545
|
+
"1:1": "1:1 方形",
|
|
1546
|
+
"3:2": "3:2 横向",
|
|
1547
|
+
"2:3": "2:3 肖像",
|
|
1548
|
+
"4:3": "4:3 横向",
|
|
1549
|
+
"3:4": "3:4 竖向",
|
|
1550
|
+
"16:9": "16:9 宽屏",
|
|
1551
|
+
"9:16": "9:16 竖屏"
|
|
1552
|
+
};
|
|
1553
|
+
/** Return only browser-safe capability data. */
|
|
1554
|
+
async function describeStudio(ctx, config) {
|
|
1555
|
+
const configuredEntries = await Promise.all(CLOUD_IMAGE_PROVIDERS.map(async (provider) => {
|
|
1556
|
+
const credential = await ctx.credentials.resolve(credentialRef(CREDENTIALS[provider]));
|
|
1557
|
+
return [provider, credential !== void 0 && credential.value.trim().length > 0];
|
|
1558
|
+
}));
|
|
1559
|
+
const configured = Object.fromEntries(configuredEntries);
|
|
1560
|
+
const profiles = CLOUD_IMAGE_PROVIDERS.map((provider) => studioProfile(config, provider, configured[provider]));
|
|
1561
|
+
const preferred = config.provider;
|
|
1562
|
+
return {
|
|
1563
|
+
providers: profiles,
|
|
1564
|
+
activeProvider: preferred !== void 0 && cloudProvider$1(preferred) ? preferred : profiles.find((profile) => profile.configured)?.provider ?? "google"
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
/** Execute one validated browser workbench request using the existing provider adapters. */
|
|
1568
|
+
async function generateFromStudio(ctx, config, input, signal, fallbackWorkspaceRoot) {
|
|
1569
|
+
assertAllowed(studioProfile(config, input.provider, true), input);
|
|
1570
|
+
const active = resolveProvider(providerConfig(config, input.provider, input.model));
|
|
1571
|
+
if (active.provider === "comfyui") throw new Error("ComfyUI 暂未接入工作台");
|
|
1572
|
+
const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
|
|
1573
|
+
if (credential === void 0 || credential.value.trim().length === 0) throw new Error(`${PROVIDER_LABELS[input.provider]} 尚未配置 API Key,请先到设置中配置`);
|
|
1574
|
+
const rawRefs = input.references ?? (input.reference ? [input.reference] : []);
|
|
1575
|
+
if (input.mode === "edit" && rawRefs.length === 0) throw new Error("图生图需要至少一张参考图");
|
|
1576
|
+
const sourceImages = input.mode === "edit" ? await Promise.all(rawRefs.map((ref) => readStudioReference(ctx, ref, signal))) : [];
|
|
1577
|
+
const startedAt = Date.now();
|
|
1578
|
+
let generated;
|
|
1579
|
+
let output;
|
|
1580
|
+
if (active.provider === "google") {
|
|
1581
|
+
const aspectRatio = input.ratio;
|
|
1582
|
+
const imageSize = input.quality;
|
|
1583
|
+
generated = input.mode === "edit" ? await editGoogleImage({
|
|
1584
|
+
apiKey: credential.value,
|
|
1585
|
+
endpoint: active.endpoint,
|
|
1586
|
+
model: active.model,
|
|
1587
|
+
prompt: input.prompt,
|
|
1588
|
+
sourceImages,
|
|
1589
|
+
aspectRatio,
|
|
1590
|
+
imageSize,
|
|
1591
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1592
|
+
signal
|
|
1593
|
+
}) : await generateGoogleImage({
|
|
1594
|
+
apiKey: credential.value,
|
|
1595
|
+
endpoint: active.endpoint,
|
|
1596
|
+
model: active.model,
|
|
1597
|
+
prompt: input.prompt,
|
|
1598
|
+
aspectRatio,
|
|
1599
|
+
imageSize,
|
|
1600
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1601
|
+
signal
|
|
1602
|
+
});
|
|
1603
|
+
output = `${aspectRatio}, ${imageSize}`;
|
|
1604
|
+
} else if (active.provider === "openai") {
|
|
1605
|
+
const size = openAISize(input.ratio);
|
|
1606
|
+
generated = input.mode === "edit" ? await editOpenAICompatibleImage({
|
|
1607
|
+
apiKey: credential.value,
|
|
1608
|
+
baseURL: active.baseURL,
|
|
1609
|
+
model: active.model,
|
|
1610
|
+
prompt: input.prompt,
|
|
1611
|
+
sourceImages,
|
|
1612
|
+
size,
|
|
1613
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1614
|
+
signal
|
|
1615
|
+
}) : await generateOpenAICompatibleImage({
|
|
1616
|
+
provider: "openai",
|
|
1617
|
+
apiKey: credential.value,
|
|
1618
|
+
baseURL: active.baseURL,
|
|
1619
|
+
model: active.model,
|
|
1620
|
+
prompt: input.prompt,
|
|
1621
|
+
size,
|
|
1622
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1623
|
+
signal
|
|
1624
|
+
});
|
|
1625
|
+
output = size;
|
|
1626
|
+
} else if (active.provider === "seedream") {
|
|
1627
|
+
const size = input.quality;
|
|
1628
|
+
generated = input.mode === "edit" ? await editSeedreamImage({
|
|
1629
|
+
apiKey: credential.value,
|
|
1630
|
+
baseURL: active.baseURL,
|
|
1631
|
+
model: active.model,
|
|
1632
|
+
prompt: input.prompt,
|
|
1633
|
+
sourceImages,
|
|
1634
|
+
size,
|
|
1635
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1636
|
+
signal
|
|
1637
|
+
}) : await generateOpenAICompatibleImage({
|
|
1638
|
+
provider: "seedream",
|
|
1639
|
+
apiKey: credential.value,
|
|
1640
|
+
baseURL: active.baseURL,
|
|
1641
|
+
model: active.model,
|
|
1642
|
+
prompt: input.prompt,
|
|
1643
|
+
size,
|
|
1644
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1645
|
+
signal
|
|
1646
|
+
});
|
|
1647
|
+
output = size;
|
|
1648
|
+
} else {
|
|
1649
|
+
if (input.mode === "edit" && sourceImages.length > 3) throw new Error("DashScope (通义万相) 图生图目前最多支持 3 张参考图,请精简后重试");
|
|
1650
|
+
const size = dashScopeSize(input.ratio);
|
|
1651
|
+
generated = input.mode === "edit" ? await editDashScopeImage({
|
|
1652
|
+
apiKey: credential.value,
|
|
1653
|
+
endpoint: active.endpoint,
|
|
1654
|
+
model: active.model,
|
|
1655
|
+
prompt: input.prompt,
|
|
1656
|
+
sourceImages,
|
|
1657
|
+
size,
|
|
1658
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1659
|
+
signal
|
|
1660
|
+
}) : await generateDashScopeImage({
|
|
1661
|
+
apiKey: credential.value,
|
|
1662
|
+
endpoint: active.endpoint,
|
|
1663
|
+
model: active.model,
|
|
1664
|
+
prompt: input.prompt,
|
|
1665
|
+
size,
|
|
1666
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1667
|
+
signal
|
|
1668
|
+
});
|
|
1669
|
+
output = size;
|
|
1670
|
+
}
|
|
1671
|
+
if (!ctx.attachments.imageLimits.mediaTypes.includes(generated.mediaType)) throw new Error(`当前 DSH 不支持保存 ${generated.mediaType} 图片`);
|
|
1672
|
+
const attachment = await ctx.attachments.saveImage({
|
|
1673
|
+
data: generated.data,
|
|
1674
|
+
mediaType: generated.mediaType,
|
|
1675
|
+
name: "studio-image"
|
|
1676
|
+
});
|
|
1677
|
+
let savedTo;
|
|
1678
|
+
const targetRoot = input.workspaceRoot || fallbackWorkspaceRoot;
|
|
1679
|
+
if (config.saveToWorkspace !== false && targetRoot) try {
|
|
1680
|
+
savedTo = await saveImageToWorkspace({
|
|
1681
|
+
workspaceRoot: targetRoot,
|
|
1682
|
+
folder: config.workspaceFolder,
|
|
1683
|
+
attachmentId: attachment.attachmentId,
|
|
1684
|
+
mediaType: generated.mediaType,
|
|
1685
|
+
data: generated.data,
|
|
1686
|
+
signal
|
|
1687
|
+
});
|
|
1688
|
+
} catch (saveError) {
|
|
1689
|
+
ctx.logger.warn(`dsh-image-gen: studio failed to save image to workspace: ${saveError instanceof Error ? saveError.message : String(saveError)}`);
|
|
1690
|
+
}
|
|
1691
|
+
return {
|
|
1692
|
+
attachment,
|
|
1693
|
+
provider: input.provider,
|
|
1694
|
+
model: input.model,
|
|
1695
|
+
prompt: input.prompt,
|
|
1696
|
+
output,
|
|
1697
|
+
createdAt: Date.now(),
|
|
1698
|
+
elapsedMs: Date.now() - startedAt,
|
|
1699
|
+
...savedTo ? { savedTo } : {}
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
function studioProfile(config, provider, configured) {
|
|
1703
|
+
const active = resolveProvider(providerConfig(config, provider));
|
|
1704
|
+
if (active.provider === "comfyui") throw new Error("Invalid cloud provider profile");
|
|
1705
|
+
const model = active.model;
|
|
1706
|
+
if (provider === "google") return profile(provider, model, configured, ASPECT_RATIOS.map(option), IMAGE_SIZES.map((value) => ({
|
|
1707
|
+
value,
|
|
1708
|
+
label: value
|
|
1709
|
+
})), "1:1", "1K");
|
|
1710
|
+
if (provider === "openai") return profile(provider, model, configured, [
|
|
1711
|
+
"1:1",
|
|
1712
|
+
"3:2",
|
|
1713
|
+
"2:3"
|
|
1714
|
+
].map(option), [{
|
|
1715
|
+
value: "standard",
|
|
1716
|
+
label: "标准(推荐)"
|
|
1717
|
+
}], "1:1", "standard");
|
|
1718
|
+
if (provider === "seedream") return profile(provider, model, configured, [{
|
|
1719
|
+
value: "auto",
|
|
1720
|
+
label: "模型自动"
|
|
1721
|
+
}], [
|
|
1722
|
+
"1K",
|
|
1723
|
+
"2K",
|
|
1724
|
+
"4K"
|
|
1725
|
+
].map((value) => ({
|
|
1726
|
+
value,
|
|
1727
|
+
label: value
|
|
1728
|
+
})), "auto", "2K");
|
|
1729
|
+
return profile(provider, model, configured, [
|
|
1730
|
+
"1:1",
|
|
1731
|
+
"3:2",
|
|
1732
|
+
"2:3",
|
|
1733
|
+
"16:9",
|
|
1734
|
+
"9:16"
|
|
1735
|
+
].map(option), [{
|
|
1736
|
+
value: "standard",
|
|
1737
|
+
label: "标准(推荐)"
|
|
1738
|
+
}], "1:1", "standard");
|
|
1739
|
+
}
|
|
1740
|
+
function profile(provider, model, configured, ratioOptions, qualityOptions, defaultRatio, defaultQuality) {
|
|
1741
|
+
return {
|
|
1742
|
+
provider,
|
|
1743
|
+
label: PROVIDER_LABELS[provider],
|
|
1744
|
+
model,
|
|
1745
|
+
configured,
|
|
1746
|
+
supportsEditing: true,
|
|
1747
|
+
ratioOptions,
|
|
1748
|
+
qualityOptions,
|
|
1749
|
+
defaultRatio,
|
|
1750
|
+
defaultQuality
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
function option(value) {
|
|
1754
|
+
return {
|
|
1755
|
+
value,
|
|
1756
|
+
label: RATIO_LABELS[value] ?? value
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
function providerConfig(config, provider, model) {
|
|
1760
|
+
if (model === void 0) return {
|
|
1761
|
+
...config,
|
|
1762
|
+
provider
|
|
1763
|
+
};
|
|
1764
|
+
switch (provider) {
|
|
1765
|
+
case "google": return {
|
|
1766
|
+
...config,
|
|
1767
|
+
provider,
|
|
1768
|
+
googleModel: model
|
|
1769
|
+
};
|
|
1770
|
+
case "openai": return {
|
|
1771
|
+
...config,
|
|
1772
|
+
provider,
|
|
1773
|
+
openaiModel: model
|
|
1774
|
+
};
|
|
1775
|
+
case "seedream": return {
|
|
1776
|
+
...config,
|
|
1777
|
+
provider,
|
|
1778
|
+
seedreamModel: model
|
|
1779
|
+
};
|
|
1780
|
+
case "dashscope": return {
|
|
1781
|
+
...config,
|
|
1782
|
+
provider,
|
|
1783
|
+
dashscopeModel: model
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
function assertAllowed(profile, input) {
|
|
1788
|
+
if (input.model !== profile.model) throw new Error("模型配置已变化,请刷新工作台后重试");
|
|
1789
|
+
if (!profile.ratioOptions.some((option) => option.value === input.ratio)) throw new Error("该 Provider 不支持所选比例");
|
|
1790
|
+
if (!profile.qualityOptions.some((option) => option.value === input.quality)) throw new Error("该 Provider 不支持所选清晰度");
|
|
1791
|
+
if (input.mode === "edit" && !profile.supportsEditing) throw new Error("该 Provider 暂不支持图生图");
|
|
1792
|
+
}
|
|
1793
|
+
async function readStudioReference(ctx, reference, signal) {
|
|
1794
|
+
if (reference === void 0) throw new Error("图生图需要至少一张参考图");
|
|
1795
|
+
if ("attachment" in reference) {
|
|
1796
|
+
const stored = await ctx.attachments.readImage(reference.attachment, signal);
|
|
1797
|
+
return {
|
|
1798
|
+
data: stored.data,
|
|
1799
|
+
mediaType: stored.ref.mediaType
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
const data = decodeCanonicalBase64(reference.data);
|
|
1803
|
+
if (data.byteLength > ctx.attachments.imageLimits.maxImageBytes) throw new Error("参考图超过当前 DSH 的大小限制");
|
|
1804
|
+
await ctx.attachments.validateImage({
|
|
1805
|
+
data,
|
|
1806
|
+
mediaType: reference.mediaType,
|
|
1807
|
+
...reference.name === void 0 ? {} : { name: reference.name }
|
|
1808
|
+
});
|
|
1809
|
+
return {
|
|
1810
|
+
data,
|
|
1811
|
+
mediaType: reference.mediaType
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
function decodeCanonicalBase64(value) {
|
|
1815
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) throw new Error("参考图编码无效");
|
|
1816
|
+
const data = Buffer.from(value, "base64");
|
|
1817
|
+
if (data.byteLength === 0 || data.toString("base64") !== value) throw new Error("参考图编码无效");
|
|
1818
|
+
return new Uint8Array(data);
|
|
1819
|
+
}
|
|
1820
|
+
function openAISize(ratio) {
|
|
1821
|
+
if (ratio === "3:2") return "1536x1024";
|
|
1822
|
+
if (ratio === "2:3") return "1024x1536";
|
|
1823
|
+
return "1024x1024";
|
|
1824
|
+
}
|
|
1825
|
+
function dashScopeSize(ratio) {
|
|
1826
|
+
return {
|
|
1827
|
+
"1:1": "1024*1024",
|
|
1828
|
+
"3:2": "1536*1024",
|
|
1829
|
+
"2:3": "1024*1536",
|
|
1830
|
+
"16:9": "1664*928",
|
|
1831
|
+
"9:16": "928*1664"
|
|
1832
|
+
}[ratio] ?? "1024*1024";
|
|
1833
|
+
}
|
|
1834
|
+
function cloudProvider$1(value) {
|
|
1835
|
+
return CLOUD_IMAGE_PROVIDERS.includes(value);
|
|
1836
|
+
}
|
|
1837
|
+
//#endregion
|
|
1838
|
+
//#region lib/types/studio-route.js
|
|
1839
|
+
/** Serve workbench capabilities and generation requests without exposing provider credentials. */
|
|
1840
|
+
async function serveStudio(req, res, deps) {
|
|
1841
|
+
if (!sameOrigin(req)) return jsonError(res, 403, "origin-rejected");
|
|
1842
|
+
if (req.method === "GET") try {
|
|
1843
|
+
return json(res, 200, await deps.describe());
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
return jsonError(res, 500, errorMessage(error, "studio-unavailable"));
|
|
1846
|
+
}
|
|
1847
|
+
if (req.method !== "POST") return jsonError(res, 405, "method-not-allowed");
|
|
1848
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError(res, 415, "json-required");
|
|
1849
|
+
const controller = new AbortController();
|
|
1850
|
+
const onConnectionClose = () => {
|
|
1851
|
+
if (!res.writableEnded) controller.abort(/* @__PURE__ */ new Error("The browser closed the image generation request."));
|
|
1852
|
+
};
|
|
1853
|
+
const onReqAborted = () => {
|
|
1854
|
+
controller.abort(/* @__PURE__ */ new Error("The browser closed the image generation request."));
|
|
1855
|
+
};
|
|
1856
|
+
res.once("close", onConnectionClose);
|
|
1857
|
+
req.once("aborted", onReqAborted);
|
|
1858
|
+
try {
|
|
1859
|
+
let input;
|
|
1860
|
+
try {
|
|
1861
|
+
input = parseStudioGenerateRequest(JSON.parse(await readBody(req, deps.maxBodyBytes)));
|
|
1862
|
+
} catch (error) {
|
|
1863
|
+
return jsonError(res, 400, errorMessage(error, "invalid-request"));
|
|
1864
|
+
}
|
|
1865
|
+
const output = await deps.generate(input, controller.signal);
|
|
1866
|
+
if (!res.headersSent && !res.writableEnded && !res.destroyed) json(res, 200, output);
|
|
1867
|
+
} catch (error) {
|
|
1868
|
+
if (res.headersSent || res.writableEnded || res.destroyed) return;
|
|
1869
|
+
jsonError(res, controller.signal.aborted ? 499 : 502, errorMessage(error, "generation-failed"));
|
|
1870
|
+
} finally {
|
|
1871
|
+
res.off("close", onConnectionClose);
|
|
1872
|
+
req.off("aborted", onReqAborted);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
/** Strictly validate the small untrusted workbench wire contract. */
|
|
1876
|
+
function parseStudioGenerateRequest(value) {
|
|
1877
|
+
const input = record(value);
|
|
1878
|
+
if (input === void 0) throw new Error("请求格式无效");
|
|
1879
|
+
if (input.mode !== "generate" && input.mode !== "edit") throw new Error("请选择生成类型");
|
|
1880
|
+
if (!cloudProvider(input.provider)) throw new Error("不支持该图像 Provider");
|
|
1881
|
+
const prompt = requiredText(input.prompt, "请输入提示词", 2e3);
|
|
1882
|
+
const model = requiredText(input.model, "请选择模型", 200);
|
|
1883
|
+
const ratio = requiredText(input.ratio, "请选择比例", 32);
|
|
1884
|
+
const quality = requiredText(input.quality, "请选择清晰度", 32);
|
|
1885
|
+
const references = (Array.isArray(input.references) ? input.references : input.reference !== void 0 ? [input.reference] : []).map(parseReference);
|
|
1886
|
+
if (input.mode === "edit" && references.length === 0) throw new Error("图生图需要至少一张参考图");
|
|
1887
|
+
if (references.length > 5) throw new Error("最多支持上传 5 张参考图");
|
|
1888
|
+
const workspaceRoot = typeof input.workspaceRoot === "string" && input.workspaceRoot.trim().length > 0 ? input.workspaceRoot.trim() : void 0;
|
|
1889
|
+
return {
|
|
1890
|
+
mode: input.mode,
|
|
1891
|
+
provider: input.provider,
|
|
1892
|
+
model,
|
|
1893
|
+
prompt,
|
|
1894
|
+
ratio,
|
|
1895
|
+
quality,
|
|
1896
|
+
...references.length > 0 ? {
|
|
1897
|
+
references,
|
|
1898
|
+
reference: references[0]
|
|
1899
|
+
} : {},
|
|
1900
|
+
...workspaceRoot !== void 0 ? { workspaceRoot } : {}
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
function parseReference(value) {
|
|
1904
|
+
const reference = record(value);
|
|
1905
|
+
if (reference === void 0) throw new Error("参考图格式无效");
|
|
1906
|
+
if ("attachment" in reference) {
|
|
1907
|
+
const attachment = parseImageAttachmentRef(reference.attachment);
|
|
1908
|
+
if (attachment === void 0) throw new Error("参考图附件无效");
|
|
1909
|
+
return { attachment };
|
|
1910
|
+
}
|
|
1911
|
+
if (!imageMediaType(reference.mediaType) || typeof reference.data !== "string") throw new Error("参考图格式无效");
|
|
1912
|
+
if (reference.data.length === 0) throw new Error("参考图内容为空");
|
|
1913
|
+
if (reference.name !== void 0 && typeof reference.name !== "string") throw new Error("参考图名称无效");
|
|
1914
|
+
return {
|
|
1915
|
+
mediaType: reference.mediaType,
|
|
1916
|
+
data: reference.data,
|
|
1917
|
+
...typeof reference.name === "string" ? { name: reference.name.slice(0, 240) } : {}
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
function requiredText(value, message, maxLength) {
|
|
1921
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(message);
|
|
1922
|
+
const text = value.trim();
|
|
1923
|
+
if (text.length > maxLength) throw new Error(`${message}(最多 ${String(maxLength)} 个字符)`);
|
|
1924
|
+
return text;
|
|
1925
|
+
}
|
|
1926
|
+
function cloudProvider(value) {
|
|
1927
|
+
return typeof value === "string" && CLOUD_IMAGE_PROVIDERS.includes(value);
|
|
1928
|
+
}
|
|
1929
|
+
function imageMediaType(value) {
|
|
1930
|
+
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
1931
|
+
}
|
|
1932
|
+
function sameOrigin(req) {
|
|
1933
|
+
const origin = req.headers.origin;
|
|
1934
|
+
const host = req.headers.host;
|
|
1935
|
+
return origin === void 0 || host === void 0 || origin === `http://${host}` || origin === `https://${host}`;
|
|
1936
|
+
}
|
|
1937
|
+
function record(value) {
|
|
1938
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1939
|
+
}
|
|
1940
|
+
async function readBody(req, maxBytes) {
|
|
1941
|
+
const chunks = [];
|
|
1942
|
+
let bytes = 0;
|
|
1943
|
+
for await (const chunk of req) {
|
|
1944
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1945
|
+
bytes += buffer.byteLength;
|
|
1946
|
+
if (bytes > maxBytes) throw new Error("参考图过大,请压缩后重试");
|
|
1947
|
+
chunks.push(buffer);
|
|
1948
|
+
}
|
|
1949
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1950
|
+
}
|
|
1951
|
+
function errorMessage(error, fallback) {
|
|
1952
|
+
return error instanceof Error && error.message.trim().length > 0 ? error.message : fallback;
|
|
1953
|
+
}
|
|
1954
|
+
function json(res, status, value) {
|
|
1955
|
+
if (res.headersSent || res.writableEnded || res.destroyed) return;
|
|
1956
|
+
res.writeHead(status, {
|
|
1957
|
+
"content-type": "application/json; charset=utf-8",
|
|
1958
|
+
"cache-control": "no-store"
|
|
1959
|
+
});
|
|
1960
|
+
res.end(JSON.stringify(value));
|
|
1961
|
+
}
|
|
1962
|
+
function jsonError(res, status, message) {
|
|
1963
|
+
if (res.headersSent || res.writableEnded || res.destroyed) return;
|
|
1964
|
+
json(res, status, { error: message });
|
|
1965
|
+
}
|
|
1378
1966
|
//#endregion
|
|
1379
1967
|
//#region lib/types/index.js
|
|
1380
1968
|
const name = "dsh-image-gen";
|
|
@@ -1386,6 +1974,7 @@ const inject = [
|
|
|
1386
1974
|
];
|
|
1387
1975
|
function apply(ctx, config = {}) {
|
|
1388
1976
|
let current = () => config;
|
|
1977
|
+
const knownWorkspaceRoots = /* @__PURE__ */ new Set();
|
|
1389
1978
|
installImageSettings(ctx, config, {
|
|
1390
1979
|
setSource: (source) => {
|
|
1391
1980
|
current = source;
|
|
@@ -1397,6 +1986,32 @@ function apply(ctx, config = {}) {
|
|
|
1397
1986
|
path: IMAGE_ROUTE,
|
|
1398
1987
|
handler: (req, res) => serveImage(req, res, { readImage: (ref) => ctx.attachments.readImage(ref) })
|
|
1399
1988
|
}), "dsh-image-gen: image route");
|
|
1989
|
+
ctx.effect(() => ctx.webServer.register({
|
|
1990
|
+
kind: "exact",
|
|
1991
|
+
path: DELETE_ROUTE,
|
|
1992
|
+
handler: (req, res) => serveDelete(req, res, { deleteWorkspaceImage: (filePath) => deleteImageFromWorkspace(filePath, knownWorkspaceRoots) })
|
|
1993
|
+
}), "dsh-image-gen: delete route");
|
|
1994
|
+
ctx.effect(() => ctx.webServer.register({
|
|
1995
|
+
kind: "exact",
|
|
1996
|
+
path: STUDIO_ROUTE,
|
|
1997
|
+
handler: (req, res) => serveStudio(req, res, {
|
|
1998
|
+
describe: async () => {
|
|
1999
|
+
const base = await describeStudio(ctx, current());
|
|
2000
|
+
const workspaces = await getDshWorkspacesFull().catch(() => []);
|
|
2001
|
+
const activeRoot = Array.from(knownWorkspaceRoots)[0] || workspaces[0]?.path || process.cwd();
|
|
2002
|
+
return {
|
|
2003
|
+
...base,
|
|
2004
|
+
workspaceRoot: activeRoot,
|
|
2005
|
+
workspaces
|
|
2006
|
+
};
|
|
2007
|
+
},
|
|
2008
|
+
generate: (input, signal) => {
|
|
2009
|
+
const fallbackRoot = Array.from(knownWorkspaceRoots)[0] || process.cwd();
|
|
2010
|
+
return generateFromStudio(ctx, current(), input, signal, fallbackRoot);
|
|
2011
|
+
},
|
|
2012
|
+
maxBodyBytes: Math.ceil(ctx.attachments.imageLimits.maxImageBytes * 1.4 * 5) + 262144
|
|
2013
|
+
})
|
|
2014
|
+
}), "dsh-image-gen: studio route");
|
|
1400
2015
|
ctx.tools.register(defineTool({
|
|
1401
2016
|
name: "generate_image",
|
|
1402
2017
|
description: "Generate a new image with the configured provider. Use when the user asks to create or draw a new image; use edit_image instead when they want to change an existing image. Give a complete visual prompt including subject, composition, style, lighting, and any exact text that should appear. A successful image is attached directly to the conversation and may also be saved under the session workspace. Do not call read, glob, or other tools to locate or verify the image.",
|
|
@@ -1449,7 +2064,7 @@ function apply(ctx, config = {}) {
|
|
|
1449
2064
|
timeoutMs: active.timeoutMs,
|
|
1450
2065
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1451
2066
|
signal: exec.signal
|
|
1452
|
-
}), active.provider, workflow.name, "API workflow", current(), exec);
|
|
2067
|
+
}), active.provider, workflow.name, "API workflow", current(), exec, knownWorkspaceRoots);
|
|
1453
2068
|
}
|
|
1454
2069
|
const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
|
|
1455
2070
|
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.`);
|
|
@@ -1465,7 +2080,7 @@ function apply(ctx, config = {}) {
|
|
|
1465
2080
|
imageSize,
|
|
1466
2081
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1467
2082
|
signal: exec.signal
|
|
1468
|
-
}), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec);
|
|
2083
|
+
}), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec, knownWorkspaceRoots);
|
|
1469
2084
|
}
|
|
1470
2085
|
if (active.provider === "dashscope") {
|
|
1471
2086
|
const size = args.size ?? active.imageSize;
|
|
@@ -1477,7 +2092,7 @@ function apply(ctx, config = {}) {
|
|
|
1477
2092
|
size,
|
|
1478
2093
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1479
2094
|
signal: exec.signal
|
|
1480
|
-
}), active.provider, active.model, size, current(), exec);
|
|
2095
|
+
}), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
|
|
1481
2096
|
}
|
|
1482
2097
|
const size = args.size ?? active.imageSize;
|
|
1483
2098
|
return saveGenerated(ctx, await generateOpenAICompatibleImage({
|
|
@@ -1489,7 +2104,7 @@ function apply(ctx, config = {}) {
|
|
|
1489
2104
|
size,
|
|
1490
2105
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1491
2106
|
signal: exec.signal
|
|
1492
|
-
}), active.provider, active.model, size, current(), exec);
|
|
2107
|
+
}), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
|
|
1493
2108
|
},
|
|
1494
2109
|
presentResult: (_args, result) => imagePresentation(result)
|
|
1495
2110
|
}));
|
|
@@ -1580,7 +2195,7 @@ function apply(ctx, config = {}) {
|
|
|
1580
2195
|
timeoutMs: active.timeoutMs,
|
|
1581
2196
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1582
2197
|
signal: exec.signal
|
|
1583
|
-
}), active.provider, workflow.name, "API workflow", current(), exec);
|
|
2198
|
+
}), active.provider, workflow.name, "API workflow", current(), exec, knownWorkspaceRoots);
|
|
1584
2199
|
}
|
|
1585
2200
|
const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
|
|
1586
2201
|
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.`);
|
|
@@ -1597,7 +2212,7 @@ function apply(ctx, config = {}) {
|
|
|
1597
2212
|
imageSize,
|
|
1598
2213
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1599
2214
|
signal: exec.signal
|
|
1600
|
-
}), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec);
|
|
2215
|
+
}), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec, knownWorkspaceRoots);
|
|
1601
2216
|
}
|
|
1602
2217
|
const size = args.size ?? active.imageSize;
|
|
1603
2218
|
if (active.provider === "openai") return saveGenerated(ctx, await editOpenAICompatibleImage({
|
|
@@ -1609,7 +2224,7 @@ function apply(ctx, config = {}) {
|
|
|
1609
2224
|
size,
|
|
1610
2225
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1611
2226
|
signal: exec.signal
|
|
1612
|
-
}), active.provider, active.model, size, current(), exec);
|
|
2227
|
+
}), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
|
|
1613
2228
|
if (active.provider === "seedream") return saveGenerated(ctx, await editSeedreamImage({
|
|
1614
2229
|
apiKey: credential.value,
|
|
1615
2230
|
baseURL: active.baseURL,
|
|
@@ -1619,7 +2234,7 @@ function apply(ctx, config = {}) {
|
|
|
1619
2234
|
size,
|
|
1620
2235
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1621
2236
|
signal: exec.signal
|
|
1622
|
-
}), active.provider, active.model, size, current(), exec);
|
|
2237
|
+
}), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
|
|
1623
2238
|
return saveGenerated(ctx, await editDashScopeImage({
|
|
1624
2239
|
apiKey: credential.value,
|
|
1625
2240
|
endpoint: active.endpoint,
|
|
@@ -1629,7 +2244,7 @@ function apply(ctx, config = {}) {
|
|
|
1629
2244
|
size,
|
|
1630
2245
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1631
2246
|
signal: exec.signal
|
|
1632
|
-
}), active.provider, active.model, size, current(), exec);
|
|
2247
|
+
}), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
|
|
1633
2248
|
},
|
|
1634
2249
|
presentResult: (_args, result) => imagePresentation(result)
|
|
1635
2250
|
}));
|
|
@@ -1736,7 +2351,7 @@ function attachmentMeta(ref) {
|
|
|
1736
2351
|
} }
|
|
1737
2352
|
};
|
|
1738
2353
|
}
|
|
1739
|
-
async function saveGenerated(ctx, generated, provider, model, output, config, exec) {
|
|
2354
|
+
async function saveGenerated(ctx, generated, provider, model, output, config, exec, knownRoots) {
|
|
1740
2355
|
if (!ctx.attachments.imageLimits.mediaTypes.includes(generated.mediaType)) throw new Error(`This DSH deployment does not accept ${generated.mediaType} generated images`);
|
|
1741
2356
|
const attachment = await ctx.attachments.saveImage({
|
|
1742
2357
|
data: generated.data,
|
|
@@ -1753,6 +2368,7 @@ async function saveGenerated(ctx, generated, provider, model, output, config, ex
|
|
|
1753
2368
|
if (config.saveToWorkspace === false) return value;
|
|
1754
2369
|
const workspaceRoot = exec.agent?.session.header.cwd;
|
|
1755
2370
|
if (workspaceRoot === void 0) return value;
|
|
2371
|
+
knownRoots?.add(workspaceRoot);
|
|
1756
2372
|
try {
|
|
1757
2373
|
value.savedTo = await saveImageToWorkspace({
|
|
1758
2374
|
workspaceRoot,
|
|
@@ -1804,4 +2420,4 @@ function installImageSettings(ctx, config, hooks) {
|
|
|
1804
2420
|
ctx.logger.warn("dsh-image-gen: this DSH exposes neither settings API generation; settings UI stays on the composition entry");
|
|
1805
2421
|
}
|
|
1806
2422
|
//#endregion
|
|
1807
|
-
export { Config, IMAGE_ROUTE, apply, imageAttachmentFromMeta, inject, name };
|
|
2423
|
+
export { Config, DELETE_ROUTE, IMAGE_ROUTE, STUDIO_ROUTE, apply, imageAttachmentFromMeta, inject, name };
|