@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.
- package/LICENSE +201 -201
- package/README.md +203 -182
- package/cordis.patch.yml +8 -8
- package/docs/images/multi-model-comparison.png +0 -0
- package/lib/client.js +1103 -837
- package/lib/client.js.map +1 -1
- package/lib/index.js +265 -135
- package/package.json +70 -68
- package/src/agent-image-tools.ts +418 -418
- package/src/client/ImageGenPanel.tsx +1699 -1508
- package/src/client/SettingsCard.tsx +936 -957
- package/src/client/TemplateLibrary.tsx +336 -336
- package/src/client/api.ts +193 -193
- package/src/client/channels-form.ts +263 -263
- package/src/client/controller.ts +46 -46
- package/src/client/conversation-sync.ts +14 -0
- package/src/client/css-modules.d.ts +5 -5
- package/src/client/helpers.ts +33 -33
- package/src/client/image-toolview.module.css +73 -73
- package/src/client/image-toolview.tsx +169 -158
- package/src/client/index.ts +32 -22
- package/src/client/locales.ts +610 -594
- package/src/client/mount.tsx +185 -96
- package/src/client/panel.module.css +1713 -1445
- package/src/client/settings-card.module.css +1023 -1023
- package/src/client/settings-form.ts +336 -336
- package/src/client/settings-scope.ts +298 -298
- package/src/client/sidebar-entry.ts +148 -102
- package/src/client/templates.module.css +453 -453
- package/src/engine.ts +520 -478
- package/src/gallery-store.ts +286 -286
- package/src/generation-runtime.ts +79 -75
- package/src/history-store.ts +250 -244
- package/src/image-format.ts +11 -11
- package/src/image-models.ts +19 -19
- package/src/index.ts +318 -318
- package/src/model-catalog.ts +115 -98
- package/src/presets.ts +71 -63
- package/src/prompt-enhancer.ts +137 -79
- package/src/protocol.ts +338 -326
- package/src/routes.ts +916 -906
- package/src/task-queue.ts +113 -103
- package/src/templates/cases.json +10196 -10196
- package/src/templates-store.ts +278 -278
- 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.
|
|
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
|
-
|
|
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 =
|
|
831
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
task.
|
|
877
|
-
task.
|
|
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.
|
|
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
|
|
1704
|
+
hint: "OpenAI 官方图像生成接口",
|
|
1588
1705
|
models: [{
|
|
1589
1706
|
alias: "gpt-image-2",
|
|
1590
1707
|
id: "gpt-image-2"
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
|
|
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
|
|
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
|
|
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 地址与密钥。`;
|