@dickpy/dsh-imagegen 1.3.0 → 1.4.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.
Files changed (45) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +203 -182
  3. package/cordis.patch.yml +8 -8
  4. package/docs/images/multi-model-comparison.png +0 -0
  5. package/lib/client.js +1103 -837
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +265 -135
  8. package/package.json +70 -68
  9. package/src/agent-image-tools.ts +418 -418
  10. package/src/client/ImageGenPanel.tsx +1699 -1508
  11. package/src/client/SettingsCard.tsx +936 -957
  12. package/src/client/TemplateLibrary.tsx +336 -336
  13. package/src/client/api.ts +193 -193
  14. package/src/client/channels-form.ts +263 -263
  15. package/src/client/controller.ts +46 -46
  16. package/src/client/conversation-sync.ts +14 -0
  17. package/src/client/css-modules.d.ts +5 -5
  18. package/src/client/helpers.ts +33 -33
  19. package/src/client/image-toolview.module.css +73 -73
  20. package/src/client/image-toolview.tsx +169 -158
  21. package/src/client/index.ts +32 -22
  22. package/src/client/locales.ts +610 -594
  23. package/src/client/mount.tsx +185 -96
  24. package/src/client/panel.module.css +1713 -1445
  25. package/src/client/settings-card.module.css +1023 -1023
  26. package/src/client/settings-form.ts +336 -336
  27. package/src/client/settings-scope.ts +298 -298
  28. package/src/client/sidebar-entry.ts +148 -102
  29. package/src/client/templates.module.css +453 -453
  30. package/src/engine.ts +520 -478
  31. package/src/gallery-store.ts +286 -286
  32. package/src/generation-runtime.ts +79 -75
  33. package/src/history-store.ts +250 -244
  34. package/src/image-format.ts +11 -11
  35. package/src/image-models.ts +19 -19
  36. package/src/index.ts +318 -318
  37. package/src/model-catalog.ts +115 -98
  38. package/src/presets.ts +71 -63
  39. package/src/prompt-enhancer.ts +137 -79
  40. package/src/protocol.ts +338 -326
  41. package/src/routes.ts +916 -906
  42. package/src/task-queue.ts +113 -103
  43. package/src/templates/cases.json +10196 -10196
  44. package/src/templates-store.ts +278 -278
  45. package/src/updater.ts +117 -117
package/lib/index.js CHANGED
@@ -16,7 +16,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
16
16
  /** Settings namespace this plugin owns (host settings seam + bridge). */
17
17
  const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
18
18
  /** Published package version shared by the host updater and the client UI. */
19
- const PLUGIN_VERSION = "1.3.0";
19
+ const PLUGIN_VERSION = "1.3.1";
20
20
  /** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
21
21
  const SETTINGS_API = {
22
22
  describe: "/api/dsh-imagegen/settings/describe",
@@ -91,94 +91,6 @@ const TEMPLATES_API = {
91
91
  image: "/api/dsh-imagegen/templates/image"
92
92
  };
93
93
  //#endregion
94
- //#region src/prompt-enhancer.ts
95
- function endpoint(base, suffix) {
96
- return `${base.replace(/\/+$/, "")}${suffix}`;
97
- }
98
- function headers(apiKey) {
99
- return {
100
- "content-type": "application/json",
101
- ...apiKey.trim() === "" ? {} : { authorization: `Bearer ${apiKey.trim()}` }
102
- };
103
- }
104
- async function responseJson(response) {
105
- const body = await response.json().catch(() => void 0);
106
- if (!response.ok || body === void 0 || body === null || typeof body !== "object") {
107
- const message = body !== null && typeof body === "object" && typeof body.error?.message === "string" ? body.error.message : `HTTP ${response.status}`;
108
- throw new Error(message);
109
- }
110
- return body;
111
- }
112
- /** List candidates exposed by an OpenAI-compatible endpoint. */
113
- async function listOpenAIModels(config) {
114
- if (config.apiUrl.trim() === "") throw new Error("API URL is required");
115
- const body = await responseJson(await fetch(endpoint(config.apiUrl, "/models"), { headers: headers(config.apiKey) }));
116
- const data = Array.isArray(body.data) ? body.data : [];
117
- return [...new Set(data.flatMap((item) => item !== null && typeof item === "object" && typeof item.id === "string" ? [item.id.trim()] : []).filter(Boolean))].sort((a, b) => a.localeCompare(b));
118
- }
119
- /** List chat models exposed by an OpenAI-compatible endpoint. */
120
- async function listPromptModels(config) {
121
- return listOpenAIModels(config);
122
- }
123
- /** Expand a concise image request into a production-ready image prompt. */
124
- async function enhancePrompt(config, prompt) {
125
- if (config.apiUrl.trim() === "" || config.model.trim() === "") throw new Error("prompt enhancement model is not configured");
126
- const body = await responseJson(await fetch(endpoint(config.apiUrl, "/chat/completions"), {
127
- method: "POST",
128
- headers: headers(config.apiKey),
129
- body: JSON.stringify({
130
- model: config.model.trim(),
131
- temperature: .7,
132
- messages: [{
133
- role: "system",
134
- content: "You are an expert image-prompt editor. Expand the user request into one vivid, specific image-generation prompt. Preserve intent and language. Add only useful visual detail: subject, composition, lighting, materials, color, camera/style and quality. Return only the finished prompt, with no preface or markdown."
135
- }, {
136
- role: "user",
137
- content: prompt
138
- }]
139
- })
140
- }));
141
- const choices = Array.isArray(body.choices) ? body.choices : [];
142
- const content = choices[0] !== null && typeof choices[0] === "object" ? choices[0].message?.content : void 0;
143
- if (typeof content !== "string" || content.trim() === "") throw new Error("chat model returned an empty prompt");
144
- return content.trim();
145
- }
146
- //#endregion
147
- //#region src/image-models.ts
148
- /**
149
- * Image-model configuration shared by the host, panel, and Agent tools.
150
- * `/models` exposes candidates only: the configured list is the explicit
151
- * allow-list because OpenAI-compatible gateways rarely advertise modalities.
152
- */
153
- const DEFAULT_IMAGE_MODELS = [
154
- "gpt-image-2",
155
- "grok-imagine-image",
156
- "nanobanana2",
157
- "nanobanana2-lite",
158
- "nanobanana-pro",
159
- "seedream-5.0-pro"
160
- ];
161
- /** Normalize user-entered model identifiers and retain a usable legacy default. */
162
- function normalizeImageModels(value) {
163
- const candidates = Array.isArray(value) ? value : [];
164
- const unique = /* @__PURE__ */ new Set();
165
- for (const candidate of candidates) {
166
- if (typeof candidate !== "string") continue;
167
- const model = candidate.trim();
168
- if (model !== "") unique.add(model);
169
- }
170
- return unique.size > 0 ? [...unique] : [...DEFAULT_IMAGE_MODELS];
171
- }
172
- //#endregion
173
- //#region src/image-format.ts
174
- function detectImageMime(data) {
175
- const startsWith = (...bytes) => bytes.every((value, index) => data[index] === value);
176
- if (startsWith(137, 80, 78, 71, 13, 10, 26, 10)) return "image/png";
177
- if (startsWith(255, 216, 255)) return "image/jpeg";
178
- if (startsWith(71, 73, 70, 56, 55, 97) || startsWith(71, 73, 70, 56, 57, 97)) return "image/gif";
179
- if (startsWith(82, 73, 70, 70) && data[8] === 87 && data[9] === 69 && data[10] === 66 && data[11] === 80) return "image/webp";
180
- }
181
- //#endregion
182
94
  //#region src/model-catalog.ts
183
95
  const ENTRIES = {
184
96
  "gpt-image": {
@@ -228,6 +140,14 @@ const ENTRIES = {
228
140
  supportsEdit: true,
229
141
  supportsAspectRatio: true,
230
142
  qualityTiers: ["1K", "2K"]
143
+ },
144
+ zhipu: {
145
+ label: "GLM-Image",
146
+ labelZh: "智谱图像",
147
+ known: true,
148
+ supportsEdit: false,
149
+ supportsAspectRatio: false,
150
+ qualityTiers: ["HD"]
231
151
  }
232
152
  };
233
153
  /** Official Gemini image ids served by Nano Banana gateways. */
@@ -262,6 +182,10 @@ function describeModel(model) {
262
182
  family: "seedream",
263
183
  ...ENTRIES.seedream
264
184
  };
185
+ if (/^(?:glm-image|cogview(?:-|$))/i.test(id)) return {
186
+ family: "zhipu",
187
+ ...ENTRIES.zhipu
188
+ };
265
189
  return {
266
190
  family: "unknown",
267
191
  label: "unknown",
@@ -272,11 +196,165 @@ function describeModel(model) {
272
196
  qualityTiers: []
273
197
  };
274
198
  }
199
+ /** Conservative fallback for providers whose /models response only has ids.
200
+ * Metadata-aware filtering lives in prompt-enhancer.ts; this catches common
201
+ * image model naming conventions without treating every unknown model as an
202
+ * image model. */
203
+ function isLikelyImageModelId(model) {
204
+ return /(?:^|[-_.])(?:image|img|diffusion|flux|cogview|imagen|seedream|nanobanana|grok-imagine|dall-e|stable-diffusion|sdxl|pixart|kolors|ideogram|midjourney|recraft|hunyuan|jimeng|wanx|hidream|playground)(?:$|[-_.])/i.test(model.trim());
205
+ }
275
206
  /** The family a model id routes its request through. */
276
207
  function modelFamily(model) {
277
208
  return describeModel(model).family;
278
209
  }
279
210
  //#endregion
211
+ //#region src/prompt-enhancer.ts
212
+ /** OpenAI-compatible chat helpers used by the optional prompt-enhancement UI. */
213
+ function endpoint(base, suffix) {
214
+ return `${base.replace(/\/+$/, "")}${suffix}`;
215
+ }
216
+ function headers(apiKey) {
217
+ return {
218
+ "content-type": "application/json",
219
+ ...apiKey.trim() === "" ? {} : { authorization: `Bearer ${apiKey.trim()}` }
220
+ };
221
+ }
222
+ async function responseJson(response) {
223
+ const body = await response.json().catch(() => void 0);
224
+ if (!response.ok || body === void 0 || body === null || typeof body !== "object") {
225
+ const message = body !== null && typeof body === "object" && typeof body.error?.message === "string" ? body.error.message : `HTTP ${response.status}`;
226
+ throw new Error(message);
227
+ }
228
+ return body;
229
+ }
230
+ async function listModelRecords(config) {
231
+ if (config.apiUrl.trim() === "") throw new Error("API URL is required");
232
+ const body = await responseJson(await fetch(endpoint(config.apiUrl, "/models"), { headers: headers(config.apiKey) }));
233
+ return (Array.isArray(body.data) ? body.data : []).flatMap((item) => {
234
+ if (item === null || typeof item !== "object" || typeof item.id !== "string") return [];
235
+ const id = item.id.trim();
236
+ return id === "" ? [] : [{
237
+ ...item,
238
+ id
239
+ }];
240
+ });
241
+ }
242
+ function textOf(value) {
243
+ if (typeof value === "string") return [value];
244
+ if (!Array.isArray(value)) return [];
245
+ return value.filter((item) => typeof item === "string");
246
+ }
247
+ function hasImageGenerationCapability(record) {
248
+ const capability = record.capabilities;
249
+ if (capability !== null && typeof capability === "object") {
250
+ const values = capability;
251
+ for (const key of [
252
+ "image_generation",
253
+ "imageGeneration",
254
+ "text_to_image",
255
+ "textToImage",
256
+ "image_gen"
257
+ ]) if (typeof values[key] === "boolean") return values[key];
258
+ const serialized = JSON.stringify(values).toLowerCase();
259
+ if (/image[ _-]?generation|text[ _-]?to[ _-]?image/.test(serialized)) return true;
260
+ }
261
+ const taskText = [
262
+ ...textOf(record.task),
263
+ ...textOf(record.task_type),
264
+ ...textOf(record.taskType),
265
+ ...textOf(record.type),
266
+ ...textOf(record.model_type),
267
+ ...textOf(record.modelType),
268
+ ...textOf(record.tasks),
269
+ ...textOf(record.description)
270
+ ].join(" ").toLowerCase();
271
+ if (/image[ _-]?generation|text[ _-]?to[ _-]?image|image[ _-]?gen/.test(taskText)) return true;
272
+ if (/^image(?:[ _-]?generation)?$/.test(taskText.trim())) return true;
273
+ if (/embedding|rerank|moderation|transcri|speech|audio|video|chat[ _-]?completion/.test(taskText)) return false;
274
+ for (const key of [
275
+ "output_modalities",
276
+ "outputModalities",
277
+ "supported_output_modalities"
278
+ ]) {
279
+ const modalities = textOf(record[key]).map((value) => value.toLowerCase());
280
+ if (modalities.length > 0) return modalities.includes("image");
281
+ }
282
+ }
283
+ function isImageModelRecord(record) {
284
+ return hasImageGenerationCapability(record) ?? isLikelyImageModelId(record.id);
285
+ }
286
+ /** List candidates exposed by an OpenAI-compatible endpoint. */
287
+ async function listOpenAIModels(config) {
288
+ return [...new Set((await listModelRecords(config)).map((record) => record.id))].sort((a, b) => a.localeCompare(b));
289
+ }
290
+ /** List only models that advertise or conventionally represent image generation. */
291
+ async function listImageModels(config) {
292
+ return [...new Set((await listModelRecords(config)).filter(isImageModelRecord).map((record) => record.id))].sort((a, b) => a.localeCompare(b));
293
+ }
294
+ /** List chat models exposed by an OpenAI-compatible endpoint. */
295
+ async function listPromptModels(config) {
296
+ return listOpenAIModels(config);
297
+ }
298
+ /** Expand a concise image request into a production-ready image prompt. */
299
+ async function enhancePrompt(config, prompt) {
300
+ if (config.apiUrl.trim() === "" || config.model.trim() === "") throw new Error("prompt enhancement model is not configured");
301
+ const body = await responseJson(await fetch(endpoint(config.apiUrl, "/chat/completions"), {
302
+ method: "POST",
303
+ headers: headers(config.apiKey),
304
+ body: JSON.stringify({
305
+ model: config.model.trim(),
306
+ temperature: .7,
307
+ messages: [{
308
+ role: "system",
309
+ content: "You are an expert image-prompt editor. Expand the user request into one vivid, specific image-generation prompt. Preserve intent and language. Add only useful visual detail: subject, composition, lighting, materials, color, camera/style and quality. Return only the finished prompt, with no preface or markdown."
310
+ }, {
311
+ role: "user",
312
+ content: prompt
313
+ }]
314
+ })
315
+ }));
316
+ const choices = Array.isArray(body.choices) ? body.choices : [];
317
+ const content = choices[0] !== null && typeof choices[0] === "object" ? choices[0].message?.content : void 0;
318
+ if (typeof content !== "string" || content.trim() === "") throw new Error("chat model returned an empty prompt");
319
+ return content.trim();
320
+ }
321
+ //#endregion
322
+ //#region src/image-models.ts
323
+ /**
324
+ * Image-model configuration shared by the host, panel, and Agent tools.
325
+ * `/models` exposes candidates only: the configured list is the explicit
326
+ * allow-list because OpenAI-compatible gateways rarely advertise modalities.
327
+ */
328
+ const DEFAULT_IMAGE_MODELS = [
329
+ "gpt-image-2",
330
+ "grok-imagine-image",
331
+ "nanobanana2",
332
+ "nanobanana2-lite",
333
+ "nanobanana-pro",
334
+ "seedream-5.0-pro",
335
+ "glm-image"
336
+ ];
337
+ /** Normalize user-entered model identifiers and retain a usable legacy default. */
338
+ function normalizeImageModels(value) {
339
+ const candidates = Array.isArray(value) ? value : [];
340
+ const unique = /* @__PURE__ */ new Set();
341
+ for (const candidate of candidates) {
342
+ if (typeof candidate !== "string") continue;
343
+ const model = candidate.trim();
344
+ if (model !== "") unique.add(model);
345
+ }
346
+ return unique.size > 0 ? [...unique] : [...DEFAULT_IMAGE_MODELS];
347
+ }
348
+ //#endregion
349
+ //#region src/image-format.ts
350
+ function detectImageMime(data) {
351
+ const startsWith = (...bytes) => bytes.every((value, index) => data[index] === value);
352
+ if (startsWith(137, 80, 78, 71, 13, 10, 26, 10)) return "image/png";
353
+ if (startsWith(255, 216, 255)) return "image/jpeg";
354
+ if (startsWith(71, 73, 70, 56, 55, 97) || startsWith(71, 73, 70, 56, 57, 97)) return "image/gif";
355
+ if (startsWith(82, 73, 70, 70) && data[8] === 87 && data[9] === 69 && data[10] === 66 && data[11] === 80) return "image/webp";
356
+ }
357
+ //#endregion
280
358
  //#region src/engine.ts
281
359
  /** A generation failure with a user-presentable message. */
282
360
  var ImageGenError = class extends Error {
@@ -331,6 +409,13 @@ function isNanoBanana(model) {
331
409
  function isSeedream(model) {
332
410
  return modelFamily(model) === "seedream";
333
411
  }
412
+ /** Whether the model uses the official Zhipu image-generation contract. */
413
+ function isZhipuImage(model) {
414
+ return modelFamily(model) === "zhipu";
415
+ }
416
+ function isGlmImage(model) {
417
+ return /^glm-image(?:-|$)/i.test(model.trim());
418
+ }
334
419
  /** Whether this is the official Volcengine Ark model naming convention. */
335
420
  function isVolcSeedream(model) {
336
421
  return /^doubao-seedream(?:-|$)/i.test(model.trim());
@@ -407,6 +492,19 @@ function bareBase64(value) {
407
492
  const parsed = parseDataUrl(value);
408
493
  return parsed !== void 0 && parsed.base64 !== void 0 ? parsed.base64 : value;
409
494
  }
495
+ /** Whether a result URL carries cloud-storage signing credentials. */
496
+ function isPresignedUrl(value) {
497
+ let url;
498
+ try {
499
+ url = new URL(value);
500
+ } catch {
501
+ return false;
502
+ }
503
+ const params = new Set(Array.from(url.searchParams.keys(), (key) => key.toLowerCase()));
504
+ if (params.has("x-goog-signature") || params.has("x-goog-credential")) return true;
505
+ if (params.has("x-amz-signature") || params.has("x-amz-credential")) return true;
506
+ return params.has("signature") && (params.has("expires") || params.has("googleaccessid") || params.has("awsaccesskeyid"));
507
+ }
410
508
  /** Clamp the requested image count into the API-accepted range. */
411
509
  function clampCount(n) {
412
510
  if (!Number.isFinite(n)) return 1;
@@ -441,6 +539,11 @@ function effectiveParams(request) {
441
539
  size: seedreamSize(request.quality),
442
540
  response_format: isVolcSeedream(model) ? "url" : "b64_json"
443
541
  };
542
+ if (isZhipuImage(model)) return {
543
+ model,
544
+ ...request.size !== "" && request.size !== "auto" && OPENAI_SIZE_BY_RATIO[request.size] !== void 0 ? { size: OPENAI_SIZE_BY_RATIO[request.size] } : {},
545
+ quality: isGlmImage(model) ? "hd" : "standard"
546
+ };
444
547
  return {
445
548
  model,
446
549
  ...request.size !== "" && request.size !== "auto" && OPENAI_SIZE_BY_RATIO[request.size] !== void 0 ? { size: OPENAI_SIZE_BY_RATIO[request.size] } : {},
@@ -458,9 +561,9 @@ function effectiveCount(request) {
458
561
  /** Normalize one upstream data item into a base64 image. */
459
562
  async function normalizeItem(item, upstream) {
460
563
  const revisedPrompt = typeof item.revised_prompt === "string" ? item.revised_prompt : void 0;
461
- if (typeof item.b64_json === "string") {
564
+ if (typeof item.b64_json === "string" && item.b64_json.trim() !== "") {
462
565
  const b64 = bareBase64(item.b64_json);
463
- return {
566
+ if (b64.trim() !== "") return {
464
567
  b64,
465
568
  mime: detectImageMime(Buffer.from(b64, "base64")) ?? "image/png",
466
569
  revisedPrompt
@@ -481,7 +584,7 @@ async function normalizeItem(item, upstream) {
481
584
  let response;
482
585
  try {
483
586
  response = await fetch(url, {
484
- headers: { ...upstream.apiKey === "" ? {} : { authorization: `Bearer ${upstream.apiKey}` } },
587
+ ...isPresignedUrl(url) || upstream.apiKey === "" ? {} : { headers: { authorization: `Bearer ${upstream.apiKey}` } },
485
588
  signal: budget.signal
486
589
  });
487
590
  } catch (error) {
@@ -607,6 +710,7 @@ async function generateImage(upstream, request, options = {}) {
607
710
  const baseUrl = upstream.apiUrl.trim().replace(/\/+$/, "");
608
711
  if (baseUrl === "") throw new ImageGenError("api_url 未配置:请先在「设置 → 插件 → 可配置」中填写", "config-missing");
609
712
  if (upstream.apiKey.trim() === "") throw new ImageGenError("api_key 未配置:请先在「设置 → 插件 → 可配置」中填写", "config-missing");
713
+ if (request.mode === "edit" && isZhipuImage(wireModel(request))) throw new ImageGenError("智谱 GLM-Image 当前仅支持文生图,请切换到文生图模式或选择支持图生图的模型", "edit-unsupported");
610
714
  const params = effectiveParams(request);
611
715
  const count = effectiveCount(request);
612
716
  return { images: (await Promise.all(Array.from({ length: count }, () => requestOneImage(baseUrl, upstream, request, params, options.signal)))).flat() };
@@ -738,7 +842,9 @@ function toWire$1(entry) {
738
842
  })),
739
843
  ...entry.refName === void 0 ? {} : { refName: entry.refName },
740
844
  ...entry.channel === void 0 ? {} : { channel: entry.channel },
741
- ...entry.channelId === void 0 ? {} : { channelId: entry.channelId }
845
+ ...entry.channelId === void 0 ? {} : { channelId: entry.channelId },
846
+ ...entry.comparisonId === void 0 ? {} : { comparisonId: entry.comparisonId },
847
+ ...entry.comparisonModels === void 0 ? {} : { comparisonModels: entry.comparisonModels }
742
848
  };
743
849
  }
744
850
  /** List the persisted history, newest first, as wire entries. */
@@ -779,7 +885,9 @@ async function appendHistory(input) {
779
885
  images: storedImages,
780
886
  ...input.refName === void 0 ? {} : { refName: input.refName },
781
887
  ...input.channelId === void 0 ? {} : { channelId: input.channelId },
782
- ...input.channel === void 0 ? {} : { channel: input.channel }
888
+ ...input.channel === void 0 ? {} : { channel: input.channel },
889
+ ...input.comparisonId === void 0 ? {} : { comparisonId: input.comparisonId },
890
+ ...input.comparisonModels === void 0 ? {} : { comparisonModels: input.comparisonModels }
783
891
  }, ...await readIndex$1()];
784
892
  const kept = merged.slice(0, 50);
785
893
  for (const dropped of merged.slice(50)) await removeEntryFiles$1(dropped);
@@ -824,12 +932,15 @@ async function readHistoryImage(file) {
824
932
  /** In-memory, host-resident image generation queue. */
825
933
  var GenerationTaskQueue = class {
826
934
  run;
935
+ concurrency;
827
936
  tasks = [];
828
937
  controllers = /* @__PURE__ */ new Map();
829
938
  listeners = /* @__PURE__ */ new Set();
830
- running = false;
831
- constructor(run) {
939
+ running = 0;
940
+ serialRunning = false;
941
+ constructor(run, concurrency = 1) {
832
942
  this.run = run;
943
+ this.concurrency = concurrency;
833
944
  }
834
945
  list() {
835
946
  return this.tasks.map((task) => this.snapshot(task));
@@ -860,45 +971,49 @@ var GenerationTaskQueue = class {
860
971
  task.finishedAt = Date.now();
861
972
  this.controllers.get(id)?.abort();
862
973
  this.publish(task);
974
+ this.drain();
863
975
  return this.snapshot(task);
864
976
  }
865
977
  retry(id) {
866
978
  const previous = this.tasks.find((item) => item.id === id);
867
979
  return previous === void 0 ? void 0 : this.submit(previous.request);
868
980
  }
869
- async drain() {
870
- if (this.running) return;
871
- this.running = true;
981
+ drain() {
982
+ while (this.running < Math.max(1, this.concurrency)) {
983
+ const task = this.tasks.find((item) => item.status === "queued" && (this.running === 0 || item.request.comparisonId !== void 0 && !this.serialRunning));
984
+ if (task === void 0) return;
985
+ this.running += 1;
986
+ if (task.request.comparisonId === void 0) this.serialRunning = true;
987
+ this.runTask(task).finally(() => {
988
+ this.running -= 1;
989
+ if (task.request.comparisonId === void 0) this.serialRunning = false;
990
+ this.drain();
991
+ });
992
+ }
993
+ }
994
+ async runTask(task) {
995
+ task.status = "running";
996
+ task.startedAt = Date.now();
997
+ this.publish(task);
998
+ const controller = new AbortController();
999
+ this.controllers.set(task.id, controller);
872
1000
  try {
873
- for (;;) {
874
- const task = this.tasks.find((item) => item.status === "queued");
875
- if (task === void 0) return;
876
- task.status = "running";
877
- task.startedAt = Date.now();
1001
+ const result = await this.run(task.request, controller.signal);
1002
+ if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
1003
+ task.status = "completed";
1004
+ task.result = result;
1005
+ task.finishedAt = Date.now();
1006
+ this.publish(task);
1007
+ }
1008
+ } catch (error) {
1009
+ if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
1010
+ task.status = "failed";
1011
+ task.error = error instanceof Error ? error.message : String(error);
1012
+ task.finishedAt = Date.now();
878
1013
  this.publish(task);
879
- const controller = new AbortController();
880
- this.controllers.set(task.id, controller);
881
- try {
882
- const result = await this.run(task.request, controller.signal);
883
- if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
884
- task.status = "completed";
885
- task.result = result;
886
- task.finishedAt = Date.now();
887
- this.publish(task);
888
- }
889
- } catch (error) {
890
- if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
891
- task.status = "failed";
892
- task.error = error instanceof Error ? error.message : String(error);
893
- task.finishedAt = Date.now();
894
- this.publish(task);
895
- }
896
- } finally {
897
- this.controllers.delete(task.id);
898
- }
899
1014
  }
900
1015
  } finally {
901
- this.running = false;
1016
+ this.controllers.delete(task.id);
902
1017
  }
903
1018
  }
904
1019
  publish(task) {
@@ -934,7 +1049,7 @@ var ImageGenerationRuntime = class {
934
1049
  constructor(resolve, history = { append: appendHistory }) {
935
1050
  this.resolve = resolve;
936
1051
  this.history = history;
937
- this.queue = new GenerationTaskQueue((request, signal) => this.run(request, signal));
1052
+ this.queue = new GenerationTaskQueue((request, signal) => this.run(request, signal), 4);
938
1053
  }
939
1054
  async run(request, signal) {
940
1055
  const view = this.resolve();
@@ -958,7 +1073,9 @@ var ImageGenerationRuntime = class {
958
1073
  images: result.images,
959
1074
  ...request.refName === void 0 ? {} : { refName: request.refName },
960
1075
  ...request.channelId === void 0 ? {} : { channelId: request.channelId },
961
- ...request.channel === void 0 ? {} : { channel: request.channel }
1076
+ ...request.channel === void 0 ? {} : { channel: request.channel },
1077
+ ...request.comparisonId === void 0 ? {} : { comparisonId: request.comparisonId },
1078
+ ...request.comparisonModels === void 0 ? {} : { comparisonModels: request.comparisonModels }
962
1079
  });
963
1080
  return {
964
1081
  ...result,
@@ -1584,13 +1701,20 @@ const IMAGE_PRESETS = [
1584
1701
  id: "openai-official",
1585
1702
  name: "OpenAI 官方",
1586
1703
  apiUrl: "https://api.openai.com/v1",
1587
- hint: "OpenAI 官方接口:gpt-image-2 / dall-e-3",
1704
+ hint: "OpenAI 官方图像生成接口",
1588
1705
  models: [{
1589
1706
  alias: "gpt-image-2",
1590
1707
  id: "gpt-image-2"
1591
- }, {
1592
- alias: "dall-e-3",
1593
- id: "dall-e-3"
1708
+ }]
1709
+ },
1710
+ {
1711
+ id: "zhipu-official",
1712
+ name: "智谱 AI 官方",
1713
+ apiUrl: "https://open.bigmodel.cn/api/paas/v4",
1714
+ hint: "智谱官方 GLM-Image 图像生成接口",
1715
+ models: [{
1716
+ alias: "glm-image",
1717
+ id: "glm-image"
1594
1718
  }]
1595
1719
  },
1596
1720
  {
@@ -1669,6 +1793,7 @@ function messageOf(error) {
1669
1793
  function parseGenerateRequest(body) {
1670
1794
  const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
1671
1795
  if (prompt === "") return void 0;
1796
+ const comparisonModels = Array.isArray(body.comparisonModels) ? [...new Set(body.comparisonModels.filter((model) => typeof model === "string").map((model) => model.trim()).filter(Boolean))] : [];
1672
1797
  return {
1673
1798
  mode: body.mode === "edit" ? "edit" : "text",
1674
1799
  model: typeof body.model === "string" ? body.model : "",
@@ -1679,7 +1804,9 @@ function parseGenerateRequest(body) {
1679
1804
  detail: typeof body.detail === "string" ? body.detail : "",
1680
1805
  ...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {},
1681
1806
  ...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {},
1682
- ...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {}
1807
+ ...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {},
1808
+ ...typeof body.comparisonId === "string" && body.comparisonId !== "" ? { comparisonId: body.comparisonId } : {},
1809
+ ...comparisonModels.length > 1 ? { comparisonModels } : {}
1683
1810
  };
1684
1811
  }
1685
1812
  /** Validate a submitted history entry (images carry base64). */
@@ -1704,6 +1831,7 @@ function parseHistoryEntryInput(body) {
1704
1831
  ...typeof image.revisedPrompt === "string" ? { revisedPrompt: image.revisedPrompt } : {}
1705
1832
  });
1706
1833
  }
1834
+ const comparisonModels = Array.isArray(entry.comparisonModels) ? [...new Set(entry.comparisonModels.filter((model) => typeof model === "string").map((model) => model.trim()).filter(Boolean))] : [];
1707
1835
  return {
1708
1836
  id: entry.id,
1709
1837
  createdAt: entry.createdAt,
@@ -1717,7 +1845,9 @@ function parseHistoryEntryInput(body) {
1717
1845
  images,
1718
1846
  ...typeof entry.refName === "string" ? { refName: entry.refName } : {},
1719
1847
  ...typeof entry.channelId === "string" ? { channelId: entry.channelId } : {},
1720
- ...typeof entry.channel === "string" ? { channel: entry.channel } : {}
1848
+ ...typeof entry.channel === "string" ? { channel: entry.channel } : {},
1849
+ ...typeof entry.comparisonId === "string" ? { comparisonId: entry.comparisonId } : {},
1850
+ ...comparisonModels.length > 1 ? { comparisonModels } : {}
1721
1851
  };
1722
1852
  }
1723
1853
  /** Extract the image file name from a history-image request URL. */
@@ -1955,7 +2085,7 @@ function makeRoutes(deps) {
1955
2085
  try {
1956
2086
  writeJson(res, 200, {
1957
2087
  ok: true,
1958
- models: await listOpenAIModels(upstream)
2088
+ models: await listImageModels(upstream)
1959
2089
  });
1960
2090
  } catch (error) {
1961
2091
  writeJson(res, 200, {
@@ -3151,7 +3281,7 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
3151
3281
  /** Order of the announcement section within the tool-guidance band. */
3152
3282
  const SECTION_ORDER = 150;
3153
3283
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
3154
- 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 数组发送)。API 地址与密钥在 GUI 设置中按渠道配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用用户在各渠道配置目录中的模型;模型出现在 /models 中不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选与复用。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片显示在工具调用对应的左侧结果区域,模型收到状态和附件引用,不会额外伪造用户消息。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问 vibeui.top。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
3284
+ 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`,当前仅支持文生图)。API 地址与密钥在 GUI 设置中按渠道配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用用户在各渠道配置目录中的模型;检测模型时会过滤聊天、Embedding 等非图片模型,但模型出现在 /models 中仍不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选与复用。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片显示在工具调用对应的左侧结果区域,模型收到状态和附件引用,不会额外伪造用户消息。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问 vibeui.top。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
3155
3285
  /** Append the live channel × model table so an Agent can honor user choices. */
3156
3286
  function guidanceFor(channels, defaultChannelId) {
3157
3287
  if (channels.length === 0) return `${IMAGEGEN_GUIDANCE} 尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥。`;