@dickpy/dsh-imagegen 1.2.3 → 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 +12 -11
- package/lib/client.js +2030 -903
- package/lib/client.js.map +1 -1
- package/lib/index.js +662 -117
- package/package.json +2 -2
- package/src/agent-image-tools.ts +132 -30
- package/src/client/ImageGenPanel.tsx +55 -19
- 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 +110 -0
- package/src/client/settings-card.module.css +487 -0
- package/src/client/settings-scope.ts +56 -8
- package/src/engine.ts +44 -30
- 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/index.ts +153 -47
- package/src/model-catalog.ts +98 -0
- package/src/presets.ts +63 -0
- package/src/protocol.ts +78 -5
- 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",
|
|
@@ -160,6 +170,113 @@ function normalizeImageModels(value) {
|
|
|
160
170
|
return unique.size > 0 ? [...unique] : [...DEFAULT_IMAGE_MODELS];
|
|
161
171
|
}
|
|
162
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
|
|
163
280
|
//#region src/engine.ts
|
|
164
281
|
/** A generation failure with a user-presentable message. */
|
|
165
282
|
var ImageGenError = class extends Error {
|
|
@@ -183,12 +300,20 @@ const DALLE3_SIZES = /* @__PURE__ */ new Set([
|
|
|
183
300
|
"1792x1024",
|
|
184
301
|
"1024x1792"
|
|
185
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
|
+
}
|
|
186
311
|
/** Whether the model is an xAI Grok Imagine model (grok-imagine-image,
|
|
187
312
|
* grok-imagine-image-2.0, …). Grok Imagine speaks JSON on both endpoints
|
|
188
313
|
* and exposes its own aspect-ratio / response-format knobs instead of the
|
|
189
314
|
* OpenAI size/quality/detail passthrough. */
|
|
190
315
|
function isGrokImagine(model) {
|
|
191
|
-
return
|
|
316
|
+
return modelFamily(model) === "grok";
|
|
192
317
|
}
|
|
193
318
|
/** Whether the model belongs to the Google Nano Banana family (nanobanana2 /
|
|
194
319
|
* nanobanana2-lite / nanobanana-pro, plus the official Gemini image IDs the
|
|
@@ -196,17 +321,24 @@ function isGrokImagine(model) {
|
|
|
196
321
|
* aspect_ratio / image_size vocabulary instead of the OpenAI size/quality
|
|
197
322
|
* passthrough. */
|
|
198
323
|
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";
|
|
324
|
+
return modelFamily(model) === "nanobanana";
|
|
201
325
|
}
|
|
202
326
|
/** Whether the model belongs to the ByteDance Seedream family (seedream-5.0-pro,
|
|
203
327
|
* seedream-5.0, seedream-4.x, doubao-seedream-…). OpenAI-compatible gateways
|
|
204
328
|
* 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). */
|
|
329
|
+
* generation AND editing both go to /images/generations and reference images
|
|
330
|
+
* are a JSON URL / data-URL array. */
|
|
208
331
|
function isSeedream(model) {
|
|
209
|
-
return
|
|
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";
|
|
210
342
|
}
|
|
211
343
|
/** The panel's aspect ratios mapped to the closest OpenAI pixel size
|
|
212
344
|
* (gpt-image-2 / generic OpenAI-compatible endpoints). */
|
|
@@ -284,7 +416,7 @@ function clampCount(n) {
|
|
|
284
416
|
* batch parameter is rejected by Responses-API-based gateways (tools[0].n),
|
|
285
417
|
* so the count is satisfied by parallel single-image requests instead. */
|
|
286
418
|
function effectiveParams(request) {
|
|
287
|
-
const model =
|
|
419
|
+
const model = wireModel(request);
|
|
288
420
|
if (model === "dall-e-3") {
|
|
289
421
|
const pixel = OPENAI_SIZE_BY_RATIO[request.size];
|
|
290
422
|
return {
|
|
@@ -306,9 +438,8 @@ function effectiveParams(request) {
|
|
|
306
438
|
};
|
|
307
439
|
if (isSeedream(model)) return {
|
|
308
440
|
model,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
response_format: "b64_json"
|
|
441
|
+
size: seedreamSize(request.quality),
|
|
442
|
+
response_format: isVolcSeedream(model) ? "url" : "b64_json"
|
|
312
443
|
};
|
|
313
444
|
return {
|
|
314
445
|
model,
|
|
@@ -321,17 +452,20 @@ function effectiveParams(request) {
|
|
|
321
452
|
}
|
|
322
453
|
/** How many single-image requests to issue for the requested image count. */
|
|
323
454
|
function effectiveCount(request) {
|
|
324
|
-
if ((request
|
|
455
|
+
if (wireModel(request) === "dall-e-3") return 1;
|
|
325
456
|
return clampCount(request.n);
|
|
326
457
|
}
|
|
327
458
|
/** Normalize one upstream data item into a base64 image. */
|
|
328
459
|
async function normalizeItem(item, upstream) {
|
|
329
460
|
const revisedPrompt = typeof item.revised_prompt === "string" ? item.revised_prompt : void 0;
|
|
330
|
-
if (typeof item.b64_json === "string")
|
|
331
|
-
b64
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
+
}
|
|
335
469
|
if (typeof item.url !== "string" || item.url === "") throw new ImageGenError("upstream image item has neither b64_json nor url");
|
|
336
470
|
const url = item.url;
|
|
337
471
|
if (url.startsWith("data:")) {
|
|
@@ -339,7 +473,7 @@ async function normalizeItem(item, upstream) {
|
|
|
339
473
|
if (parsed === void 0) throw new ImageGenError("upstream returned a malformed data: url");
|
|
340
474
|
return {
|
|
341
475
|
b64: parsed.base64,
|
|
342
|
-
mime: parsed.mime,
|
|
476
|
+
mime: detectImageMime(Buffer.from(parsed.base64, "base64")) ?? parsed.mime,
|
|
343
477
|
revisedPrompt
|
|
344
478
|
};
|
|
345
479
|
}
|
|
@@ -358,7 +492,7 @@ async function normalizeItem(item, upstream) {
|
|
|
358
492
|
if (!response.ok) throw new ImageGenError(`failed to fetch the generated image url: HTTP ${response.status}`);
|
|
359
493
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
360
494
|
const contentType = response.headers.get("content-type");
|
|
361
|
-
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");
|
|
362
496
|
return {
|
|
363
497
|
b64: buffer.toString("base64"),
|
|
364
498
|
mime,
|
|
@@ -411,7 +545,7 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
411
545
|
image: [request.image],
|
|
412
546
|
...params.size !== void 0 ? { size: params.size } : {},
|
|
413
547
|
...params.resolution !== void 0 ? { resolution: params.resolution } : {},
|
|
414
|
-
response_format: "b64_json"
|
|
548
|
+
response_format: isVolcSeedream(params.model) ? "url" : "b64_json"
|
|
415
549
|
});
|
|
416
550
|
} else {
|
|
417
551
|
const form = new FormData();
|
|
@@ -602,7 +736,9 @@ function toWire$1(entry) {
|
|
|
602
736
|
mime: image.mime,
|
|
603
737
|
...image.revisedPrompt === void 0 ? {} : { revisedPrompt: image.revisedPrompt }
|
|
604
738
|
})),
|
|
605
|
-
...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 }
|
|
606
742
|
};
|
|
607
743
|
}
|
|
608
744
|
/** List the persisted history, newest first, as wire entries. */
|
|
@@ -641,7 +777,9 @@ async function appendHistory(input) {
|
|
|
641
777
|
detail: input.detail,
|
|
642
778
|
n: input.n,
|
|
643
779
|
images: storedImages,
|
|
644
|
-
...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 }
|
|
645
783
|
}, ...await readIndex$1()];
|
|
646
784
|
const kept = merged.slice(0, 50);
|
|
647
785
|
for (const dropped of merged.slice(50)) await removeEntryFiles$1(dropped);
|
|
@@ -783,6 +921,11 @@ var GenerationTaskQueue = class {
|
|
|
783
921
|
* Shared host-side generation runtime. Both the browser routes and Agent tools
|
|
784
922
|
* submit to this one queue so persisted history and cancellation semantics stay
|
|
785
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.
|
|
786
929
|
*/
|
|
787
930
|
var ImageGenerationRuntime = class {
|
|
788
931
|
resolve;
|
|
@@ -794,7 +937,13 @@ var ImageGenerationRuntime = class {
|
|
|
794
937
|
this.queue = new GenerationTaskQueue((request, signal) => this.run(request, signal));
|
|
795
938
|
}
|
|
796
939
|
async run(request, signal) {
|
|
797
|
-
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 });
|
|
798
947
|
try {
|
|
799
948
|
const history = await this.history.append({
|
|
800
949
|
id: randomUUID(),
|
|
@@ -807,7 +956,9 @@ var ImageGenerationRuntime = class {
|
|
|
807
956
|
detail: request.detail,
|
|
808
957
|
n: request.n,
|
|
809
958
|
images: result.images,
|
|
810
|
-
...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 }
|
|
811
962
|
});
|
|
812
963
|
return {
|
|
813
964
|
...result,
|
|
@@ -931,7 +1082,9 @@ function toWire(entry) {
|
|
|
931
1082
|
...image.revisedPrompt === void 0 ? {} : { revisedPrompt: image.revisedPrompt }
|
|
932
1083
|
})),
|
|
933
1084
|
...entry.refName === void 0 ? {} : { refName: entry.refName },
|
|
934
|
-
...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 }
|
|
935
1088
|
};
|
|
936
1089
|
}
|
|
937
1090
|
/** List the persisted gallery, newest first, as wire entries. */
|
|
@@ -981,7 +1134,9 @@ async function appendGallery(input) {
|
|
|
981
1134
|
n: input.n,
|
|
982
1135
|
images: storedImages,
|
|
983
1136
|
...hash === void 0 ? {} : { hash },
|
|
984
|
-
...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 }
|
|
985
1140
|
}, ...await readIndex()];
|
|
986
1141
|
await writeIndex(merged);
|
|
987
1142
|
return {
|
|
@@ -1403,6 +1558,57 @@ function clearUpdateCache() {
|
|
|
1403
1558
|
cached = void 0;
|
|
1404
1559
|
}
|
|
1405
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
|
|
1406
1612
|
//#region src/routes.ts
|
|
1407
1613
|
/** Cap on JSON request bodies (settings ops and generate payloads are small). */
|
|
1408
1614
|
const MAX_JSON_BODY_BYTES = 24 * 1024 * 1024;
|
|
@@ -1472,7 +1678,8 @@ function parseGenerateRequest(body) {
|
|
|
1472
1678
|
n: typeof body.n === "number" ? body.n : 1,
|
|
1473
1679
|
detail: typeof body.detail === "string" ? body.detail : "",
|
|
1474
1680
|
...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {},
|
|
1475
|
-
...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 } : {}
|
|
1476
1683
|
};
|
|
1477
1684
|
}
|
|
1478
1685
|
/** Validate a submitted history entry (images carry base64). */
|
|
@@ -1508,7 +1715,9 @@ function parseHistoryEntryInput(body) {
|
|
|
1508
1715
|
detail: entry.detail,
|
|
1509
1716
|
n: entry.n,
|
|
1510
1717
|
images,
|
|
1511
|
-
...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 } : {}
|
|
1512
1721
|
};
|
|
1513
1722
|
}
|
|
1514
1723
|
/** Extract the image file name from a history-image request URL. */
|
|
@@ -1523,6 +1732,33 @@ function imageFileFrom(rawUrl, basePath) {
|
|
|
1523
1732
|
if (!pathname.startsWith(`${basePath}/`)) return void 0;
|
|
1524
1733
|
return decodeURIComponent(pathname.slice(basePath.length + 1));
|
|
1525
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
|
+
}
|
|
1526
1762
|
/** Project one settings descriptor onto the bridge wire view. */
|
|
1527
1763
|
function toView(descriptor) {
|
|
1528
1764
|
return {
|
|
@@ -1583,18 +1819,85 @@ function makeRoutes(deps) {
|
|
|
1583
1819
|
model: ""
|
|
1584
1820
|
}));
|
|
1585
1821
|
const resolveImageModels = deps.resolveImageModels ?? (() => normalizeImageModels(void 0));
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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
|
+
};
|
|
1592
1836
|
return {
|
|
1593
|
-
|
|
1594
|
-
|
|
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
|
+
}
|
|
1595
1899
|
};
|
|
1596
1900
|
};
|
|
1597
|
-
const runtime = deps.runtime ?? new ImageGenerationRuntime(deps.resolve, history);
|
|
1598
1901
|
const guard = (req, res, method) => {
|
|
1599
1902
|
if (!isLoopbackRequest(req)) {
|
|
1600
1903
|
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
@@ -1607,15 +1910,52 @@ function makeRoutes(deps) {
|
|
|
1607
1910
|
return true;
|
|
1608
1911
|
};
|
|
1609
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
|
+
}],
|
|
1610
1943
|
{
|
|
1611
1944
|
kind: "exact",
|
|
1612
1945
|
path: IMAGE_MODEL_API.models,
|
|
1613
1946
|
handler: async (req, res) => {
|
|
1614
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
|
+
};
|
|
1615
1955
|
try {
|
|
1616
1956
|
writeJson(res, 200, {
|
|
1617
1957
|
ok: true,
|
|
1618
|
-
models: await listOpenAIModels(
|
|
1958
|
+
models: await listOpenAIModels(upstream)
|
|
1619
1959
|
});
|
|
1620
1960
|
} catch (error) {
|
|
1621
1961
|
writeJson(res, 200, {
|
|
@@ -1626,6 +1966,55 @@ function makeRoutes(deps) {
|
|
|
1626
1966
|
}
|
|
1627
1967
|
}
|
|
1628
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
|
+
},
|
|
1629
2018
|
{
|
|
1630
2019
|
kind: "exact",
|
|
1631
2020
|
path: PROMPT_ENHANCE_API.models,
|
|
@@ -1740,37 +2129,28 @@ function makeRoutes(deps) {
|
|
|
1740
2129
|
handler: async (req, res) => {
|
|
1741
2130
|
if (!guard(req, res, "POST")) return;
|
|
1742
2131
|
const body = await readJsonBody(req);
|
|
1743
|
-
|
|
2132
|
+
const parsed = body === void 0 ? void 0 : parseGenerateRequest(body);
|
|
2133
|
+
if (parsed === void 0) {
|
|
1744
2134
|
writeJson(res, 200, {
|
|
1745
2135
|
ok: false,
|
|
1746
2136
|
code: "bad-request",
|
|
1747
|
-
message: "
|
|
1748
|
-
});
|
|
1749
|
-
return;
|
|
1750
|
-
}
|
|
1751
|
-
let request;
|
|
1752
|
-
try {
|
|
1753
|
-
request = parseConfiguredRequest(body);
|
|
1754
|
-
} catch (error) {
|
|
1755
|
-
writeJson(res, 200, {
|
|
1756
|
-
ok: false,
|
|
1757
|
-
code: "image-model-not-configured",
|
|
1758
|
-
message: messageOf(error)
|
|
2137
|
+
message: "prompt is required"
|
|
1759
2138
|
});
|
|
1760
2139
|
return;
|
|
1761
2140
|
}
|
|
1762
|
-
|
|
2141
|
+
const resolved = resolveChannelRequest(parsed);
|
|
2142
|
+
if (!resolved.ok) {
|
|
1763
2143
|
writeJson(res, 200, {
|
|
1764
2144
|
ok: false,
|
|
1765
|
-
code:
|
|
1766
|
-
message:
|
|
2145
|
+
code: resolved.code,
|
|
2146
|
+
message: resolved.message
|
|
1767
2147
|
});
|
|
1768
2148
|
return;
|
|
1769
2149
|
}
|
|
1770
2150
|
try {
|
|
1771
2151
|
writeJson(res, 200, {
|
|
1772
2152
|
ok: true,
|
|
1773
|
-
...await runtime.run(request)
|
|
2153
|
+
...await runtime.run(resolved.request)
|
|
1774
2154
|
});
|
|
1775
2155
|
} catch (error) {
|
|
1776
2156
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1788,28 +2168,27 @@ function makeRoutes(deps) {
|
|
|
1788
2168
|
handler: async (req, res) => {
|
|
1789
2169
|
if (!guard(req, res, "POST")) return;
|
|
1790
2170
|
const body = await readJsonBody(req);
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
request = body === void 0 ? void 0 : parseConfiguredRequest(body);
|
|
1794
|
-
} catch (error) {
|
|
2171
|
+
const parsed = body === void 0 ? void 0 : parseGenerateRequest(body);
|
|
2172
|
+
if (parsed === void 0) {
|
|
1795
2173
|
writeJson(res, 200, {
|
|
1796
2174
|
ok: false,
|
|
1797
|
-
code: "
|
|
1798
|
-
message:
|
|
2175
|
+
code: "bad-request",
|
|
2176
|
+
message: "prompt is required"
|
|
1799
2177
|
});
|
|
1800
2178
|
return;
|
|
1801
2179
|
}
|
|
1802
|
-
|
|
2180
|
+
const resolved = resolveChannelRequest(parsed);
|
|
2181
|
+
if (!resolved.ok) {
|
|
1803
2182
|
writeJson(res, 200, {
|
|
1804
2183
|
ok: false,
|
|
1805
|
-
code:
|
|
1806
|
-
message:
|
|
2184
|
+
code: resolved.code,
|
|
2185
|
+
message: resolved.message
|
|
1807
2186
|
});
|
|
1808
2187
|
return;
|
|
1809
2188
|
}
|
|
1810
2189
|
writeJson(res, 200, {
|
|
1811
2190
|
ok: true,
|
|
1812
|
-
task: runtime.queue.submit(request)
|
|
2191
|
+
task: runtime.queue.submit(resolved.request)
|
|
1813
2192
|
});
|
|
1814
2193
|
}
|
|
1815
2194
|
},
|
|
@@ -2386,10 +2765,54 @@ function renderTaskResult(value) {
|
|
|
2386
2765
|
return [{
|
|
2387
2766
|
type: "text",
|
|
2388
2767
|
text: JSON.stringify(value)
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
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
|
+
};
|
|
2393
2816
|
}
|
|
2394
2817
|
/** Register the global Agent tools and unregister them with the plugin lifecycle. */
|
|
2395
2818
|
function registerAgentImageTools(ctx, runtime, resolve) {
|
|
@@ -2398,13 +2821,32 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2398
2821
|
const config = resolve();
|
|
2399
2822
|
if (!config.enabled) throw new ImageGenError("AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.", "plugin-disabled");
|
|
2400
2823
|
if (!config.allowAgentImageGeneration) throw new ImageGenError("Agent image generation is disabled in Settings > Plugins > AI Image.", "agent-generation-disabled");
|
|
2401
|
-
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");
|
|
2402
2825
|
};
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
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];
|
|
2408
2850
|
};
|
|
2409
2851
|
const materializeTaskImages = (task) => {
|
|
2410
2852
|
if (task.status !== "completed") return Promise.resolve([]);
|
|
@@ -2422,7 +2864,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2422
2864
|
return {
|
|
2423
2865
|
task_id: task.id,
|
|
2424
2866
|
status: task.status,
|
|
2425
|
-
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.",
|
|
2426
2868
|
...task.error === void 0 ? {} : { error: task.error },
|
|
2427
2869
|
images
|
|
2428
2870
|
};
|
|
@@ -2491,7 +2933,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2491
2933
|
const disposers = [
|
|
2492
2934
|
ctx.tools.register(defineTool({
|
|
2493
2935
|
name: "generate_image",
|
|
2494
|
-
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.",
|
|
2495
2937
|
parameters: {
|
|
2496
2938
|
prompt: {
|
|
2497
2939
|
type: "string",
|
|
@@ -2525,13 +2967,19 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2525
2967
|
},
|
|
2526
2968
|
output: {
|
|
2527
2969
|
schema: taskResultSchema,
|
|
2528
|
-
render: (_args, value) => renderTaskResult(value)
|
|
2970
|
+
render: (_args, value) => renderTaskResult(value),
|
|
2971
|
+
presentationMeta: (_args, value) => imagePresentationMeta(value)
|
|
2529
2972
|
},
|
|
2973
|
+
presentResult: presentImageResult,
|
|
2530
2974
|
async execute(args, exec) {
|
|
2531
2975
|
ensureConfigured();
|
|
2976
|
+
const picked = resolveModel(args.model);
|
|
2532
2977
|
const task = runtime.queue.submit({
|
|
2533
2978
|
mode: "text",
|
|
2534
|
-
model:
|
|
2979
|
+
model: picked.alias,
|
|
2980
|
+
upstream: picked.upstream,
|
|
2981
|
+
channelId: picked.channel.id,
|
|
2982
|
+
channel: picked.channel.name,
|
|
2535
2983
|
prompt: args.prompt.trim(),
|
|
2536
2984
|
size: args.size ?? "auto",
|
|
2537
2985
|
quality: args.quality ?? "auto",
|
|
@@ -2543,7 +2991,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2543
2991
|
})),
|
|
2544
2992
|
ctx.tools.register(defineTool({
|
|
2545
2993
|
name: "edit_image",
|
|
2546
|
-
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.",
|
|
2547
2995
|
parameters: {
|
|
2548
2996
|
prompt: {
|
|
2549
2997
|
type: "string",
|
|
@@ -2582,14 +3030,20 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2582
3030
|
},
|
|
2583
3031
|
output: {
|
|
2584
3032
|
schema: taskResultSchema,
|
|
2585
|
-
render: (_args, value) => renderTaskResult(value)
|
|
3033
|
+
render: (_args, value) => renderTaskResult(value),
|
|
3034
|
+
presentationMeta: (_args, value) => imagePresentationMeta(value)
|
|
2586
3035
|
},
|
|
3036
|
+
presentResult: presentImageResult,
|
|
2587
3037
|
async execute(args, exec) {
|
|
2588
3038
|
ensureConfigured();
|
|
2589
3039
|
const reference = await ctx.attachments.readImage(restoreRef(args.source_image), exec.signal);
|
|
3040
|
+
const picked = resolveModel(args.model);
|
|
2590
3041
|
const task = runtime.queue.submit({
|
|
2591
3042
|
mode: "edit",
|
|
2592
|
-
model:
|
|
3043
|
+
model: picked.alias,
|
|
3044
|
+
upstream: picked.upstream,
|
|
3045
|
+
channelId: picked.channel.id,
|
|
3046
|
+
channel: picked.channel.name,
|
|
2593
3047
|
prompt: args.prompt.trim(),
|
|
2594
3048
|
size: args.size ?? "auto",
|
|
2595
3049
|
quality: args.quality ?? "auto",
|
|
@@ -2603,7 +3057,7 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2603
3057
|
})),
|
|
2604
3058
|
ctx.tools.register(defineTool({
|
|
2605
3059
|
name: "get_image_generation_task",
|
|
2606
|
-
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.",
|
|
2607
3061
|
parameters: { task_id: {
|
|
2608
3062
|
type: "string",
|
|
2609
3063
|
required: true,
|
|
@@ -2611,8 +3065,10 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2611
3065
|
} },
|
|
2612
3066
|
output: {
|
|
2613
3067
|
schema: taskResultSchema,
|
|
2614
|
-
render: (_args, value) => renderTaskResult(value)
|
|
3068
|
+
render: (_args, value) => renderTaskResult(value),
|
|
3069
|
+
presentationMeta: (_args, value) => imagePresentationMeta(value)
|
|
2615
3070
|
},
|
|
3071
|
+
presentResult: presentImageResult,
|
|
2616
3072
|
async execute(args) {
|
|
2617
3073
|
ensureConfigured();
|
|
2618
3074
|
return taskResult(findTask(args.task_id));
|
|
@@ -2628,8 +3084,10 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2628
3084
|
} },
|
|
2629
3085
|
output: {
|
|
2630
3086
|
schema: taskResultSchema,
|
|
2631
|
-
render: (_args, value) => renderTaskResult(value)
|
|
3087
|
+
render: (_args, value) => renderTaskResult(value),
|
|
3088
|
+
presentationMeta: (_args, value) => imagePresentationMeta(value)
|
|
2632
3089
|
},
|
|
3090
|
+
presentResult: presentImageResult,
|
|
2633
3091
|
async execute(args) {
|
|
2634
3092
|
ensureConfigured();
|
|
2635
3093
|
const task = runtime.queue.cancel(args.task_id);
|
|
@@ -2646,9 +3104,11 @@ function isFinalTask(task) {
|
|
|
2646
3104
|
return task.status === "completed" || task.status === "failed" || task.status === "cancelled";
|
|
2647
3105
|
}
|
|
2648
3106
|
function toSaveImage(image, taskId, index) {
|
|
2649
|
-
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;
|
|
2650
3110
|
return {
|
|
2651
|
-
data
|
|
3111
|
+
data,
|
|
2652
3112
|
mediaType,
|
|
2653
3113
|
name: `imagegen-${taskId}-${index + 1}.${mediaType === "image/jpeg" ? "jpg" : mediaType.slice(6)}`
|
|
2654
3114
|
};
|
|
@@ -2665,12 +3125,24 @@ const Config = z.object({
|
|
|
2665
3125
|
enabled: z.boolean().default(true),
|
|
2666
3126
|
announceToAgent: z.boolean().default(true),
|
|
2667
3127
|
allowAgentImageGeneration: z.boolean().default(true),
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
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(""),
|
|
2671
3140
|
promptApiUrl: z.string().default(""),
|
|
2672
3141
|
promptApiKey: z.string().role("secret").default(""),
|
|
2673
|
-
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([])
|
|
2674
3146
|
});
|
|
2675
3147
|
/** Schema defaults, re-read for hand-built contexts (the loader applies them normally). */
|
|
2676
3148
|
const DEFAULT_ENABLED = true;
|
|
@@ -2679,10 +3151,49 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
|
|
|
2679
3151
|
/** Order of the announcement section within the tool-guidance band. */
|
|
2680
3152
|
const SECTION_ORDER = 150;
|
|
2681
3153
|
/** 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}
|
|
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;
|
|
2686
3197
|
}
|
|
2687
3198
|
/**
|
|
2688
3199
|
* Mount the settings section, routes, and announcement.
|
|
@@ -2692,47 +3203,82 @@ function guidanceFor(imageModels) {
|
|
|
2692
3203
|
function apply(ctx, config) {
|
|
2693
3204
|
let current = () => config ?? {};
|
|
2694
3205
|
const resolve = () => {
|
|
2695
|
-
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 ?? "";
|
|
2696
3232
|
return {
|
|
2697
3233
|
enabled: value.enabled ?? DEFAULT_ENABLED,
|
|
2698
3234
|
announceToAgent: value.announceToAgent ?? DEFAULT_ANNOUNCE,
|
|
2699
3235
|
allowAgentImageGeneration: value.allowAgentImageGeneration ?? DEFAULT_ALLOW_AGENT_IMAGE_GENERATION,
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
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() : ""
|
|
2706
3244
|
};
|
|
2707
3245
|
};
|
|
2708
|
-
const
|
|
3246
|
+
const channelsView = () => {
|
|
2709
3247
|
const value = resolve();
|
|
2710
3248
|
return {
|
|
2711
|
-
|
|
2712
|
-
|
|
3249
|
+
channels: value.channels,
|
|
3250
|
+
defaultChannelId: value.defaultChannelId
|
|
2713
3251
|
};
|
|
2714
|
-
}
|
|
2715
|
-
|
|
3252
|
+
};
|
|
3253
|
+
const runtime = new ImageGenerationRuntime(channelsView);
|
|
3254
|
+
ctx.inject(["settings", "attachments"], (sctx) => {
|
|
2716
3255
|
const seam = sctx.get("settings");
|
|
2717
3256
|
sctx.effect(() => {
|
|
2718
3257
|
const disposers = makeRoutes({
|
|
2719
3258
|
settings: seam,
|
|
2720
3259
|
resolve: () => {
|
|
2721
3260
|
const value = resolve();
|
|
3261
|
+
const channel = value.channels.find((candidate) => candidate.id === value.defaultChannelId) ?? value.channels[0];
|
|
2722
3262
|
return {
|
|
2723
|
-
apiUrl:
|
|
2724
|
-
apiKey:
|
|
3263
|
+
apiUrl: channel?.apiUrl ?? "",
|
|
3264
|
+
apiKey: channel?.apiKey ?? ""
|
|
2725
3265
|
};
|
|
2726
3266
|
},
|
|
3267
|
+
resolveChannels: channelsView,
|
|
2727
3268
|
resolvePrompt: () => {
|
|
2728
3269
|
const value = resolve();
|
|
3270
|
+
const channel = value.channels.find((candidate) => candidate.id === value.defaultChannelId) ?? value.channels[0];
|
|
2729
3271
|
return {
|
|
2730
|
-
apiUrl: value.promptApiUrl
|
|
2731
|
-
apiKey: value.promptApiKey
|
|
3272
|
+
apiUrl: value.promptApiUrl !== "" ? value.promptApiUrl : channel?.apiUrl ?? "",
|
|
3273
|
+
apiKey: value.promptApiKey !== "" ? value.promptApiKey : channel?.apiKey ?? "",
|
|
2732
3274
|
model: value.promptModel
|
|
2733
3275
|
};
|
|
2734
3276
|
},
|
|
2735
|
-
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,
|
|
2736
3282
|
runtime
|
|
2737
3283
|
}).map((route) => ctx.webServer.register(route));
|
|
2738
3284
|
return () => {
|
|
@@ -2746,9 +3292,8 @@ function apply(ctx, config) {
|
|
|
2746
3292
|
return {
|
|
2747
3293
|
enabled: value.enabled,
|
|
2748
3294
|
allowAgentImageGeneration: value.allowAgentImageGeneration,
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
imageModels: value.imageModels
|
|
3295
|
+
channels: value.channels,
|
|
3296
|
+
defaultChannelId: value.defaultChannelId
|
|
2752
3297
|
};
|
|
2753
3298
|
}), "dsh-imagegen: agent image tools");
|
|
2754
3299
|
});
|
|
@@ -2763,7 +3308,7 @@ function apply(ctx, config) {
|
|
|
2763
3308
|
disposeSection = ctx.systemPrompt.section({
|
|
2764
3309
|
name: "plugin:dsh-imagegen",
|
|
2765
3310
|
order: SECTION_ORDER,
|
|
2766
|
-
text: guidanceFor(value.
|
|
3311
|
+
text: guidanceFor(value.channels, value.defaultChannelId)
|
|
2767
3312
|
});
|
|
2768
3313
|
};
|
|
2769
3314
|
installSettingsSection(ctx, ImageGenSettingsNamespace, Config, config ?? {}, {
|