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/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$4(value);
216
- 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);
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$4(node)?.inputs;
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$4(value) {
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$3(parseJsonRecord(text, "ComfyUI history returned invalid JSON")[promptId]);
399
+ const entry = record$4(parseJsonRecord(text, "ComfyUI history returned invalid JSON")[promptId]);
377
400
  if (entry !== void 0) {
378
- const status = record$3(entry.status);
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$3(value);
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$3(nodeOutput)?.images;
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$3(image);
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$3(response.headers.get("content-type")) ?? imageMediaTypeFromName(output.filename);
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$3(value) {
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$3(value);
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$3(value) {
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$2(value);
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$2(step);
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$2(value);
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$2(value) {
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$1(value);
859
+ const ref = record$2(value);
826
860
  if (ref === void 0) return void 0;
827
- 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;
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$1(value);
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$2(value) {
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$1(value) {
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 === 1 ? "image" : "image[]";
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
- 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);
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$1(image.mime_type) ?? "image/png"
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$1(parsed.mediaType) ?? "image/png"
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$1(response.headers.get("content-type"));
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$1(value) {
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
- * Safely find and delete a generated image file by its attachment ID within a single workspace root.
1475
- * Strictly checks sha256:64hex format and avoids scanning unrelated workspaces.
1476
- * Only used as fallback for historical images where savedTo was not persisted.
1477
- */
1478
- async function deleteImageByAttachmentIdFromWorkspace(attachmentId, options = {}) {
1479
- const match = attachmentId.match(/^sha256:([0-9a-f]{64})$/i);
1480
- if (!match || !match[1]) return false;
1481
- const prefix = match[1].slice(0, 8);
1482
- const rootsToCheck = /* @__PURE__ */ new Set();
1483
- if (options.workspaceRoot && options.workspaceRoot.trim()) rootsToCheck.add(resolve(options.workspaceRoot.trim()));
1484
- else if (options.allowedWorkspaceRoots) {
1485
- for (const r of options.allowedWorkspaceRoots) if (typeof r === "string" && r.trim()) rootsToCheck.add(resolve(r));
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
- rootsToCheck.add(resolve(process.cwd()));
1488
- const candidateDirs = /* @__PURE__ */ new Set();
1489
- if (options.folder && options.folder.trim()) candidateDirs.add(options.folder.trim());
1490
- candidateDirs.add("dsh-image-gen");
1491
- candidateDirs.add("image");
1492
- candidateDirs.add("images");
1493
- candidateDirs.add("");
1494
- const candidateExtensions = [
1495
- "png",
1496
- "jpg",
1497
- "jpeg",
1498
- "webp",
1499
- "gif"
1500
- ];
1501
- let deleted = false;
1502
- for (const root of rootsToCheck) for (const dir of candidateDirs) for (const ext of candidateExtensions) {
1503
- const targetPath = join(root, dir, `image-${prefix}.${ext}`);
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
- if (await deleteImageFromWorkspace(targetPath, rootsToCheck)) deleted = true;
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 deleted;
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 };