@dickpy/dsh-imagegen 1.2.3 → 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 -181
- package/cordis.patch.yml +8 -8
- package/docs/images/multi-model-comparison.png +0 -0
- package/lib/client.js +2711 -1318
- package/lib/client.js.map +1 -1
- package/lib/index.js +830 -155
- package/package.json +70 -68
- package/src/agent-image-tools.ts +418 -316
- package/src/client/ImageGenPanel.tsx +1703 -1476
- package/src/client/SettingsCard.tsx +936 -648
- package/src/client/TemplateLibrary.tsx +336 -336
- package/src/client/api.ts +193 -193
- package/src/client/channels-form.ts +263 -0
- 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 +170 -152
- package/src/client/index.ts +32 -22
- package/src/client/locales.ts +610 -484
- package/src/client/mount.tsx +185 -96
- package/src/client/panel.module.css +1713 -1445
- package/src/client/settings-card.module.css +1023 -536
- package/src/client/settings-form.ts +336 -336
- package/src/client/settings-scope.ts +298 -250
- package/src/client/sidebar-entry.ts +148 -102
- package/src/client/templates.module.css +453 -453
- package/src/engine.ts +520 -464
- package/src/gallery-store.ts +286 -280
- package/src/generation-runtime.ts +79 -48
- package/src/history-store.ts +250 -238
- package/src/image-format.ts +11 -0
- package/src/image-models.ts +19 -19
- package/src/index.ts +318 -212
- package/src/model-catalog.ts +115 -0
- package/src/presets.ts +71 -0
- package/src/prompt-enhancer.ts +137 -79
- package/src/protocol.ts +338 -253
- package/src/routes.ts +916 -738
- 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.
|
|
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",
|
|
@@ -31,6 +31,16 @@ const PROMPT_ENHANCE_API = {
|
|
|
31
31
|
};
|
|
32
32
|
/** Host-mediated candidate discovery for the configured image API. */
|
|
33
33
|
const IMAGE_MODEL_API = { models: "/api/dsh-imagegen/image-models" };
|
|
34
|
+
/** Host-served built-in provider catalog (channels the user can instantiate). */
|
|
35
|
+
const PRESETS_API = "/api/dsh-imagegen/presets";
|
|
36
|
+
/** Loopback-only image reader for Agent tool-result previews. */
|
|
37
|
+
const AGENT_IMAGE_API = "/api/dsh-imagegen/agent-image";
|
|
38
|
+
/**
|
|
39
|
+
* Host-computed per-channel usage counters (generation-count badges in the
|
|
40
|
+
* settings card): entries are tallied from the persisted history and gallery
|
|
41
|
+
* by channel + model alias.
|
|
42
|
+
*/
|
|
43
|
+
const USAGE_API = "/api/dsh-imagegen/usage";
|
|
34
44
|
/** Host-resident generation queue endpoints. */
|
|
35
45
|
const TASK_API = {
|
|
36
46
|
submit: "/api/dsh-imagegen/tasks/submit",
|
|
@@ -81,7 +91,125 @@ const TEMPLATES_API = {
|
|
|
81
91
|
image: "/api/dsh-imagegen/templates/image"
|
|
82
92
|
};
|
|
83
93
|
//#endregion
|
|
94
|
+
//#region src/model-catalog.ts
|
|
95
|
+
const ENTRIES = {
|
|
96
|
+
"gpt-image": {
|
|
97
|
+
label: "gpt-image",
|
|
98
|
+
labelZh: "GPT 图像",
|
|
99
|
+
known: true,
|
|
100
|
+
supportsEdit: true,
|
|
101
|
+
supportsAspectRatio: false,
|
|
102
|
+
qualityTiers: [
|
|
103
|
+
"1K",
|
|
104
|
+
"2K",
|
|
105
|
+
"4K"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"dall-e": {
|
|
109
|
+
label: "DALL·E",
|
|
110
|
+
labelZh: "DALL·E",
|
|
111
|
+
known: true,
|
|
112
|
+
supportsEdit: true,
|
|
113
|
+
supportsAspectRatio: false,
|
|
114
|
+
qualityTiers: ["auto"]
|
|
115
|
+
},
|
|
116
|
+
grok: {
|
|
117
|
+
label: "grok",
|
|
118
|
+
labelZh: "Grok",
|
|
119
|
+
known: true,
|
|
120
|
+
supportsEdit: true,
|
|
121
|
+
supportsAspectRatio: true,
|
|
122
|
+
qualityTiers: ["1K", "2K"]
|
|
123
|
+
},
|
|
124
|
+
nanobanana: {
|
|
125
|
+
label: "nanobanana",
|
|
126
|
+
labelZh: "Nano Banana",
|
|
127
|
+
known: true,
|
|
128
|
+
supportsEdit: true,
|
|
129
|
+
supportsAspectRatio: true,
|
|
130
|
+
qualityTiers: [
|
|
131
|
+
"1K",
|
|
132
|
+
"2K",
|
|
133
|
+
"4K"
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
seedream: {
|
|
137
|
+
label: "seedream",
|
|
138
|
+
labelZh: "Seedream",
|
|
139
|
+
known: true,
|
|
140
|
+
supportsEdit: true,
|
|
141
|
+
supportsAspectRatio: true,
|
|
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"]
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
/** Official Gemini image ids served by Nano Banana gateways. */
|
|
154
|
+
const NANOBANANA_GEMINI_IDS = /* @__PURE__ */ new Set([
|
|
155
|
+
"gemini-3-pro-image",
|
|
156
|
+
"gemini-3-pro-image-preview",
|
|
157
|
+
"gemini-3.1-flash-image",
|
|
158
|
+
"gemini-3.1-flash-image-preview",
|
|
159
|
+
"gemini-3.1-flash-lite-image",
|
|
160
|
+
"gemini-2.5-flash-image"
|
|
161
|
+
]);
|
|
162
|
+
/** Classify one upstream model id into its request-shaping family. */
|
|
163
|
+
function describeModel(model) {
|
|
164
|
+
const id = model.trim();
|
|
165
|
+
if (/^gpt-image/i.test(id)) return {
|
|
166
|
+
family: "gpt-image",
|
|
167
|
+
...ENTRIES["gpt-image"]
|
|
168
|
+
};
|
|
169
|
+
if (/^dall-e/i.test(id)) return {
|
|
170
|
+
family: "dall-e",
|
|
171
|
+
...ENTRIES["dall-e"]
|
|
172
|
+
};
|
|
173
|
+
if (/^grok-imagine(?:-|$)/.test(id)) return {
|
|
174
|
+
family: "grok",
|
|
175
|
+
...ENTRIES.grok
|
|
176
|
+
};
|
|
177
|
+
if (/^nanobanana/i.test(id) || NANOBANANA_GEMINI_IDS.has(id)) return {
|
|
178
|
+
family: "nanobanana",
|
|
179
|
+
...ENTRIES.nanobanana
|
|
180
|
+
};
|
|
181
|
+
if (/^(?:doubao-)?seedream/i.test(id)) return {
|
|
182
|
+
family: "seedream",
|
|
183
|
+
...ENTRIES.seedream
|
|
184
|
+
};
|
|
185
|
+
if (/^(?:glm-image|cogview(?:-|$))/i.test(id)) return {
|
|
186
|
+
family: "zhipu",
|
|
187
|
+
...ENTRIES.zhipu
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
family: "unknown",
|
|
191
|
+
label: "unknown",
|
|
192
|
+
labelZh: "未知协议",
|
|
193
|
+
known: false,
|
|
194
|
+
supportsEdit: true,
|
|
195
|
+
supportsAspectRatio: false,
|
|
196
|
+
qualityTiers: []
|
|
197
|
+
};
|
|
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
|
+
}
|
|
206
|
+
/** The family a model id routes its request through. */
|
|
207
|
+
function modelFamily(model) {
|
|
208
|
+
return describeModel(model).family;
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
84
211
|
//#region src/prompt-enhancer.ts
|
|
212
|
+
/** OpenAI-compatible chat helpers used by the optional prompt-enhancement UI. */
|
|
85
213
|
function endpoint(base, suffix) {
|
|
86
214
|
return `${base.replace(/\/+$/, "")}${suffix}`;
|
|
87
215
|
}
|
|
@@ -99,12 +227,69 @@ async function responseJson(response) {
|
|
|
99
227
|
}
|
|
100
228
|
return body;
|
|
101
229
|
}
|
|
102
|
-
|
|
103
|
-
async function listOpenAIModels(config) {
|
|
230
|
+
async function listModelRecords(config) {
|
|
104
231
|
if (config.apiUrl.trim() === "") throw new Error("API URL is required");
|
|
105
232
|
const body = await responseJson(await fetch(endpoint(config.apiUrl, "/models"), { headers: headers(config.apiKey) }));
|
|
106
|
-
|
|
107
|
-
|
|
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));
|
|
108
293
|
}
|
|
109
294
|
/** List chat models exposed by an OpenAI-compatible endpoint. */
|
|
110
295
|
async function listPromptModels(config) {
|
|
@@ -146,7 +331,8 @@ const DEFAULT_IMAGE_MODELS = [
|
|
|
146
331
|
"nanobanana2",
|
|
147
332
|
"nanobanana2-lite",
|
|
148
333
|
"nanobanana-pro",
|
|
149
|
-
"seedream-5.0-pro"
|
|
334
|
+
"seedream-5.0-pro",
|
|
335
|
+
"glm-image"
|
|
150
336
|
];
|
|
151
337
|
/** Normalize user-entered model identifiers and retain a usable legacy default. */
|
|
152
338
|
function normalizeImageModels(value) {
|
|
@@ -160,6 +346,15 @@ function normalizeImageModels(value) {
|
|
|
160
346
|
return unique.size > 0 ? [...unique] : [...DEFAULT_IMAGE_MODELS];
|
|
161
347
|
}
|
|
162
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
|
|
163
358
|
//#region src/engine.ts
|
|
164
359
|
/** A generation failure with a user-presentable message. */
|
|
165
360
|
var ImageGenError = class extends Error {
|
|
@@ -183,12 +378,20 @@ const DALLE3_SIZES = /* @__PURE__ */ new Set([
|
|
|
183
378
|
"1792x1024",
|
|
184
379
|
"1024x1792"
|
|
185
380
|
]);
|
|
381
|
+
/** The wire model id for a request: `upstream` (host-filled alias mapping)
|
|
382
|
+
* wins, then the alias, then the family default. */
|
|
383
|
+
function wireModel(request) {
|
|
384
|
+
const upstream = request.upstream?.trim();
|
|
385
|
+
if (upstream !== void 0 && upstream !== "") return upstream;
|
|
386
|
+
const alias = request.model.trim();
|
|
387
|
+
return alias === "" ? "gpt-image-2" : alias;
|
|
388
|
+
}
|
|
186
389
|
/** Whether the model is an xAI Grok Imagine model (grok-imagine-image,
|
|
187
390
|
* grok-imagine-image-2.0, …). Grok Imagine speaks JSON on both endpoints
|
|
188
391
|
* and exposes its own aspect-ratio / response-format knobs instead of the
|
|
189
392
|
* OpenAI size/quality/detail passthrough. */
|
|
190
393
|
function isGrokImagine(model) {
|
|
191
|
-
return
|
|
394
|
+
return modelFamily(model) === "grok";
|
|
192
395
|
}
|
|
193
396
|
/** Whether the model belongs to the Google Nano Banana family (nanobanana2 /
|
|
194
397
|
* nanobanana2-lite / nanobanana-pro, plus the official Gemini image IDs the
|
|
@@ -196,17 +399,31 @@ function isGrokImagine(model) {
|
|
|
196
399
|
* aspect_ratio / image_size vocabulary instead of the OpenAI size/quality
|
|
197
400
|
* passthrough. */
|
|
198
401
|
function isNanoBanana(model) {
|
|
199
|
-
|
|
200
|
-
return model === "gemini-3-pro-image" || model === "gemini-3-pro-image-preview" || model === "gemini-3.1-flash-image" || model === "gemini-3.1-flash-image-preview" || model === "gemini-3.1-flash-lite-image" || model === "gemini-2.5-flash-image";
|
|
402
|
+
return modelFamily(model) === "nanobanana";
|
|
201
403
|
}
|
|
202
404
|
/** Whether the model belongs to the ByteDance Seedream family (seedream-5.0-pro,
|
|
203
405
|
* seedream-5.0, seedream-4.x, doubao-seedream-…). OpenAI-compatible gateways
|
|
204
406
|
* serve Seedream through a unified generate-and-edit architecture:
|
|
205
|
-
* generation AND editing both go to /images/generations
|
|
206
|
-
* a JSON URL / data-URL array
|
|
207
|
-
* `size` carries the aspect ratio (or exact pixels). */
|
|
407
|
+
* generation AND editing both go to /images/generations and reference images
|
|
408
|
+
* are a JSON URL / data-URL array. */
|
|
208
409
|
function isSeedream(model) {
|
|
209
|
-
return
|
|
410
|
+
return modelFamily(model) === "seedream";
|
|
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
|
+
}
|
|
419
|
+
/** Whether this is the official Volcengine Ark model naming convention. */
|
|
420
|
+
function isVolcSeedream(model) {
|
|
421
|
+
return /^doubao-seedream(?:-|$)/i.test(model.trim());
|
|
422
|
+
}
|
|
423
|
+
/** Volcengine uses `size` for the output tier, not the panel's aspect ratio. */
|
|
424
|
+
function seedreamSize(quality) {
|
|
425
|
+
if (quality === "1k") return "1K";
|
|
426
|
+
return "2K";
|
|
210
427
|
}
|
|
211
428
|
/** The panel's aspect ratios mapped to the closest OpenAI pixel size
|
|
212
429
|
* (gpt-image-2 / generic OpenAI-compatible endpoints). */
|
|
@@ -275,6 +492,19 @@ function bareBase64(value) {
|
|
|
275
492
|
const parsed = parseDataUrl(value);
|
|
276
493
|
return parsed !== void 0 && parsed.base64 !== void 0 ? parsed.base64 : value;
|
|
277
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
|
+
}
|
|
278
508
|
/** Clamp the requested image count into the API-accepted range. */
|
|
279
509
|
function clampCount(n) {
|
|
280
510
|
if (!Number.isFinite(n)) return 1;
|
|
@@ -284,7 +514,7 @@ function clampCount(n) {
|
|
|
284
514
|
* batch parameter is rejected by Responses-API-based gateways (tools[0].n),
|
|
285
515
|
* so the count is satisfied by parallel single-image requests instead. */
|
|
286
516
|
function effectiveParams(request) {
|
|
287
|
-
const model =
|
|
517
|
+
const model = wireModel(request);
|
|
288
518
|
if (model === "dall-e-3") {
|
|
289
519
|
const pixel = OPENAI_SIZE_BY_RATIO[request.size];
|
|
290
520
|
return {
|
|
@@ -306,9 +536,13 @@ function effectiveParams(request) {
|
|
|
306
536
|
};
|
|
307
537
|
if (isSeedream(model)) return {
|
|
308
538
|
model,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
539
|
+
size: seedreamSize(request.quality),
|
|
540
|
+
response_format: isVolcSeedream(model) ? "url" : "b64_json"
|
|
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"
|
|
312
546
|
};
|
|
313
547
|
return {
|
|
314
548
|
model,
|
|
@@ -321,17 +555,20 @@ function effectiveParams(request) {
|
|
|
321
555
|
}
|
|
322
556
|
/** How many single-image requests to issue for the requested image count. */
|
|
323
557
|
function effectiveCount(request) {
|
|
324
|
-
if ((request
|
|
558
|
+
if (wireModel(request) === "dall-e-3") return 1;
|
|
325
559
|
return clampCount(request.n);
|
|
326
560
|
}
|
|
327
561
|
/** Normalize one upstream data item into a base64 image. */
|
|
328
562
|
async function normalizeItem(item, upstream) {
|
|
329
563
|
const revisedPrompt = typeof item.revised_prompt === "string" ? item.revised_prompt : void 0;
|
|
330
|
-
if (typeof item.b64_json === "string")
|
|
331
|
-
b64
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
564
|
+
if (typeof item.b64_json === "string" && item.b64_json.trim() !== "") {
|
|
565
|
+
const b64 = bareBase64(item.b64_json);
|
|
566
|
+
if (b64.trim() !== "") return {
|
|
567
|
+
b64,
|
|
568
|
+
mime: detectImageMime(Buffer.from(b64, "base64")) ?? "image/png",
|
|
569
|
+
revisedPrompt
|
|
570
|
+
};
|
|
571
|
+
}
|
|
335
572
|
if (typeof item.url !== "string" || item.url === "") throw new ImageGenError("upstream image item has neither b64_json nor url");
|
|
336
573
|
const url = item.url;
|
|
337
574
|
if (url.startsWith("data:")) {
|
|
@@ -339,7 +576,7 @@ async function normalizeItem(item, upstream) {
|
|
|
339
576
|
if (parsed === void 0) throw new ImageGenError("upstream returned a malformed data: url");
|
|
340
577
|
return {
|
|
341
578
|
b64: parsed.base64,
|
|
342
|
-
mime: parsed.mime,
|
|
579
|
+
mime: detectImageMime(Buffer.from(parsed.base64, "base64")) ?? parsed.mime,
|
|
343
580
|
revisedPrompt
|
|
344
581
|
};
|
|
345
582
|
}
|
|
@@ -347,7 +584,7 @@ async function normalizeItem(item, upstream) {
|
|
|
347
584
|
let response;
|
|
348
585
|
try {
|
|
349
586
|
response = await fetch(url, {
|
|
350
|
-
|
|
587
|
+
...isPresignedUrl(url) || upstream.apiKey === "" ? {} : { headers: { authorization: `Bearer ${upstream.apiKey}` } },
|
|
351
588
|
signal: budget.signal
|
|
352
589
|
});
|
|
353
590
|
} catch (error) {
|
|
@@ -358,7 +595,7 @@ async function normalizeItem(item, upstream) {
|
|
|
358
595
|
if (!response.ok) throw new ImageGenError(`failed to fetch the generated image url: HTTP ${response.status}`);
|
|
359
596
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
360
597
|
const contentType = response.headers.get("content-type");
|
|
361
|
-
const mime = contentType !== null && contentType !== "" ? contentType.split(";")[0].trim() : mimeOfExtension(url) ?? "image/png";
|
|
598
|
+
const mime = detectImageMime(buffer) ?? (contentType !== null && contentType !== "" ? contentType.split(";")[0].trim() : mimeOfExtension(url) ?? "image/png");
|
|
362
599
|
return {
|
|
363
600
|
b64: buffer.toString("base64"),
|
|
364
601
|
mime,
|
|
@@ -411,7 +648,7 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
411
648
|
image: [request.image],
|
|
412
649
|
...params.size !== void 0 ? { size: params.size } : {},
|
|
413
650
|
...params.resolution !== void 0 ? { resolution: params.resolution } : {},
|
|
414
|
-
response_format: "b64_json"
|
|
651
|
+
response_format: isVolcSeedream(params.model) ? "url" : "b64_json"
|
|
415
652
|
});
|
|
416
653
|
} else {
|
|
417
654
|
const form = new FormData();
|
|
@@ -473,6 +710,7 @@ async function generateImage(upstream, request, options = {}) {
|
|
|
473
710
|
const baseUrl = upstream.apiUrl.trim().replace(/\/+$/, "");
|
|
474
711
|
if (baseUrl === "") throw new ImageGenError("api_url 未配置:请先在「设置 → 插件 → 可配置」中填写", "config-missing");
|
|
475
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");
|
|
476
714
|
const params = effectiveParams(request);
|
|
477
715
|
const count = effectiveCount(request);
|
|
478
716
|
return { images: (await Promise.all(Array.from({ length: count }, () => requestOneImage(baseUrl, upstream, request, params, options.signal)))).flat() };
|
|
@@ -602,7 +840,11 @@ function toWire$1(entry) {
|
|
|
602
840
|
mime: image.mime,
|
|
603
841
|
...image.revisedPrompt === void 0 ? {} : { revisedPrompt: image.revisedPrompt }
|
|
604
842
|
})),
|
|
605
|
-
...entry.refName === void 0 ? {} : { refName: entry.refName }
|
|
843
|
+
...entry.refName === void 0 ? {} : { refName: entry.refName },
|
|
844
|
+
...entry.channel === void 0 ? {} : { channel: entry.channel },
|
|
845
|
+
...entry.channelId === void 0 ? {} : { channelId: entry.channelId },
|
|
846
|
+
...entry.comparisonId === void 0 ? {} : { comparisonId: entry.comparisonId },
|
|
847
|
+
...entry.comparisonModels === void 0 ? {} : { comparisonModels: entry.comparisonModels }
|
|
606
848
|
};
|
|
607
849
|
}
|
|
608
850
|
/** List the persisted history, newest first, as wire entries. */
|
|
@@ -641,7 +883,11 @@ async function appendHistory(input) {
|
|
|
641
883
|
detail: input.detail,
|
|
642
884
|
n: input.n,
|
|
643
885
|
images: storedImages,
|
|
644
|
-
...input.refName === void 0 ? {} : { refName: input.refName }
|
|
886
|
+
...input.refName === void 0 ? {} : { refName: input.refName },
|
|
887
|
+
...input.channelId === void 0 ? {} : { channelId: input.channelId },
|
|
888
|
+
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
889
|
+
...input.comparisonId === void 0 ? {} : { comparisonId: input.comparisonId },
|
|
890
|
+
...input.comparisonModels === void 0 ? {} : { comparisonModels: input.comparisonModels }
|
|
645
891
|
}, ...await readIndex$1()];
|
|
646
892
|
const kept = merged.slice(0, 50);
|
|
647
893
|
for (const dropped of merged.slice(50)) await removeEntryFiles$1(dropped);
|
|
@@ -686,12 +932,15 @@ async function readHistoryImage(file) {
|
|
|
686
932
|
/** In-memory, host-resident image generation queue. */
|
|
687
933
|
var GenerationTaskQueue = class {
|
|
688
934
|
run;
|
|
935
|
+
concurrency;
|
|
689
936
|
tasks = [];
|
|
690
937
|
controllers = /* @__PURE__ */ new Map();
|
|
691
938
|
listeners = /* @__PURE__ */ new Set();
|
|
692
|
-
running =
|
|
693
|
-
|
|
939
|
+
running = 0;
|
|
940
|
+
serialRunning = false;
|
|
941
|
+
constructor(run, concurrency = 1) {
|
|
694
942
|
this.run = run;
|
|
943
|
+
this.concurrency = concurrency;
|
|
695
944
|
}
|
|
696
945
|
list() {
|
|
697
946
|
return this.tasks.map((task) => this.snapshot(task));
|
|
@@ -722,45 +971,49 @@ var GenerationTaskQueue = class {
|
|
|
722
971
|
task.finishedAt = Date.now();
|
|
723
972
|
this.controllers.get(id)?.abort();
|
|
724
973
|
this.publish(task);
|
|
974
|
+
this.drain();
|
|
725
975
|
return this.snapshot(task);
|
|
726
976
|
}
|
|
727
977
|
retry(id) {
|
|
728
978
|
const previous = this.tasks.find((item) => item.id === id);
|
|
729
979
|
return previous === void 0 ? void 0 : this.submit(previous.request);
|
|
730
980
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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);
|
|
734
1000
|
try {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
task.
|
|
739
|
-
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();
|
|
740
1013
|
this.publish(task);
|
|
741
|
-
const controller = new AbortController();
|
|
742
|
-
this.controllers.set(task.id, controller);
|
|
743
|
-
try {
|
|
744
|
-
const result = await this.run(task.request, controller.signal);
|
|
745
|
-
if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
|
|
746
|
-
task.status = "completed";
|
|
747
|
-
task.result = result;
|
|
748
|
-
task.finishedAt = Date.now();
|
|
749
|
-
this.publish(task);
|
|
750
|
-
}
|
|
751
|
-
} catch (error) {
|
|
752
|
-
if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
|
|
753
|
-
task.status = "failed";
|
|
754
|
-
task.error = error instanceof Error ? error.message : String(error);
|
|
755
|
-
task.finishedAt = Date.now();
|
|
756
|
-
this.publish(task);
|
|
757
|
-
}
|
|
758
|
-
} finally {
|
|
759
|
-
this.controllers.delete(task.id);
|
|
760
|
-
}
|
|
761
1014
|
}
|
|
762
1015
|
} finally {
|
|
763
|
-
this.
|
|
1016
|
+
this.controllers.delete(task.id);
|
|
764
1017
|
}
|
|
765
1018
|
}
|
|
766
1019
|
publish(task) {
|
|
@@ -783,6 +1036,11 @@ var GenerationTaskQueue = class {
|
|
|
783
1036
|
* Shared host-side generation runtime. Both the browser routes and Agent tools
|
|
784
1037
|
* submit to this one queue so persisted history and cancellation semantics stay
|
|
785
1038
|
* identical regardless of where a request originated.
|
|
1039
|
+
*
|
|
1040
|
+
* Requests carry a channel id (host-filled by the route/tool resolution); the
|
|
1041
|
+
* runtime picks that channel's upstream credentials, otherwise the default
|
|
1042
|
+
* channel, and records a channel snapshot on the history entry so usage
|
|
1043
|
+
* counters and filters survive channel deletion.
|
|
786
1044
|
*/
|
|
787
1045
|
var ImageGenerationRuntime = class {
|
|
788
1046
|
resolve;
|
|
@@ -791,10 +1049,16 @@ var ImageGenerationRuntime = class {
|
|
|
791
1049
|
constructor(resolve, history = { append: appendHistory }) {
|
|
792
1050
|
this.resolve = resolve;
|
|
793
1051
|
this.history = history;
|
|
794
|
-
this.queue = new GenerationTaskQueue((request, signal) => this.run(request, signal));
|
|
1052
|
+
this.queue = new GenerationTaskQueue((request, signal) => this.run(request, signal), 4);
|
|
795
1053
|
}
|
|
796
1054
|
async run(request, signal) {
|
|
797
|
-
const
|
|
1055
|
+
const view = this.resolve();
|
|
1056
|
+
const channel = view.channels.find((candidate) => candidate.id === request.channelId) ?? view.channels.find((candidate) => candidate.id === view.defaultChannelId) ?? view.channels[0];
|
|
1057
|
+
if (channel === void 0) throw new ImageGenError("尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥", "no-channels");
|
|
1058
|
+
const result = await generateImage({
|
|
1059
|
+
apiUrl: channel.apiUrl,
|
|
1060
|
+
apiKey: channel.apiKey
|
|
1061
|
+
}, request, { signal });
|
|
798
1062
|
try {
|
|
799
1063
|
const history = await this.history.append({
|
|
800
1064
|
id: randomUUID(),
|
|
@@ -807,7 +1071,11 @@ var ImageGenerationRuntime = class {
|
|
|
807
1071
|
detail: request.detail,
|
|
808
1072
|
n: request.n,
|
|
809
1073
|
images: result.images,
|
|
810
|
-
...request.refName === void 0 ? {} : { refName: request.refName }
|
|
1074
|
+
...request.refName === void 0 ? {} : { refName: request.refName },
|
|
1075
|
+
...request.channelId === void 0 ? {} : { channelId: request.channelId },
|
|
1076
|
+
...request.channel === void 0 ? {} : { channel: request.channel },
|
|
1077
|
+
...request.comparisonId === void 0 ? {} : { comparisonId: request.comparisonId },
|
|
1078
|
+
...request.comparisonModels === void 0 ? {} : { comparisonModels: request.comparisonModels }
|
|
811
1079
|
});
|
|
812
1080
|
return {
|
|
813
1081
|
...result,
|
|
@@ -931,7 +1199,9 @@ function toWire(entry) {
|
|
|
931
1199
|
...image.revisedPrompt === void 0 ? {} : { revisedPrompt: image.revisedPrompt }
|
|
932
1200
|
})),
|
|
933
1201
|
...entry.refName === void 0 ? {} : { refName: entry.refName },
|
|
934
|
-
...entry.tags === void 0 ? {} : { tags: entry.tags }
|
|
1202
|
+
...entry.tags === void 0 ? {} : { tags: entry.tags },
|
|
1203
|
+
...entry.channel === void 0 ? {} : { channel: entry.channel },
|
|
1204
|
+
...entry.channelId === void 0 ? {} : { channelId: entry.channelId }
|
|
935
1205
|
};
|
|
936
1206
|
}
|
|
937
1207
|
/** List the persisted gallery, newest first, as wire entries. */
|
|
@@ -981,7 +1251,9 @@ async function appendGallery(input) {
|
|
|
981
1251
|
n: input.n,
|
|
982
1252
|
images: storedImages,
|
|
983
1253
|
...hash === void 0 ? {} : { hash },
|
|
984
|
-
...input.refName === void 0 ? {} : { refName: input.refName }
|
|
1254
|
+
...input.refName === void 0 ? {} : { refName: input.refName },
|
|
1255
|
+
...input.channelId === void 0 ? {} : { channelId: input.channelId },
|
|
1256
|
+
...input.channel === void 0 ? {} : { channel: input.channel }
|
|
985
1257
|
}, ...await readIndex()];
|
|
986
1258
|
await writeIndex(merged);
|
|
987
1259
|
return {
|
|
@@ -1403,6 +1675,64 @@ function clearUpdateCache() {
|
|
|
1403
1675
|
cached = void 0;
|
|
1404
1676
|
}
|
|
1405
1677
|
//#endregion
|
|
1678
|
+
//#region src/presets.ts
|
|
1679
|
+
const IMAGE_PRESETS = [
|
|
1680
|
+
{
|
|
1681
|
+
id: "volc-ark-seedream",
|
|
1682
|
+
name: "字节 · 火山方舟(Seedream)",
|
|
1683
|
+
apiUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
|
1684
|
+
hint: "字节跳动官方 Seedream 文生图/图生图入口",
|
|
1685
|
+
models: [
|
|
1686
|
+
{
|
|
1687
|
+
alias: "seedream-5.0-pro",
|
|
1688
|
+
id: "seedream-5.0-pro"
|
|
1689
|
+
},
|
|
1690
|
+
{
|
|
1691
|
+
alias: "seedream-5.0",
|
|
1692
|
+
id: "seedream-5.0"
|
|
1693
|
+
},
|
|
1694
|
+
{
|
|
1695
|
+
alias: "seedream-4.0",
|
|
1696
|
+
id: "seedream-4.0"
|
|
1697
|
+
}
|
|
1698
|
+
]
|
|
1699
|
+
},
|
|
1700
|
+
{
|
|
1701
|
+
id: "openai-official",
|
|
1702
|
+
name: "OpenAI 官方",
|
|
1703
|
+
apiUrl: "https://api.openai.com/v1",
|
|
1704
|
+
hint: "OpenAI 官方图像生成接口",
|
|
1705
|
+
models: [{
|
|
1706
|
+
alias: "gpt-image-2",
|
|
1707
|
+
id: "gpt-image-2"
|
|
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"
|
|
1718
|
+
}]
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
id: "xai-grok",
|
|
1722
|
+
name: "xAI(Grok)",
|
|
1723
|
+
apiUrl: "https://api.x.ai/v1",
|
|
1724
|
+
hint: "xAI 官方接口:Grok Imagine 系列",
|
|
1725
|
+
models: [{
|
|
1726
|
+
alias: "grok-imagine-image",
|
|
1727
|
+
id: "grok-imagine-image"
|
|
1728
|
+
}]
|
|
1729
|
+
}
|
|
1730
|
+
];
|
|
1731
|
+
/** Look up one built-in provider by id. */
|
|
1732
|
+
function presetById(id) {
|
|
1733
|
+
return IMAGE_PRESETS.find((preset) => preset.id === id);
|
|
1734
|
+
}
|
|
1735
|
+
//#endregion
|
|
1406
1736
|
//#region src/routes.ts
|
|
1407
1737
|
/** Cap on JSON request bodies (settings ops and generate payloads are small). */
|
|
1408
1738
|
const MAX_JSON_BODY_BYTES = 24 * 1024 * 1024;
|
|
@@ -1463,6 +1793,7 @@ function messageOf(error) {
|
|
|
1463
1793
|
function parseGenerateRequest(body) {
|
|
1464
1794
|
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
1465
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))] : [];
|
|
1466
1797
|
return {
|
|
1467
1798
|
mode: body.mode === "edit" ? "edit" : "text",
|
|
1468
1799
|
model: typeof body.model === "string" ? body.model : "",
|
|
@@ -1472,7 +1803,10 @@ function parseGenerateRequest(body) {
|
|
|
1472
1803
|
n: typeof body.n === "number" ? body.n : 1,
|
|
1473
1804
|
detail: typeof body.detail === "string" ? body.detail : "",
|
|
1474
1805
|
...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {},
|
|
1475
|
-
...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {}
|
|
1806
|
+
...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {},
|
|
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 } : {}
|
|
1476
1810
|
};
|
|
1477
1811
|
}
|
|
1478
1812
|
/** Validate a submitted history entry (images carry base64). */
|
|
@@ -1497,6 +1831,7 @@ function parseHistoryEntryInput(body) {
|
|
|
1497
1831
|
...typeof image.revisedPrompt === "string" ? { revisedPrompt: image.revisedPrompt } : {}
|
|
1498
1832
|
});
|
|
1499
1833
|
}
|
|
1834
|
+
const comparisonModels = Array.isArray(entry.comparisonModels) ? [...new Set(entry.comparisonModels.filter((model) => typeof model === "string").map((model) => model.trim()).filter(Boolean))] : [];
|
|
1500
1835
|
return {
|
|
1501
1836
|
id: entry.id,
|
|
1502
1837
|
createdAt: entry.createdAt,
|
|
@@ -1508,7 +1843,11 @@ function parseHistoryEntryInput(body) {
|
|
|
1508
1843
|
detail: entry.detail,
|
|
1509
1844
|
n: entry.n,
|
|
1510
1845
|
images,
|
|
1511
|
-
...typeof entry.refName === "string" ? { refName: entry.refName } : {}
|
|
1846
|
+
...typeof entry.refName === "string" ? { refName: entry.refName } : {},
|
|
1847
|
+
...typeof entry.channelId === "string" ? { channelId: entry.channelId } : {},
|
|
1848
|
+
...typeof entry.channel === "string" ? { channel: entry.channel } : {},
|
|
1849
|
+
...typeof entry.comparisonId === "string" ? { comparisonId: entry.comparisonId } : {},
|
|
1850
|
+
...comparisonModels.length > 1 ? { comparisonModels } : {}
|
|
1512
1851
|
};
|
|
1513
1852
|
}
|
|
1514
1853
|
/** Extract the image file name from a history-image request URL. */
|
|
@@ -1523,6 +1862,33 @@ function imageFileFrom(rawUrl, basePath) {
|
|
|
1523
1862
|
if (!pathname.startsWith(`${basePath}/`)) return void 0;
|
|
1524
1863
|
return decodeURIComponent(pathname.slice(basePath.length + 1));
|
|
1525
1864
|
}
|
|
1865
|
+
/** Parse the durable image reference carried by an Agent tool-result view. */
|
|
1866
|
+
function agentImageRefFrom(rawUrl) {
|
|
1867
|
+
if (rawUrl === void 0) return void 0;
|
|
1868
|
+
let url;
|
|
1869
|
+
try {
|
|
1870
|
+
url = new URL(rawUrl, "http://localhost");
|
|
1871
|
+
} catch {
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
if (url.pathname !== "/api/dsh-imagegen/agent-image") return void 0;
|
|
1875
|
+
const attachmentId = url.searchParams.get("attachment_id") ?? "";
|
|
1876
|
+
const mediaType = url.searchParams.get("media_type") ?? "";
|
|
1877
|
+
const bytes = Number(url.searchParams.get("bytes"));
|
|
1878
|
+
const width = Number(url.searchParams.get("width"));
|
|
1879
|
+
const height = Number(url.searchParams.get("height"));
|
|
1880
|
+
if (attachmentId === "" || !isImageMediaType(mediaType) || !Number.isSafeInteger(bytes) || bytes < 1 || !Number.isSafeInteger(width) || width < 1 || !Number.isSafeInteger(height) || height < 1) return void 0;
|
|
1881
|
+
return {
|
|
1882
|
+
attachmentId,
|
|
1883
|
+
mediaType,
|
|
1884
|
+
bytes,
|
|
1885
|
+
width,
|
|
1886
|
+
height
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
function isImageMediaType(value) {
|
|
1890
|
+
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
1891
|
+
}
|
|
1526
1892
|
/** Project one settings descriptor onto the bridge wire view. */
|
|
1527
1893
|
function toView(descriptor) {
|
|
1528
1894
|
return {
|
|
@@ -1583,18 +1949,85 @@ function makeRoutes(deps) {
|
|
|
1583
1949
|
model: ""
|
|
1584
1950
|
}));
|
|
1585
1951
|
const resolveImageModels = deps.resolveImageModels ?? (() => normalizeImageModels(void 0));
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1952
|
+
/** The current channel view: the channel-aware resolver, or a synthesized
|
|
1953
|
+
* single default channel from the legacy flat upstream config (tests and
|
|
1954
|
+
* older hosts). */
|
|
1955
|
+
const channelViewOf = () => {
|
|
1956
|
+
if (deps.resolveChannels !== void 0) return deps.resolveChannels();
|
|
1957
|
+
const upstream = deps.resolve();
|
|
1958
|
+
const models = normalizeImageModels(resolveImageModels()).map((id) => ({
|
|
1959
|
+
alias: id,
|
|
1960
|
+
id
|
|
1961
|
+
}));
|
|
1962
|
+
if (upstream.apiUrl.trim() === "" && models.length === 0) return {
|
|
1963
|
+
channels: [],
|
|
1964
|
+
defaultChannelId: ""
|
|
1965
|
+
};
|
|
1966
|
+
return {
|
|
1967
|
+
channels: [{
|
|
1968
|
+
id: "default",
|
|
1969
|
+
preset: "",
|
|
1970
|
+
name: "默认渠道",
|
|
1971
|
+
apiUrl: upstream.apiUrl,
|
|
1972
|
+
apiKey: upstream.apiKey,
|
|
1973
|
+
models
|
|
1974
|
+
}],
|
|
1975
|
+
defaultChannelId: "default"
|
|
1976
|
+
};
|
|
1977
|
+
};
|
|
1978
|
+
const runtime = deps.runtime ?? new ImageGenerationRuntime(channelViewOf, history);
|
|
1979
|
+
/** Resolve an alias (or the channel fallback) into a concrete generation
|
|
1980
|
+
* request: picks the channel (explicit then default), maps alias → upstream
|
|
1981
|
+
* id, and fills the channel snapshot kept on history entries. */
|
|
1982
|
+
const resolveChannelRequest = (request) => {
|
|
1983
|
+
const view = channelViewOf();
|
|
1984
|
+
if (view.channels.length === 0) return {
|
|
1985
|
+
ok: false,
|
|
1986
|
+
code: "no-channels",
|
|
1987
|
+
message: "尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥"
|
|
1988
|
+
};
|
|
1989
|
+
const explicit = view.channels.find((candidate) => candidate.id === request.channelId);
|
|
1990
|
+
const defaults = view.channels.find((candidate) => candidate.id === view.defaultChannelId) ?? view.channels[0];
|
|
1991
|
+
const target = explicit ?? defaults;
|
|
1992
|
+
const asked = request.model.trim();
|
|
1993
|
+
if (asked === "") {
|
|
1994
|
+
const alias = target?.models[0]?.alias ?? "";
|
|
1995
|
+
if (alias === "") return {
|
|
1996
|
+
ok: false,
|
|
1997
|
+
code: "no-models",
|
|
1998
|
+
message: `渠道「${target?.name ?? ""}」尚未配置模型,请先在设置中添加`
|
|
1999
|
+
};
|
|
2000
|
+
const mapping = target.models.find((model) => model.alias === alias);
|
|
2001
|
+
return {
|
|
2002
|
+
ok: true,
|
|
2003
|
+
request: {
|
|
2004
|
+
...request,
|
|
2005
|
+
model: alias,
|
|
2006
|
+
upstream: mapping.id,
|
|
2007
|
+
channelId: target.id,
|
|
2008
|
+
channel: target.name
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
const hosting = view.channels.filter((channel) => channel.models.some((model) => model.alias === asked));
|
|
2013
|
+
if (hosting.length === 0) return {
|
|
2014
|
+
ok: false,
|
|
2015
|
+
code: "image-model-not-configured",
|
|
2016
|
+
message: `模型「${asked}」未在任一渠道配置;可用模型:${[...new Set(view.channels.flatMap((channel) => channel.models.map((model) => model.alias)))].join("、") || "(无)"}`
|
|
2017
|
+
};
|
|
2018
|
+
const picked = target !== void 0 && target.models.some((model) => model.alias === asked) ? target : hosting[0];
|
|
2019
|
+
const mapping = picked.models.find((model) => model.alias === asked);
|
|
1592
2020
|
return {
|
|
1593
|
-
|
|
1594
|
-
|
|
2021
|
+
ok: true,
|
|
2022
|
+
request: {
|
|
2023
|
+
...request,
|
|
2024
|
+
model: asked,
|
|
2025
|
+
upstream: mapping.id,
|
|
2026
|
+
channelId: picked.id,
|
|
2027
|
+
channel: picked.name
|
|
2028
|
+
}
|
|
1595
2029
|
};
|
|
1596
2030
|
};
|
|
1597
|
-
const runtime = deps.runtime ?? new ImageGenerationRuntime(deps.resolve, history);
|
|
1598
2031
|
const guard = (req, res, method) => {
|
|
1599
2032
|
if (!isLoopbackRequest(req)) {
|
|
1600
2033
|
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
@@ -1607,15 +2040,52 @@ function makeRoutes(deps) {
|
|
|
1607
2040
|
return true;
|
|
1608
2041
|
};
|
|
1609
2042
|
return [
|
|
2043
|
+
...deps.attachments === void 0 ? [] : [{
|
|
2044
|
+
kind: "prefix",
|
|
2045
|
+
path: AGENT_IMAGE_API,
|
|
2046
|
+
handler: async (req, res) => {
|
|
2047
|
+
if (!isLoopbackRequest(req)) {
|
|
2048
|
+
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
if (req.method !== "GET") {
|
|
2052
|
+
writeJson(res, 405, { error: `method not allowed: ${req.method}` });
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
const ref = agentImageRefFrom(req.url);
|
|
2056
|
+
if (ref === void 0) {
|
|
2057
|
+
writeJson(res, 400, { error: "invalid image reference" });
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
try {
|
|
2061
|
+
const stored = await deps.attachments.readImage(ref);
|
|
2062
|
+
res.writeHead(200, {
|
|
2063
|
+
"content-type": stored.ref.mediaType,
|
|
2064
|
+
"content-length": stored.data.byteLength,
|
|
2065
|
+
"cache-control": "private, max-age=3600"
|
|
2066
|
+
});
|
|
2067
|
+
res.end(Buffer.from(stored.data));
|
|
2068
|
+
} catch {
|
|
2069
|
+
writeJson(res, 404, { error: "image attachment not found" });
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}],
|
|
1610
2073
|
{
|
|
1611
2074
|
kind: "exact",
|
|
1612
2075
|
path: IMAGE_MODEL_API.models,
|
|
1613
2076
|
handler: async (req, res) => {
|
|
1614
2077
|
if (!guard(req, res, "POST")) return;
|
|
2078
|
+
const body = await readJsonBody(req);
|
|
2079
|
+
const view = channelViewOf();
|
|
2080
|
+
const stored = view.channels.find((candidate) => candidate.id === (typeof body?.channelId === "string" ? body.channelId : void 0)) ?? view.channels.find((candidate) => candidate.id === view.defaultChannelId) ?? view.channels[0];
|
|
2081
|
+
const upstream = {
|
|
2082
|
+
apiUrl: typeof body?.apiUrl === "string" && body.apiUrl.trim() !== "" ? body.apiUrl.trim() : stored?.apiUrl ?? "",
|
|
2083
|
+
apiKey: typeof body?.apiKey === "string" && body.apiKey.trim() !== "" ? body.apiKey.trim() : stored?.apiKey ?? ""
|
|
2084
|
+
};
|
|
1615
2085
|
try {
|
|
1616
2086
|
writeJson(res, 200, {
|
|
1617
2087
|
ok: true,
|
|
1618
|
-
models: await
|
|
2088
|
+
models: await listImageModels(upstream)
|
|
1619
2089
|
});
|
|
1620
2090
|
} catch (error) {
|
|
1621
2091
|
writeJson(res, 200, {
|
|
@@ -1626,6 +2096,55 @@ function makeRoutes(deps) {
|
|
|
1626
2096
|
}
|
|
1627
2097
|
}
|
|
1628
2098
|
},
|
|
2099
|
+
{
|
|
2100
|
+
kind: "exact",
|
|
2101
|
+
path: PRESETS_API,
|
|
2102
|
+
handler: async (req, res) => {
|
|
2103
|
+
if (!guard(req, res, "POST")) return;
|
|
2104
|
+
writeJson(res, 200, {
|
|
2105
|
+
ok: true,
|
|
2106
|
+
presets: IMAGE_PRESETS.map((preset) => ({
|
|
2107
|
+
id: preset.id,
|
|
2108
|
+
name: preset.name,
|
|
2109
|
+
apiUrl: preset.apiUrl,
|
|
2110
|
+
hint: preset.hint,
|
|
2111
|
+
models: preset.models
|
|
2112
|
+
}))
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
kind: "exact",
|
|
2118
|
+
path: USAGE_API,
|
|
2119
|
+
handler: async (req, res) => {
|
|
2120
|
+
if (!guard(req, res, "POST")) return;
|
|
2121
|
+
try {
|
|
2122
|
+
const entries = [...await history.list(), ...await gallery.list()];
|
|
2123
|
+
const byChannel = {};
|
|
2124
|
+
const totals = {};
|
|
2125
|
+
for (const entry of entries) {
|
|
2126
|
+
const channelKey = entry.channelId !== void 0 ? entry.channelId : entry.channel !== void 0 ? `name:${entry.channel}` : "";
|
|
2127
|
+
const alias = entry.model;
|
|
2128
|
+
const bucket = byChannel[channelKey] ?? (byChannel[channelKey] = {});
|
|
2129
|
+
bucket[alias] = (bucket[alias] ?? 0) + 1;
|
|
2130
|
+
totals[alias] = (totals[alias] ?? 0) + 1;
|
|
2131
|
+
}
|
|
2132
|
+
writeJson(res, 200, {
|
|
2133
|
+
ok: true,
|
|
2134
|
+
usage: {
|
|
2135
|
+
byChannel,
|
|
2136
|
+
totals
|
|
2137
|
+
}
|
|
2138
|
+
});
|
|
2139
|
+
} catch (error) {
|
|
2140
|
+
writeJson(res, 200, {
|
|
2141
|
+
ok: false,
|
|
2142
|
+
code: "usage-failed",
|
|
2143
|
+
message: messageOf(error)
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
},
|
|
1629
2148
|
{
|
|
1630
2149
|
kind: "exact",
|
|
1631
2150
|
path: PROMPT_ENHANCE_API.models,
|
|
@@ -1740,37 +2259,28 @@ function makeRoutes(deps) {
|
|
|
1740
2259
|
handler: async (req, res) => {
|
|
1741
2260
|
if (!guard(req, res, "POST")) return;
|
|
1742
2261
|
const body = await readJsonBody(req);
|
|
1743
|
-
|
|
2262
|
+
const parsed = body === void 0 ? void 0 : parseGenerateRequest(body);
|
|
2263
|
+
if (parsed === void 0) {
|
|
1744
2264
|
writeJson(res, 200, {
|
|
1745
2265
|
ok: false,
|
|
1746
2266
|
code: "bad-request",
|
|
1747
|
-
message: "
|
|
2267
|
+
message: "prompt is required"
|
|
1748
2268
|
});
|
|
1749
2269
|
return;
|
|
1750
2270
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
request = parseConfiguredRequest(body);
|
|
1754
|
-
} catch (error) {
|
|
2271
|
+
const resolved = resolveChannelRequest(parsed);
|
|
2272
|
+
if (!resolved.ok) {
|
|
1755
2273
|
writeJson(res, 200, {
|
|
1756
2274
|
ok: false,
|
|
1757
|
-
code:
|
|
1758
|
-
message:
|
|
1759
|
-
});
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
if (request === void 0) {
|
|
1763
|
-
writeJson(res, 200, {
|
|
1764
|
-
ok: false,
|
|
1765
|
-
code: "bad-request",
|
|
1766
|
-
message: "prompt is required"
|
|
2275
|
+
code: resolved.code,
|
|
2276
|
+
message: resolved.message
|
|
1767
2277
|
});
|
|
1768
2278
|
return;
|
|
1769
2279
|
}
|
|
1770
2280
|
try {
|
|
1771
2281
|
writeJson(res, 200, {
|
|
1772
2282
|
ok: true,
|
|
1773
|
-
...await runtime.run(request)
|
|
2283
|
+
...await runtime.run(resolved.request)
|
|
1774
2284
|
});
|
|
1775
2285
|
} catch (error) {
|
|
1776
2286
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1788,28 +2298,27 @@ function makeRoutes(deps) {
|
|
|
1788
2298
|
handler: async (req, res) => {
|
|
1789
2299
|
if (!guard(req, res, "POST")) return;
|
|
1790
2300
|
const body = await readJsonBody(req);
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
request = body === void 0 ? void 0 : parseConfiguredRequest(body);
|
|
1794
|
-
} catch (error) {
|
|
2301
|
+
const parsed = body === void 0 ? void 0 : parseGenerateRequest(body);
|
|
2302
|
+
if (parsed === void 0) {
|
|
1795
2303
|
writeJson(res, 200, {
|
|
1796
2304
|
ok: false,
|
|
1797
|
-
code: "
|
|
1798
|
-
message:
|
|
2305
|
+
code: "bad-request",
|
|
2306
|
+
message: "prompt is required"
|
|
1799
2307
|
});
|
|
1800
2308
|
return;
|
|
1801
2309
|
}
|
|
1802
|
-
|
|
2310
|
+
const resolved = resolveChannelRequest(parsed);
|
|
2311
|
+
if (!resolved.ok) {
|
|
1803
2312
|
writeJson(res, 200, {
|
|
1804
2313
|
ok: false,
|
|
1805
|
-
code:
|
|
1806
|
-
message:
|
|
2314
|
+
code: resolved.code,
|
|
2315
|
+
message: resolved.message
|
|
1807
2316
|
});
|
|
1808
2317
|
return;
|
|
1809
2318
|
}
|
|
1810
2319
|
writeJson(res, 200, {
|
|
1811
2320
|
ok: true,
|
|
1812
|
-
task: runtime.queue.submit(request)
|
|
2321
|
+
task: runtime.queue.submit(resolved.request)
|
|
1813
2322
|
});
|
|
1814
2323
|
}
|
|
1815
2324
|
},
|
|
@@ -2386,10 +2895,54 @@ function renderTaskResult(value) {
|
|
|
2386
2895
|
return [{
|
|
2387
2896
|
type: "text",
|
|
2388
2897
|
text: JSON.stringify(value)
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2898
|
+
}];
|
|
2899
|
+
}
|
|
2900
|
+
/** The UI-only projection that keeps generated images beside the tool call. */
|
|
2901
|
+
function imagePresentationMeta(value) {
|
|
2902
|
+
return { images: value.images.map((image) => {
|
|
2903
|
+
const ref = {
|
|
2904
|
+
attachment_id: image.attachment_id,
|
|
2905
|
+
media_type: image.media_type,
|
|
2906
|
+
bytes: image.bytes,
|
|
2907
|
+
width: image.width,
|
|
2908
|
+
height: image.height
|
|
2909
|
+
};
|
|
2910
|
+
if (image.name !== void 0) ref.name = image.name;
|
|
2911
|
+
return ref;
|
|
2912
|
+
}) };
|
|
2913
|
+
}
|
|
2914
|
+
function imageBlocksFromMeta(meta) {
|
|
2915
|
+
if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return [];
|
|
2916
|
+
const images = meta.images;
|
|
2917
|
+
if (!Array.isArray(images)) return [];
|
|
2918
|
+
return images.flatMap((value) => {
|
|
2919
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return [];
|
|
2920
|
+
const raw = value;
|
|
2921
|
+
if (typeof raw.attachment_id !== "string" || typeof raw.media_type !== "string" || typeof raw.bytes !== "number" || typeof raw.width !== "number" || typeof raw.height !== "number") return [];
|
|
2922
|
+
try {
|
|
2923
|
+
return [{
|
|
2924
|
+
type: "image",
|
|
2925
|
+
attachment: restoreRef({
|
|
2926
|
+
attachment_id: raw.attachment_id,
|
|
2927
|
+
media_type: raw.media_type,
|
|
2928
|
+
bytes: raw.bytes,
|
|
2929
|
+
width: raw.width,
|
|
2930
|
+
height: raw.height,
|
|
2931
|
+
...typeof raw.name === "string" ? { name: raw.name } : {}
|
|
2932
|
+
})
|
|
2933
|
+
}];
|
|
2934
|
+
} catch {
|
|
2935
|
+
return [];
|
|
2936
|
+
}
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
/** Rehydrate image attachments for the host-computed tool result view only. */
|
|
2940
|
+
function presentImageResult(_args, result) {
|
|
2941
|
+
const content = result.isError ? [] : imageBlocksFromMeta(result.meta);
|
|
2942
|
+
return content.length === 0 ? void 0 : {
|
|
2943
|
+
card: "generic",
|
|
2944
|
+
content
|
|
2945
|
+
};
|
|
2393
2946
|
}
|
|
2394
2947
|
/** Register the global Agent tools and unregister them with the plugin lifecycle. */
|
|
2395
2948
|
function registerAgentImageTools(ctx, runtime, resolve) {
|
|
@@ -2398,13 +2951,32 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2398
2951
|
const config = resolve();
|
|
2399
2952
|
if (!config.enabled) throw new ImageGenError("AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.", "plugin-disabled");
|
|
2400
2953
|
if (!config.allowAgentImageGeneration) throw new ImageGenError("Agent image generation is disabled in Settings > Plugins > AI Image.", "agent-generation-disabled");
|
|
2401
|
-
if (config.apiUrl.trim()
|
|
2954
|
+
if (!config.channels.some((channel) => channel.apiUrl.trim() !== "" && channel.apiKey.trim() !== "")) throw new ImageGenError("Image API credentials are not configured. Open Settings > Plugins > AI Image, add a channel and fill in its API URL and API key.", "image-api-not-configured");
|
|
2402
2955
|
};
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2956
|
+
/**
|
|
2957
|
+
* Resolve the requested model alias onto a channel. Rules:
|
|
2958
|
+
* - a named alias must exist in some channel's catalog (several channels
|
|
2959
|
+
* may host it; the default channel wins);
|
|
2960
|
+
* - with no alias, a single configured model is used directly, while
|
|
2961
|
+
* multiple models require the Agent to ask the user first.
|
|
2962
|
+
* @returns the channel plus the alias and its upstream id.
|
|
2963
|
+
*/
|
|
2964
|
+
const resolveModel = (requested) => {
|
|
2965
|
+
const config = resolve();
|
|
2966
|
+
const entries = config.channels.flatMap((channel) => channel.models.map((model) => ({
|
|
2967
|
+
channel,
|
|
2968
|
+
alias: model.alias,
|
|
2969
|
+
upstream: model.id
|
|
2970
|
+
})));
|
|
2971
|
+
if (entries.length === 0) throw new ImageGenError("No image models are configured. Open Settings > Plugins > AI Image and add a channel with at least one model.", "no-models-configured");
|
|
2972
|
+
const wanted = typeof requested === "string" && requested.trim() !== "" ? requested.trim() : "";
|
|
2973
|
+
if (wanted === "") {
|
|
2974
|
+
if (entries.length === 1) return entries[0];
|
|
2975
|
+
throw new ImageGenError(`Multiple image models are available — ask the user which channel and model to use, then call this tool again with that exact model name. Options: ${config.channels.flatMap((channel) => channel.models.map((model) => `"${channel.name} · ${model.alias}"`)).join(", ")}.`, "model-choice-required");
|
|
2976
|
+
}
|
|
2977
|
+
const hosting = entries.filter((entry) => entry.alias === wanted);
|
|
2978
|
+
if (hosting.length === 0) throw new ImageGenError(`Image model "${wanted}" is not configured in any channel. Choose one of: ${[...new Set(entries.map((entry) => entry.alias))].join(", ")}.`, "image-model-not-configured");
|
|
2979
|
+
return hosting.find((entry) => entry.channel.id === config.defaultChannelId) ?? hosting[0];
|
|
2408
2980
|
};
|
|
2409
2981
|
const materializeTaskImages = (task) => {
|
|
2410
2982
|
if (task.status !== "completed") return Promise.resolve([]);
|
|
@@ -2422,7 +2994,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2422
2994
|
return {
|
|
2423
2995
|
task_id: task.id,
|
|
2424
2996
|
status: task.status,
|
|
2425
|
-
message: task.status === "completed" ? "Generation completed. The images are
|
|
2997
|
+
message: task.status === "completed" ? "Generation completed. The images are shown beside this tool call and can be reused as source_image in edit_image." : task.status === "failed" ? "Generation failed." : task.status === "cancelled" ? "Generation was cancelled." : "Generation is still running. Query the task again when you need its current status.",
|
|
2426
2998
|
...task.error === void 0 ? {} : { error: task.error },
|
|
2427
2999
|
images
|
|
2428
3000
|
};
|
|
@@ -2491,7 +3063,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2491
3063
|
const disposers = [
|
|
2492
3064
|
ctx.tools.register(defineTool({
|
|
2493
3065
|
name: "generate_image",
|
|
2494
|
-
description: "Generate an image. By default this tool call stays pending until the task reaches a final state
|
|
3066
|
+
description: "Generate an image. By default this tool call stays pending until the task reaches a final state; completed images are shown beside this tool call, while the model receives their attachment references, without creating a user message. Set wait_for_completion to false for background mode, then use get_image_generation_task explicitly. Only use models configured for this plugin; omit model to use the first configured image model.",
|
|
2495
3067
|
parameters: {
|
|
2496
3068
|
prompt: {
|
|
2497
3069
|
type: "string",
|
|
@@ -2525,13 +3097,19 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2525
3097
|
},
|
|
2526
3098
|
output: {
|
|
2527
3099
|
schema: taskResultSchema,
|
|
2528
|
-
render: (_args, value) => renderTaskResult(value)
|
|
3100
|
+
render: (_args, value) => renderTaskResult(value),
|
|
3101
|
+
presentationMeta: (_args, value) => imagePresentationMeta(value)
|
|
2529
3102
|
},
|
|
3103
|
+
presentResult: presentImageResult,
|
|
2530
3104
|
async execute(args, exec) {
|
|
2531
3105
|
ensureConfigured();
|
|
3106
|
+
const picked = resolveModel(args.model);
|
|
2532
3107
|
const task = runtime.queue.submit({
|
|
2533
3108
|
mode: "text",
|
|
2534
|
-
model:
|
|
3109
|
+
model: picked.alias,
|
|
3110
|
+
upstream: picked.upstream,
|
|
3111
|
+
channelId: picked.channel.id,
|
|
3112
|
+
channel: picked.channel.name,
|
|
2535
3113
|
prompt: args.prompt.trim(),
|
|
2536
3114
|
size: args.size ?? "auto",
|
|
2537
3115
|
quality: args.quality ?? "auto",
|
|
@@ -2543,7 +3121,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2543
3121
|
})),
|
|
2544
3122
|
ctx.tools.register(defineTool({
|
|
2545
3123
|
name: "edit_image",
|
|
2546
|
-
description: "Edit an image. By default this tool call stays pending until the task reaches a final state
|
|
3124
|
+
description: "Edit an image. By default this tool call stays pending until the task reaches a final state; completed images are shown beside this tool call, while the model receives their attachment references, without creating a user message. Set wait_for_completion to false for background mode, then use get_image_generation_task explicitly. source_image must be an image reference returned by a completed generation or get_image_generation_task; pass that entire object unchanged. Only configured image models are allowed; omit model to use the first configured model.",
|
|
2547
3125
|
parameters: {
|
|
2548
3126
|
prompt: {
|
|
2549
3127
|
type: "string",
|
|
@@ -2582,14 +3160,20 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2582
3160
|
},
|
|
2583
3161
|
output: {
|
|
2584
3162
|
schema: taskResultSchema,
|
|
2585
|
-
render: (_args, value) => renderTaskResult(value)
|
|
3163
|
+
render: (_args, value) => renderTaskResult(value),
|
|
3164
|
+
presentationMeta: (_args, value) => imagePresentationMeta(value)
|
|
2586
3165
|
},
|
|
3166
|
+
presentResult: presentImageResult,
|
|
2587
3167
|
async execute(args, exec) {
|
|
2588
3168
|
ensureConfigured();
|
|
2589
3169
|
const reference = await ctx.attachments.readImage(restoreRef(args.source_image), exec.signal);
|
|
3170
|
+
const picked = resolveModel(args.model);
|
|
2590
3171
|
const task = runtime.queue.submit({
|
|
2591
3172
|
mode: "edit",
|
|
2592
|
-
model:
|
|
3173
|
+
model: picked.alias,
|
|
3174
|
+
upstream: picked.upstream,
|
|
3175
|
+
channelId: picked.channel.id,
|
|
3176
|
+
channel: picked.channel.name,
|
|
2593
3177
|
prompt: args.prompt.trim(),
|
|
2594
3178
|
size: args.size ?? "auto",
|
|
2595
3179
|
quality: args.quality ?? "auto",
|
|
@@ -2603,7 +3187,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2603
3187
|
})),
|
|
2604
3188
|
ctx.tools.register(defineTool({
|
|
2605
3189
|
name: "get_image_generation_task",
|
|
2606
|
-
description: "Check an image-generation task status. Completed tasks return image references and
|
|
3190
|
+
description: "Check an image-generation task status. Completed tasks return image references; their images are shown beside this tool call and the references can be passed to edit_image. Generation tools normally wait for completion, so use this for explicit recovery or status checks.",
|
|
2607
3191
|
parameters: { task_id: {
|
|
2608
3192
|
type: "string",
|
|
2609
3193
|
required: true,
|
|
@@ -2611,8 +3195,10 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2611
3195
|
} },
|
|
2612
3196
|
output: {
|
|
2613
3197
|
schema: taskResultSchema,
|
|
2614
|
-
render: (_args, value) => renderTaskResult(value)
|
|
3198
|
+
render: (_args, value) => renderTaskResult(value),
|
|
3199
|
+
presentationMeta: (_args, value) => imagePresentationMeta(value)
|
|
2615
3200
|
},
|
|
3201
|
+
presentResult: presentImageResult,
|
|
2616
3202
|
async execute(args) {
|
|
2617
3203
|
ensureConfigured();
|
|
2618
3204
|
return taskResult(findTask(args.task_id));
|
|
@@ -2628,8 +3214,10 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2628
3214
|
} },
|
|
2629
3215
|
output: {
|
|
2630
3216
|
schema: taskResultSchema,
|
|
2631
|
-
render: (_args, value) => renderTaskResult(value)
|
|
3217
|
+
render: (_args, value) => renderTaskResult(value),
|
|
3218
|
+
presentationMeta: (_args, value) => imagePresentationMeta(value)
|
|
2632
3219
|
},
|
|
3220
|
+
presentResult: presentImageResult,
|
|
2633
3221
|
async execute(args) {
|
|
2634
3222
|
ensureConfigured();
|
|
2635
3223
|
const task = runtime.queue.cancel(args.task_id);
|
|
@@ -2646,9 +3234,11 @@ function isFinalTask(task) {
|
|
|
2646
3234
|
return task.status === "completed" || task.status === "failed" || task.status === "cancelled";
|
|
2647
3235
|
}
|
|
2648
3236
|
function toSaveImage(image, taskId, index) {
|
|
2649
|
-
const
|
|
3237
|
+
const data = Buffer.from(image.b64, "base64");
|
|
3238
|
+
const declaredMediaType = acceptedMediaType(image.mime) ? image.mime : "image/png";
|
|
3239
|
+
const mediaType = detectImageMime(data) ?? declaredMediaType;
|
|
2650
3240
|
return {
|
|
2651
|
-
data
|
|
3241
|
+
data,
|
|
2652
3242
|
mediaType,
|
|
2653
3243
|
name: `imagegen-${taskId}-${index + 1}.${mediaType === "image/jpeg" ? "jpg" : mediaType.slice(6)}`
|
|
2654
3244
|
};
|
|
@@ -2665,12 +3255,24 @@ const Config = z.object({
|
|
|
2665
3255
|
enabled: z.boolean().default(true),
|
|
2666
3256
|
announceToAgent: z.boolean().default(true),
|
|
2667
3257
|
allowAgentImageGeneration: z.boolean().default(true),
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
3258
|
+
channels: z.array(z.object({
|
|
3259
|
+
id: z.string(),
|
|
3260
|
+
preset: z.string().default(""),
|
|
3261
|
+
name: z.string().default(""),
|
|
3262
|
+
apiUrl: z.string().default(""),
|
|
3263
|
+
models: z.array(z.object({
|
|
3264
|
+
alias: z.string(),
|
|
3265
|
+
id: z.string()
|
|
3266
|
+
})).default([])
|
|
3267
|
+
})).default([]),
|
|
3268
|
+
channelSecrets: z.dict(z.string().role("secret")).default({}),
|
|
3269
|
+
defaultChannelId: z.string().default(""),
|
|
2671
3270
|
promptApiUrl: z.string().default(""),
|
|
2672
3271
|
promptApiKey: z.string().role("secret").default(""),
|
|
2673
|
-
promptModel: z.string().default("")
|
|
3272
|
+
promptModel: z.string().default(""),
|
|
3273
|
+
apiUrl: z.string().default(""),
|
|
3274
|
+
apiKey: z.string().role("secret").default(""),
|
|
3275
|
+
imageModels: z.array(z.string()).default([])
|
|
2674
3276
|
});
|
|
2675
3277
|
/** Schema defaults, re-read for hand-built contexts (the loader applies them normally). */
|
|
2676
3278
|
const DEFAULT_ENABLED = true;
|
|
@@ -2679,10 +3281,49 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
|
|
|
2679
3281
|
/** Order of the announcement section within the tool-guidance band. */
|
|
2680
3282
|
const SECTION_ORDER = 150;
|
|
2681
3283
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
2682
|
-
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI
|
|
2683
|
-
/**
|
|
2684
|
-
function guidanceFor(
|
|
2685
|
-
return `${IMAGEGEN_GUIDANCE}
|
|
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。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
|
|
3285
|
+
/** Append the live channel × model table so an Agent can honor user choices. */
|
|
3286
|
+
function guidanceFor(channels, defaultChannelId) {
|
|
3287
|
+
if (channels.length === 0) return `${IMAGEGEN_GUIDANCE} 尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥。`;
|
|
3288
|
+
const table = channels.map((channel) => {
|
|
3289
|
+
const aliases = channel.models.map((model) => model.alias).join("、");
|
|
3290
|
+
const mark = channel.id === defaultChannelId ? "(默认渠道)" : "";
|
|
3291
|
+
const key = channel.apiKey === "" ? "(未填密钥)" : "";
|
|
3292
|
+
const models = channel.models.length === 0 ? "未配置模型" : `可用模型:${aliases}`;
|
|
3293
|
+
return `渠道「${channel.name}」${mark}[${channel.apiUrl}] ${models}${key}`;
|
|
3294
|
+
}).join(";");
|
|
3295
|
+
return `${IMAGEGEN_GUIDANCE} 当前渠道与模型:${table}。用户指定模型名时取该模型所属渠道(多渠道同名用默认渠道);未指定模型时若仅一个可用模型可直接生成,若有多个应先询问用户选择「渠道 + 模型」。`;
|
|
3296
|
+
}
|
|
3297
|
+
/** Normalize raw channel entries into the wire shape (schema-adjacent guard). */
|
|
3298
|
+
function normalizeChannels(value) {
|
|
3299
|
+
if (!Array.isArray(value)) return [];
|
|
3300
|
+
const out = [];
|
|
3301
|
+
for (const item of value) {
|
|
3302
|
+
if (item === null || typeof item !== "object") continue;
|
|
3303
|
+
const raw = item;
|
|
3304
|
+
const id = typeof raw.id === "string" ? raw.id.trim() : "";
|
|
3305
|
+
if (id === "") continue;
|
|
3306
|
+
const models = [];
|
|
3307
|
+
if (Array.isArray(raw.models)) for (const entry of raw.models) {
|
|
3308
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
3309
|
+
const record = entry;
|
|
3310
|
+
const alias = typeof record.alias === "string" ? record.alias.trim() : "";
|
|
3311
|
+
const upstream = typeof record.id === "string" ? record.id.trim() : "";
|
|
3312
|
+
if (alias === "") continue;
|
|
3313
|
+
models.push({
|
|
3314
|
+
alias,
|
|
3315
|
+
id: upstream === "" ? alias : upstream
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
out.push({
|
|
3319
|
+
id,
|
|
3320
|
+
preset: typeof raw.preset === "string" ? raw.preset : "",
|
|
3321
|
+
name: typeof raw.name === "string" ? raw.name.trim() : "",
|
|
3322
|
+
apiUrl: typeof raw.apiUrl === "string" ? raw.apiUrl.trim() : "",
|
|
3323
|
+
models
|
|
3324
|
+
});
|
|
3325
|
+
}
|
|
3326
|
+
return out;
|
|
2686
3327
|
}
|
|
2687
3328
|
/**
|
|
2688
3329
|
* Mount the settings section, routes, and announcement.
|
|
@@ -2692,47 +3333,82 @@ function guidanceFor(imageModels) {
|
|
|
2692
3333
|
function apply(ctx, config) {
|
|
2693
3334
|
let current = () => config ?? {};
|
|
2694
3335
|
const resolve = () => {
|
|
2695
|
-
const value = current();
|
|
3336
|
+
const value = current() ?? {};
|
|
3337
|
+
let channels = normalizeChannels(value.channels);
|
|
3338
|
+
const secrets = { ...value.channelSecrets ?? {} };
|
|
3339
|
+
if (channels.length === 0) {
|
|
3340
|
+
const legacyUrl = typeof value.apiUrl === "string" ? value.apiUrl.trim() : "";
|
|
3341
|
+
const legacyModels = Array.isArray(value.imageModels) ? value.imageModels.filter((model) => typeof model === "string" && model.trim() !== "").map((model) => ({
|
|
3342
|
+
alias: model.trim(),
|
|
3343
|
+
id: model.trim()
|
|
3344
|
+
})) : [];
|
|
3345
|
+
if (legacyUrl !== "" || legacyModels.length > 0) {
|
|
3346
|
+
channels = [{
|
|
3347
|
+
id: "default",
|
|
3348
|
+
preset: "",
|
|
3349
|
+
name: "默认渠道",
|
|
3350
|
+
apiUrl: legacyUrl,
|
|
3351
|
+
models: legacyModels
|
|
3352
|
+
}];
|
|
3353
|
+
const legacyKey = typeof value.apiKey === "string" ? value.apiKey.trim() : "";
|
|
3354
|
+
if (legacyKey !== "") secrets["default"] = legacyKey;
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
const named = channels.map((channel) => ({
|
|
3358
|
+
...channel,
|
|
3359
|
+
name: channel.name === "" ? presetById(channel.preset)?.name ?? "未命名渠道" : channel.name
|
|
3360
|
+
}));
|
|
3361
|
+
const defaultChannelId = typeof value.defaultChannelId === "string" && named.some((channel) => channel.id === value.defaultChannelId) ? value.defaultChannelId : named[0]?.id ?? "";
|
|
2696
3362
|
return {
|
|
2697
3363
|
enabled: value.enabled ?? DEFAULT_ENABLED,
|
|
2698
3364
|
announceToAgent: value.announceToAgent ?? DEFAULT_ANNOUNCE,
|
|
2699
3365
|
allowAgentImageGeneration: value.allowAgentImageGeneration ?? DEFAULT_ALLOW_AGENT_IMAGE_GENERATION,
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
3366
|
+
channels: named.map((channel) => ({
|
|
3367
|
+
...channel,
|
|
3368
|
+
apiKey: typeof secrets[channel.id] === "string" ? secrets[channel.id] : ""
|
|
3369
|
+
})),
|
|
3370
|
+
defaultChannelId,
|
|
3371
|
+
promptApiUrl: typeof value.promptApiUrl === "string" ? value.promptApiUrl.trim() : "",
|
|
3372
|
+
promptApiKey: typeof value.promptApiKey === "string" ? value.promptApiKey.trim() : "",
|
|
3373
|
+
promptModel: typeof value.promptModel === "string" ? value.promptModel.trim() : ""
|
|
2706
3374
|
};
|
|
2707
3375
|
};
|
|
2708
|
-
const
|
|
3376
|
+
const channelsView = () => {
|
|
2709
3377
|
const value = resolve();
|
|
2710
3378
|
return {
|
|
2711
|
-
|
|
2712
|
-
|
|
3379
|
+
channels: value.channels,
|
|
3380
|
+
defaultChannelId: value.defaultChannelId
|
|
2713
3381
|
};
|
|
2714
|
-
}
|
|
2715
|
-
|
|
3382
|
+
};
|
|
3383
|
+
const runtime = new ImageGenerationRuntime(channelsView);
|
|
3384
|
+
ctx.inject(["settings", "attachments"], (sctx) => {
|
|
2716
3385
|
const seam = sctx.get("settings");
|
|
2717
3386
|
sctx.effect(() => {
|
|
2718
3387
|
const disposers = makeRoutes({
|
|
2719
3388
|
settings: seam,
|
|
2720
3389
|
resolve: () => {
|
|
2721
3390
|
const value = resolve();
|
|
3391
|
+
const channel = value.channels.find((candidate) => candidate.id === value.defaultChannelId) ?? value.channels[0];
|
|
2722
3392
|
return {
|
|
2723
|
-
apiUrl:
|
|
2724
|
-
apiKey:
|
|
3393
|
+
apiUrl: channel?.apiUrl ?? "",
|
|
3394
|
+
apiKey: channel?.apiKey ?? ""
|
|
2725
3395
|
};
|
|
2726
3396
|
},
|
|
3397
|
+
resolveChannels: channelsView,
|
|
2727
3398
|
resolvePrompt: () => {
|
|
2728
3399
|
const value = resolve();
|
|
3400
|
+
const channel = value.channels.find((candidate) => candidate.id === value.defaultChannelId) ?? value.channels[0];
|
|
2729
3401
|
return {
|
|
2730
|
-
apiUrl: value.promptApiUrl
|
|
2731
|
-
apiKey: value.promptApiKey
|
|
3402
|
+
apiUrl: value.promptApiUrl !== "" ? value.promptApiUrl : channel?.apiUrl ?? "",
|
|
3403
|
+
apiKey: value.promptApiKey !== "" ? value.promptApiKey : channel?.apiKey ?? "",
|
|
2732
3404
|
model: value.promptModel
|
|
2733
3405
|
};
|
|
2734
3406
|
},
|
|
2735
|
-
resolveImageModels: () =>
|
|
3407
|
+
resolveImageModels: () => {
|
|
3408
|
+
const value = resolve();
|
|
3409
|
+
return [...new Set(value.channels.flatMap((channel) => channel.models.map((model) => model.alias)))];
|
|
3410
|
+
},
|
|
3411
|
+
attachments: sctx.attachments,
|
|
2736
3412
|
runtime
|
|
2737
3413
|
}).map((route) => ctx.webServer.register(route));
|
|
2738
3414
|
return () => {
|
|
@@ -2746,9 +3422,8 @@ function apply(ctx, config) {
|
|
|
2746
3422
|
return {
|
|
2747
3423
|
enabled: value.enabled,
|
|
2748
3424
|
allowAgentImageGeneration: value.allowAgentImageGeneration,
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
imageModels: value.imageModels
|
|
3425
|
+
channels: value.channels,
|
|
3426
|
+
defaultChannelId: value.defaultChannelId
|
|
2752
3427
|
};
|
|
2753
3428
|
}), "dsh-imagegen: agent image tools");
|
|
2754
3429
|
});
|
|
@@ -2763,7 +3438,7 @@ function apply(ctx, config) {
|
|
|
2763
3438
|
disposeSection = ctx.systemPrompt.section({
|
|
2764
3439
|
name: "plugin:dsh-imagegen",
|
|
2765
3440
|
order: SECTION_ORDER,
|
|
2766
|
-
text: guidanceFor(value.
|
|
3441
|
+
text: guidanceFor(value.channels, value.defaultChannelId)
|
|
2767
3442
|
});
|
|
2768
3443
|
};
|
|
2769
3444
|
installSettingsSection(ctx, ImageGenSettingsNamespace, Config, config ?? {}, {
|