@dickpy/dsh-imagegen 1.2.2 → 1.3.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/README.md +24 -11
- package/docs/images/community-qq.png +0 -0
- package/lib/client.js +2040 -906
- package/lib/client.js.map +1 -1
- package/lib/index.js +710 -109
- package/package.json +2 -2
- package/src/agent-image-tools.ts +132 -30
- package/src/client/ImageGenPanel.tsx +56 -20
- package/src/client/SettingsCard.tsx +505 -196
- package/src/client/channels-form.ts +263 -0
- package/src/client/image-toolview.tsx +19 -12
- package/src/client/locales.ts +112 -2
- package/src/client/settings-card.module.css +487 -0
- package/src/client/settings-scope.ts +56 -8
- package/src/engine.ts +107 -8
- package/src/gallery-store.ts +6 -0
- package/src/generation-runtime.ts +32 -5
- package/src/history-store.ts +6 -0
- package/src/image-format.ts +11 -0
- package/src/image-models.ts +1 -1
- package/src/index.ts +153 -47
- package/src/model-catalog.ts +98 -0
- package/src/presets.ts +63 -0
- package/src/protocol.ts +81 -6
- package/src/routes.ts +192 -24
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.0";
|
|
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",
|
|
@@ -140,7 +150,14 @@ async function enhancePrompt(config, prompt) {
|
|
|
140
150
|
* `/models` exposes candidates only: the configured list is the explicit
|
|
141
151
|
* allow-list because OpenAI-compatible gateways rarely advertise modalities.
|
|
142
152
|
*/
|
|
143
|
-
const DEFAULT_IMAGE_MODELS = [
|
|
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
|
+
];
|
|
144
161
|
/** Normalize user-entered model identifiers and retain a usable legacy default. */
|
|
145
162
|
function normalizeImageModels(value) {
|
|
146
163
|
const candidates = Array.isArray(value) ? value : [];
|
|
@@ -153,6 +170,113 @@ function normalizeImageModels(value) {
|
|
|
153
170
|
return unique.size > 0 ? [...unique] : [...DEFAULT_IMAGE_MODELS];
|
|
154
171
|
}
|
|
155
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
|
+
//#region src/model-catalog.ts
|
|
183
|
+
const ENTRIES = {
|
|
184
|
+
"gpt-image": {
|
|
185
|
+
label: "gpt-image",
|
|
186
|
+
labelZh: "GPT 图像",
|
|
187
|
+
known: true,
|
|
188
|
+
supportsEdit: true,
|
|
189
|
+
supportsAspectRatio: false,
|
|
190
|
+
qualityTiers: [
|
|
191
|
+
"1K",
|
|
192
|
+
"2K",
|
|
193
|
+
"4K"
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
"dall-e": {
|
|
197
|
+
label: "DALL·E",
|
|
198
|
+
labelZh: "DALL·E",
|
|
199
|
+
known: true,
|
|
200
|
+
supportsEdit: true,
|
|
201
|
+
supportsAspectRatio: false,
|
|
202
|
+
qualityTiers: ["auto"]
|
|
203
|
+
},
|
|
204
|
+
grok: {
|
|
205
|
+
label: "grok",
|
|
206
|
+
labelZh: "Grok",
|
|
207
|
+
known: true,
|
|
208
|
+
supportsEdit: true,
|
|
209
|
+
supportsAspectRatio: true,
|
|
210
|
+
qualityTiers: ["1K", "2K"]
|
|
211
|
+
},
|
|
212
|
+
nanobanana: {
|
|
213
|
+
label: "nanobanana",
|
|
214
|
+
labelZh: "Nano Banana",
|
|
215
|
+
known: true,
|
|
216
|
+
supportsEdit: true,
|
|
217
|
+
supportsAspectRatio: true,
|
|
218
|
+
qualityTiers: [
|
|
219
|
+
"1K",
|
|
220
|
+
"2K",
|
|
221
|
+
"4K"
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
seedream: {
|
|
225
|
+
label: "seedream",
|
|
226
|
+
labelZh: "Seedream",
|
|
227
|
+
known: true,
|
|
228
|
+
supportsEdit: true,
|
|
229
|
+
supportsAspectRatio: true,
|
|
230
|
+
qualityTiers: ["1K", "2K"]
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
/** Official Gemini image ids served by Nano Banana gateways. */
|
|
234
|
+
const NANOBANANA_GEMINI_IDS = /* @__PURE__ */ new Set([
|
|
235
|
+
"gemini-3-pro-image",
|
|
236
|
+
"gemini-3-pro-image-preview",
|
|
237
|
+
"gemini-3.1-flash-image",
|
|
238
|
+
"gemini-3.1-flash-image-preview",
|
|
239
|
+
"gemini-3.1-flash-lite-image",
|
|
240
|
+
"gemini-2.5-flash-image"
|
|
241
|
+
]);
|
|
242
|
+
/** Classify one upstream model id into its request-shaping family. */
|
|
243
|
+
function describeModel(model) {
|
|
244
|
+
const id = model.trim();
|
|
245
|
+
if (/^gpt-image/i.test(id)) return {
|
|
246
|
+
family: "gpt-image",
|
|
247
|
+
...ENTRIES["gpt-image"]
|
|
248
|
+
};
|
|
249
|
+
if (/^dall-e/i.test(id)) return {
|
|
250
|
+
family: "dall-e",
|
|
251
|
+
...ENTRIES["dall-e"]
|
|
252
|
+
};
|
|
253
|
+
if (/^grok-imagine(?:-|$)/.test(id)) return {
|
|
254
|
+
family: "grok",
|
|
255
|
+
...ENTRIES.grok
|
|
256
|
+
};
|
|
257
|
+
if (/^nanobanana/i.test(id) || NANOBANANA_GEMINI_IDS.has(id)) return {
|
|
258
|
+
family: "nanobanana",
|
|
259
|
+
...ENTRIES.nanobanana
|
|
260
|
+
};
|
|
261
|
+
if (/^(?:doubao-)?seedream/i.test(id)) return {
|
|
262
|
+
family: "seedream",
|
|
263
|
+
...ENTRIES.seedream
|
|
264
|
+
};
|
|
265
|
+
return {
|
|
266
|
+
family: "unknown",
|
|
267
|
+
label: "unknown",
|
|
268
|
+
labelZh: "未知协议",
|
|
269
|
+
known: false,
|
|
270
|
+
supportsEdit: true,
|
|
271
|
+
supportsAspectRatio: false,
|
|
272
|
+
qualityTiers: []
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/** The family a model id routes its request through. */
|
|
276
|
+
function modelFamily(model) {
|
|
277
|
+
return describeModel(model).family;
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
156
280
|
//#region src/engine.ts
|
|
157
281
|
/** A generation failure with a user-presentable message. */
|
|
158
282
|
var ImageGenError = class extends Error {
|
|
@@ -176,12 +300,45 @@ const DALLE3_SIZES = /* @__PURE__ */ new Set([
|
|
|
176
300
|
"1792x1024",
|
|
177
301
|
"1024x1792"
|
|
178
302
|
]);
|
|
303
|
+
/** The wire model id for a request: `upstream` (host-filled alias mapping)
|
|
304
|
+
* wins, then the alias, then the family default. */
|
|
305
|
+
function wireModel(request) {
|
|
306
|
+
const upstream = request.upstream?.trim();
|
|
307
|
+
if (upstream !== void 0 && upstream !== "") return upstream;
|
|
308
|
+
const alias = request.model.trim();
|
|
309
|
+
return alias === "" ? "gpt-image-2" : alias;
|
|
310
|
+
}
|
|
179
311
|
/** Whether the model is an xAI Grok Imagine model (grok-imagine-image,
|
|
180
312
|
* grok-imagine-image-2.0, …). Grok Imagine speaks JSON on both endpoints
|
|
181
313
|
* and exposes its own aspect-ratio / response-format knobs instead of the
|
|
182
314
|
* OpenAI size/quality/detail passthrough. */
|
|
183
315
|
function isGrokImagine(model) {
|
|
184
|
-
return
|
|
316
|
+
return modelFamily(model) === "grok";
|
|
317
|
+
}
|
|
318
|
+
/** Whether the model belongs to the Google Nano Banana family (nanobanana2 /
|
|
319
|
+
* nanobanana2-lite / nanobanana-pro, plus the official Gemini image IDs the
|
|
320
|
+
* gateways expose). OpenAI-compatible gateways serve these with their own
|
|
321
|
+
* aspect_ratio / image_size vocabulary instead of the OpenAI size/quality
|
|
322
|
+
* passthrough. */
|
|
323
|
+
function isNanoBanana(model) {
|
|
324
|
+
return modelFamily(model) === "nanobanana";
|
|
325
|
+
}
|
|
326
|
+
/** Whether the model belongs to the ByteDance Seedream family (seedream-5.0-pro,
|
|
327
|
+
* seedream-5.0, seedream-4.x, doubao-seedream-…). OpenAI-compatible gateways
|
|
328
|
+
* serve Seedream through a unified generate-and-edit architecture:
|
|
329
|
+
* generation AND editing both go to /images/generations and reference images
|
|
330
|
+
* are a JSON URL / data-URL array. */
|
|
331
|
+
function isSeedream(model) {
|
|
332
|
+
return modelFamily(model) === "seedream";
|
|
333
|
+
}
|
|
334
|
+
/** Whether this is the official Volcengine Ark model naming convention. */
|
|
335
|
+
function isVolcSeedream(model) {
|
|
336
|
+
return /^doubao-seedream(?:-|$)/i.test(model.trim());
|
|
337
|
+
}
|
|
338
|
+
/** Volcengine uses `size` for the output tier, not the panel's aspect ratio. */
|
|
339
|
+
function seedreamSize(quality) {
|
|
340
|
+
if (quality === "1k") return "1K";
|
|
341
|
+
return "2K";
|
|
185
342
|
}
|
|
186
343
|
/** The panel's aspect ratios mapped to the closest OpenAI pixel size
|
|
187
344
|
* (gpt-image-2 / generic OpenAI-compatible endpoints). */
|
|
@@ -259,7 +416,7 @@ function clampCount(n) {
|
|
|
259
416
|
* batch parameter is rejected by Responses-API-based gateways (tools[0].n),
|
|
260
417
|
* so the count is satisfied by parallel single-image requests instead. */
|
|
261
418
|
function effectiveParams(request) {
|
|
262
|
-
const model =
|
|
419
|
+
const model = wireModel(request);
|
|
263
420
|
if (model === "dall-e-3") {
|
|
264
421
|
const pixel = OPENAI_SIZE_BY_RATIO[request.size];
|
|
265
422
|
return {
|
|
@@ -273,6 +430,17 @@ function effectiveParams(request) {
|
|
|
273
430
|
...request.quality !== "" && request.quality !== "auto" ? { resolution: request.quality === "4k" ? "2k" : request.quality } : {},
|
|
274
431
|
response_format: "b64_json"
|
|
275
432
|
};
|
|
433
|
+
if (isNanoBanana(model)) return {
|
|
434
|
+
model,
|
|
435
|
+
...request.size !== "" && request.size !== "auto" ? { aspect_ratio: request.size } : {},
|
|
436
|
+
...request.quality !== "" && request.quality !== "auto" ? { image_size: request.quality.toUpperCase() } : {},
|
|
437
|
+
response_format: "b64_json"
|
|
438
|
+
};
|
|
439
|
+
if (isSeedream(model)) return {
|
|
440
|
+
model,
|
|
441
|
+
size: seedreamSize(request.quality),
|
|
442
|
+
response_format: isVolcSeedream(model) ? "url" : "b64_json"
|
|
443
|
+
};
|
|
276
444
|
return {
|
|
277
445
|
model,
|
|
278
446
|
...request.size !== "" && request.size !== "auto" && OPENAI_SIZE_BY_RATIO[request.size] !== void 0 ? { size: OPENAI_SIZE_BY_RATIO[request.size] } : {},
|
|
@@ -284,17 +452,20 @@ function effectiveParams(request) {
|
|
|
284
452
|
}
|
|
285
453
|
/** How many single-image requests to issue for the requested image count. */
|
|
286
454
|
function effectiveCount(request) {
|
|
287
|
-
if ((request
|
|
455
|
+
if (wireModel(request) === "dall-e-3") return 1;
|
|
288
456
|
return clampCount(request.n);
|
|
289
457
|
}
|
|
290
458
|
/** Normalize one upstream data item into a base64 image. */
|
|
291
459
|
async function normalizeItem(item, upstream) {
|
|
292
460
|
const revisedPrompt = typeof item.revised_prompt === "string" ? item.revised_prompt : void 0;
|
|
293
|
-
if (typeof item.b64_json === "string")
|
|
294
|
-
b64
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
461
|
+
if (typeof item.b64_json === "string") {
|
|
462
|
+
const b64 = bareBase64(item.b64_json);
|
|
463
|
+
return {
|
|
464
|
+
b64,
|
|
465
|
+
mime: detectImageMime(Buffer.from(b64, "base64")) ?? "image/png",
|
|
466
|
+
revisedPrompt
|
|
467
|
+
};
|
|
468
|
+
}
|
|
298
469
|
if (typeof item.url !== "string" || item.url === "") throw new ImageGenError("upstream image item has neither b64_json nor url");
|
|
299
470
|
const url = item.url;
|
|
300
471
|
if (url.startsWith("data:")) {
|
|
@@ -302,7 +473,7 @@ async function normalizeItem(item, upstream) {
|
|
|
302
473
|
if (parsed === void 0) throw new ImageGenError("upstream returned a malformed data: url");
|
|
303
474
|
return {
|
|
304
475
|
b64: parsed.base64,
|
|
305
|
-
mime: parsed.mime,
|
|
476
|
+
mime: detectImageMime(Buffer.from(parsed.base64, "base64")) ?? parsed.mime,
|
|
306
477
|
revisedPrompt
|
|
307
478
|
};
|
|
308
479
|
}
|
|
@@ -321,7 +492,7 @@ async function normalizeItem(item, upstream) {
|
|
|
321
492
|
if (!response.ok) throw new ImageGenError(`failed to fetch the generated image url: HTTP ${response.status}`);
|
|
322
493
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
323
494
|
const contentType = response.headers.get("content-type");
|
|
324
|
-
const mime = contentType !== null && contentType !== "" ? contentType.split(";")[0].trim() : mimeOfExtension(url) ?? "image/png";
|
|
495
|
+
const mime = detectImageMime(buffer) ?? (contentType !== null && contentType !== "" ? contentType.split(";")[0].trim() : mimeOfExtension(url) ?? "image/png");
|
|
325
496
|
return {
|
|
326
497
|
b64: buffer.toString("base64"),
|
|
327
498
|
mime,
|
|
@@ -358,6 +529,24 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
358
529
|
...params.aspect_ratio !== void 0 ? { aspect_ratio: params.aspect_ratio } : {},
|
|
359
530
|
response_format: "b64_json"
|
|
360
531
|
});
|
|
532
|
+
} else if (isNanoBanana(params.model)) {
|
|
533
|
+
const form = new FormData();
|
|
534
|
+
form.append("image", new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf$2(parsed.mime)}`);
|
|
535
|
+
form.append("prompt", request.prompt);
|
|
536
|
+
form.append("model", params.model);
|
|
537
|
+
if (params.aspect_ratio !== void 0) form.append("aspect_ratio", params.aspect_ratio);
|
|
538
|
+
if (params.image_size !== void 0) form.append("image_size", params.image_size);
|
|
539
|
+
body = form;
|
|
540
|
+
} else if (isSeedream(params.model)) {
|
|
541
|
+
headers["content-type"] = "application/json";
|
|
542
|
+
body = JSON.stringify({
|
|
543
|
+
model: params.model,
|
|
544
|
+
prompt: request.prompt,
|
|
545
|
+
image: [request.image],
|
|
546
|
+
...params.size !== void 0 ? { size: params.size } : {},
|
|
547
|
+
...params.resolution !== void 0 ? { resolution: params.resolution } : {},
|
|
548
|
+
response_format: isVolcSeedream(params.model) ? "url" : "b64_json"
|
|
549
|
+
});
|
|
361
550
|
} else {
|
|
362
551
|
const form = new FormData();
|
|
363
552
|
form.append("image", new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf$2(parsed.mime)}`);
|
|
@@ -378,7 +567,8 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
378
567
|
const budget = requestSignal(signal, UPSTREAM_TIMEOUT_MS);
|
|
379
568
|
let response;
|
|
380
569
|
try {
|
|
381
|
-
|
|
570
|
+
const endpoint = request.mode === "edit" && !isSeedream(params.model) ? "/images/edits" : "/images/generations";
|
|
571
|
+
response = await fetch(`${baseUrl}${endpoint}`, {
|
|
382
572
|
method: "POST",
|
|
383
573
|
headers,
|
|
384
574
|
body,
|
|
@@ -546,7 +736,9 @@ function toWire$1(entry) {
|
|
|
546
736
|
mime: image.mime,
|
|
547
737
|
...image.revisedPrompt === void 0 ? {} : { revisedPrompt: image.revisedPrompt }
|
|
548
738
|
})),
|
|
549
|
-
...entry.refName === void 0 ? {} : { refName: entry.refName }
|
|
739
|
+
...entry.refName === void 0 ? {} : { refName: entry.refName },
|
|
740
|
+
...entry.channel === void 0 ? {} : { channel: entry.channel },
|
|
741
|
+
...entry.channelId === void 0 ? {} : { channelId: entry.channelId }
|
|
550
742
|
};
|
|
551
743
|
}
|
|
552
744
|
/** List the persisted history, newest first, as wire entries. */
|
|
@@ -585,7 +777,9 @@ async function appendHistory(input) {
|
|
|
585
777
|
detail: input.detail,
|
|
586
778
|
n: input.n,
|
|
587
779
|
images: storedImages,
|
|
588
|
-
...input.refName === void 0 ? {} : { refName: input.refName }
|
|
780
|
+
...input.refName === void 0 ? {} : { refName: input.refName },
|
|
781
|
+
...input.channelId === void 0 ? {} : { channelId: input.channelId },
|
|
782
|
+
...input.channel === void 0 ? {} : { channel: input.channel }
|
|
589
783
|
}, ...await readIndex$1()];
|
|
590
784
|
const kept = merged.slice(0, 50);
|
|
591
785
|
for (const dropped of merged.slice(50)) await removeEntryFiles$1(dropped);
|
|
@@ -727,6 +921,11 @@ var GenerationTaskQueue = class {
|
|
|
727
921
|
* Shared host-side generation runtime. Both the browser routes and Agent tools
|
|
728
922
|
* submit to this one queue so persisted history and cancellation semantics stay
|
|
729
923
|
* identical regardless of where a request originated.
|
|
924
|
+
*
|
|
925
|
+
* Requests carry a channel id (host-filled by the route/tool resolution); the
|
|
926
|
+
* runtime picks that channel's upstream credentials, otherwise the default
|
|
927
|
+
* channel, and records a channel snapshot on the history entry so usage
|
|
928
|
+
* counters and filters survive channel deletion.
|
|
730
929
|
*/
|
|
731
930
|
var ImageGenerationRuntime = class {
|
|
732
931
|
resolve;
|
|
@@ -738,7 +937,13 @@ var ImageGenerationRuntime = class {
|
|
|
738
937
|
this.queue = new GenerationTaskQueue((request, signal) => this.run(request, signal));
|
|
739
938
|
}
|
|
740
939
|
async run(request, signal) {
|
|
741
|
-
const
|
|
940
|
+
const view = this.resolve();
|
|
941
|
+
const channel = view.channels.find((candidate) => candidate.id === request.channelId) ?? view.channels.find((candidate) => candidate.id === view.defaultChannelId) ?? view.channels[0];
|
|
942
|
+
if (channel === void 0) throw new ImageGenError("尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥", "no-channels");
|
|
943
|
+
const result = await generateImage({
|
|
944
|
+
apiUrl: channel.apiUrl,
|
|
945
|
+
apiKey: channel.apiKey
|
|
946
|
+
}, request, { signal });
|
|
742
947
|
try {
|
|
743
948
|
const history = await this.history.append({
|
|
744
949
|
id: randomUUID(),
|
|
@@ -751,7 +956,9 @@ var ImageGenerationRuntime = class {
|
|
|
751
956
|
detail: request.detail,
|
|
752
957
|
n: request.n,
|
|
753
958
|
images: result.images,
|
|
754
|
-
...request.refName === void 0 ? {} : { refName: request.refName }
|
|
959
|
+
...request.refName === void 0 ? {} : { refName: request.refName },
|
|
960
|
+
...request.channelId === void 0 ? {} : { channelId: request.channelId },
|
|
961
|
+
...request.channel === void 0 ? {} : { channel: request.channel }
|
|
755
962
|
});
|
|
756
963
|
return {
|
|
757
964
|
...result,
|
|
@@ -875,7 +1082,9 @@ function toWire(entry) {
|
|
|
875
1082
|
...image.revisedPrompt === void 0 ? {} : { revisedPrompt: image.revisedPrompt }
|
|
876
1083
|
})),
|
|
877
1084
|
...entry.refName === void 0 ? {} : { refName: entry.refName },
|
|
878
|
-
...entry.tags === void 0 ? {} : { tags: entry.tags }
|
|
1085
|
+
...entry.tags === void 0 ? {} : { tags: entry.tags },
|
|
1086
|
+
...entry.channel === void 0 ? {} : { channel: entry.channel },
|
|
1087
|
+
...entry.channelId === void 0 ? {} : { channelId: entry.channelId }
|
|
879
1088
|
};
|
|
880
1089
|
}
|
|
881
1090
|
/** List the persisted gallery, newest first, as wire entries. */
|
|
@@ -925,7 +1134,9 @@ async function appendGallery(input) {
|
|
|
925
1134
|
n: input.n,
|
|
926
1135
|
images: storedImages,
|
|
927
1136
|
...hash === void 0 ? {} : { hash },
|
|
928
|
-
...input.refName === void 0 ? {} : { refName: input.refName }
|
|
1137
|
+
...input.refName === void 0 ? {} : { refName: input.refName },
|
|
1138
|
+
...input.channelId === void 0 ? {} : { channelId: input.channelId },
|
|
1139
|
+
...input.channel === void 0 ? {} : { channel: input.channel }
|
|
929
1140
|
}, ...await readIndex()];
|
|
930
1141
|
await writeIndex(merged);
|
|
931
1142
|
return {
|
|
@@ -1347,6 +1558,57 @@ function clearUpdateCache() {
|
|
|
1347
1558
|
cached = void 0;
|
|
1348
1559
|
}
|
|
1349
1560
|
//#endregion
|
|
1561
|
+
//#region src/presets.ts
|
|
1562
|
+
const IMAGE_PRESETS = [
|
|
1563
|
+
{
|
|
1564
|
+
id: "volc-ark-seedream",
|
|
1565
|
+
name: "字节 · 火山方舟(Seedream)",
|
|
1566
|
+
apiUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
|
1567
|
+
hint: "字节跳动官方 Seedream 文生图/图生图入口",
|
|
1568
|
+
models: [
|
|
1569
|
+
{
|
|
1570
|
+
alias: "seedream-5.0-pro",
|
|
1571
|
+
id: "seedream-5.0-pro"
|
|
1572
|
+
},
|
|
1573
|
+
{
|
|
1574
|
+
alias: "seedream-5.0",
|
|
1575
|
+
id: "seedream-5.0"
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
alias: "seedream-4.0",
|
|
1579
|
+
id: "seedream-4.0"
|
|
1580
|
+
}
|
|
1581
|
+
]
|
|
1582
|
+
},
|
|
1583
|
+
{
|
|
1584
|
+
id: "openai-official",
|
|
1585
|
+
name: "OpenAI 官方",
|
|
1586
|
+
apiUrl: "https://api.openai.com/v1",
|
|
1587
|
+
hint: "OpenAI 官方接口:gpt-image-2 / dall-e-3",
|
|
1588
|
+
models: [{
|
|
1589
|
+
alias: "gpt-image-2",
|
|
1590
|
+
id: "gpt-image-2"
|
|
1591
|
+
}, {
|
|
1592
|
+
alias: "dall-e-3",
|
|
1593
|
+
id: "dall-e-3"
|
|
1594
|
+
}]
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
id: "xai-grok",
|
|
1598
|
+
name: "xAI(Grok)",
|
|
1599
|
+
apiUrl: "https://api.x.ai/v1",
|
|
1600
|
+
hint: "xAI 官方接口:Grok Imagine 系列",
|
|
1601
|
+
models: [{
|
|
1602
|
+
alias: "grok-imagine-image",
|
|
1603
|
+
id: "grok-imagine-image"
|
|
1604
|
+
}]
|
|
1605
|
+
}
|
|
1606
|
+
];
|
|
1607
|
+
/** Look up one built-in provider by id. */
|
|
1608
|
+
function presetById(id) {
|
|
1609
|
+
return IMAGE_PRESETS.find((preset) => preset.id === id);
|
|
1610
|
+
}
|
|
1611
|
+
//#endregion
|
|
1350
1612
|
//#region src/routes.ts
|
|
1351
1613
|
/** Cap on JSON request bodies (settings ops and generate payloads are small). */
|
|
1352
1614
|
const MAX_JSON_BODY_BYTES = 24 * 1024 * 1024;
|
|
@@ -1416,7 +1678,8 @@ function parseGenerateRequest(body) {
|
|
|
1416
1678
|
n: typeof body.n === "number" ? body.n : 1,
|
|
1417
1679
|
detail: typeof body.detail === "string" ? body.detail : "",
|
|
1418
1680
|
...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {},
|
|
1419
|
-
...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {}
|
|
1681
|
+
...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {},
|
|
1682
|
+
...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {}
|
|
1420
1683
|
};
|
|
1421
1684
|
}
|
|
1422
1685
|
/** Validate a submitted history entry (images carry base64). */
|
|
@@ -1452,7 +1715,9 @@ function parseHistoryEntryInput(body) {
|
|
|
1452
1715
|
detail: entry.detail,
|
|
1453
1716
|
n: entry.n,
|
|
1454
1717
|
images,
|
|
1455
|
-
...typeof entry.refName === "string" ? { refName: entry.refName } : {}
|
|
1718
|
+
...typeof entry.refName === "string" ? { refName: entry.refName } : {},
|
|
1719
|
+
...typeof entry.channelId === "string" ? { channelId: entry.channelId } : {},
|
|
1720
|
+
...typeof entry.channel === "string" ? { channel: entry.channel } : {}
|
|
1456
1721
|
};
|
|
1457
1722
|
}
|
|
1458
1723
|
/** Extract the image file name from a history-image request URL. */
|
|
@@ -1467,6 +1732,33 @@ function imageFileFrom(rawUrl, basePath) {
|
|
|
1467
1732
|
if (!pathname.startsWith(`${basePath}/`)) return void 0;
|
|
1468
1733
|
return decodeURIComponent(pathname.slice(basePath.length + 1));
|
|
1469
1734
|
}
|
|
1735
|
+
/** Parse the durable image reference carried by an Agent tool-result view. */
|
|
1736
|
+
function agentImageRefFrom(rawUrl) {
|
|
1737
|
+
if (rawUrl === void 0) return void 0;
|
|
1738
|
+
let url;
|
|
1739
|
+
try {
|
|
1740
|
+
url = new URL(rawUrl, "http://localhost");
|
|
1741
|
+
} catch {
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
if (url.pathname !== "/api/dsh-imagegen/agent-image") return void 0;
|
|
1745
|
+
const attachmentId = url.searchParams.get("attachment_id") ?? "";
|
|
1746
|
+
const mediaType = url.searchParams.get("media_type") ?? "";
|
|
1747
|
+
const bytes = Number(url.searchParams.get("bytes"));
|
|
1748
|
+
const width = Number(url.searchParams.get("width"));
|
|
1749
|
+
const height = Number(url.searchParams.get("height"));
|
|
1750
|
+
if (attachmentId === "" || !isImageMediaType(mediaType) || !Number.isSafeInteger(bytes) || bytes < 1 || !Number.isSafeInteger(width) || width < 1 || !Number.isSafeInteger(height) || height < 1) return void 0;
|
|
1751
|
+
return {
|
|
1752
|
+
attachmentId,
|
|
1753
|
+
mediaType,
|
|
1754
|
+
bytes,
|
|
1755
|
+
width,
|
|
1756
|
+
height
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
function isImageMediaType(value) {
|
|
1760
|
+
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
1761
|
+
}
|
|
1470
1762
|
/** Project one settings descriptor onto the bridge wire view. */
|
|
1471
1763
|
function toView(descriptor) {
|
|
1472
1764
|
return {
|
|
@@ -1527,18 +1819,85 @@ function makeRoutes(deps) {
|
|
|
1527
1819
|
model: ""
|
|
1528
1820
|
}));
|
|
1529
1821
|
const resolveImageModels = deps.resolveImageModels ?? (() => normalizeImageModels(void 0));
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1822
|
+
/** The current channel view: the channel-aware resolver, or a synthesized
|
|
1823
|
+
* single default channel from the legacy flat upstream config (tests and
|
|
1824
|
+
* older hosts). */
|
|
1825
|
+
const channelViewOf = () => {
|
|
1826
|
+
if (deps.resolveChannels !== void 0) return deps.resolveChannels();
|
|
1827
|
+
const upstream = deps.resolve();
|
|
1828
|
+
const models = normalizeImageModels(resolveImageModels()).map((id) => ({
|
|
1829
|
+
alias: id,
|
|
1830
|
+
id
|
|
1831
|
+
}));
|
|
1832
|
+
if (upstream.apiUrl.trim() === "" && models.length === 0) return {
|
|
1833
|
+
channels: [],
|
|
1834
|
+
defaultChannelId: ""
|
|
1835
|
+
};
|
|
1536
1836
|
return {
|
|
1537
|
-
|
|
1538
|
-
|
|
1837
|
+
channels: [{
|
|
1838
|
+
id: "default",
|
|
1839
|
+
preset: "",
|
|
1840
|
+
name: "默认渠道",
|
|
1841
|
+
apiUrl: upstream.apiUrl,
|
|
1842
|
+
apiKey: upstream.apiKey,
|
|
1843
|
+
models
|
|
1844
|
+
}],
|
|
1845
|
+
defaultChannelId: "default"
|
|
1846
|
+
};
|
|
1847
|
+
};
|
|
1848
|
+
const runtime = deps.runtime ?? new ImageGenerationRuntime(channelViewOf, history);
|
|
1849
|
+
/** Resolve an alias (or the channel fallback) into a concrete generation
|
|
1850
|
+
* request: picks the channel (explicit then default), maps alias → upstream
|
|
1851
|
+
* id, and fills the channel snapshot kept on history entries. */
|
|
1852
|
+
const resolveChannelRequest = (request) => {
|
|
1853
|
+
const view = channelViewOf();
|
|
1854
|
+
if (view.channels.length === 0) return {
|
|
1855
|
+
ok: false,
|
|
1856
|
+
code: "no-channels",
|
|
1857
|
+
message: "尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥"
|
|
1858
|
+
};
|
|
1859
|
+
const explicit = view.channels.find((candidate) => candidate.id === request.channelId);
|
|
1860
|
+
const defaults = view.channels.find((candidate) => candidate.id === view.defaultChannelId) ?? view.channels[0];
|
|
1861
|
+
const target = explicit ?? defaults;
|
|
1862
|
+
const asked = request.model.trim();
|
|
1863
|
+
if (asked === "") {
|
|
1864
|
+
const alias = target?.models[0]?.alias ?? "";
|
|
1865
|
+
if (alias === "") return {
|
|
1866
|
+
ok: false,
|
|
1867
|
+
code: "no-models",
|
|
1868
|
+
message: `渠道「${target?.name ?? ""}」尚未配置模型,请先在设置中添加`
|
|
1869
|
+
};
|
|
1870
|
+
const mapping = target.models.find((model) => model.alias === alias);
|
|
1871
|
+
return {
|
|
1872
|
+
ok: true,
|
|
1873
|
+
request: {
|
|
1874
|
+
...request,
|
|
1875
|
+
model: alias,
|
|
1876
|
+
upstream: mapping.id,
|
|
1877
|
+
channelId: target.id,
|
|
1878
|
+
channel: target.name
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
const hosting = view.channels.filter((channel) => channel.models.some((model) => model.alias === asked));
|
|
1883
|
+
if (hosting.length === 0) return {
|
|
1884
|
+
ok: false,
|
|
1885
|
+
code: "image-model-not-configured",
|
|
1886
|
+
message: `模型「${asked}」未在任一渠道配置;可用模型:${[...new Set(view.channels.flatMap((channel) => channel.models.map((model) => model.alias)))].join("、") || "(无)"}`
|
|
1887
|
+
};
|
|
1888
|
+
const picked = target !== void 0 && target.models.some((model) => model.alias === asked) ? target : hosting[0];
|
|
1889
|
+
const mapping = picked.models.find((model) => model.alias === asked);
|
|
1890
|
+
return {
|
|
1891
|
+
ok: true,
|
|
1892
|
+
request: {
|
|
1893
|
+
...request,
|
|
1894
|
+
model: asked,
|
|
1895
|
+
upstream: mapping.id,
|
|
1896
|
+
channelId: picked.id,
|
|
1897
|
+
channel: picked.name
|
|
1898
|
+
}
|
|
1539
1899
|
};
|
|
1540
1900
|
};
|
|
1541
|
-
const runtime = deps.runtime ?? new ImageGenerationRuntime(deps.resolve, history);
|
|
1542
1901
|
const guard = (req, res, method) => {
|
|
1543
1902
|
if (!isLoopbackRequest(req)) {
|
|
1544
1903
|
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
@@ -1551,15 +1910,52 @@ function makeRoutes(deps) {
|
|
|
1551
1910
|
return true;
|
|
1552
1911
|
};
|
|
1553
1912
|
return [
|
|
1913
|
+
...deps.attachments === void 0 ? [] : [{
|
|
1914
|
+
kind: "prefix",
|
|
1915
|
+
path: AGENT_IMAGE_API,
|
|
1916
|
+
handler: async (req, res) => {
|
|
1917
|
+
if (!isLoopbackRequest(req)) {
|
|
1918
|
+
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
if (req.method !== "GET") {
|
|
1922
|
+
writeJson(res, 405, { error: `method not allowed: ${req.method}` });
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
const ref = agentImageRefFrom(req.url);
|
|
1926
|
+
if (ref === void 0) {
|
|
1927
|
+
writeJson(res, 400, { error: "invalid image reference" });
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
try {
|
|
1931
|
+
const stored = await deps.attachments.readImage(ref);
|
|
1932
|
+
res.writeHead(200, {
|
|
1933
|
+
"content-type": stored.ref.mediaType,
|
|
1934
|
+
"content-length": stored.data.byteLength,
|
|
1935
|
+
"cache-control": "private, max-age=3600"
|
|
1936
|
+
});
|
|
1937
|
+
res.end(Buffer.from(stored.data));
|
|
1938
|
+
} catch {
|
|
1939
|
+
writeJson(res, 404, { error: "image attachment not found" });
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
}],
|
|
1554
1943
|
{
|
|
1555
1944
|
kind: "exact",
|
|
1556
1945
|
path: IMAGE_MODEL_API.models,
|
|
1557
1946
|
handler: async (req, res) => {
|
|
1558
1947
|
if (!guard(req, res, "POST")) return;
|
|
1948
|
+
const body = await readJsonBody(req);
|
|
1949
|
+
const view = channelViewOf();
|
|
1950
|
+
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];
|
|
1951
|
+
const upstream = {
|
|
1952
|
+
apiUrl: typeof body?.apiUrl === "string" && body.apiUrl.trim() !== "" ? body.apiUrl.trim() : stored?.apiUrl ?? "",
|
|
1953
|
+
apiKey: typeof body?.apiKey === "string" && body.apiKey.trim() !== "" ? body.apiKey.trim() : stored?.apiKey ?? ""
|
|
1954
|
+
};
|
|
1559
1955
|
try {
|
|
1560
1956
|
writeJson(res, 200, {
|
|
1561
1957
|
ok: true,
|
|
1562
|
-
models: await listOpenAIModels(
|
|
1958
|
+
models: await listOpenAIModels(upstream)
|
|
1563
1959
|
});
|
|
1564
1960
|
} catch (error) {
|
|
1565
1961
|
writeJson(res, 200, {
|
|
@@ -1570,6 +1966,55 @@ function makeRoutes(deps) {
|
|
|
1570
1966
|
}
|
|
1571
1967
|
}
|
|
1572
1968
|
},
|
|
1969
|
+
{
|
|
1970
|
+
kind: "exact",
|
|
1971
|
+
path: PRESETS_API,
|
|
1972
|
+
handler: async (req, res) => {
|
|
1973
|
+
if (!guard(req, res, "POST")) return;
|
|
1974
|
+
writeJson(res, 200, {
|
|
1975
|
+
ok: true,
|
|
1976
|
+
presets: IMAGE_PRESETS.map((preset) => ({
|
|
1977
|
+
id: preset.id,
|
|
1978
|
+
name: preset.name,
|
|
1979
|
+
apiUrl: preset.apiUrl,
|
|
1980
|
+
hint: preset.hint,
|
|
1981
|
+
models: preset.models
|
|
1982
|
+
}))
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
},
|
|
1986
|
+
{
|
|
1987
|
+
kind: "exact",
|
|
1988
|
+
path: USAGE_API,
|
|
1989
|
+
handler: async (req, res) => {
|
|
1990
|
+
if (!guard(req, res, "POST")) return;
|
|
1991
|
+
try {
|
|
1992
|
+
const entries = [...await history.list(), ...await gallery.list()];
|
|
1993
|
+
const byChannel = {};
|
|
1994
|
+
const totals = {};
|
|
1995
|
+
for (const entry of entries) {
|
|
1996
|
+
const channelKey = entry.channelId !== void 0 ? entry.channelId : entry.channel !== void 0 ? `name:${entry.channel}` : "";
|
|
1997
|
+
const alias = entry.model;
|
|
1998
|
+
const bucket = byChannel[channelKey] ?? (byChannel[channelKey] = {});
|
|
1999
|
+
bucket[alias] = (bucket[alias] ?? 0) + 1;
|
|
2000
|
+
totals[alias] = (totals[alias] ?? 0) + 1;
|
|
2001
|
+
}
|
|
2002
|
+
writeJson(res, 200, {
|
|
2003
|
+
ok: true,
|
|
2004
|
+
usage: {
|
|
2005
|
+
byChannel,
|
|
2006
|
+
totals
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
writeJson(res, 200, {
|
|
2011
|
+
ok: false,
|
|
2012
|
+
code: "usage-failed",
|
|
2013
|
+
message: messageOf(error)
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
},
|
|
1573
2018
|
{
|
|
1574
2019
|
kind: "exact",
|
|
1575
2020
|
path: PROMPT_ENHANCE_API.models,
|
|
@@ -1684,37 +2129,28 @@ function makeRoutes(deps) {
|
|
|
1684
2129
|
handler: async (req, res) => {
|
|
1685
2130
|
if (!guard(req, res, "POST")) return;
|
|
1686
2131
|
const body = await readJsonBody(req);
|
|
1687
|
-
|
|
2132
|
+
const parsed = body === void 0 ? void 0 : parseGenerateRequest(body);
|
|
2133
|
+
if (parsed === void 0) {
|
|
1688
2134
|
writeJson(res, 200, {
|
|
1689
2135
|
ok: false,
|
|
1690
2136
|
code: "bad-request",
|
|
1691
|
-
message: "
|
|
1692
|
-
});
|
|
1693
|
-
return;
|
|
1694
|
-
}
|
|
1695
|
-
let request;
|
|
1696
|
-
try {
|
|
1697
|
-
request = parseConfiguredRequest(body);
|
|
1698
|
-
} catch (error) {
|
|
1699
|
-
writeJson(res, 200, {
|
|
1700
|
-
ok: false,
|
|
1701
|
-
code: "image-model-not-configured",
|
|
1702
|
-
message: messageOf(error)
|
|
2137
|
+
message: "prompt is required"
|
|
1703
2138
|
});
|
|
1704
2139
|
return;
|
|
1705
2140
|
}
|
|
1706
|
-
|
|
2141
|
+
const resolved = resolveChannelRequest(parsed);
|
|
2142
|
+
if (!resolved.ok) {
|
|
1707
2143
|
writeJson(res, 200, {
|
|
1708
2144
|
ok: false,
|
|
1709
|
-
code:
|
|
1710
|
-
message:
|
|
2145
|
+
code: resolved.code,
|
|
2146
|
+
message: resolved.message
|
|
1711
2147
|
});
|
|
1712
2148
|
return;
|
|
1713
2149
|
}
|
|
1714
2150
|
try {
|
|
1715
2151
|
writeJson(res, 200, {
|
|
1716
2152
|
ok: true,
|
|
1717
|
-
...await runtime.run(request)
|
|
2153
|
+
...await runtime.run(resolved.request)
|
|
1718
2154
|
});
|
|
1719
2155
|
} catch (error) {
|
|
1720
2156
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1732,28 +2168,27 @@ function makeRoutes(deps) {
|
|
|
1732
2168
|
handler: async (req, res) => {
|
|
1733
2169
|
if (!guard(req, res, "POST")) return;
|
|
1734
2170
|
const body = await readJsonBody(req);
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
request = body === void 0 ? void 0 : parseConfiguredRequest(body);
|
|
1738
|
-
} catch (error) {
|
|
2171
|
+
const parsed = body === void 0 ? void 0 : parseGenerateRequest(body);
|
|
2172
|
+
if (parsed === void 0) {
|
|
1739
2173
|
writeJson(res, 200, {
|
|
1740
2174
|
ok: false,
|
|
1741
|
-
code: "
|
|
1742
|
-
message:
|
|
2175
|
+
code: "bad-request",
|
|
2176
|
+
message: "prompt is required"
|
|
1743
2177
|
});
|
|
1744
2178
|
return;
|
|
1745
2179
|
}
|
|
1746
|
-
|
|
2180
|
+
const resolved = resolveChannelRequest(parsed);
|
|
2181
|
+
if (!resolved.ok) {
|
|
1747
2182
|
writeJson(res, 200, {
|
|
1748
2183
|
ok: false,
|
|
1749
|
-
code:
|
|
1750
|
-
message:
|
|
2184
|
+
code: resolved.code,
|
|
2185
|
+
message: resolved.message
|
|
1751
2186
|
});
|
|
1752
2187
|
return;
|
|
1753
2188
|
}
|
|
1754
2189
|
writeJson(res, 200, {
|
|
1755
2190
|
ok: true,
|
|
1756
|
-
task: runtime.queue.submit(request)
|
|
2191
|
+
task: runtime.queue.submit(resolved.request)
|
|
1757
2192
|
});
|
|
1758
2193
|
}
|
|
1759
2194
|
},
|
|
@@ -2330,10 +2765,54 @@ function renderTaskResult(value) {
|
|
|
2330
2765
|
return [{
|
|
2331
2766
|
type: "text",
|
|
2332
2767
|
text: JSON.stringify(value)
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2768
|
+
}];
|
|
2769
|
+
}
|
|
2770
|
+
/** The UI-only projection that keeps generated images beside the tool call. */
|
|
2771
|
+
function imagePresentationMeta(value) {
|
|
2772
|
+
return { images: value.images.map((image) => {
|
|
2773
|
+
const ref = {
|
|
2774
|
+
attachment_id: image.attachment_id,
|
|
2775
|
+
media_type: image.media_type,
|
|
2776
|
+
bytes: image.bytes,
|
|
2777
|
+
width: image.width,
|
|
2778
|
+
height: image.height
|
|
2779
|
+
};
|
|
2780
|
+
if (image.name !== void 0) ref.name = image.name;
|
|
2781
|
+
return ref;
|
|
2782
|
+
}) };
|
|
2783
|
+
}
|
|
2784
|
+
function imageBlocksFromMeta(meta) {
|
|
2785
|
+
if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return [];
|
|
2786
|
+
const images = meta.images;
|
|
2787
|
+
if (!Array.isArray(images)) return [];
|
|
2788
|
+
return images.flatMap((value) => {
|
|
2789
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return [];
|
|
2790
|
+
const raw = value;
|
|
2791
|
+
if (typeof raw.attachment_id !== "string" || typeof raw.media_type !== "string" || typeof raw.bytes !== "number" || typeof raw.width !== "number" || typeof raw.height !== "number") return [];
|
|
2792
|
+
try {
|
|
2793
|
+
return [{
|
|
2794
|
+
type: "image",
|
|
2795
|
+
attachment: restoreRef({
|
|
2796
|
+
attachment_id: raw.attachment_id,
|
|
2797
|
+
media_type: raw.media_type,
|
|
2798
|
+
bytes: raw.bytes,
|
|
2799
|
+
width: raw.width,
|
|
2800
|
+
height: raw.height,
|
|
2801
|
+
...typeof raw.name === "string" ? { name: raw.name } : {}
|
|
2802
|
+
})
|
|
2803
|
+
}];
|
|
2804
|
+
} catch {
|
|
2805
|
+
return [];
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
/** Rehydrate image attachments for the host-computed tool result view only. */
|
|
2810
|
+
function presentImageResult(_args, result) {
|
|
2811
|
+
const content = result.isError ? [] : imageBlocksFromMeta(result.meta);
|
|
2812
|
+
return content.length === 0 ? void 0 : {
|
|
2813
|
+
card: "generic",
|
|
2814
|
+
content
|
|
2815
|
+
};
|
|
2337
2816
|
}
|
|
2338
2817
|
/** Register the global Agent tools and unregister them with the plugin lifecycle. */
|
|
2339
2818
|
function registerAgentImageTools(ctx, runtime, resolve) {
|
|
@@ -2342,13 +2821,32 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2342
2821
|
const config = resolve();
|
|
2343
2822
|
if (!config.enabled) throw new ImageGenError("AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.", "plugin-disabled");
|
|
2344
2823
|
if (!config.allowAgentImageGeneration) throw new ImageGenError("Agent image generation is disabled in Settings > Plugins > AI Image.", "agent-generation-disabled");
|
|
2345
|
-
if (config.apiUrl.trim()
|
|
2824
|
+
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");
|
|
2346
2825
|
};
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2826
|
+
/**
|
|
2827
|
+
* Resolve the requested model alias onto a channel. Rules:
|
|
2828
|
+
* - a named alias must exist in some channel's catalog (several channels
|
|
2829
|
+
* may host it; the default channel wins);
|
|
2830
|
+
* - with no alias, a single configured model is used directly, while
|
|
2831
|
+
* multiple models require the Agent to ask the user first.
|
|
2832
|
+
* @returns the channel plus the alias and its upstream id.
|
|
2833
|
+
*/
|
|
2834
|
+
const resolveModel = (requested) => {
|
|
2835
|
+
const config = resolve();
|
|
2836
|
+
const entries = config.channels.flatMap((channel) => channel.models.map((model) => ({
|
|
2837
|
+
channel,
|
|
2838
|
+
alias: model.alias,
|
|
2839
|
+
upstream: model.id
|
|
2840
|
+
})));
|
|
2841
|
+
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");
|
|
2842
|
+
const wanted = typeof requested === "string" && requested.trim() !== "" ? requested.trim() : "";
|
|
2843
|
+
if (wanted === "") {
|
|
2844
|
+
if (entries.length === 1) return entries[0];
|
|
2845
|
+
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");
|
|
2846
|
+
}
|
|
2847
|
+
const hosting = entries.filter((entry) => entry.alias === wanted);
|
|
2848
|
+
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");
|
|
2849
|
+
return hosting.find((entry) => entry.channel.id === config.defaultChannelId) ?? hosting[0];
|
|
2352
2850
|
};
|
|
2353
2851
|
const materializeTaskImages = (task) => {
|
|
2354
2852
|
if (task.status !== "completed") return Promise.resolve([]);
|
|
@@ -2366,7 +2864,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2366
2864
|
return {
|
|
2367
2865
|
task_id: task.id,
|
|
2368
2866
|
status: task.status,
|
|
2369
|
-
message: task.status === "completed" ? "Generation completed. The images are
|
|
2867
|
+
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.",
|
|
2370
2868
|
...task.error === void 0 ? {} : { error: task.error },
|
|
2371
2869
|
images
|
|
2372
2870
|
};
|
|
@@ -2435,7 +2933,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2435
2933
|
const disposers = [
|
|
2436
2934
|
ctx.tools.register(defineTool({
|
|
2437
2935
|
name: "generate_image",
|
|
2438
|
-
description: "Generate an image. By default this tool call stays pending until the task reaches a final state
|
|
2936
|
+
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.",
|
|
2439
2937
|
parameters: {
|
|
2440
2938
|
prompt: {
|
|
2441
2939
|
type: "string",
|
|
@@ -2469,13 +2967,19 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2469
2967
|
},
|
|
2470
2968
|
output: {
|
|
2471
2969
|
schema: taskResultSchema,
|
|
2472
|
-
render: (_args, value) => renderTaskResult(value)
|
|
2970
|
+
render: (_args, value) => renderTaskResult(value),
|
|
2971
|
+
presentationMeta: (_args, value) => imagePresentationMeta(value)
|
|
2473
2972
|
},
|
|
2973
|
+
presentResult: presentImageResult,
|
|
2474
2974
|
async execute(args, exec) {
|
|
2475
2975
|
ensureConfigured();
|
|
2976
|
+
const picked = resolveModel(args.model);
|
|
2476
2977
|
const task = runtime.queue.submit({
|
|
2477
2978
|
mode: "text",
|
|
2478
|
-
model:
|
|
2979
|
+
model: picked.alias,
|
|
2980
|
+
upstream: picked.upstream,
|
|
2981
|
+
channelId: picked.channel.id,
|
|
2982
|
+
channel: picked.channel.name,
|
|
2479
2983
|
prompt: args.prompt.trim(),
|
|
2480
2984
|
size: args.size ?? "auto",
|
|
2481
2985
|
quality: args.quality ?? "auto",
|
|
@@ -2487,7 +2991,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2487
2991
|
})),
|
|
2488
2992
|
ctx.tools.register(defineTool({
|
|
2489
2993
|
name: "edit_image",
|
|
2490
|
-
description: "Edit an image. By default this tool call stays pending until the task reaches a final state
|
|
2994
|
+
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.",
|
|
2491
2995
|
parameters: {
|
|
2492
2996
|
prompt: {
|
|
2493
2997
|
type: "string",
|
|
@@ -2526,14 +3030,20 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2526
3030
|
},
|
|
2527
3031
|
output: {
|
|
2528
3032
|
schema: taskResultSchema,
|
|
2529
|
-
render: (_args, value) => renderTaskResult(value)
|
|
3033
|
+
render: (_args, value) => renderTaskResult(value),
|
|
3034
|
+
presentationMeta: (_args, value) => imagePresentationMeta(value)
|
|
2530
3035
|
},
|
|
3036
|
+
presentResult: presentImageResult,
|
|
2531
3037
|
async execute(args, exec) {
|
|
2532
3038
|
ensureConfigured();
|
|
2533
3039
|
const reference = await ctx.attachments.readImage(restoreRef(args.source_image), exec.signal);
|
|
3040
|
+
const picked = resolveModel(args.model);
|
|
2534
3041
|
const task = runtime.queue.submit({
|
|
2535
3042
|
mode: "edit",
|
|
2536
|
-
model:
|
|
3043
|
+
model: picked.alias,
|
|
3044
|
+
upstream: picked.upstream,
|
|
3045
|
+
channelId: picked.channel.id,
|
|
3046
|
+
channel: picked.channel.name,
|
|
2537
3047
|
prompt: args.prompt.trim(),
|
|
2538
3048
|
size: args.size ?? "auto",
|
|
2539
3049
|
quality: args.quality ?? "auto",
|
|
@@ -2547,7 +3057,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2547
3057
|
})),
|
|
2548
3058
|
ctx.tools.register(defineTool({
|
|
2549
3059
|
name: "get_image_generation_task",
|
|
2550
|
-
description: "Check an image-generation task status. Completed tasks return image references and
|
|
3060
|
+
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.",
|
|
2551
3061
|
parameters: { task_id: {
|
|
2552
3062
|
type: "string",
|
|
2553
3063
|
required: true,
|
|
@@ -2555,8 +3065,10 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2555
3065
|
} },
|
|
2556
3066
|
output: {
|
|
2557
3067
|
schema: taskResultSchema,
|
|
2558
|
-
render: (_args, value) => renderTaskResult(value)
|
|
3068
|
+
render: (_args, value) => renderTaskResult(value),
|
|
3069
|
+
presentationMeta: (_args, value) => imagePresentationMeta(value)
|
|
2559
3070
|
},
|
|
3071
|
+
presentResult: presentImageResult,
|
|
2560
3072
|
async execute(args) {
|
|
2561
3073
|
ensureConfigured();
|
|
2562
3074
|
return taskResult(findTask(args.task_id));
|
|
@@ -2572,8 +3084,10 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2572
3084
|
} },
|
|
2573
3085
|
output: {
|
|
2574
3086
|
schema: taskResultSchema,
|
|
2575
|
-
render: (_args, value) => renderTaskResult(value)
|
|
3087
|
+
render: (_args, value) => renderTaskResult(value),
|
|
3088
|
+
presentationMeta: (_args, value) => imagePresentationMeta(value)
|
|
2576
3089
|
},
|
|
3090
|
+
presentResult: presentImageResult,
|
|
2577
3091
|
async execute(args) {
|
|
2578
3092
|
ensureConfigured();
|
|
2579
3093
|
const task = runtime.queue.cancel(args.task_id);
|
|
@@ -2590,9 +3104,11 @@ function isFinalTask(task) {
|
|
|
2590
3104
|
return task.status === "completed" || task.status === "failed" || task.status === "cancelled";
|
|
2591
3105
|
}
|
|
2592
3106
|
function toSaveImage(image, taskId, index) {
|
|
2593
|
-
const
|
|
3107
|
+
const data = Buffer.from(image.b64, "base64");
|
|
3108
|
+
const declaredMediaType = acceptedMediaType(image.mime) ? image.mime : "image/png";
|
|
3109
|
+
const mediaType = detectImageMime(data) ?? declaredMediaType;
|
|
2594
3110
|
return {
|
|
2595
|
-
data
|
|
3111
|
+
data,
|
|
2596
3112
|
mediaType,
|
|
2597
3113
|
name: `imagegen-${taskId}-${index + 1}.${mediaType === "image/jpeg" ? "jpg" : mediaType.slice(6)}`
|
|
2598
3114
|
};
|
|
@@ -2609,12 +3125,24 @@ const Config = z.object({
|
|
|
2609
3125
|
enabled: z.boolean().default(true),
|
|
2610
3126
|
announceToAgent: z.boolean().default(true),
|
|
2611
3127
|
allowAgentImageGeneration: z.boolean().default(true),
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
3128
|
+
channels: z.array(z.object({
|
|
3129
|
+
id: z.string(),
|
|
3130
|
+
preset: z.string().default(""),
|
|
3131
|
+
name: z.string().default(""),
|
|
3132
|
+
apiUrl: z.string().default(""),
|
|
3133
|
+
models: z.array(z.object({
|
|
3134
|
+
alias: z.string(),
|
|
3135
|
+
id: z.string()
|
|
3136
|
+
})).default([])
|
|
3137
|
+
})).default([]),
|
|
3138
|
+
channelSecrets: z.dict(z.string().role("secret")).default({}),
|
|
3139
|
+
defaultChannelId: z.string().default(""),
|
|
2615
3140
|
promptApiUrl: z.string().default(""),
|
|
2616
3141
|
promptApiKey: z.string().role("secret").default(""),
|
|
2617
|
-
promptModel: z.string().default("")
|
|
3142
|
+
promptModel: z.string().default(""),
|
|
3143
|
+
apiUrl: z.string().default(""),
|
|
3144
|
+
apiKey: z.string().role("secret").default(""),
|
|
3145
|
+
imageModels: z.array(z.string()).default([])
|
|
2618
3146
|
});
|
|
2619
3147
|
/** Schema defaults, re-read for hand-built contexts (the loader applies them normally). */
|
|
2620
3148
|
const DEFAULT_ENABLED = true;
|
|
@@ -2623,10 +3151,49 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
|
|
|
2623
3151
|
/** Order of the announcement section within the tool-guidance band. */
|
|
2624
3152
|
const SECTION_ORDER = 150;
|
|
2625
3153
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
2626
|
-
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI
|
|
2627
|
-
/**
|
|
2628
|
-
function guidanceFor(
|
|
2629
|
-
return `${IMAGEGEN_GUIDANCE}
|
|
3154
|
+
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:通过「渠道」对接 OpenAI 兼容图像生成 API(每个渠道 = 一个 API 端点 + 各自的模型目录),支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送,nanobanana 系列按 aspect_ratio / image_size 参数协议发送;seedream 系列统一走 /images/generations,参考图以 JSON image 数组发送)。API 地址与密钥在 GUI 设置中按渠道配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用用户在各渠道配置目录中的模型;模型出现在 /models 中不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选与复用。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片显示在工具调用对应的左侧结果区域,模型收到状态和附件引用,不会额外伪造用户消息。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问 vibeui.top。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
|
|
3155
|
+
/** Append the live channel × model table so an Agent can honor user choices. */
|
|
3156
|
+
function guidanceFor(channels, defaultChannelId) {
|
|
3157
|
+
if (channels.length === 0) return `${IMAGEGEN_GUIDANCE} 尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥。`;
|
|
3158
|
+
const table = channels.map((channel) => {
|
|
3159
|
+
const aliases = channel.models.map((model) => model.alias).join("、");
|
|
3160
|
+
const mark = channel.id === defaultChannelId ? "(默认渠道)" : "";
|
|
3161
|
+
const key = channel.apiKey === "" ? "(未填密钥)" : "";
|
|
3162
|
+
const models = channel.models.length === 0 ? "未配置模型" : `可用模型:${aliases}`;
|
|
3163
|
+
return `渠道「${channel.name}」${mark}[${channel.apiUrl}] ${models}${key}`;
|
|
3164
|
+
}).join(";");
|
|
3165
|
+
return `${IMAGEGEN_GUIDANCE} 当前渠道与模型:${table}。用户指定模型名时取该模型所属渠道(多渠道同名用默认渠道);未指定模型时若仅一个可用模型可直接生成,若有多个应先询问用户选择「渠道 + 模型」。`;
|
|
3166
|
+
}
|
|
3167
|
+
/** Normalize raw channel entries into the wire shape (schema-adjacent guard). */
|
|
3168
|
+
function normalizeChannels(value) {
|
|
3169
|
+
if (!Array.isArray(value)) return [];
|
|
3170
|
+
const out = [];
|
|
3171
|
+
for (const item of value) {
|
|
3172
|
+
if (item === null || typeof item !== "object") continue;
|
|
3173
|
+
const raw = item;
|
|
3174
|
+
const id = typeof raw.id === "string" ? raw.id.trim() : "";
|
|
3175
|
+
if (id === "") continue;
|
|
3176
|
+
const models = [];
|
|
3177
|
+
if (Array.isArray(raw.models)) for (const entry of raw.models) {
|
|
3178
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
3179
|
+
const record = entry;
|
|
3180
|
+
const alias = typeof record.alias === "string" ? record.alias.trim() : "";
|
|
3181
|
+
const upstream = typeof record.id === "string" ? record.id.trim() : "";
|
|
3182
|
+
if (alias === "") continue;
|
|
3183
|
+
models.push({
|
|
3184
|
+
alias,
|
|
3185
|
+
id: upstream === "" ? alias : upstream
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
out.push({
|
|
3189
|
+
id,
|
|
3190
|
+
preset: typeof raw.preset === "string" ? raw.preset : "",
|
|
3191
|
+
name: typeof raw.name === "string" ? raw.name.trim() : "",
|
|
3192
|
+
apiUrl: typeof raw.apiUrl === "string" ? raw.apiUrl.trim() : "",
|
|
3193
|
+
models
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
return out;
|
|
2630
3197
|
}
|
|
2631
3198
|
/**
|
|
2632
3199
|
* Mount the settings section, routes, and announcement.
|
|
@@ -2636,47 +3203,82 @@ function guidanceFor(imageModels) {
|
|
|
2636
3203
|
function apply(ctx, config) {
|
|
2637
3204
|
let current = () => config ?? {};
|
|
2638
3205
|
const resolve = () => {
|
|
2639
|
-
const value = current();
|
|
3206
|
+
const value = current() ?? {};
|
|
3207
|
+
let channels = normalizeChannels(value.channels);
|
|
3208
|
+
const secrets = { ...value.channelSecrets ?? {} };
|
|
3209
|
+
if (channels.length === 0) {
|
|
3210
|
+
const legacyUrl = typeof value.apiUrl === "string" ? value.apiUrl.trim() : "";
|
|
3211
|
+
const legacyModels = Array.isArray(value.imageModels) ? value.imageModels.filter((model) => typeof model === "string" && model.trim() !== "").map((model) => ({
|
|
3212
|
+
alias: model.trim(),
|
|
3213
|
+
id: model.trim()
|
|
3214
|
+
})) : [];
|
|
3215
|
+
if (legacyUrl !== "" || legacyModels.length > 0) {
|
|
3216
|
+
channels = [{
|
|
3217
|
+
id: "default",
|
|
3218
|
+
preset: "",
|
|
3219
|
+
name: "默认渠道",
|
|
3220
|
+
apiUrl: legacyUrl,
|
|
3221
|
+
models: legacyModels
|
|
3222
|
+
}];
|
|
3223
|
+
const legacyKey = typeof value.apiKey === "string" ? value.apiKey.trim() : "";
|
|
3224
|
+
if (legacyKey !== "") secrets["default"] = legacyKey;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
const named = channels.map((channel) => ({
|
|
3228
|
+
...channel,
|
|
3229
|
+
name: channel.name === "" ? presetById(channel.preset)?.name ?? "未命名渠道" : channel.name
|
|
3230
|
+
}));
|
|
3231
|
+
const defaultChannelId = typeof value.defaultChannelId === "string" && named.some((channel) => channel.id === value.defaultChannelId) ? value.defaultChannelId : named[0]?.id ?? "";
|
|
2640
3232
|
return {
|
|
2641
3233
|
enabled: value.enabled ?? DEFAULT_ENABLED,
|
|
2642
3234
|
announceToAgent: value.announceToAgent ?? DEFAULT_ANNOUNCE,
|
|
2643
3235
|
allowAgentImageGeneration: value.allowAgentImageGeneration ?? DEFAULT_ALLOW_AGENT_IMAGE_GENERATION,
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
3236
|
+
channels: named.map((channel) => ({
|
|
3237
|
+
...channel,
|
|
3238
|
+
apiKey: typeof secrets[channel.id] === "string" ? secrets[channel.id] : ""
|
|
3239
|
+
})),
|
|
3240
|
+
defaultChannelId,
|
|
3241
|
+
promptApiUrl: typeof value.promptApiUrl === "string" ? value.promptApiUrl.trim() : "",
|
|
3242
|
+
promptApiKey: typeof value.promptApiKey === "string" ? value.promptApiKey.trim() : "",
|
|
3243
|
+
promptModel: typeof value.promptModel === "string" ? value.promptModel.trim() : ""
|
|
2650
3244
|
};
|
|
2651
3245
|
};
|
|
2652
|
-
const
|
|
3246
|
+
const channelsView = () => {
|
|
2653
3247
|
const value = resolve();
|
|
2654
3248
|
return {
|
|
2655
|
-
|
|
2656
|
-
|
|
3249
|
+
channels: value.channels,
|
|
3250
|
+
defaultChannelId: value.defaultChannelId
|
|
2657
3251
|
};
|
|
2658
|
-
}
|
|
2659
|
-
|
|
3252
|
+
};
|
|
3253
|
+
const runtime = new ImageGenerationRuntime(channelsView);
|
|
3254
|
+
ctx.inject(["settings", "attachments"], (sctx) => {
|
|
2660
3255
|
const seam = sctx.get("settings");
|
|
2661
3256
|
sctx.effect(() => {
|
|
2662
3257
|
const disposers = makeRoutes({
|
|
2663
3258
|
settings: seam,
|
|
2664
3259
|
resolve: () => {
|
|
2665
3260
|
const value = resolve();
|
|
3261
|
+
const channel = value.channels.find((candidate) => candidate.id === value.defaultChannelId) ?? value.channels[0];
|
|
2666
3262
|
return {
|
|
2667
|
-
apiUrl:
|
|
2668
|
-
apiKey:
|
|
3263
|
+
apiUrl: channel?.apiUrl ?? "",
|
|
3264
|
+
apiKey: channel?.apiKey ?? ""
|
|
2669
3265
|
};
|
|
2670
3266
|
},
|
|
3267
|
+
resolveChannels: channelsView,
|
|
2671
3268
|
resolvePrompt: () => {
|
|
2672
3269
|
const value = resolve();
|
|
3270
|
+
const channel = value.channels.find((candidate) => candidate.id === value.defaultChannelId) ?? value.channels[0];
|
|
2673
3271
|
return {
|
|
2674
|
-
apiUrl: value.promptApiUrl
|
|
2675
|
-
apiKey: value.promptApiKey
|
|
3272
|
+
apiUrl: value.promptApiUrl !== "" ? value.promptApiUrl : channel?.apiUrl ?? "",
|
|
3273
|
+
apiKey: value.promptApiKey !== "" ? value.promptApiKey : channel?.apiKey ?? "",
|
|
2676
3274
|
model: value.promptModel
|
|
2677
3275
|
};
|
|
2678
3276
|
},
|
|
2679
|
-
resolveImageModels: () =>
|
|
3277
|
+
resolveImageModels: () => {
|
|
3278
|
+
const value = resolve();
|
|
3279
|
+
return [...new Set(value.channels.flatMap((channel) => channel.models.map((model) => model.alias)))];
|
|
3280
|
+
},
|
|
3281
|
+
attachments: sctx.attachments,
|
|
2680
3282
|
runtime
|
|
2681
3283
|
}).map((route) => ctx.webServer.register(route));
|
|
2682
3284
|
return () => {
|
|
@@ -2690,9 +3292,8 @@ function apply(ctx, config) {
|
|
|
2690
3292
|
return {
|
|
2691
3293
|
enabled: value.enabled,
|
|
2692
3294
|
allowAgentImageGeneration: value.allowAgentImageGeneration,
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
imageModels: value.imageModels
|
|
3295
|
+
channels: value.channels,
|
|
3296
|
+
defaultChannelId: value.defaultChannelId
|
|
2696
3297
|
};
|
|
2697
3298
|
}), "dsh-imagegen: agent image tools");
|
|
2698
3299
|
});
|
|
@@ -2707,7 +3308,7 @@ function apply(ctx, config) {
|
|
|
2707
3308
|
disposeSection = ctx.systemPrompt.section({
|
|
2708
3309
|
name: "plugin:dsh-imagegen",
|
|
2709
3310
|
order: SECTION_ORDER,
|
|
2710
|
-
text: guidanceFor(value.
|
|
3311
|
+
text: guidanceFor(value.channels, value.defaultChannelId)
|
|
2711
3312
|
});
|
|
2712
3313
|
};
|
|
2713
3314
|
installSettingsSection(ctx, ImageGenSettingsNamespace, Config, config ?? {}, {
|