@dickpy/dsh-imagegen 1.5.4 → 1.5.6

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
@@ -41,7 +41,7 @@ function installSettingsSectionCompat(ctx, ns, schema, entry, hooks) {
41
41
  /** Settings namespace this plugin owns (host settings seam + bridge). */
42
42
  const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
43
43
  /** Published package version shared by the host updater and the client UI. */
44
- const PLUGIN_VERSION = "1.5.4";
44
+ const PLUGIN_VERSION = "1.5.6";
45
45
  /** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
46
46
  const SETTINGS_API = {
47
47
  describe: "/api/dsh-imagegen/settings/describe",
@@ -588,8 +588,13 @@ function requestSignal(source, timeoutMs) {
588
588
  }
589
589
  };
590
590
  }
591
- /** Content-type extension hints for URL-fetched images. */
592
- function mimeOfExtension(path) {
591
+ /** Whether an error was produced by a requestSignal budget timeout. These can
592
+ * surface from the fetch call itself or from reading the response body, so the
593
+ * budget must stay armed until the body has been consumed. */
594
+ function isBudgetTimeout(error) {
595
+ return (error instanceof DOMException || error instanceof Error) && error.name === "TimeoutError";
596
+ }
597
+ /** Content-type extension hints for URL-fetched images. */ function mimeOfExtension(path) {
593
598
  const match = /\.([a-z0-9]+)$/i.exec(path);
594
599
  if (match === null) return void 0;
595
600
  switch (match[1].toLowerCase()) {
@@ -705,26 +710,28 @@ async function normalizeItem(item, upstream, signal) {
705
710
  };
706
711
  }
707
712
  const budget = requestSignal(signal, IMAGE_FETCH_TIMEOUT_MS);
708
- let response;
709
713
  try {
710
- response = await fetch(url, {
711
- ...isPresignedUrl(url) || upstream.apiKey === "" ? {} : { headers: { authorization: `Bearer ${upstream.apiKey}` } },
712
- signal: budget.signal
713
- });
714
- } catch (error) {
715
- throw new ImageGenError(`failed to fetch the generated image url: ${error instanceof Error ? error.message : String(error)}`);
714
+ let response;
715
+ try {
716
+ response = await fetch(url, {
717
+ ...isPresignedUrl(url) || upstream.apiKey === "" ? {} : { headers: { authorization: `Bearer ${upstream.apiKey}` } },
718
+ signal: budget.signal
719
+ });
720
+ } catch (error) {
721
+ throw new ImageGenError(`failed to fetch the generated image url: ${error instanceof Error ? error.message : String(error)}`);
722
+ }
723
+ if (!response.ok) throw new ImageGenError(`failed to fetch the generated image url: HTTP ${response.status}`);
724
+ const buffer = Buffer.from(await response.arrayBuffer());
725
+ const contentType = response.headers.get("content-type");
726
+ const mime = detectImageMime(buffer) ?? (contentType !== null && contentType !== "" ? contentType.split(";")[0].trim() : mimeOfExtension(url) ?? "image/png");
727
+ return {
728
+ b64: buffer.toString("base64"),
729
+ mime,
730
+ revisedPrompt
731
+ };
716
732
  } finally {
717
733
  budget.dispose();
718
734
  }
719
- if (!response.ok) throw new ImageGenError(`failed to fetch the generated image url: HTTP ${response.status}`);
720
- const buffer = Buffer.from(await response.arrayBuffer());
721
- const contentType = response.headers.get("content-type");
722
- const mime = detectImageMime(buffer) ?? (contentType !== null && contentType !== "" ? contentType.split(";")[0].trim() : mimeOfExtension(url) ?? "image/png");
723
- return {
724
- b64: buffer.toString("base64"),
725
- mime,
726
- revisedPrompt
727
- };
728
735
  }
729
736
  /** Expand a provider image item whose URL may be a string or an array. */
730
737
  function imageItemsOf(value) {
@@ -821,43 +828,45 @@ async function pollAsyncTask(baseUrl, upstream, taskId, signal) {
821
828
  while (Date.now() < deadline) {
822
829
  const remaining = deadline - Date.now();
823
830
  const budget = requestSignal(signal, Math.min(ASYNC_POLL_REQUEST_TIMEOUT_MS, remaining));
824
- let response;
825
831
  try {
826
- response = await fetch(`${baseUrl}/tasks/${encodeURIComponent(taskId)}`, {
827
- method: "GET",
828
- headers: { authorization: `Bearer ${upstream.apiKey.trim()}` },
829
- signal: budget.signal
830
- });
831
- } catch (error) {
832
- if (signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
833
- const message = error instanceof Error ? error.message : String(error);
834
- if (/timeout|abort/i.test(message)) throw new ImageGenError("上游异步任务轮询超时", "upstream-timeout");
835
- throw new ImageGenError(`无法轮询上游异步任务:${message}`, "upstream-unreachable");
832
+ let response;
833
+ try {
834
+ response = await fetch(`${baseUrl}/tasks/${encodeURIComponent(taskId)}`, {
835
+ method: "GET",
836
+ headers: { authorization: `Bearer ${upstream.apiKey.trim()}` },
837
+ signal: budget.signal
838
+ });
839
+ } catch (error) {
840
+ if (signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
841
+ if (isBudgetTimeout(error)) throw new ImageGenError("上游异步任务轮询超时", "upstream-timeout");
842
+ throw new ImageGenError(`无法轮询上游异步任务:${error instanceof Error ? error.message : String(error)}`, "upstream-unreachable");
843
+ }
844
+ let payload;
845
+ try {
846
+ payload = await response.json();
847
+ } catch (error) {
848
+ if (isBudgetTimeout(error)) throw new ImageGenError("上游异步任务轮询超时", "upstream-timeout");
849
+ throw new ImageGenError(`上游任务接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
850
+ }
851
+ if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(asyncErrorMessage(payload, `上游任务轮询失败(HTTP ${response.status})`), "upstream-rejected");
852
+ const record = payload;
853
+ const data = record.data;
854
+ const statusRecord = Array.isArray(data) ? data[0] : data !== null && typeof data === "object" ? data : record;
855
+ const statusValue = statusRecord !== null && typeof statusRecord === "object" ? statusRecord.status : void 0;
856
+ const status = typeof statusValue === "string" ? statusValue.toLowerCase() : "";
857
+ if (ASYNC_FAILED_STATUSES.has(status)) throw new ImageGenError(asyncErrorMessage(payload, `上游异步任务失败(${status || "unknown"})`), "upstream-rejected");
858
+ const nested = statusRecord !== null && typeof statusRecord === "object" ? statusRecord : record;
859
+ const result = nested.result ?? (nested.output !== null && typeof nested.output === "object" ? nested.output.result : void 0) ?? record.result;
860
+ const images = (result !== null && typeof result === "object" ? result : void 0)?.images ?? nested.images ?? record.images;
861
+ if (ASYNC_COMPLETED_STATUSES.has(status) || images !== void 0) {
862
+ const items = Array.isArray(images) ? images.flatMap(imageItemsOf) : imageItemsOf(images);
863
+ if (items.length > 0) return items;
864
+ if (ASYNC_COMPLETED_STATUSES.has(status)) throw new ImageGenError("上游异步任务完成但没有图片结果", "upstream-empty");
865
+ }
866
+ if (status !== "" && !ASYNC_PENDING_STATUSES.has(status) && !ASYNC_COMPLETED_STATUSES.has(status)) throw new ImageGenError(`上游返回了未知异步任务状态:${status}`, "upstream-invalid");
836
867
  } finally {
837
868
  budget.dispose();
838
869
  }
839
- let payload;
840
- try {
841
- payload = await response.json();
842
- } catch {
843
- throw new ImageGenError(`上游任务接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
844
- }
845
- if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(asyncErrorMessage(payload, `上游任务轮询失败(HTTP ${response.status})`), "upstream-rejected");
846
- const record = payload;
847
- const data = record.data;
848
- const statusRecord = Array.isArray(data) ? data[0] : data !== null && typeof data === "object" ? data : record;
849
- const statusValue = statusRecord !== null && typeof statusRecord === "object" ? statusRecord.status : void 0;
850
- const status = typeof statusValue === "string" ? statusValue.toLowerCase() : "";
851
- if (ASYNC_FAILED_STATUSES.has(status)) throw new ImageGenError(asyncErrorMessage(payload, `上游异步任务失败(${status || "unknown"})`), "upstream-rejected");
852
- const nested = statusRecord !== null && typeof statusRecord === "object" ? statusRecord : record;
853
- const result = nested.result ?? (nested.output !== null && typeof nested.output === "object" ? nested.output.result : void 0) ?? record.result;
854
- const images = (result !== null && typeof result === "object" ? result : void 0)?.images ?? nested.images ?? record.images;
855
- if (ASYNC_COMPLETED_STATUSES.has(status) || images !== void 0) {
856
- const items = Array.isArray(images) ? images.flatMap(imageItemsOf) : imageItemsOf(images);
857
- if (items.length > 0) return items;
858
- if (ASYNC_COMPLETED_STATUSES.has(status)) throw new ImageGenError("上游异步任务完成但没有图片结果", "upstream-empty");
859
- }
860
- if (status !== "" && !ASYNC_PENDING_STATUSES.has(status) && !ASYNC_COMPLETED_STATUSES.has(status)) throw new ImageGenError(`上游返回了未知异步任务状态:${status}`, "upstream-invalid");
861
870
  await waitForPoll(Math.min(delay, Math.max(1, deadline - Date.now())), signal);
862
871
  delay = Math.min(5e3, delay * 2);
863
872
  }
@@ -872,15 +881,24 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
872
881
  let body;
873
882
  if (request.mode === "edit") {
874
883
  if (typeof request.image !== "string" || request.image === "") throw new ImageGenError("图生图需要上传参考图片", "edit-image-missing");
875
- const parsed = parseDataUrl(request.image);
876
- if (parsed === void 0) throw new ImageGenError("参考图片格式无效", "edit-image-invalid");
877
- let bytes;
878
- try {
879
- bytes = Buffer.from(parsed.base64, "base64");
880
- } catch {
881
- throw new ImageGenError("参考图片数据无法解码", "edit-image-invalid");
882
- }
883
- if (bytes.byteLength > MAX_EDIT_IMAGE_BYTES) throw new ImageGenError("参考图片超过 10MB 上限", "edit-image-too-large");
884
+ const decodeReference = (dataUrl) => {
885
+ const parsed = parseDataUrl(dataUrl);
886
+ if (parsed === void 0) throw new ImageGenError("参考图片格式无效", "edit-image-invalid");
887
+ let bytes;
888
+ try {
889
+ bytes = Buffer.from(parsed.base64, "base64");
890
+ } catch {
891
+ throw new ImageGenError("参考图片数据无法解码", "edit-image-invalid");
892
+ }
893
+ if (bytes.byteLength > MAX_EDIT_IMAGE_BYTES) throw new ImageGenError("参考图片超过 10MB 上限", "edit-image-too-large");
894
+ return {
895
+ bytes,
896
+ mime: parsed.mime,
897
+ filename: `reference.${extensionOf$3(parsed.mime)}`
898
+ };
899
+ };
900
+ const primary = decodeReference(request.image);
901
+ const extras = (request.images ?? []).filter((img) => typeof img === "string" && img !== "").slice(0, 4).map(decodeReference);
884
902
  if (isGrokImagine(params.model)) {
885
903
  headers["content-type"] = "application/json";
886
904
  body = JSON.stringify({
@@ -895,7 +913,7 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
895
913
  });
896
914
  } else if (isNanoBanana(params.model)) {
897
915
  const form = new FormData();
898
- form.append("image", new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf$3(parsed.mime)}`);
916
+ form.append("image", new Blob([primary.bytes], { type: primary.mime }), primary.filename);
899
917
  form.append("prompt", request.prompt);
900
918
  form.append("model", params.model);
901
919
  if (params.aspect_ratio !== void 0) form.append("aspect_ratio", params.aspect_ratio);
@@ -906,14 +924,15 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
906
924
  body = JSON.stringify({
907
925
  model: params.model,
908
926
  prompt: request.prompt,
909
- image: [request.image],
927
+ image: [request.image, ...(request.images ?? []).filter((img) => typeof img === "string" && img !== "").slice(0, 4)],
910
928
  ...params.size !== void 0 ? { size: params.size } : {},
911
929
  ...params.resolution !== void 0 ? { resolution: params.resolution } : {},
912
930
  response_format: isVolcSeedream(params.model) ? "url" : "b64_json"
913
931
  });
914
932
  } else {
915
933
  const form = new FormData();
916
- form.append("image", new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf$3(parsed.mime)}`);
934
+ if (extras.length > 0) for (const [index, reference] of [primary, ...extras].entries()) form.append("image[]", new Blob([reference.bytes], { type: reference.mime }), `reference-${index}.${extensionOf$3(reference.mime)}`);
935
+ else form.append("image", new Blob([primary.bytes], { type: primary.mime }), primary.filename);
917
936
  form.append("prompt", request.prompt);
918
937
  form.append("model", params.model);
919
938
  if (params.size !== void 0) form.append("size", params.size);
@@ -929,39 +948,43 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
929
948
  });
930
949
  }
931
950
  const budget = requestSignal(signal, UPSTREAM_TIMEOUT_MS);
932
- let response;
933
951
  try {
934
- const endpoint = request.mode === "edit" && !isSeedream(params.model) ? "/images/edits" : "/images/generations";
935
- response = await fetch(`${baseUrl}${endpoint}`, {
936
- method: "POST",
937
- headers,
938
- body,
939
- signal: budget.signal
940
- });
941
- } catch (error) {
942
- const message = error instanceof Error ? error.message : String(error);
943
- if (/aborter/i.test(message) || /timeout/i.test(message)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
944
- throw new ImageGenError(`无法连接上游接口:${message}`, "upstream-unreachable");
952
+ let response;
953
+ try {
954
+ const endpoint = request.mode === "edit" && !isSeedream(params.model) ? "/images/edits" : "/images/generations";
955
+ response = await fetch(`${baseUrl}${endpoint}`, {
956
+ method: "POST",
957
+ headers,
958
+ body,
959
+ signal: budget.signal
960
+ });
961
+ } catch (error) {
962
+ if (isBudgetTimeout(error)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
963
+ if (signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
964
+ throw new ImageGenError(`无法连接上游接口:${error instanceof Error ? error.message : String(error)}`, "upstream-unreachable");
965
+ }
966
+ let payload;
967
+ try {
968
+ payload = await response.json();
969
+ } catch (error) {
970
+ if (isBudgetTimeout(error)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
971
+ if (signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
972
+ throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
973
+ }
974
+ if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(upstreamMessage(payload, response.status), "upstream-rejected");
975
+ const data = dataRecordsOf(payload);
976
+ if (data === void 0) throw new ImageGenError("上游响应缺少 data 数组", "upstream-invalid");
977
+ if (data.length === 0) throw new ImageGenError("上游返回了 0 张图片", "upstream-empty");
978
+ const asyncEntries = data.filter((entry) => typeof entry.task_id === "string" && entry.task_id.trim() !== "");
979
+ if (asyncEntries.length > 0) {
980
+ const asyncRecords = (await Promise.all(asyncEntries.map((entry) => pollAsyncTask(baseUrl, upstream, entry.task_id, signal)))).flat();
981
+ if (asyncRecords.length === 0) throw new ImageGenError("上游异步任务完成但没有图片结果", "upstream-empty");
982
+ return Promise.all(asyncRecords.flatMap(imageItemsOf).map((item) => normalizeItem(item, upstream, signal)));
983
+ }
984
+ return Promise.all(data.flatMap(imageItemsOf).map((item) => normalizeItem(item, upstream, signal)));
945
985
  } finally {
946
986
  budget.dispose();
947
987
  }
948
- let payload;
949
- try {
950
- payload = await response.json();
951
- } catch {
952
- throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
953
- }
954
- if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(upstreamMessage(payload, response.status), "upstream-rejected");
955
- const data = dataRecordsOf(payload);
956
- if (data === void 0) throw new ImageGenError("上游响应缺少 data 数组", "upstream-invalid");
957
- if (data.length === 0) throw new ImageGenError("上游返回了 0 张图片", "upstream-empty");
958
- const asyncEntries = data.filter((entry) => typeof entry.task_id === "string" && entry.task_id.trim() !== "");
959
- if (asyncEntries.length > 0) {
960
- const asyncRecords = (await Promise.all(asyncEntries.map((entry) => pollAsyncTask(baseUrl, upstream, entry.task_id, signal)))).flat();
961
- if (asyncRecords.length === 0) throw new ImageGenError("上游异步任务完成但没有图片结果", "upstream-empty");
962
- return Promise.all(asyncRecords.flatMap(imageItemsOf).map((item) => normalizeItem(item, upstream, signal)));
963
- }
964
- return Promise.all(data.flatMap(imageItemsOf).map((item) => normalizeItem(item, upstream, signal)));
965
988
  }
966
989
  /**
967
990
  * Qwen-Image (DashScope native multimodal-generation): one chat-style request
@@ -995,50 +1018,54 @@ async function generateQwenImage(baseUrl, upstream, request, options) {
995
1018
  }
996
1019
  };
997
1020
  const budget = requestSignal(options.signal, UPSTREAM_TIMEOUT_MS);
998
- let response;
999
1021
  try {
1000
- response = await fetch(`${baseUrl}/services/aigc/multimodal-generation/generation`, {
1001
- method: "POST",
1002
- headers: {
1003
- authorization: `Bearer ${upstream.apiKey.trim()}`,
1004
- "content-type": "application/json"
1005
- },
1006
- body: JSON.stringify(body),
1007
- signal: budget.signal
1008
- });
1009
- } catch (error) {
1010
- const message = error instanceof Error ? error.message : String(error);
1011
- if (/aborter/i.test(message) || /timeout/i.test(message)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
1012
- throw new ImageGenError(`无法连接上游接口:${message}`, "upstream-unreachable");
1022
+ let response;
1023
+ try {
1024
+ response = await fetch(`${baseUrl}/services/aigc/multimodal-generation/generation`, {
1025
+ method: "POST",
1026
+ headers: {
1027
+ authorization: `Bearer ${upstream.apiKey.trim()}`,
1028
+ "content-type": "application/json"
1029
+ },
1030
+ body: JSON.stringify(body),
1031
+ signal: budget.signal
1032
+ });
1033
+ } catch (error) {
1034
+ if (isBudgetTimeout(error)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
1035
+ if (options.signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
1036
+ throw new ImageGenError(`无法连接上游接口:${error instanceof Error ? error.message : String(error)}`, "upstream-unreachable");
1037
+ }
1038
+ let payload;
1039
+ try {
1040
+ payload = await response.json();
1041
+ } catch (error) {
1042
+ if (isBudgetTimeout(error)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
1043
+ if (options.signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
1044
+ throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
1045
+ }
1046
+ if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(upstreamMessage(payload, response.status), "upstream-rejected");
1047
+ const output = payload.output;
1048
+ const choices = output !== void 0 && Array.isArray(output.choices) ? output.choices : [];
1049
+ const urls = [];
1050
+ for (const choice of choices) {
1051
+ const message = choice !== null && typeof choice === "object" ? choice.message : void 0;
1052
+ const items = message !== null && typeof message === "object" && Array.isArray(message.content) ? message.content : [];
1053
+ for (const item of items) if (item !== null && typeof item === "object") {
1054
+ const image = item.image;
1055
+ if (typeof image === "string" && image !== "") urls.push(image);
1056
+ }
1057
+ }
1058
+ if (urls.length === 0) throw new ImageGenError("上游响应缺少图片内容", "upstream-empty");
1059
+ return { images: await Promise.all(urls.map(async (url) => {
1060
+ const normalized = await normalizeItem({ url }, upstream);
1061
+ return {
1062
+ b64: normalized.b64,
1063
+ mime: normalized.mime
1064
+ };
1065
+ })) };
1013
1066
  } finally {
1014
1067
  budget.dispose();
1015
1068
  }
1016
- let payload;
1017
- try {
1018
- payload = await response.json();
1019
- } catch {
1020
- throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
1021
- }
1022
- if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(upstreamMessage(payload, response.status), "upstream-rejected");
1023
- const output = payload.output;
1024
- const choices = output !== void 0 && Array.isArray(output.choices) ? output.choices : [];
1025
- const urls = [];
1026
- for (const choice of choices) {
1027
- const message = choice !== null && typeof choice === "object" ? choice.message : void 0;
1028
- const items = message !== null && typeof message === "object" && Array.isArray(message.content) ? message.content : [];
1029
- for (const item of items) if (item !== null && typeof item === "object") {
1030
- const image = item.image;
1031
- if (typeof image === "string" && image !== "") urls.push(image);
1032
- }
1033
- }
1034
- if (urls.length === 0) throw new ImageGenError("上游响应缺少图片内容", "upstream-empty");
1035
- return { images: await Promise.all(urls.map(async (url) => {
1036
- const normalized = await normalizeItem({ url }, upstream);
1037
- return {
1038
- b64: normalized.b64,
1039
- mime: normalized.mime
1040
- };
1041
- })) };
1042
1069
  }
1043
1070
  /**
1044
1071
  * Forward one generate request to the configured endpoint. The requested image
@@ -1161,6 +1188,17 @@ async function testStorage(config) {
1161
1188
  };
1162
1189
  }
1163
1190
  //#endregion
1191
+ //#region src/image-storage-path.ts
1192
+ const DEFAULT_ROOT = path.join(process.env.DSH_HOME?.trim() || path.join(homedir(), ".dsh"), "dsh-imagegen");
1193
+ let root = DEFAULT_ROOT;
1194
+ function imageDataRoot() {
1195
+ return root;
1196
+ }
1197
+ function setImageDataRoot(value) {
1198
+ const trimmed = value?.trim();
1199
+ root = trimmed === void 0 || trimmed === "" ? DEFAULT_ROOT : path.resolve(trimmed);
1200
+ }
1201
+ //#endregion
1164
1202
  //#region src/history-store.ts
1165
1203
  /**
1166
1204
  * Host-persisted generation history: images are stored as individual files
@@ -1171,9 +1209,15 @@ async function testStorage(config) {
1171
1209
  *
1172
1210
  * Framework-free (node:fs only) so the route layer can drive it directly.
1173
1211
  */
1174
- const HISTORY_DIR$1 = path.join(homedir(), ".dsh", "dsh-imagegen");
1175
- const INDEX_PATH$1 = path.join(HISTORY_DIR$1, "index.json");
1176
- const IMAGES_DIR$1 = path.join(HISTORY_DIR$1, "images");
1212
+ function historyDir() {
1213
+ return imageDataRoot();
1214
+ }
1215
+ function indexPath$1() {
1216
+ return path.join(historyDir(), "index.json");
1217
+ }
1218
+ function imagesDir$1() {
1219
+ return path.join(historyDir(), "images");
1220
+ }
1177
1221
  let pendingMutation$1 = Promise.resolve();
1178
1222
  function mutateHistory(operation) {
1179
1223
  const next = pendingMutation$1.then(operation, operation);
@@ -1206,12 +1250,12 @@ function safeId$2(id) {
1206
1250
  }
1207
1251
  /** Ensure the storage directories exist. */
1208
1252
  async function ensureDirs$1() {
1209
- await promises.mkdir(IMAGES_DIR$1, { recursive: true });
1253
+ await promises.mkdir(imagesDir$1(), { recursive: true });
1210
1254
  }
1211
1255
  /** Read the index, tolerating a missing/corrupt file. */
1212
1256
  async function readIndex$1() {
1213
1257
  try {
1214
- const raw = await promises.readFile(INDEX_PATH$1, "utf8");
1258
+ const raw = await promises.readFile(indexPath$1(), "utf8");
1215
1259
  const parsed = JSON.parse(raw);
1216
1260
  if (parsed === null || typeof parsed !== "object") return [];
1217
1261
  const entries = parsed.entries;
@@ -1225,9 +1269,9 @@ async function readIndex$1() {
1225
1269
  async function writeIndex$1(entries) {
1226
1270
  await ensureDirs$1();
1227
1271
  const payload = { entries };
1228
- const tmp = `${INDEX_PATH$1}.tmp-${process.pid}`;
1272
+ const tmp = `${indexPath$1()}.tmp-${process.pid}`;
1229
1273
  await promises.writeFile(tmp, JSON.stringify(payload), "utf8");
1230
- await promises.rename(tmp, INDEX_PATH$1);
1274
+ await promises.rename(tmp, indexPath$1());
1231
1275
  }
1232
1276
  /** Structural guard for a stored entry. */
1233
1277
  function isStoredEntry$1(value) {
@@ -1242,7 +1286,7 @@ function isStoredEntry$1(value) {
1242
1286
  /** Remove one entry's image files (best effort). */
1243
1287
  async function removeEntryFiles$1(entry) {
1244
1288
  for (const image of entry.images) try {
1245
- await promises.rm(path.join(IMAGES_DIR$1, image.file), { force: true });
1289
+ await promises.rm(path.join(imagesDir$1(), image.file), { force: true });
1246
1290
  } catch {}
1247
1291
  }
1248
1292
  /** Project a stored entry onto the wire shape (image URLs). */
@@ -1289,8 +1333,8 @@ async function appendHistory(input) {
1289
1333
  for (let index = 0; index < input.images.length; index++) {
1290
1334
  const image = input.images[index];
1291
1335
  const file = `${prefix}-${index}.${extensionOf$2(image.mime)}`;
1292
- await promises.writeFile(path.join(IMAGES_DIR$1, file), Buffer.from(image.b64, "base64"));
1293
- notifyImageSaved("history", path.join(IMAGES_DIR$1, file));
1336
+ await promises.writeFile(path.join(imagesDir$1(), file), Buffer.from(image.b64, "base64"));
1337
+ notifyImageSaved("history", path.join(imagesDir$1(), file));
1294
1338
  storedImages.push({
1295
1339
  file,
1296
1340
  mime: image.mime,
@@ -1355,7 +1399,7 @@ async function readHistoryImage(file) {
1355
1399
  if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*-[0-9]+\.(png|jpg|jpeg|webp|gif)$/.test(file)) return void 0;
1356
1400
  try {
1357
1401
  return {
1358
- data: await promises.readFile(path.join(IMAGES_DIR$1, file)),
1402
+ data: await promises.readFile(path.join(imagesDir$1(), file)),
1359
1403
  mime: mimeOfFile$2(file)
1360
1404
  };
1361
1405
  } catch {
@@ -1372,7 +1416,6 @@ var GenerationTaskQueue = class {
1372
1416
  controllers = /* @__PURE__ */ new Map();
1373
1417
  listeners = /* @__PURE__ */ new Set();
1374
1418
  running = 0;
1375
- serialRunning = false;
1376
1419
  constructor(run, concurrency = 1) {
1377
1420
  this.run = run;
1378
1421
  this.concurrency = concurrency;
@@ -1413,15 +1456,16 @@ var GenerationTaskQueue = class {
1413
1456
  const previous = this.tasks.find((item) => item.id === id);
1414
1457
  return previous === void 0 ? void 0 : this.submit(previous.request);
1415
1458
  }
1459
+ /** Start queued tasks while capacity remains. Plain submissions and
1460
+ * comparison batches alike run in parallel up to the host-wide limit, so one
1461
+ * slow upstream can no longer hold back unrelated generations. */
1416
1462
  drain() {
1417
1463
  while (this.running < Math.max(1, this.concurrency)) {
1418
- const task = this.tasks.find((item) => item.status === "queued" && (this.running === 0 || item.request.comparisonId !== void 0 && !this.serialRunning));
1464
+ const task = this.tasks.find((item) => item.status === "queued");
1419
1465
  if (task === void 0) return;
1420
1466
  this.running += 1;
1421
- if (task.request.comparisonId === void 0) this.serialRunning = true;
1422
1467
  this.runTask(task).finally(() => {
1423
1468
  this.running -= 1;
1424
- if (task.request.comparisonId === void 0) this.serialRunning = false;
1425
1469
  this.drain();
1426
1470
  });
1427
1471
  }
@@ -1541,10 +1585,15 @@ var ImageGenerationRuntime = class {
1541
1585
  * Framework-free (node:fs + node:crypto only) so the route layer can drive it
1542
1586
  * directly.
1543
1587
  */
1544
- const HISTORY_DIR = path.join(homedir(), ".dsh", "dsh-imagegen");
1545
- const GALLERY_DIR = path.join(HISTORY_DIR, "gallery");
1546
- const INDEX_PATH = path.join(GALLERY_DIR, "index.json");
1547
- const IMAGES_DIR = path.join(GALLERY_DIR, "images");
1588
+ function galleryDir() {
1589
+ return path.join(imageDataRoot(), "gallery");
1590
+ }
1591
+ function indexPath() {
1592
+ return path.join(galleryDir(), "index.json");
1593
+ }
1594
+ function imagesDir() {
1595
+ return path.join(galleryDir(), "images");
1596
+ }
1548
1597
  let pendingMutation = Promise.resolve();
1549
1598
  function mutateGallery(operation) {
1550
1599
  const next = pendingMutation.then(operation, operation);
@@ -1583,12 +1632,12 @@ function fingerprint(input) {
1583
1632
  }
1584
1633
  /** Ensure the storage directories exist. */
1585
1634
  async function ensureDirs() {
1586
- await promises.mkdir(IMAGES_DIR, { recursive: true });
1635
+ await promises.mkdir(imagesDir(), { recursive: true });
1587
1636
  }
1588
1637
  /** Read the index, tolerating a missing/corrupt file. */
1589
1638
  async function readIndex() {
1590
1639
  try {
1591
- const raw = await promises.readFile(INDEX_PATH, "utf8");
1640
+ const raw = await promises.readFile(indexPath(), "utf8");
1592
1641
  const parsed = JSON.parse(raw);
1593
1642
  if (parsed === null || typeof parsed !== "object") return [];
1594
1643
  const entries = parsed.entries;
@@ -1602,9 +1651,9 @@ async function readIndex() {
1602
1651
  async function writeIndex(entries) {
1603
1652
  await ensureDirs();
1604
1653
  const payload = { entries };
1605
- const tmp = `${INDEX_PATH}.tmp-${process.pid}`;
1654
+ const tmp = `${indexPath()}.tmp-${process.pid}`;
1606
1655
  await promises.writeFile(tmp, JSON.stringify(payload), "utf8");
1607
- await promises.rename(tmp, INDEX_PATH);
1656
+ await promises.rename(tmp, indexPath());
1608
1657
  }
1609
1658
  /** Structural guard for a stored entry. */
1610
1659
  function isStoredEntry(value) {
@@ -1619,7 +1668,7 @@ function isStoredEntry(value) {
1619
1668
  /** Remove one entry's image files (best effort). */
1620
1669
  async function removeEntryFiles(entry) {
1621
1670
  for (const image of entry.images) try {
1622
- await promises.rm(path.join(IMAGES_DIR, image.file), { force: true });
1671
+ await promises.rm(path.join(imagesDir(), image.file), { force: true });
1623
1672
  } catch {}
1624
1673
  }
1625
1674
  /** Project a stored entry onto the wire shape (image URLs). */
@@ -1675,8 +1724,8 @@ async function appendGallery(input) {
1675
1724
  for (let index = 0; index < input.images.length; index++) {
1676
1725
  const image = input.images[index];
1677
1726
  const file = `${prefix}-${index}.${extensionOf$1(image.mime)}`;
1678
- await promises.writeFile(path.join(IMAGES_DIR, file), Buffer.from(image.b64, "base64"));
1679
- notifyImageSaved("gallery", path.join(IMAGES_DIR, file));
1727
+ await promises.writeFile(path.join(imagesDir(), file), Buffer.from(image.b64, "base64"));
1728
+ notifyImageSaved("gallery", path.join(imagesDir(), file));
1680
1729
  storedImages.push({
1681
1730
  file,
1682
1731
  mime: image.mime,
@@ -1752,7 +1801,7 @@ async function readGalleryImage(file) {
1752
1801
  if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*-[0-9]+\.(png|jpg|jpeg|webp|gif)$/.test(file)) return void 0;
1753
1802
  try {
1754
1803
  return {
1755
- data: await promises.readFile(path.join(IMAGES_DIR, file)),
1804
+ data: await promises.readFile(path.join(imagesDir(), file)),
1756
1805
  mime: mimeOfFile$1(file)
1757
1806
  };
1758
1807
  } catch {
@@ -1762,11 +1811,6 @@ async function readGalleryImage(file) {
1762
1811
  //#endregion
1763
1812
  //#region src/canvas-store.ts
1764
1813
  /** Host-persisted infinite canvas documents and content-addressed assets. */
1765
- const DATA_ROOT = process.env.DSH_HOME?.trim() || path.join(homedir(), ".dsh");
1766
- const CANVAS_ROOT = path.join(DATA_ROOT, "dsh-imagegen", "canvas");
1767
- path.join(CANVAS_ROOT, "pages");
1768
- path.join(CANVAS_ROOT, "assets");
1769
- path.join(CANVAS_ROOT, "index.json");
1770
1814
  var CanvasConflictError = class extends Error {
1771
1815
  code = "canvas-conflict";
1772
1816
  constructor(message = "画布已在其他窗口更新,请重新加载后再保存。") {
@@ -1850,7 +1894,7 @@ function isNode(value) {
1850
1894
  function isDocument(value) {
1851
1895
  if (value === null || typeof value !== "object") return false;
1852
1896
  const document = value;
1853
- return document.version === 2 && typeof document.id === "string" && typeof document.title === "string" && typeof document.revision === "number" && document.viewport !== null && typeof document.viewport === "object" && typeof document.viewport.x === "number" && typeof document.viewport.y === "number" && typeof document.viewport.k === "number" && (document.background === "dots" || document.background === "lines" || document.background === "blank") && Array.isArray(document.nodes) && document.nodes.every(isNode) && Array.isArray(document.connections);
1897
+ return document.version === 2 && typeof document.id === "string" && typeof document.title === "string" && typeof document.revision === "number" && document.viewport !== null && typeof document.viewport === "object" && typeof document.viewport.x === "number" && typeof document.viewport.y === "number" && typeof document.viewport.k === "number" && (document.background === "dots" || document.background === "lines" || document.background === "diagonal" || document.background === "checker" || document.background === "blank" || document.background === "image") && (document.backgroundImage === void 0 || typeof document.backgroundImage === "string") && Array.isArray(document.nodes) && document.nodes.every(isNode) && Array.isArray(document.connections);
1854
1898
  }
1855
1899
  /** Upgrade a v1 (image/text/annotation + edges) document to the v2 node-graph model.
1856
1900
  * Images and text notes keep their geometry; annotation prompt cards become plain
@@ -1970,17 +2014,17 @@ function serialize(operation) {
1970
2014
  }
1971
2015
  var CanvasStore = class {
1972
2016
  root;
1973
- constructor(root = CANVAS_ROOT) {
2017
+ constructor(root) {
1974
2018
  this.root = root;
1975
2019
  }
1976
2020
  pagesDir() {
1977
- return path.join(this.root, "pages");
2021
+ return path.join(this.root ?? path.join(imageDataRoot(), "canvas"), "pages");
1978
2022
  }
1979
2023
  assetsDir() {
1980
- return path.join(this.root, "assets");
2024
+ return path.join(this.root ?? path.join(imageDataRoot(), "canvas"), "assets");
1981
2025
  }
1982
2026
  indexPath() {
1983
- return path.join(this.root, "index.json");
2027
+ return path.join(this.root ?? path.join(imageDataRoot(), "canvas"), "index.json");
1984
2028
  }
1985
2029
  async ensure() {
1986
2030
  await promises.mkdir(this.pagesDir(), { recursive: true });
@@ -2844,6 +2888,7 @@ function parseGenerateRequest(body) {
2844
2888
  n: typeof body.n === "number" ? body.n : 1,
2845
2889
  detail: typeof body.detail === "string" ? body.detail : "",
2846
2890
  ...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {},
2891
+ ...Array.isArray(body.images) ? { images: body.images.filter((item) => typeof item === "string" && item !== "").slice(0, 4) } : {},
2847
2892
  ...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {},
2848
2893
  ...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {},
2849
2894
  ...typeof body.comparisonId === "string" && body.comparisonId !== "" ? { comparisonId: body.comparisonId } : {},
@@ -4959,6 +5004,7 @@ const Config = z.object({
4959
5004
  promptApiUrl: z.string().default(""),
4960
5005
  promptApiKey: z.string().role("secret").default(""),
4961
5006
  promptModel: z.string().default(""),
5007
+ localStoragePath: z.string().default(""),
4962
5008
  storageEnabled: z.boolean().default(false),
4963
5009
  storageEndpoint: z.string().default(""),
4964
5010
  storageRegion: z.string().default(""),
@@ -5031,6 +5077,7 @@ function apply(ctx, config) {
5031
5077
  let current = () => config ?? {};
5032
5078
  const resolve = () => {
5033
5079
  const value = current() ?? {};
5080
+ setImageDataRoot(value.localStoragePath);
5034
5081
  let channels = normalizeChannels(value.channels);
5035
5082
  const secrets = { ...value.channelSecrets ?? {} };
5036
5083
  if (channels.length === 0) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dickpy/dsh-imagegen",
3
3
  "description": "AI image generation plugin for the dsh web GUI: text-to-image and image-to-image through configurable provider channels (gpt-image-2 / grok-imagine-image / nanobanana series / seedream-5.0-pro / dall-e-3, with native xAI Grok Imagine, Google Nano Banana and ByteDance Seedream request shaping), with per-channel model catalogs and a New Session / Image Generation tab entry opening a three-column studio beside the native conversation.",
4
- "version": "1.5.4",
4
+ "version": "1.5.6",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {