dsh-image-gen 0.2.6 → 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 +2809 -300
- package/lib/client.js.map +1 -1
- package/lib/index.js +579 -108
- 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 +19 -0
- package/lib/types/client/gallery-view.d.ts +27 -5
- 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 +0 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/shared.d.ts +71 -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 +3 -0
- package/package.json +8 -10
- package/skills/install-dsh-image-gen/SKILL.md +22 -22
package/lib/index.js
CHANGED
|
@@ -3,14 +3,15 @@ import * as dshSettings from "@deepseek-ai/dsh-settings";
|
|
|
3
3
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { lstat, mkdir, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
6
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
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";
|
|
12
11
|
/** Browser route used for deleting generated images and workspace files. */
|
|
13
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";
|
|
14
15
|
/** Namespace persisted through DSH Settings. */
|
|
15
16
|
const IMAGE_GENERATION_NAMESPACE = "image-generation";
|
|
16
17
|
/** Supported providers. */
|
|
@@ -21,6 +22,13 @@ const IMAGE_PROVIDERS = [
|
|
|
21
22
|
"dashscope",
|
|
22
23
|
"comfyui"
|
|
23
24
|
];
|
|
25
|
+
/** Providers supported by the first browser workbench release. */
|
|
26
|
+
const CLOUD_IMAGE_PROVIDERS = [
|
|
27
|
+
"google",
|
|
28
|
+
"openai",
|
|
29
|
+
"seedream",
|
|
30
|
+
"dashscope"
|
|
31
|
+
];
|
|
24
32
|
/** Default endpoints and base URLs. */
|
|
25
33
|
const DEFAULT_GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/interactions";
|
|
26
34
|
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
@@ -93,6 +101,21 @@ const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
|
|
|
93
101
|
const SEEDREAM_API_KEY_ENV = "ARK_API_KEY";
|
|
94
102
|
/** DashScope credential reference. */
|
|
95
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
|
+
];
|
|
96
119
|
/** Cordis configuration schema. */
|
|
97
120
|
const Config = z.object({
|
|
98
121
|
provider: z.union(IMAGE_PROVIDERS).default("google"),
|
|
@@ -212,8 +235,8 @@ function prepareComfyUIWorkflow(workflowJson, prompt, seed = randomSeed(), image
|
|
|
212
235
|
};
|
|
213
236
|
let imageInputs = 0;
|
|
214
237
|
const prepared = Object.fromEntries(Object.entries(workflow).map(([nodeId, value]) => {
|
|
215
|
-
const node = record$
|
|
216
|
-
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);
|
|
217
240
|
if (inputs === void 0) return [nodeId, value];
|
|
218
241
|
const nextInputs = inject(inputs);
|
|
219
242
|
if (isImagePlaceholder(nextInputs[IMAGE_INPUT_KEY])) {
|
|
@@ -249,7 +272,7 @@ function parseWorkflow(workflowJson) {
|
|
|
249
272
|
}
|
|
250
273
|
if (!isRecord(value) || Object.keys(value).length === 0) throw new Error("ComfyUI workflow must be a non-empty API-format JSON object");
|
|
251
274
|
if (!Object.values(value).some((node) => {
|
|
252
|
-
const inputs = record$
|
|
275
|
+
const inputs = record$5(node)?.inputs;
|
|
253
276
|
return inputs !== void 0 && containsPromptPlaceholder(inputs);
|
|
254
277
|
})) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} (or ${LEGACY_PROMPT_PLACEHOLDER}) in a text input`);
|
|
255
278
|
return value;
|
|
@@ -265,7 +288,7 @@ function containsPromptPlaceholder(value) {
|
|
|
265
288
|
function isRecord(value) {
|
|
266
289
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
267
290
|
}
|
|
268
|
-
function record$
|
|
291
|
+
function record$5(value) {
|
|
269
292
|
return isRecord(value) ? value : void 0;
|
|
270
293
|
}
|
|
271
294
|
//#endregion
|
|
@@ -373,9 +396,9 @@ async function waitForOutput(baseURL, promptId, signal) {
|
|
|
373
396
|
});
|
|
374
397
|
const text = await readBoundedText$3(response, MAX_HISTORY_BYTES);
|
|
375
398
|
if (!response.ok) throw new Error(`ComfyUI history request failed (${response.status}): ${text.slice(0, ERROR_LIMIT$3)}`);
|
|
376
|
-
const entry = record$
|
|
399
|
+
const entry = record$4(parseJsonRecord(text, "ComfyUI history returned invalid JSON")[promptId]);
|
|
377
400
|
if (entry !== void 0) {
|
|
378
|
-
const status = record$
|
|
401
|
+
const status = record$4(entry.status);
|
|
379
402
|
if (status?.status_str === "error") throw new Error(`ComfyUI workflow failed: ${JSON.stringify(status.messages ?? status).slice(0, ERROR_LIMIT$3)}`);
|
|
380
403
|
const output = firstOutputImage(entry.outputs);
|
|
381
404
|
if (output !== void 0) return output;
|
|
@@ -385,13 +408,13 @@ async function waitForOutput(baseURL, promptId, signal) {
|
|
|
385
408
|
}
|
|
386
409
|
}
|
|
387
410
|
function firstOutputImage(value) {
|
|
388
|
-
const outputs = record$
|
|
411
|
+
const outputs = record$4(value);
|
|
389
412
|
if (outputs === void 0) return void 0;
|
|
390
413
|
for (const nodeOutput of Object.values(outputs)) {
|
|
391
|
-
const images = record$
|
|
414
|
+
const images = record$4(nodeOutput)?.images;
|
|
392
415
|
if (!Array.isArray(images)) continue;
|
|
393
416
|
for (const image of images) {
|
|
394
|
-
const item = record$
|
|
417
|
+
const item = record$4(image);
|
|
395
418
|
if (typeof item?.filename !== "string" || item.filename.length === 0) continue;
|
|
396
419
|
const output = {
|
|
397
420
|
filename: item.filename,
|
|
@@ -412,7 +435,7 @@ async function downloadOutput(baseURL, output, maxBytes, signal) {
|
|
|
412
435
|
signal
|
|
413
436
|
});
|
|
414
437
|
if (!response.ok) throw new Error(`ComfyUI image download failed (${response.status})`);
|
|
415
|
-
const mediaType = imageMediaType$
|
|
438
|
+
const mediaType = imageMediaType$4(response.headers.get("content-type")) ?? imageMediaTypeFromName(output.filename);
|
|
416
439
|
if (mediaType === void 0) throw new Error("ComfyUI image download returned an unsupported content type");
|
|
417
440
|
return {
|
|
418
441
|
data: await readBoundedBytes$2(response, maxBytes),
|
|
@@ -432,7 +455,7 @@ function comfyUIBaseURL(value) {
|
|
|
432
455
|
function endpoint(baseURL, path) {
|
|
433
456
|
return new URL(path, baseURL);
|
|
434
457
|
}
|
|
435
|
-
function record$
|
|
458
|
+
function record$4(value) {
|
|
436
459
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
437
460
|
}
|
|
438
461
|
function parseJsonRecord(text, message) {
|
|
@@ -442,11 +465,11 @@ function parseJsonRecord(text, message) {
|
|
|
442
465
|
} catch {
|
|
443
466
|
throw new Error(message);
|
|
444
467
|
}
|
|
445
|
-
const parsed = record$
|
|
468
|
+
const parsed = record$4(value);
|
|
446
469
|
if (parsed === void 0) throw new Error(message);
|
|
447
470
|
return parsed;
|
|
448
471
|
}
|
|
449
|
-
function imageMediaType$
|
|
472
|
+
function imageMediaType$4(value) {
|
|
450
473
|
const mediaType = value?.split(";", 1)[0]?.trim().toLowerCase();
|
|
451
474
|
return mediaType === "image/png" || mediaType === "image/jpeg" || mediaType === "image/webp" || mediaType === "image/gif" ? mediaType : void 0;
|
|
452
475
|
}
|
|
@@ -601,17 +624,28 @@ function generateGoogleImage(input) {
|
|
|
601
624
|
}
|
|
602
625
|
/** Send one native Google image-editing request using already-resolved bytes. */
|
|
603
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
|
+
}];
|
|
604
645
|
return requestGoogleImage({
|
|
605
646
|
...input,
|
|
606
647
|
operation: "editing",
|
|
607
|
-
interactionInput
|
|
608
|
-
type: "text",
|
|
609
|
-
text: input.prompt
|
|
610
|
-
}, ...input.sourceImages.map((sourceImage) => ({
|
|
611
|
-
type: "image",
|
|
612
|
-
mime_type: sourceImage.mediaType,
|
|
613
|
-
data: Buffer.from(sourceImage.data).toString("base64")
|
|
614
|
-
}))]
|
|
648
|
+
interactionInput
|
|
615
649
|
});
|
|
616
650
|
}
|
|
617
651
|
/** Shared Google request, response parsing, decoding, and size enforcement. */
|
|
@@ -656,13 +690,13 @@ async function requestGoogleImage(input) {
|
|
|
656
690
|
};
|
|
657
691
|
}
|
|
658
692
|
function outputImage(value) {
|
|
659
|
-
const interaction = record$
|
|
693
|
+
const interaction = record$3(value);
|
|
660
694
|
if (interaction === void 0) return void 0;
|
|
661
695
|
const direct = imageContent(interaction.output_image, false);
|
|
662
696
|
if (direct !== void 0) return direct;
|
|
663
697
|
if (!Array.isArray(interaction.steps)) return void 0;
|
|
664
698
|
for (const step of interaction.steps) {
|
|
665
|
-
const modelOutput = record$
|
|
699
|
+
const modelOutput = record$3(step);
|
|
666
700
|
if (modelOutput?.type !== "model_output" || !Array.isArray(modelOutput.content)) continue;
|
|
667
701
|
for (const content of modelOutput.content) {
|
|
668
702
|
const image = imageContent(content, true);
|
|
@@ -671,14 +705,14 @@ function outputImage(value) {
|
|
|
671
705
|
}
|
|
672
706
|
}
|
|
673
707
|
function imageContent(value, requiresImageType) {
|
|
674
|
-
const image = record$
|
|
708
|
+
const image = record$3(value);
|
|
675
709
|
if (image === void 0 || requiresImageType && image.type !== "image" || typeof image.data !== "string") return void 0;
|
|
676
710
|
return {
|
|
677
711
|
data: image.data,
|
|
678
712
|
mime_type: image.mime_type
|
|
679
713
|
};
|
|
680
714
|
}
|
|
681
|
-
function record$
|
|
715
|
+
function record$3(value) {
|
|
682
716
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
683
717
|
}
|
|
684
718
|
function mediaTypeOf(value) {
|
|
@@ -822,9 +856,9 @@ function findReferenceImage(messages, sourceAttachmentId) {
|
|
|
822
856
|
}
|
|
823
857
|
/** Validate an untrusted serialized image reference at an HTTP/UI boundary. */
|
|
824
858
|
function parseImageAttachmentRef(value) {
|
|
825
|
-
const ref = record$
|
|
859
|
+
const ref = record$2(value);
|
|
826
860
|
if (ref === void 0) return void 0;
|
|
827
|
-
if (typeof ref.attachmentId !== "string" || !imageMediaType$
|
|
861
|
+
if (typeof ref.attachmentId !== "string" || !imageMediaType$3(ref.mediaType)) return void 0;
|
|
828
862
|
if (!nonNegativeInteger(ref.bytes) || !positiveInteger(ref.width) || !positiveInteger(ref.height)) return void 0;
|
|
829
863
|
if (ref.name !== void 0 && typeof ref.name !== "string") return void 0;
|
|
830
864
|
const originalDimensions = ref.originalDimensions === void 0 ? void 0 : parseDimensions(ref.originalDimensions);
|
|
@@ -854,14 +888,14 @@ function findInBlocks(blocks, sourceAttachmentId) {
|
|
|
854
888
|
}
|
|
855
889
|
}
|
|
856
890
|
function parseDimensions(value) {
|
|
857
|
-
const dimensions = record$
|
|
891
|
+
const dimensions = record$2(value);
|
|
858
892
|
if (dimensions === void 0 || !positiveInteger(dimensions.width) || !positiveInteger(dimensions.height)) return void 0;
|
|
859
893
|
return {
|
|
860
894
|
width: dimensions.width,
|
|
861
895
|
height: dimensions.height
|
|
862
896
|
};
|
|
863
897
|
}
|
|
864
|
-
function imageMediaType$
|
|
898
|
+
function imageMediaType$3(value) {
|
|
865
899
|
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
866
900
|
}
|
|
867
901
|
function collectInBlocks(blocks) {
|
|
@@ -926,7 +960,7 @@ function positiveInteger(value) {
|
|
|
926
960
|
function nonNegativeInteger(value) {
|
|
927
961
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
928
962
|
}
|
|
929
|
-
function record$
|
|
963
|
+
function record$2(value) {
|
|
930
964
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
931
965
|
}
|
|
932
966
|
//#endregion
|
|
@@ -934,19 +968,19 @@ function record$1(value) {
|
|
|
934
968
|
const MAX_BODY_BYTES = 65536;
|
|
935
969
|
/** Serve one verified durable image reference to a same-origin browser request. */
|
|
936
970
|
async function serveImage(req, res, deps) {
|
|
937
|
-
if (req.method !== "POST") return jsonError(res, 405, "method-not-allowed");
|
|
938
|
-
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");
|
|
939
973
|
const origin = req.headers.origin;
|
|
940
974
|
const host = req.headers.host;
|
|
941
|
-
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");
|
|
942
976
|
let body;
|
|
943
977
|
try {
|
|
944
|
-
body = JSON.parse(await readBody(req));
|
|
978
|
+
body = JSON.parse(await readBody$1(req));
|
|
945
979
|
} catch {
|
|
946
|
-
return jsonError(res, 400, "invalid-request");
|
|
980
|
+
return jsonError$1(res, 400, "invalid-request");
|
|
947
981
|
}
|
|
948
982
|
const attachment = attachmentFromRequest(body);
|
|
949
|
-
if (attachment === void 0) return jsonError(res, 400, "invalid-attachment");
|
|
983
|
+
if (attachment === void 0) return jsonError$1(res, 400, "invalid-attachment");
|
|
950
984
|
try {
|
|
951
985
|
const stored = await deps.readImage(attachment);
|
|
952
986
|
res.writeHead(200, {
|
|
@@ -957,24 +991,23 @@ async function serveImage(req, res, deps) {
|
|
|
957
991
|
});
|
|
958
992
|
res.end(stored.data);
|
|
959
993
|
} catch {
|
|
960
|
-
jsonError(res, 404, "image-unavailable");
|
|
994
|
+
jsonError$1(res, 404, "image-unavailable");
|
|
961
995
|
}
|
|
962
996
|
}
|
|
963
997
|
/** Safely delete one or more generated image files from the workspace disk. */
|
|
964
998
|
async function serveDelete(req, res, deps) {
|
|
965
|
-
if (req.method !== "POST") return jsonError(res, 405, "method-not-allowed");
|
|
966
|
-
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError(res, 415, "json-required");
|
|
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");
|
|
967
1001
|
const origin = req.headers.origin;
|
|
968
1002
|
const host = req.headers.host;
|
|
969
|
-
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError(res, 403, "origin-rejected");
|
|
1003
|
+
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError$1(res, 403, "origin-rejected");
|
|
970
1004
|
let body;
|
|
971
1005
|
try {
|
|
972
|
-
body = JSON.parse(await readBody(req));
|
|
1006
|
+
body = JSON.parse(await readBody$1(req));
|
|
973
1007
|
} catch {
|
|
974
|
-
return jsonError(res, 400, "invalid-request");
|
|
1008
|
+
return jsonError$1(res, 400, "invalid-request");
|
|
975
1009
|
}
|
|
976
1010
|
const rawPaths = typeof body === "object" && body !== null && "paths" in body && Array.isArray(body.paths) ? body.paths : [];
|
|
977
|
-
const rawAttachmentIds = typeof body === "object" && body !== null && "attachmentIds" in body && Array.isArray(body.attachmentIds) ? body.attachmentIds : [];
|
|
978
1011
|
const deletedFiles = [];
|
|
979
1012
|
const failedFiles = [];
|
|
980
1013
|
for (const p of rawPaths) if (typeof p === "string" && p.trim()) try {
|
|
@@ -989,14 +1022,6 @@ async function serveDelete(req, res, deps) {
|
|
|
989
1022
|
error: err instanceof Error ? err.message : String(err)
|
|
990
1023
|
});
|
|
991
1024
|
}
|
|
992
|
-
for (const id of rawAttachmentIds) if (typeof id === "string" && id.trim()) try {
|
|
993
|
-
if (await deps.deleteWorkspaceImageByAttachmentId(id)) deletedFiles.push(id);
|
|
994
|
-
} catch (err) {
|
|
995
|
-
failedFiles.push({
|
|
996
|
-
path: id,
|
|
997
|
-
error: err instanceof Error ? err.message : String(err)
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
1025
|
res.writeHead(200, {
|
|
1001
1026
|
"content-type": "application/json",
|
|
1002
1027
|
"cache-control": "no-store"
|
|
@@ -1010,17 +1035,17 @@ async function serveDelete(req, res, deps) {
|
|
|
1010
1035
|
}
|
|
1011
1036
|
/** Validate the persisted reference carried by a tool presentation. */
|
|
1012
1037
|
function imageAttachmentFromMeta(meta) {
|
|
1013
|
-
const value = record(meta);
|
|
1038
|
+
const value = record$1(meta);
|
|
1014
1039
|
if (value?.kind !== "dsh-image-gen") return void 0;
|
|
1015
1040
|
return parseImageAttachmentRef(value.attachment);
|
|
1016
1041
|
}
|
|
1017
1042
|
function attachmentFromRequest(value) {
|
|
1018
|
-
return parseImageAttachmentRef(record(value)?.attachment);
|
|
1043
|
+
return parseImageAttachmentRef(record$1(value)?.attachment);
|
|
1019
1044
|
}
|
|
1020
|
-
function record(value) {
|
|
1045
|
+
function record$1(value) {
|
|
1021
1046
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1022
1047
|
}
|
|
1023
|
-
async function readBody(req) {
|
|
1048
|
+
async function readBody$1(req) {
|
|
1024
1049
|
const chunks = [];
|
|
1025
1050
|
let bytes = 0;
|
|
1026
1051
|
for await (const chunk of req) {
|
|
@@ -1031,7 +1056,7 @@ async function readBody(req) {
|
|
|
1031
1056
|
}
|
|
1032
1057
|
return Buffer.concat(chunks).toString("utf8");
|
|
1033
1058
|
}
|
|
1034
|
-
function jsonError(res, status, code) {
|
|
1059
|
+
function jsonError$1(res, status, code) {
|
|
1035
1060
|
res.writeHead(status, {
|
|
1036
1061
|
"content-type": "application/json",
|
|
1037
1062
|
"cache-control": "no-store"
|
|
@@ -1060,10 +1085,12 @@ async function generateOpenAICompatibleImage(input) {
|
|
|
1060
1085
|
}
|
|
1061
1086
|
async function editOpenAICompatibleImage(input) {
|
|
1062
1087
|
const form = new FormData();
|
|
1063
|
-
const imageField = input.sourceImages.length
|
|
1088
|
+
const imageField = input.sourceImages.length > 1 ? "image[]" : "image";
|
|
1064
1089
|
input.sourceImages.forEach((sourceImage, index) => {
|
|
1065
1090
|
const uploadBytes = new Uint8Array(sourceImage.data);
|
|
1066
|
-
|
|
1091
|
+
const blob = new Blob([uploadBytes], { type: sourceImage.mediaType });
|
|
1092
|
+
const filename = `reference-${index + 1}.${extensionOf(sourceImage.mediaType)}`;
|
|
1093
|
+
form.append(imageField, blob, filename);
|
|
1067
1094
|
});
|
|
1068
1095
|
form.append("prompt", input.prompt);
|
|
1069
1096
|
form.append("model", input.model);
|
|
@@ -1089,7 +1116,7 @@ async function parseImageResponse(response, provider, input) {
|
|
|
1089
1116
|
if (image === void 0) throw new Error(`${provider} image request returned no image: ${text.slice(0, ERROR_LIMIT$1)}`);
|
|
1090
1117
|
if (image.b64_json !== void 0) return {
|
|
1091
1118
|
data: decodeBase64$1(image.b64_json, provider),
|
|
1092
|
-
mediaType: imageMediaType$
|
|
1119
|
+
mediaType: imageMediaType$2(image.mime_type) ?? "image/png"
|
|
1093
1120
|
};
|
|
1094
1121
|
return downloadImage$1(image.url, provider, input);
|
|
1095
1122
|
}
|
|
@@ -1124,7 +1151,7 @@ async function downloadImage$1(url, provider, input) {
|
|
|
1124
1151
|
if (parsed === void 0) throw new Error(`${provider} image request returned invalid data URL`);
|
|
1125
1152
|
return {
|
|
1126
1153
|
data: decodeBase64$1(parsed.base64, provider),
|
|
1127
|
-
mediaType: imageMediaType$
|
|
1154
|
+
mediaType: imageMediaType$2(parsed.mediaType) ?? "image/png"
|
|
1128
1155
|
};
|
|
1129
1156
|
}
|
|
1130
1157
|
const response = await fetch(url, {
|
|
@@ -1133,7 +1160,7 @@ async function downloadImage$1(url, provider, input) {
|
|
|
1133
1160
|
...input.apiKey === void 0 ? {} : { headers: { authorization: `Bearer ${input.apiKey}` } }
|
|
1134
1161
|
});
|
|
1135
1162
|
if (!response.ok) throw new Error(`${provider} image download failed (${response.status})`);
|
|
1136
|
-
const mediaType = imageMediaType$
|
|
1163
|
+
const mediaType = imageMediaType$2(response.headers.get("content-type"));
|
|
1137
1164
|
if (mediaType === void 0) throw new Error(`${provider} image download returned unsupported content type`);
|
|
1138
1165
|
return {
|
|
1139
1166
|
data: await readBoundedBytes$1(response, input.maxBytes),
|
|
@@ -1162,7 +1189,7 @@ function decodeBase64$1(value, provider) {
|
|
|
1162
1189
|
if (decoded.length === 0) throw new Error(`${provider} image request returned invalid base64 image data`);
|
|
1163
1190
|
return new Uint8Array(decoded);
|
|
1164
1191
|
}
|
|
1165
|
-
function imageMediaType$
|
|
1192
|
+
function imageMediaType$2(value) {
|
|
1166
1193
|
const mediaType = value?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1167
1194
|
return mediaType === "image/png" || mediaType === "image/jpeg" || mediaType === "image/webp" || mediaType === "image/gif" ? mediaType : void 0;
|
|
1168
1195
|
}
|
|
@@ -1226,7 +1253,7 @@ async function editSeedreamImage(input) {
|
|
|
1226
1253
|
if (image === void 0) throw new Error(`seedream image editing returned no image: ${text.slice(0, ERROR_LIMIT)}`);
|
|
1227
1254
|
if (image.b64_json !== void 0) return {
|
|
1228
1255
|
data: decodeBase64(image.b64_json),
|
|
1229
|
-
mediaType: imageMediaType(image.mime_type) ?? "image/png"
|
|
1256
|
+
mediaType: imageMediaType$1(image.mime_type) ?? "image/png"
|
|
1230
1257
|
};
|
|
1231
1258
|
return downloadImage(image.url, input);
|
|
1232
1259
|
}
|
|
@@ -1264,7 +1291,7 @@ async function downloadImage(url, input) {
|
|
|
1264
1291
|
signal: input.signal
|
|
1265
1292
|
});
|
|
1266
1293
|
if (!response.ok) throw new Error(`seedream image download failed (${response.status})`);
|
|
1267
|
-
const mediaType = imageMediaType(response.headers.get("content-type"));
|
|
1294
|
+
const mediaType = imageMediaType$1(response.headers.get("content-type"));
|
|
1268
1295
|
if (mediaType === void 0) throw new Error("seedream image download returned unsupported content type");
|
|
1269
1296
|
return {
|
|
1270
1297
|
data: await readBoundedBytes(response, input.maxBytes),
|
|
@@ -1278,7 +1305,7 @@ function decodeBase64(value) {
|
|
|
1278
1305
|
if (decoded.length === 0) throw new Error("seedream image editing returned invalid base64 image data");
|
|
1279
1306
|
return new Uint8Array(decoded);
|
|
1280
1307
|
}
|
|
1281
|
-
function imageMediaType(value) {
|
|
1308
|
+
function imageMediaType$1(value) {
|
|
1282
1309
|
const mediaType = value?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1283
1310
|
return mediaType === "image/png" || mediaType === "image/jpeg" || mediaType === "image/webp" || mediaType === "image/gif" ? mediaType : void 0;
|
|
1284
1311
|
}
|
|
@@ -1427,6 +1454,35 @@ async function saveImageToWorkspace(options) {
|
|
|
1427
1454
|
}
|
|
1428
1455
|
/** Regex pattern strictly matching generated image file names (e.g. image-01234567.png). */
|
|
1429
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
|
+
}
|
|
1430
1486
|
/**
|
|
1431
1487
|
* Safely delete a generated image file from workspace disk by its path.
|
|
1432
1488
|
* Enforces strict safety gates:
|
|
@@ -1470,42 +1526,442 @@ async function deleteImageFromWorkspace(filePath, allowedWorkspaceRoots) {
|
|
|
1470
1526
|
throw err;
|
|
1471
1527
|
}
|
|
1472
1528
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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)}`);
|
|
1486
1690
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
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;
|
|
1504
1860
|
try {
|
|
1505
|
-
|
|
1506
|
-
} catch {
|
|
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);
|
|
1507
1948
|
}
|
|
1508
|
-
return
|
|
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 });
|
|
1509
1965
|
}
|
|
1510
1966
|
//#endregion
|
|
1511
1967
|
//#region lib/types/index.js
|
|
@@ -1533,14 +1989,29 @@ function apply(ctx, config = {}) {
|
|
|
1533
1989
|
ctx.effect(() => ctx.webServer.register({
|
|
1534
1990
|
kind: "exact",
|
|
1535
1991
|
path: DELETE_ROUTE,
|
|
1536
|
-
handler: (req, res) => serveDelete(req, res, {
|
|
1537
|
-
deleteWorkspaceImage: (filePath) => deleteImageFromWorkspace(filePath, knownWorkspaceRoots),
|
|
1538
|
-
deleteWorkspaceImageByAttachmentId: (attachmentId) => deleteImageByAttachmentIdFromWorkspace(attachmentId, {
|
|
1539
|
-
folder: current().workspaceFolder,
|
|
1540
|
-
allowedWorkspaceRoots: knownWorkspaceRoots
|
|
1541
|
-
})
|
|
1542
|
-
})
|
|
1992
|
+
handler: (req, res) => serveDelete(req, res, { deleteWorkspaceImage: (filePath) => deleteImageFromWorkspace(filePath, knownWorkspaceRoots) })
|
|
1543
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");
|
|
1544
2015
|
ctx.tools.register(defineTool({
|
|
1545
2016
|
name: "generate_image",
|
|
1546
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.",
|
|
@@ -1949,4 +2420,4 @@ function installImageSettings(ctx, config, hooks) {
|
|
|
1949
2420
|
ctx.logger.warn("dsh-image-gen: this DSH exposes neither settings API generation; settings UI stays on the composition entry");
|
|
1950
2421
|
}
|
|
1951
2422
|
//#endregion
|
|
1952
|
-
export { Config, DELETE_ROUTE, IMAGE_ROUTE, apply, imageAttachmentFromMeta, inject, name };
|
|
2423
|
+
export { Config, DELETE_ROUTE, IMAGE_ROUTE, STUDIO_ROUTE, apply, imageAttachmentFromMeta, inject, name };
|