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/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$4(value);
214
- const inputs = node === void 0 ? void 0 : record$4(node.inputs);
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$4(node)?.inputs;
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$4(value) {
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$3(parseJsonRecord(text, "ComfyUI history returned invalid JSON")[promptId]);
399
+ const entry = record$4(parseJsonRecord(text, "ComfyUI history returned invalid JSON")[promptId]);
375
400
  if (entry !== void 0) {
376
- const status = record$3(entry.status);
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$3(value);
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$3(nodeOutput)?.images;
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$3(image);
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$3(response.headers.get("content-type")) ?? imageMediaTypeFromName(output.filename);
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$3(value) {
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$3(value);
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$3(value) {
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$2(value);
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$2(step);
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$2(value);
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$2(value) {
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$1(value);
859
+ const ref = record$2(value);
824
860
  if (ref === void 0) return void 0;
825
- if (typeof ref.attachmentId !== "string" || !imageMediaType$2(ref.mediaType)) return void 0;
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$1(value);
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$2(value) {
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$1(value) {
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 = 4096;
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 === 1 ? "image" : "image[]";
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
- form.append(imageField, new Blob([uploadBytes], { type: sourceImage.mediaType }), `reference-${index + 1}.${extensionOf(sourceImage.mediaType)}`);
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$1(image.mime_type) ?? "image/png"
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$1(parsed.mediaType) ?? "image/png"
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$1(response.headers.get("content-type"));
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$1(value) {
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 };