@dickpy/dsh-imagegen 1.5.5 → 1.5.7

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.5";
44
+ const PLUGIN_VERSION = "1.5.7";
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",
@@ -230,8 +230,27 @@ const ENTRIES = {
230
230
  supportsEdit: true,
231
231
  supportsAspectRatio: true,
232
232
  qualityTiers: ["auto"]
233
+ },
234
+ minimax: {
235
+ label: "MiniMax",
236
+ labelZh: "MiniMax 图像",
237
+ known: true,
238
+ supportsEdit: true,
239
+ supportsAspectRatio: true,
240
+ qualityTiers: ["auto"]
233
241
  }
234
242
  };
243
+ /** Documented upstream prompt hard limits (UTF-16 code units). The engine
244
+ * fast-fails on these before any network call and the panel counter shows
245
+ * them live; families absent here have no documented limit. */
246
+ const PROMPT_CHAR_LIMITS = { minimax: 1500 };
247
+ /** The documented prompt character limit for a model id, or null when the
248
+ * family has no known limit. Single source of truth for engine enforcement
249
+ * and the panel's live counter. */
250
+ function promptCharLimit(model) {
251
+ const { family } = describeModel(model);
252
+ return (family !== "unknown" ? PROMPT_CHAR_LIMITS[family] : void 0) ?? null;
253
+ }
235
254
  /** Official Gemini image ids served by Nano Banana gateways. */
236
255
  const NANOBANANA_GEMINI_IDS = /* @__PURE__ */ new Set([
237
256
  "gemini-3-pro-image",
@@ -272,6 +291,10 @@ function describeModel(model) {
272
291
  family: "qwen",
273
292
  ...ENTRIES.qwen
274
293
  };
294
+ if (/^(?:minimax[-_/])?image-\d+/i.test(id)) return {
295
+ family: "minimax",
296
+ ...ENTRIES.minimax
297
+ };
275
298
  return {
276
299
  family: "unknown",
277
300
  label: "unknown",
@@ -381,6 +404,17 @@ async function listImageModels(config) {
381
404
  async function listPromptModels(config) {
382
405
  return listOpenAIModels(config);
383
406
  }
407
+ /** Remove reasoning-model artifacts from a chat model's visible content:
408
+ * complete `<think>…</think>` blocks first, then anything from a dangling
409
+ * unclosed `<think>` to the end of the text. Reasoning models served through
410
+ * OpenAI-compatible endpoints (MiniMax M3, DeepSeek R1, Qwen QVQ, …) inline
411
+ * these blocks in `message.content`; leaking them into the prompt box both
412
+ * pollutes the prompt and can push it past image models' length limits. */
413
+ function stripReasoning(text) {
414
+ const withoutClosed = text.replace(/<think>[\s\S]*?<\/think>/gi, "");
415
+ const dangling = /<think>/i.exec(withoutClosed);
416
+ return (dangling === null ? withoutClosed : withoutClosed.slice(0, dangling.index)).trim();
417
+ }
384
418
  /** Expand a concise image request into a production-ready image prompt. */
385
419
  async function enhancePrompt(config, prompt) {
386
420
  if (config.apiUrl.trim() === "" || config.model.trim() === "") throw new Error("prompt enhancement model is not configured");
@@ -402,7 +436,9 @@ async function enhancePrompt(config, prompt) {
402
436
  const choices = Array.isArray(body.choices) ? body.choices : [];
403
437
  const content = choices[0] !== null && typeof choices[0] === "object" ? choices[0].message?.content : void 0;
404
438
  if (typeof content !== "string" || content.trim() === "") throw new Error("chat model returned an empty prompt");
405
- return content.trim();
439
+ const enhanced = stripReasoning(content);
440
+ if (enhanced === "") throw new Error("chat model returned only reasoning content (empty <think> payload)");
441
+ return enhanced;
406
442
  }
407
443
  //#endregion
408
444
  //#region src/image-models.ts
@@ -505,6 +541,28 @@ function isZhipuImage(model) {
505
541
  function isQwenImage(model) {
506
542
  return modelFamily(model) === "qwen";
507
543
  }
544
+ /** Whether the model is MiniMax image-01, which speaks MiniMax's native
545
+ * `/image_generation` contract (NOT OpenAI-compatible): `aspect_ratio`,
546
+ * `subject_reference` for image-to-image, `data.image_base64[]` results, and
547
+ * errors reported as HTTP 200 + non-zero `base_resp.status_code`. */
548
+ function isMiniMaxImage(model) {
549
+ return modelFamily(model) === "minimax";
550
+ }
551
+ /** Aspect ratios MiniMax image-01 documents (the panel vocabulary is a superset). */
552
+ const MINIMAX_RATIOS = /* @__PURE__ */ new Set([
553
+ "1:1",
554
+ "16:9",
555
+ "4:3",
556
+ "3:2",
557
+ "2:3",
558
+ "3:4",
559
+ "9:16",
560
+ "21:9"
561
+ ]);
562
+ /** MiniMax caps one request at 9 images. */
563
+ const MINIMAX_MAX_N = 9;
564
+ /** MiniMax image-01 rejects prompts of 1500+ characters; the exact number
565
+ * lives in model-catalog.ts so the panel counter and this guard agree. */
508
566
  function isGlmImage(model) {
509
567
  return /^glm-image(?:-|$)/i.test(model.trim());
510
568
  }
@@ -634,6 +692,19 @@ function isPresignedUrl(value) {
634
692
  if (params.has("x-amz-signature") || params.has("x-amz-credential")) return true;
635
693
  return params.has("signature") && (params.has("expires") || params.has("googleaccessid") || params.has("awsaccesskeyid"));
636
694
  }
695
+ /**
696
+ * Whether a result URL lives on the same origin as the configured API base.
697
+ * The upstream Bearer key is only ever forwarded to this origin: a provider
698
+ * (or a compromised relay) that hands back an image URL on a foreign host
699
+ * must not be able to harvest the key through that download.
700
+ */
701
+ function isSameOriginAsApi(value, apiUrl) {
702
+ try {
703
+ return new URL(value).origin === new URL(apiUrl).origin;
704
+ } catch {
705
+ return false;
706
+ }
707
+ }
637
708
  /** Clamp the requested image count into the API-accepted range. */
638
709
  function clampCount(n) {
639
710
  if (!Number.isFinite(n)) return 1;
@@ -713,8 +784,9 @@ async function normalizeItem(item, upstream, signal) {
713
784
  try {
714
785
  let response;
715
786
  try {
787
+ const forwardKey = upstream.apiKey !== "" && !isPresignedUrl(url) && isSameOriginAsApi(url, upstream.apiUrl);
716
788
  response = await fetch(url, {
717
- ...isPresignedUrl(url) || upstream.apiKey === "" ? {} : { headers: { authorization: `Bearer ${upstream.apiKey}` } },
789
+ ...forwardKey ? { headers: { authorization: `Bearer ${upstream.apiKey}` } } : {},
718
790
  signal: budget.signal
719
791
  });
720
792
  } catch (error) {
@@ -881,15 +953,24 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
881
953
  let body;
882
954
  if (request.mode === "edit") {
883
955
  if (typeof request.image !== "string" || request.image === "") throw new ImageGenError("图生图需要上传参考图片", "edit-image-missing");
884
- const parsed = parseDataUrl(request.image);
885
- if (parsed === void 0) throw new ImageGenError("参考图片格式无效", "edit-image-invalid");
886
- let bytes;
887
- try {
888
- bytes = Buffer.from(parsed.base64, "base64");
889
- } catch {
890
- throw new ImageGenError("参考图片数据无法解码", "edit-image-invalid");
891
- }
892
- if (bytes.byteLength > MAX_EDIT_IMAGE_BYTES) throw new ImageGenError("参考图片超过 10MB 上限", "edit-image-too-large");
956
+ const decodeReference = (dataUrl) => {
957
+ const parsed = parseDataUrl(dataUrl);
958
+ if (parsed === void 0) throw new ImageGenError("参考图片格式无效", "edit-image-invalid");
959
+ let bytes;
960
+ try {
961
+ bytes = Buffer.from(parsed.base64, "base64");
962
+ } catch {
963
+ throw new ImageGenError("参考图片数据无法解码", "edit-image-invalid");
964
+ }
965
+ if (bytes.byteLength > MAX_EDIT_IMAGE_BYTES) throw new ImageGenError("参考图片超过 10MB 上限", "edit-image-too-large");
966
+ return {
967
+ bytes,
968
+ mime: parsed.mime,
969
+ filename: `reference.${extensionOf$3(parsed.mime)}`
970
+ };
971
+ };
972
+ const primary = decodeReference(request.image);
973
+ const extras = (request.images ?? []).filter((img) => typeof img === "string" && img !== "").slice(0, 4).map(decodeReference);
893
974
  if (isGrokImagine(params.model)) {
894
975
  headers["content-type"] = "application/json";
895
976
  body = JSON.stringify({
@@ -904,7 +985,7 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
904
985
  });
905
986
  } else if (isNanoBanana(params.model)) {
906
987
  const form = new FormData();
907
- form.append("image", new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf$3(parsed.mime)}`);
988
+ form.append("image", new Blob([primary.bytes], { type: primary.mime }), primary.filename);
908
989
  form.append("prompt", request.prompt);
909
990
  form.append("model", params.model);
910
991
  if (params.aspect_ratio !== void 0) form.append("aspect_ratio", params.aspect_ratio);
@@ -915,14 +996,15 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
915
996
  body = JSON.stringify({
916
997
  model: params.model,
917
998
  prompt: request.prompt,
918
- image: [request.image],
999
+ image: [request.image, ...(request.images ?? []).filter((img) => typeof img === "string" && img !== "").slice(0, 4)],
919
1000
  ...params.size !== void 0 ? { size: params.size } : {},
920
1001
  ...params.resolution !== void 0 ? { resolution: params.resolution } : {},
921
1002
  response_format: isVolcSeedream(params.model) ? "url" : "b64_json"
922
1003
  });
923
1004
  } else {
924
1005
  const form = new FormData();
925
- form.append("image", new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf$3(parsed.mime)}`);
1006
+ 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)}`);
1007
+ else form.append("image", new Blob([primary.bytes], { type: primary.mime }), primary.filename);
926
1008
  form.append("prompt", request.prompt);
927
1009
  form.append("model", params.model);
928
1010
  if (params.size !== void 0) form.append("size", params.size);
@@ -1058,6 +1140,99 @@ async function generateQwenImage(baseUrl, upstream, request, options) {
1058
1140
  }
1059
1141
  }
1060
1142
  /**
1143
+ * MiniMax image-01 (native `/image_generation`): one JSON request that batches
1144
+ * up to 9 images and returns them inline as base64. Image-to-image rides the
1145
+ * `subject_reference` array (a character reference, data URL accepted). The
1146
+ * endpoint answers HTTP 200 even on failure, so `base_resp.status_code` is the
1147
+ * real verdict.
1148
+ */
1149
+ async function generateMiniMaxImage(baseUrl, upstream, request, options) {
1150
+ const model = wireModel(request);
1151
+ const promptLimit = promptCharLimit(model);
1152
+ if (promptLimit !== null && request.prompt.length >= promptLimit) throw new ImageGenError(`MiniMax image-01 要求提示词少于 ${promptLimit} 字符(当前 ${request.prompt.length}),请精简后重试`, "prompt-too-long");
1153
+ const body = {
1154
+ model,
1155
+ prompt: request.prompt,
1156
+ response_format: "base64"
1157
+ };
1158
+ const ratio = request.size.trim();
1159
+ if (ratio !== "" && ratio !== "auto") {
1160
+ if (!MINIMAX_RATIOS.has(ratio)) throw new ImageGenError(`MiniMax image-01 不支持 ${ratio} 宽高比,可选:${Array.from(MINIMAX_RATIOS).join(" / ")}`, "size-unsupported");
1161
+ body.aspect_ratio = ratio;
1162
+ }
1163
+ const count = Math.min(MINIMAX_MAX_N, clampCount(request.n));
1164
+ if (count > 1) body.n = count;
1165
+ if (request.mode === "edit") {
1166
+ if (typeof request.image !== "string" || request.image === "") throw new ImageGenError("图生图需要上传参考图片", "edit-image-missing");
1167
+ const parsed = parseDataUrl(request.image);
1168
+ if (parsed === void 0) throw new ImageGenError("参考图片格式无效", "edit-image-invalid");
1169
+ if (Buffer.from(parsed.base64, "base64").byteLength > MAX_EDIT_IMAGE_BYTES) throw new ImageGenError("参考图片超过 10MB 上限", "edit-image-too-large");
1170
+ body.subject_reference = [{
1171
+ type: "character",
1172
+ image_file: request.image
1173
+ }];
1174
+ }
1175
+ const budget = requestSignal(options.signal, UPSTREAM_TIMEOUT_MS);
1176
+ try {
1177
+ let response;
1178
+ try {
1179
+ response = await fetch(`${baseUrl}/image_generation`, {
1180
+ method: "POST",
1181
+ headers: {
1182
+ authorization: `Bearer ${upstream.apiKey.trim()}`,
1183
+ "content-type": "application/json"
1184
+ },
1185
+ body: JSON.stringify(body),
1186
+ signal: budget.signal
1187
+ });
1188
+ } catch (error) {
1189
+ if (isBudgetTimeout(error)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
1190
+ if (options.signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
1191
+ throw new ImageGenError(`无法连接上游接口:${error instanceof Error ? error.message : String(error)}`, "upstream-unreachable");
1192
+ }
1193
+ let payload;
1194
+ try {
1195
+ payload = await response.json();
1196
+ } catch (error) {
1197
+ if (isBudgetTimeout(error)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
1198
+ if (options.signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
1199
+ throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
1200
+ }
1201
+ if (payload === null || typeof payload !== "object") throw new ImageGenError(upstreamMessage(payload, response.status), "upstream-rejected");
1202
+ const record = payload;
1203
+ const baseResp = record.base_resp;
1204
+ if (baseResp !== null && typeof baseResp === "object") {
1205
+ const status = baseResp.status_code;
1206
+ if (typeof status === "number" && status !== 0) {
1207
+ const msg = baseResp.status_msg;
1208
+ throw new ImageGenError(`MiniMax 拒绝请求(${status}):${typeof msg === "string" && msg !== "" ? msg : "unknown error"}`, status === 1004 || status === 2049 ? "upstream-unauthorized" : "upstream-rejected");
1209
+ }
1210
+ }
1211
+ if (!response.ok) throw new ImageGenError(upstreamMessage(payload, response.status), "upstream-rejected");
1212
+ const data = record.data;
1213
+ const b64s = data !== null && typeof data === "object" && Array.isArray(data.image_base64) ? data.image_base64.filter((item) => typeof item === "string" && item.trim() !== "") : [];
1214
+ const urls = data !== null && typeof data === "object" && Array.isArray(data.image_urls) ? data.image_urls.filter((item) => typeof item === "string" && item !== "") : [];
1215
+ if (b64s.length === 0 && urls.length === 0) throw new ImageGenError("上游响应缺少图片内容", "upstream-empty");
1216
+ const images = b64s.map((raw) => {
1217
+ const b64 = bareBase64(raw);
1218
+ return {
1219
+ b64,
1220
+ mime: detectImageMime(Buffer.from(b64, "base64")) ?? "image/jpeg"
1221
+ };
1222
+ });
1223
+ for (const url of urls) {
1224
+ const normalized = await normalizeItem({ url }, upstream, options.signal);
1225
+ images.push({
1226
+ b64: normalized.b64,
1227
+ mime: normalized.mime
1228
+ });
1229
+ }
1230
+ return { images };
1231
+ } finally {
1232
+ budget.dispose();
1233
+ }
1234
+ }
1235
+ /**
1061
1236
  * Forward one generate request to the configured endpoint. The requested image
1062
1237
  * count is satisfied with N parallel single-image requests (the `n` batch
1063
1238
  * parameter is never sent, because Responses-API-based gateways reject it as
@@ -1068,6 +1243,7 @@ async function generateImage(upstream, request, options = {}) {
1068
1243
  if (baseUrl === "") throw new ImageGenError("api_url 未配置:请先在「设置 → 插件 → 可配置」中填写", "config-missing");
1069
1244
  if (upstream.apiKey.trim() === "") throw new ImageGenError("api_key 未配置:请先在「设置 → 插件 → 可配置」中填写", "config-missing");
1070
1245
  if (isQwenImage(wireModel(request))) return generateQwenImage(baseUrl, upstream, request, options);
1246
+ if (isMiniMaxImage(wireModel(request))) return generateMiniMaxImage(baseUrl, upstream, request, options);
1071
1247
  if (request.mode === "edit" && isZhipuImage(wireModel(request))) throw new ImageGenError("智谱 GLM-Image 当前仅支持文生图,请切换到文生图模式或选择支持图生图的模型", "edit-unsupported");
1072
1248
  const params = effectiveParams(request);
1073
1249
  const count = effectiveCount(request);
@@ -1178,6 +1354,17 @@ async function testStorage(config) {
1178
1354
  };
1179
1355
  }
1180
1356
  //#endregion
1357
+ //#region src/image-storage-path.ts
1358
+ const DEFAULT_ROOT = path.join(process.env.DSH_HOME?.trim() || path.join(homedir(), ".dsh"), "dsh-imagegen");
1359
+ let root = DEFAULT_ROOT;
1360
+ function imageDataRoot() {
1361
+ return root;
1362
+ }
1363
+ function setImageDataRoot(value) {
1364
+ const trimmed = value?.trim();
1365
+ root = trimmed === void 0 || trimmed === "" ? DEFAULT_ROOT : path.resolve(trimmed);
1366
+ }
1367
+ //#endregion
1181
1368
  //#region src/history-store.ts
1182
1369
  /**
1183
1370
  * Host-persisted generation history: images are stored as individual files
@@ -1188,9 +1375,15 @@ async function testStorage(config) {
1188
1375
  *
1189
1376
  * Framework-free (node:fs only) so the route layer can drive it directly.
1190
1377
  */
1191
- const HISTORY_DIR$1 = path.join(homedir(), ".dsh", "dsh-imagegen");
1192
- const INDEX_PATH$1 = path.join(HISTORY_DIR$1, "index.json");
1193
- const IMAGES_DIR$1 = path.join(HISTORY_DIR$1, "images");
1378
+ function historyDir() {
1379
+ return imageDataRoot();
1380
+ }
1381
+ function indexPath$1() {
1382
+ return path.join(historyDir(), "index.json");
1383
+ }
1384
+ function imagesDir$1() {
1385
+ return path.join(historyDir(), "images");
1386
+ }
1194
1387
  let pendingMutation$1 = Promise.resolve();
1195
1388
  function mutateHistory(operation) {
1196
1389
  const next = pendingMutation$1.then(operation, operation);
@@ -1223,12 +1416,12 @@ function safeId$2(id) {
1223
1416
  }
1224
1417
  /** Ensure the storage directories exist. */
1225
1418
  async function ensureDirs$1() {
1226
- await promises.mkdir(IMAGES_DIR$1, { recursive: true });
1419
+ await promises.mkdir(imagesDir$1(), { recursive: true });
1227
1420
  }
1228
1421
  /** Read the index, tolerating a missing/corrupt file. */
1229
1422
  async function readIndex$1() {
1230
1423
  try {
1231
- const raw = await promises.readFile(INDEX_PATH$1, "utf8");
1424
+ const raw = await promises.readFile(indexPath$1(), "utf8");
1232
1425
  const parsed = JSON.parse(raw);
1233
1426
  if (parsed === null || typeof parsed !== "object") return [];
1234
1427
  const entries = parsed.entries;
@@ -1242,9 +1435,9 @@ async function readIndex$1() {
1242
1435
  async function writeIndex$1(entries) {
1243
1436
  await ensureDirs$1();
1244
1437
  const payload = { entries };
1245
- const tmp = `${INDEX_PATH$1}.tmp-${process.pid}`;
1438
+ const tmp = `${indexPath$1()}.tmp-${process.pid}`;
1246
1439
  await promises.writeFile(tmp, JSON.stringify(payload), "utf8");
1247
- await promises.rename(tmp, INDEX_PATH$1);
1440
+ await promises.rename(tmp, indexPath$1());
1248
1441
  }
1249
1442
  /** Structural guard for a stored entry. */
1250
1443
  function isStoredEntry$1(value) {
@@ -1259,7 +1452,7 @@ function isStoredEntry$1(value) {
1259
1452
  /** Remove one entry's image files (best effort). */
1260
1453
  async function removeEntryFiles$1(entry) {
1261
1454
  for (const image of entry.images) try {
1262
- await promises.rm(path.join(IMAGES_DIR$1, image.file), { force: true });
1455
+ await promises.rm(path.join(imagesDir$1(), image.file), { force: true });
1263
1456
  } catch {}
1264
1457
  }
1265
1458
  /** Project a stored entry onto the wire shape (image URLs). */
@@ -1306,8 +1499,8 @@ async function appendHistory(input) {
1306
1499
  for (let index = 0; index < input.images.length; index++) {
1307
1500
  const image = input.images[index];
1308
1501
  const file = `${prefix}-${index}.${extensionOf$2(image.mime)}`;
1309
- await promises.writeFile(path.join(IMAGES_DIR$1, file), Buffer.from(image.b64, "base64"));
1310
- notifyImageSaved("history", path.join(IMAGES_DIR$1, file));
1502
+ await promises.writeFile(path.join(imagesDir$1(), file), Buffer.from(image.b64, "base64"));
1503
+ notifyImageSaved("history", path.join(imagesDir$1(), file));
1311
1504
  storedImages.push({
1312
1505
  file,
1313
1506
  mime: image.mime,
@@ -1372,7 +1565,7 @@ async function readHistoryImage(file) {
1372
1565
  if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*-[0-9]+\.(png|jpg|jpeg|webp|gif)$/.test(file)) return void 0;
1373
1566
  try {
1374
1567
  return {
1375
- data: await promises.readFile(path.join(IMAGES_DIR$1, file)),
1568
+ data: await promises.readFile(path.join(imagesDir$1(), file)),
1376
1569
  mime: mimeOfFile$2(file)
1377
1570
  };
1378
1571
  } catch {
@@ -1389,7 +1582,6 @@ var GenerationTaskQueue = class {
1389
1582
  controllers = /* @__PURE__ */ new Map();
1390
1583
  listeners = /* @__PURE__ */ new Set();
1391
1584
  running = 0;
1392
- serialRunning = false;
1393
1585
  constructor(run, concurrency = 1) {
1394
1586
  this.run = run;
1395
1587
  this.concurrency = concurrency;
@@ -1430,15 +1622,16 @@ var GenerationTaskQueue = class {
1430
1622
  const previous = this.tasks.find((item) => item.id === id);
1431
1623
  return previous === void 0 ? void 0 : this.submit(previous.request);
1432
1624
  }
1625
+ /** Start queued tasks while capacity remains. Plain submissions and
1626
+ * comparison batches alike run in parallel up to the host-wide limit, so one
1627
+ * slow upstream can no longer hold back unrelated generations. */
1433
1628
  drain() {
1434
1629
  while (this.running < Math.max(1, this.concurrency)) {
1435
- const task = this.tasks.find((item) => item.status === "queued" && (this.running === 0 || item.request.comparisonId !== void 0 && !this.serialRunning));
1630
+ const task = this.tasks.find((item) => item.status === "queued");
1436
1631
  if (task === void 0) return;
1437
1632
  this.running += 1;
1438
- if (task.request.comparisonId === void 0) this.serialRunning = true;
1439
1633
  this.runTask(task).finally(() => {
1440
1634
  this.running -= 1;
1441
- if (task.request.comparisonId === void 0) this.serialRunning = false;
1442
1635
  this.drain();
1443
1636
  });
1444
1637
  }
@@ -1558,10 +1751,15 @@ var ImageGenerationRuntime = class {
1558
1751
  * Framework-free (node:fs + node:crypto only) so the route layer can drive it
1559
1752
  * directly.
1560
1753
  */
1561
- const HISTORY_DIR = path.join(homedir(), ".dsh", "dsh-imagegen");
1562
- const GALLERY_DIR = path.join(HISTORY_DIR, "gallery");
1563
- const INDEX_PATH = path.join(GALLERY_DIR, "index.json");
1564
- const IMAGES_DIR = path.join(GALLERY_DIR, "images");
1754
+ function galleryDir() {
1755
+ return path.join(imageDataRoot(), "gallery");
1756
+ }
1757
+ function indexPath() {
1758
+ return path.join(galleryDir(), "index.json");
1759
+ }
1760
+ function imagesDir() {
1761
+ return path.join(galleryDir(), "images");
1762
+ }
1565
1763
  let pendingMutation = Promise.resolve();
1566
1764
  function mutateGallery(operation) {
1567
1765
  const next = pendingMutation.then(operation, operation);
@@ -1600,12 +1798,12 @@ function fingerprint(input) {
1600
1798
  }
1601
1799
  /** Ensure the storage directories exist. */
1602
1800
  async function ensureDirs() {
1603
- await promises.mkdir(IMAGES_DIR, { recursive: true });
1801
+ await promises.mkdir(imagesDir(), { recursive: true });
1604
1802
  }
1605
1803
  /** Read the index, tolerating a missing/corrupt file. */
1606
1804
  async function readIndex() {
1607
1805
  try {
1608
- const raw = await promises.readFile(INDEX_PATH, "utf8");
1806
+ const raw = await promises.readFile(indexPath(), "utf8");
1609
1807
  const parsed = JSON.parse(raw);
1610
1808
  if (parsed === null || typeof parsed !== "object") return [];
1611
1809
  const entries = parsed.entries;
@@ -1619,9 +1817,9 @@ async function readIndex() {
1619
1817
  async function writeIndex(entries) {
1620
1818
  await ensureDirs();
1621
1819
  const payload = { entries };
1622
- const tmp = `${INDEX_PATH}.tmp-${process.pid}`;
1820
+ const tmp = `${indexPath()}.tmp-${process.pid}`;
1623
1821
  await promises.writeFile(tmp, JSON.stringify(payload), "utf8");
1624
- await promises.rename(tmp, INDEX_PATH);
1822
+ await promises.rename(tmp, indexPath());
1625
1823
  }
1626
1824
  /** Structural guard for a stored entry. */
1627
1825
  function isStoredEntry(value) {
@@ -1636,7 +1834,7 @@ function isStoredEntry(value) {
1636
1834
  /** Remove one entry's image files (best effort). */
1637
1835
  async function removeEntryFiles(entry) {
1638
1836
  for (const image of entry.images) try {
1639
- await promises.rm(path.join(IMAGES_DIR, image.file), { force: true });
1837
+ await promises.rm(path.join(imagesDir(), image.file), { force: true });
1640
1838
  } catch {}
1641
1839
  }
1642
1840
  /** Project a stored entry onto the wire shape (image URLs). */
@@ -1692,8 +1890,8 @@ async function appendGallery(input) {
1692
1890
  for (let index = 0; index < input.images.length; index++) {
1693
1891
  const image = input.images[index];
1694
1892
  const file = `${prefix}-${index}.${extensionOf$1(image.mime)}`;
1695
- await promises.writeFile(path.join(IMAGES_DIR, file), Buffer.from(image.b64, "base64"));
1696
- notifyImageSaved("gallery", path.join(IMAGES_DIR, file));
1893
+ await promises.writeFile(path.join(imagesDir(), file), Buffer.from(image.b64, "base64"));
1894
+ notifyImageSaved("gallery", path.join(imagesDir(), file));
1697
1895
  storedImages.push({
1698
1896
  file,
1699
1897
  mime: image.mime,
@@ -1769,7 +1967,7 @@ async function readGalleryImage(file) {
1769
1967
  if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*-[0-9]+\.(png|jpg|jpeg|webp|gif)$/.test(file)) return void 0;
1770
1968
  try {
1771
1969
  return {
1772
- data: await promises.readFile(path.join(IMAGES_DIR, file)),
1970
+ data: await promises.readFile(path.join(imagesDir(), file)),
1773
1971
  mime: mimeOfFile$1(file)
1774
1972
  };
1775
1973
  } catch {
@@ -1779,11 +1977,6 @@ async function readGalleryImage(file) {
1779
1977
  //#endregion
1780
1978
  //#region src/canvas-store.ts
1781
1979
  /** Host-persisted infinite canvas documents and content-addressed assets. */
1782
- const DATA_ROOT = process.env.DSH_HOME?.trim() || path.join(homedir(), ".dsh");
1783
- const CANVAS_ROOT = path.join(DATA_ROOT, "dsh-imagegen", "canvas");
1784
- path.join(CANVAS_ROOT, "pages");
1785
- path.join(CANVAS_ROOT, "assets");
1786
- path.join(CANVAS_ROOT, "index.json");
1787
1980
  var CanvasConflictError = class extends Error {
1788
1981
  code = "canvas-conflict";
1789
1982
  constructor(message = "画布已在其他窗口更新,请重新加载后再保存。") {
@@ -1867,7 +2060,7 @@ function isNode(value) {
1867
2060
  function isDocument(value) {
1868
2061
  if (value === null || typeof value !== "object") return false;
1869
2062
  const document = value;
1870
- 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);
2063
+ 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);
1871
2064
  }
1872
2065
  /** Upgrade a v1 (image/text/annotation + edges) document to the v2 node-graph model.
1873
2066
  * Images and text notes keep their geometry; annotation prompt cards become plain
@@ -1987,17 +2180,17 @@ function serialize(operation) {
1987
2180
  }
1988
2181
  var CanvasStore = class {
1989
2182
  root;
1990
- constructor(root = CANVAS_ROOT) {
2183
+ constructor(root) {
1991
2184
  this.root = root;
1992
2185
  }
1993
2186
  pagesDir() {
1994
- return path.join(this.root, "pages");
2187
+ return path.join(this.root ?? path.join(imageDataRoot(), "canvas"), "pages");
1995
2188
  }
1996
2189
  assetsDir() {
1997
- return path.join(this.root, "assets");
2190
+ return path.join(this.root ?? path.join(imageDataRoot(), "canvas"), "assets");
1998
2191
  }
1999
2192
  indexPath() {
2000
- return path.join(this.root, "index.json");
2193
+ return path.join(this.root ?? path.join(imageDataRoot(), "canvas"), "index.json");
2001
2194
  }
2002
2195
  async ensure() {
2003
2196
  await promises.mkdir(this.pagesDir(), { recursive: true });
@@ -2768,6 +2961,16 @@ const IMAGE_PRESETS = [
2768
2961
  }
2769
2962
  ]
2770
2963
  },
2964
+ {
2965
+ id: "minimax-official",
2966
+ name: "MiniMax 官方(国际站)",
2967
+ apiUrl: "https://api.minimax.io/v1",
2968
+ hint: "MiniMax 原生 /image_generation 接口:image-01(国内站请把地址改为 https://api.minimaxi.com/v1;/models 不列出图片模型,请直接使用预填目录)",
2969
+ models: [{
2970
+ alias: "image-01",
2971
+ id: "image-01"
2972
+ }]
2973
+ },
2771
2974
  {
2772
2975
  id: "xai-grok",
2773
2976
  name: "xAI(Grok)",
@@ -2861,6 +3064,7 @@ function parseGenerateRequest(body) {
2861
3064
  n: typeof body.n === "number" ? body.n : 1,
2862
3065
  detail: typeof body.detail === "string" ? body.detail : "",
2863
3066
  ...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {},
3067
+ ...Array.isArray(body.images) ? { images: body.images.filter((item) => typeof item === "string" && item !== "").slice(0, 4) } : {},
2864
3068
  ...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {},
2865
3069
  ...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {},
2866
3070
  ...typeof body.comparisonId === "string" && body.comparisonId !== "" ? { comparisonId: body.comparisonId } : {},
@@ -4976,6 +5180,7 @@ const Config = z.object({
4976
5180
  promptApiUrl: z.string().default(""),
4977
5181
  promptApiKey: z.string().role("secret").default(""),
4978
5182
  promptModel: z.string().default(""),
5183
+ localStoragePath: z.string().default(""),
4979
5184
  storageEnabled: z.boolean().default(false),
4980
5185
  storageEndpoint: z.string().default(""),
4981
5186
  storageRegion: z.string().default(""),
@@ -4995,7 +5200,7 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
4995
5200
  /** Order of the announcement section within the tool-guidance band. */
4996
5201
  const SECTION_ORDER = 150;
4997
5202
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
4998
- const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:通过「渠道」对接 OpenAI 兼容图像生成 API(每个渠道 = 一个 API 端点 + 各自的模型目录),支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送,nanobanana 系列按 aspect_ratio / image_size 参数协议发送;seedream 系列统一走 /images/generations,参考图以 JSON image 数组发送;智谱 `glm-image` 使用官方 `/api/paas/v4/images/generations`,当前仅支持文生图;qwen-image 系列使用阿里云 DashScope 原生接口(api_url 填 https://dashscope.aliyuncs.com/api/v1,不支持 OpenAI 兼容模式,该渠道不可复用于提示词增强,尺寸自动映射为宽*高)。API 地址与密钥在 GUI 设置中按渠道配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用用户在各渠道配置目录中的模型;检测模型时会过滤聊天、Embedding 等非图片模型,但模型出现在 /models 中仍不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):多来源标签页(精选案例库 / 沧河案例库,后续可扩展),打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选、收藏(星标,宿主持久化)与复用;各来源列表独立刷新,宿主每 12 小时后台自动同步一次。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片显示在工具调用对应的左侧结果区域,模型收到状态和附件引用,不会额外伪造用户消息。用户也可以使用 `/edit_image <修改描述>`,命令会直接读取当前对话最近图片并调用插件图片模型,不经过对话模型的图片能力检查。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问对应来源站点(vibeui.top / gpt-image2.canghe.ai)。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
5203
+ const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:通过「渠道」对接 OpenAI 兼容图像生成 API(每个渠道 = 一个 API 端点 + 各自的模型目录),支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送,nanobanana 系列按 aspect_ratio / image_size 参数协议发送;seedream 系列统一走 /images/generations,参考图以 JSON image 数组发送;智谱 `glm-image` 使用官方 `/api/paas/v4/images/generations`,当前仅支持文生图;qwen-image 系列使用阿里云 DashScope 原生接口(api_url 填 https://dashscope.aliyuncs.com/api/v1,不支持 OpenAI 兼容模式,该渠道不可复用于提示词增强,尺寸自动映射为宽*高)。MiniMax `image-01` 使用 MiniMax 原生 `/image_generation` 接口(api_url 填 https://api.minimax.io/v1 或国内站 https://api.minimaxi.com/v1,支持 1:1/16:9/4:3/3:2/2:3/3:4/9:16/21:9 宽高比,一次最多 9 张;图生图为单张 subject_reference 主体参考(保持人物/主体一致,非像素级局部编辑);其 /models 只列聊天模型,图片模型需用预设目录)。API 地址与密钥在 GUI 设置中按渠道配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用用户在各渠道配置目录中的模型;检测模型时会过滤聊天、Embedding 等非图片模型,但模型出现在 /models 中仍不等于其网关原生支持生图协议,遇到 Qwen、MiniMax、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):多来源标签页(精选案例库 / 沧河案例库,后续可扩展),打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选、收藏(星标,宿主持久化)与复用;各来源列表独立刷新,宿主每 12 小时后台自动同步一次。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片显示在工具调用对应的左侧结果区域,模型收到状态和附件引用,不会额外伪造用户消息。用户也可以使用 `/edit_image <修改描述>`,命令会直接读取当前对话最近图片并调用插件图片模型,不经过对话模型的图片能力检查。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问对应来源站点(vibeui.top / gpt-image2.canghe.ai)。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
4999
5204
  /** Append the live channel × model table so an Agent can honor user choices. */
5000
5205
  function guidanceFor(channels, defaultChannelId) {
5001
5206
  if (channels.length === 0) return `${IMAGEGEN_GUIDANCE} 尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥。`;
@@ -5048,6 +5253,7 @@ function apply(ctx, config) {
5048
5253
  let current = () => config ?? {};
5049
5254
  const resolve = () => {
5050
5255
  const value = current() ?? {};
5256
+ setImageDataRoot(value.localStoragePath);
5051
5257
  let channels = normalizeChannels(value.channels);
5052
5258
  const secrets = { ...value.channelSecrets ?? {} };
5053
5259
  if (channels.length === 0) {
@@ -5217,4 +5423,4 @@ function apply(ctx, config) {
5217
5423
  };
5218
5424
  }
5219
5425
  //#endregion
5220
- export { CURRENT_VERSION, Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, ImageGenerationRuntime, addTemplateFavorite, appendGallery, apply, checkForUpdate, clearGallery, clearTemplateFavoritesMemo, clearTemplateMemo, clearUpdateCache, compareVersions, generateImage, inject, installUpdate, latestSessionImage, listGallery, listTemplateFavorites, listTemplates, makeRoutes, name, profileFromProcess, putObject, readGalleryImage, readTemplateImage, refreshTemplates, registerAgentImageTools, registerEditImageCommand, removeGallery, removeTemplateFavorite, sampleTemplates, setStorageSyncHandler, syncAllTemplates, testStorage, updateGalleryTags };
5426
+ export { CURRENT_VERSION, Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, ImageGenerationRuntime, addTemplateFavorite, appendGallery, apply, checkForUpdate, clearGallery, clearTemplateFavoritesMemo, clearTemplateMemo, clearUpdateCache, compareVersions, generateImage, inject, installUpdate, latestSessionImage, listGallery, listTemplateFavorites, listTemplates, makeRoutes, name, profileFromProcess, promptCharLimit, putObject, readGalleryImage, readTemplateImage, refreshTemplates, registerAgentImageTools, registerEditImageCommand, removeGallery, removeTemplateFavorite, sampleTemplates, setStorageSyncHandler, syncAllTemplates, testStorage, updateGalleryTags };