@dickpy/dsh-imagegen 1.3.0 → 1.5.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 +363 -196
- package/docs/images/ecommerce-mode.png +0 -0
- package/docs/images/image-generation-studio-three-column.png +0 -0
- package/docs/images/imagegen-overview.png +0 -0
- package/docs/images/multi-model-comparison.png +0 -0
- package/docs/videos/agent-chat-edit.gif +0 -0
- package/docs/videos/agent-chat-edit.mp4 +0 -0
- package/lib/client.js +2589 -884
- package/lib/client.js.map +1 -1
- package/lib/index.js +585 -240
- package/package.json +5 -2
- package/src/agent-image-tools.ts +131 -102
- package/src/client/ImageGenPanel.tsx +2679 -1594
- package/src/client/SettingsCard.tsx +6 -27
- package/src/client/api.ts +11 -1
- package/src/client/conversation-sync.ts +14 -0
- package/src/client/image-toolview.tsx +176 -165
- package/src/client/index.ts +25 -15
- package/src/client/locales.ts +746 -602
- package/src/client/mount.tsx +213 -124
- package/src/client/panel.module.css +2619 -1563
- package/src/client/sidebar-entry.ts +190 -144
- package/src/edit-image-command.ts +110 -0
- package/src/engine.ts +47 -5
- package/src/gallery-store.ts +20 -0
- package/src/generation-runtime.ts +11 -2
- package/src/history-store.ts +26 -0
- package/src/image-models.ts +1 -1
- package/src/index.ts +31 -12
- package/src/model-catalog.ts +19 -2
- package/src/presets.ts +11 -3
- package/src/prompt-enhancer.ts +63 -5
- package/src/protocol.ts +59 -5
- package/src/routes.ts +62 -4
- package/src/task-queue.ts +42 -32
- package/docs/images/agent-chat-edit.png +0 -0
- package/docs/images/agent-chat-generate.png +0 -0
- package/docs/images/agent-chat-poster-workflow.png +0 -0
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.5.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",
|
|
@@ -35,6 +35,8 @@ const IMAGE_MODEL_API = { models: "/api/dsh-imagegen/image-models" };
|
|
|
35
35
|
const PRESETS_API = "/api/dsh-imagegen/presets";
|
|
36
36
|
/** Loopback-only image reader for Agent tool-result previews. */
|
|
37
37
|
const AGENT_IMAGE_API = "/api/dsh-imagegen/agent-image";
|
|
38
|
+
/** Store the current composer image for the direct edit_image command. */
|
|
39
|
+
const CONVERSATION_IMAGE_API = "/api/dsh-imagegen/conversation-image";
|
|
38
40
|
/**
|
|
39
41
|
* Host-computed per-channel usage counters (generation-count badges in the
|
|
40
42
|
* settings card): entries are tallied from the persisted history and gallery
|
|
@@ -91,94 +93,6 @@ const TEMPLATES_API = {
|
|
|
91
93
|
image: "/api/dsh-imagegen/templates/image"
|
|
92
94
|
};
|
|
93
95
|
//#endregion
|
|
94
|
-
//#region src/prompt-enhancer.ts
|
|
95
|
-
function endpoint(base, suffix) {
|
|
96
|
-
return `${base.replace(/\/+$/, "")}${suffix}`;
|
|
97
|
-
}
|
|
98
|
-
function headers(apiKey) {
|
|
99
|
-
return {
|
|
100
|
-
"content-type": "application/json",
|
|
101
|
-
...apiKey.trim() === "" ? {} : { authorization: `Bearer ${apiKey.trim()}` }
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
async function responseJson(response) {
|
|
105
|
-
const body = await response.json().catch(() => void 0);
|
|
106
|
-
if (!response.ok || body === void 0 || body === null || typeof body !== "object") {
|
|
107
|
-
const message = body !== null && typeof body === "object" && typeof body.error?.message === "string" ? body.error.message : `HTTP ${response.status}`;
|
|
108
|
-
throw new Error(message);
|
|
109
|
-
}
|
|
110
|
-
return body;
|
|
111
|
-
}
|
|
112
|
-
/** List candidates exposed by an OpenAI-compatible endpoint. */
|
|
113
|
-
async function listOpenAIModels(config) {
|
|
114
|
-
if (config.apiUrl.trim() === "") throw new Error("API URL is required");
|
|
115
|
-
const body = await responseJson(await fetch(endpoint(config.apiUrl, "/models"), { headers: headers(config.apiKey) }));
|
|
116
|
-
const data = Array.isArray(body.data) ? body.data : [];
|
|
117
|
-
return [...new Set(data.flatMap((item) => item !== null && typeof item === "object" && typeof item.id === "string" ? [item.id.trim()] : []).filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
|
118
|
-
}
|
|
119
|
-
/** List chat models exposed by an OpenAI-compatible endpoint. */
|
|
120
|
-
async function listPromptModels(config) {
|
|
121
|
-
return listOpenAIModels(config);
|
|
122
|
-
}
|
|
123
|
-
/** Expand a concise image request into a production-ready image prompt. */
|
|
124
|
-
async function enhancePrompt(config, prompt) {
|
|
125
|
-
if (config.apiUrl.trim() === "" || config.model.trim() === "") throw new Error("prompt enhancement model is not configured");
|
|
126
|
-
const body = await responseJson(await fetch(endpoint(config.apiUrl, "/chat/completions"), {
|
|
127
|
-
method: "POST",
|
|
128
|
-
headers: headers(config.apiKey),
|
|
129
|
-
body: JSON.stringify({
|
|
130
|
-
model: config.model.trim(),
|
|
131
|
-
temperature: .7,
|
|
132
|
-
messages: [{
|
|
133
|
-
role: "system",
|
|
134
|
-
content: "You are an expert image-prompt editor. Expand the user request into one vivid, specific image-generation prompt. Preserve intent and language. Add only useful visual detail: subject, composition, lighting, materials, color, camera/style and quality. Return only the finished prompt, with no preface or markdown."
|
|
135
|
-
}, {
|
|
136
|
-
role: "user",
|
|
137
|
-
content: prompt
|
|
138
|
-
}]
|
|
139
|
-
})
|
|
140
|
-
}));
|
|
141
|
-
const choices = Array.isArray(body.choices) ? body.choices : [];
|
|
142
|
-
const content = choices[0] !== null && typeof choices[0] === "object" ? choices[0].message?.content : void 0;
|
|
143
|
-
if (typeof content !== "string" || content.trim() === "") throw new Error("chat model returned an empty prompt");
|
|
144
|
-
return content.trim();
|
|
145
|
-
}
|
|
146
|
-
//#endregion
|
|
147
|
-
//#region src/image-models.ts
|
|
148
|
-
/**
|
|
149
|
-
* Image-model configuration shared by the host, panel, and Agent tools.
|
|
150
|
-
* `/models` exposes candidates only: the configured list is the explicit
|
|
151
|
-
* allow-list because OpenAI-compatible gateways rarely advertise modalities.
|
|
152
|
-
*/
|
|
153
|
-
const DEFAULT_IMAGE_MODELS = [
|
|
154
|
-
"gpt-image-2",
|
|
155
|
-
"grok-imagine-image",
|
|
156
|
-
"nanobanana2",
|
|
157
|
-
"nanobanana2-lite",
|
|
158
|
-
"nanobanana-pro",
|
|
159
|
-
"seedream-5.0-pro"
|
|
160
|
-
];
|
|
161
|
-
/** Normalize user-entered model identifiers and retain a usable legacy default. */
|
|
162
|
-
function normalizeImageModels(value) {
|
|
163
|
-
const candidates = Array.isArray(value) ? value : [];
|
|
164
|
-
const unique = /* @__PURE__ */ new Set();
|
|
165
|
-
for (const candidate of candidates) {
|
|
166
|
-
if (typeof candidate !== "string") continue;
|
|
167
|
-
const model = candidate.trim();
|
|
168
|
-
if (model !== "") unique.add(model);
|
|
169
|
-
}
|
|
170
|
-
return unique.size > 0 ? [...unique] : [...DEFAULT_IMAGE_MODELS];
|
|
171
|
-
}
|
|
172
|
-
//#endregion
|
|
173
|
-
//#region src/image-format.ts
|
|
174
|
-
function detectImageMime(data) {
|
|
175
|
-
const startsWith = (...bytes) => bytes.every((value, index) => data[index] === value);
|
|
176
|
-
if (startsWith(137, 80, 78, 71, 13, 10, 26, 10)) return "image/png";
|
|
177
|
-
if (startsWith(255, 216, 255)) return "image/jpeg";
|
|
178
|
-
if (startsWith(71, 73, 70, 56, 55, 97) || startsWith(71, 73, 70, 56, 57, 97)) return "image/gif";
|
|
179
|
-
if (startsWith(82, 73, 70, 70) && data[8] === 87 && data[9] === 69 && data[10] === 66 && data[11] === 80) return "image/webp";
|
|
180
|
-
}
|
|
181
|
-
//#endregion
|
|
182
96
|
//#region src/model-catalog.ts
|
|
183
97
|
const ENTRIES = {
|
|
184
98
|
"gpt-image": {
|
|
@@ -228,6 +142,14 @@ const ENTRIES = {
|
|
|
228
142
|
supportsEdit: true,
|
|
229
143
|
supportsAspectRatio: true,
|
|
230
144
|
qualityTiers: ["1K", "2K"]
|
|
145
|
+
},
|
|
146
|
+
zhipu: {
|
|
147
|
+
label: "GLM-Image",
|
|
148
|
+
labelZh: "智谱图像",
|
|
149
|
+
known: true,
|
|
150
|
+
supportsEdit: false,
|
|
151
|
+
supportsAspectRatio: false,
|
|
152
|
+
qualityTiers: ["HD"]
|
|
231
153
|
}
|
|
232
154
|
};
|
|
233
155
|
/** Official Gemini image ids served by Nano Banana gateways. */
|
|
@@ -262,6 +184,10 @@ function describeModel(model) {
|
|
|
262
184
|
family: "seedream",
|
|
263
185
|
...ENTRIES.seedream
|
|
264
186
|
};
|
|
187
|
+
if (/^(?:glm-image|cogview(?:-|$))/i.test(id)) return {
|
|
188
|
+
family: "zhipu",
|
|
189
|
+
...ENTRIES.zhipu
|
|
190
|
+
};
|
|
265
191
|
return {
|
|
266
192
|
family: "unknown",
|
|
267
193
|
label: "unknown",
|
|
@@ -272,11 +198,165 @@ function describeModel(model) {
|
|
|
272
198
|
qualityTiers: []
|
|
273
199
|
};
|
|
274
200
|
}
|
|
201
|
+
/** Conservative fallback for providers whose /models response only has ids.
|
|
202
|
+
* Metadata-aware filtering lives in prompt-enhancer.ts; this catches common
|
|
203
|
+
* image model naming conventions without treating every unknown model as an
|
|
204
|
+
* image model. */
|
|
205
|
+
function isLikelyImageModelId(model) {
|
|
206
|
+
return /(?:^|[-_.])(?:image|img|diffusion|flux|cogview|imagen|seedream|nanobanana|grok-imagine|dall-e|stable-diffusion|sdxl|pixart|kolors|ideogram|midjourney|recraft|hunyuan|jimeng|wanx|hidream|playground)(?:$|[-_.])/i.test(model.trim());
|
|
207
|
+
}
|
|
275
208
|
/** The family a model id routes its request through. */
|
|
276
209
|
function modelFamily(model) {
|
|
277
210
|
return describeModel(model).family;
|
|
278
211
|
}
|
|
279
212
|
//#endregion
|
|
213
|
+
//#region src/prompt-enhancer.ts
|
|
214
|
+
/** OpenAI-compatible chat helpers used by the optional prompt-enhancement UI. */
|
|
215
|
+
function endpoint(base, suffix) {
|
|
216
|
+
return `${base.replace(/\/+$/, "")}${suffix}`;
|
|
217
|
+
}
|
|
218
|
+
function headers(apiKey) {
|
|
219
|
+
return {
|
|
220
|
+
"content-type": "application/json",
|
|
221
|
+
...apiKey.trim() === "" ? {} : { authorization: `Bearer ${apiKey.trim()}` }
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async function responseJson(response) {
|
|
225
|
+
const body = await response.json().catch(() => void 0);
|
|
226
|
+
if (!response.ok || body === void 0 || body === null || typeof body !== "object") {
|
|
227
|
+
const message = body !== null && typeof body === "object" && typeof body.error?.message === "string" ? body.error.message : `HTTP ${response.status}`;
|
|
228
|
+
throw new Error(message);
|
|
229
|
+
}
|
|
230
|
+
return body;
|
|
231
|
+
}
|
|
232
|
+
async function listModelRecords(config) {
|
|
233
|
+
if (config.apiUrl.trim() === "") throw new Error("API URL is required");
|
|
234
|
+
const body = await responseJson(await fetch(endpoint(config.apiUrl, "/models"), { headers: headers(config.apiKey) }));
|
|
235
|
+
return (Array.isArray(body.data) ? body.data : []).flatMap((item) => {
|
|
236
|
+
if (item === null || typeof item !== "object" || typeof item.id !== "string") return [];
|
|
237
|
+
const id = item.id.trim();
|
|
238
|
+
return id === "" ? [] : [{
|
|
239
|
+
...item,
|
|
240
|
+
id
|
|
241
|
+
}];
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function textOf(value) {
|
|
245
|
+
if (typeof value === "string") return [value];
|
|
246
|
+
if (!Array.isArray(value)) return [];
|
|
247
|
+
return value.filter((item) => typeof item === "string");
|
|
248
|
+
}
|
|
249
|
+
function hasImageGenerationCapability(record) {
|
|
250
|
+
const capability = record.capabilities;
|
|
251
|
+
if (capability !== null && typeof capability === "object") {
|
|
252
|
+
const values = capability;
|
|
253
|
+
for (const key of [
|
|
254
|
+
"image_generation",
|
|
255
|
+
"imageGeneration",
|
|
256
|
+
"text_to_image",
|
|
257
|
+
"textToImage",
|
|
258
|
+
"image_gen"
|
|
259
|
+
]) if (typeof values[key] === "boolean") return values[key];
|
|
260
|
+
const serialized = JSON.stringify(values).toLowerCase();
|
|
261
|
+
if (/image[ _-]?generation|text[ _-]?to[ _-]?image/.test(serialized)) return true;
|
|
262
|
+
}
|
|
263
|
+
const taskText = [
|
|
264
|
+
...textOf(record.task),
|
|
265
|
+
...textOf(record.task_type),
|
|
266
|
+
...textOf(record.taskType),
|
|
267
|
+
...textOf(record.type),
|
|
268
|
+
...textOf(record.model_type),
|
|
269
|
+
...textOf(record.modelType),
|
|
270
|
+
...textOf(record.tasks),
|
|
271
|
+
...textOf(record.description)
|
|
272
|
+
].join(" ").toLowerCase();
|
|
273
|
+
if (/image[ _-]?generation|text[ _-]?to[ _-]?image|image[ _-]?gen/.test(taskText)) return true;
|
|
274
|
+
if (/^image(?:[ _-]?generation)?$/.test(taskText.trim())) return true;
|
|
275
|
+
if (/embedding|rerank|moderation|transcri|speech|audio|video|chat[ _-]?completion/.test(taskText)) return false;
|
|
276
|
+
for (const key of [
|
|
277
|
+
"output_modalities",
|
|
278
|
+
"outputModalities",
|
|
279
|
+
"supported_output_modalities"
|
|
280
|
+
]) {
|
|
281
|
+
const modalities = textOf(record[key]).map((value) => value.toLowerCase());
|
|
282
|
+
if (modalities.length > 0) return modalities.includes("image");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function isImageModelRecord(record) {
|
|
286
|
+
return hasImageGenerationCapability(record) ?? isLikelyImageModelId(record.id);
|
|
287
|
+
}
|
|
288
|
+
/** List candidates exposed by an OpenAI-compatible endpoint. */
|
|
289
|
+
async function listOpenAIModels(config) {
|
|
290
|
+
return [...new Set((await listModelRecords(config)).map((record) => record.id))].sort((a, b) => a.localeCompare(b));
|
|
291
|
+
}
|
|
292
|
+
/** List only models that advertise or conventionally represent image generation. */
|
|
293
|
+
async function listImageModels(config) {
|
|
294
|
+
return [...new Set((await listModelRecords(config)).filter(isImageModelRecord).map((record) => record.id))].sort((a, b) => a.localeCompare(b));
|
|
295
|
+
}
|
|
296
|
+
/** List chat models exposed by an OpenAI-compatible endpoint. */
|
|
297
|
+
async function listPromptModels(config) {
|
|
298
|
+
return listOpenAIModels(config);
|
|
299
|
+
}
|
|
300
|
+
/** Expand a concise image request into a production-ready image prompt. */
|
|
301
|
+
async function enhancePrompt(config, prompt) {
|
|
302
|
+
if (config.apiUrl.trim() === "" || config.model.trim() === "") throw new Error("prompt enhancement model is not configured");
|
|
303
|
+
const body = await responseJson(await fetch(endpoint(config.apiUrl, "/chat/completions"), {
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers: headers(config.apiKey),
|
|
306
|
+
body: JSON.stringify({
|
|
307
|
+
model: config.model.trim(),
|
|
308
|
+
temperature: .7,
|
|
309
|
+
messages: [{
|
|
310
|
+
role: "system",
|
|
311
|
+
content: "You are an expert image-prompt editor. Expand the user request into one vivid, specific image-generation prompt. Preserve intent and language. Add only useful visual detail: subject, composition, lighting, materials, color, camera/style and quality. Return only the finished prompt, with no preface or markdown."
|
|
312
|
+
}, {
|
|
313
|
+
role: "user",
|
|
314
|
+
content: prompt
|
|
315
|
+
}]
|
|
316
|
+
})
|
|
317
|
+
}));
|
|
318
|
+
const choices = Array.isArray(body.choices) ? body.choices : [];
|
|
319
|
+
const content = choices[0] !== null && typeof choices[0] === "object" ? choices[0].message?.content : void 0;
|
|
320
|
+
if (typeof content !== "string" || content.trim() === "") throw new Error("chat model returned an empty prompt");
|
|
321
|
+
return content.trim();
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/image-models.ts
|
|
325
|
+
/**
|
|
326
|
+
* Image-model configuration shared by the host, panel, and Agent tools.
|
|
327
|
+
* `/models` exposes candidates only: the configured list is the explicit
|
|
328
|
+
* allow-list because OpenAI-compatible gateways rarely advertise modalities.
|
|
329
|
+
*/
|
|
330
|
+
const DEFAULT_IMAGE_MODELS = [
|
|
331
|
+
"gpt-image-2",
|
|
332
|
+
"grok-imagine-image",
|
|
333
|
+
"nanobanana2",
|
|
334
|
+
"nanobanana2-lite",
|
|
335
|
+
"nanobanana-pro",
|
|
336
|
+
"seedream-5.0-pro",
|
|
337
|
+
"glm-image"
|
|
338
|
+
];
|
|
339
|
+
/** Normalize user-entered model identifiers and retain a usable legacy default. */
|
|
340
|
+
function normalizeImageModels(value) {
|
|
341
|
+
const candidates = Array.isArray(value) ? value : [];
|
|
342
|
+
const unique = /* @__PURE__ */ new Set();
|
|
343
|
+
for (const candidate of candidates) {
|
|
344
|
+
if (typeof candidate !== "string") continue;
|
|
345
|
+
const model = candidate.trim();
|
|
346
|
+
if (model !== "") unique.add(model);
|
|
347
|
+
}
|
|
348
|
+
return unique.size > 0 ? [...unique] : [...DEFAULT_IMAGE_MODELS];
|
|
349
|
+
}
|
|
350
|
+
//#endregion
|
|
351
|
+
//#region src/image-format.ts
|
|
352
|
+
function detectImageMime(data) {
|
|
353
|
+
const startsWith = (...bytes) => bytes.every((value, index) => data[index] === value);
|
|
354
|
+
if (startsWith(137, 80, 78, 71, 13, 10, 26, 10)) return "image/png";
|
|
355
|
+
if (startsWith(255, 216, 255)) return "image/jpeg";
|
|
356
|
+
if (startsWith(71, 73, 70, 56, 55, 97) || startsWith(71, 73, 70, 56, 57, 97)) return "image/gif";
|
|
357
|
+
if (startsWith(82, 73, 70, 70) && data[8] === 87 && data[9] === 69 && data[10] === 66 && data[11] === 80) return "image/webp";
|
|
358
|
+
}
|
|
359
|
+
//#endregion
|
|
280
360
|
//#region src/engine.ts
|
|
281
361
|
/** A generation failure with a user-presentable message. */
|
|
282
362
|
var ImageGenError = class extends Error {
|
|
@@ -331,6 +411,13 @@ function isNanoBanana(model) {
|
|
|
331
411
|
function isSeedream(model) {
|
|
332
412
|
return modelFamily(model) === "seedream";
|
|
333
413
|
}
|
|
414
|
+
/** Whether the model uses the official Zhipu image-generation contract. */
|
|
415
|
+
function isZhipuImage(model) {
|
|
416
|
+
return modelFamily(model) === "zhipu";
|
|
417
|
+
}
|
|
418
|
+
function isGlmImage(model) {
|
|
419
|
+
return /^glm-image(?:-|$)/i.test(model.trim());
|
|
420
|
+
}
|
|
334
421
|
/** Whether this is the official Volcengine Ark model naming convention. */
|
|
335
422
|
function isVolcSeedream(model) {
|
|
336
423
|
return /^doubao-seedream(?:-|$)/i.test(model.trim());
|
|
@@ -407,6 +494,19 @@ function bareBase64(value) {
|
|
|
407
494
|
const parsed = parseDataUrl(value);
|
|
408
495
|
return parsed !== void 0 && parsed.base64 !== void 0 ? parsed.base64 : value;
|
|
409
496
|
}
|
|
497
|
+
/** Whether a result URL carries cloud-storage signing credentials. */
|
|
498
|
+
function isPresignedUrl(value) {
|
|
499
|
+
let url;
|
|
500
|
+
try {
|
|
501
|
+
url = new URL(value);
|
|
502
|
+
} catch {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
const params = new Set(Array.from(url.searchParams.keys(), (key) => key.toLowerCase()));
|
|
506
|
+
if (params.has("x-goog-signature") || params.has("x-goog-credential")) return true;
|
|
507
|
+
if (params.has("x-amz-signature") || params.has("x-amz-credential")) return true;
|
|
508
|
+
return params.has("signature") && (params.has("expires") || params.has("googleaccessid") || params.has("awsaccesskeyid"));
|
|
509
|
+
}
|
|
410
510
|
/** Clamp the requested image count into the API-accepted range. */
|
|
411
511
|
function clampCount(n) {
|
|
412
512
|
if (!Number.isFinite(n)) return 1;
|
|
@@ -441,6 +541,11 @@ function effectiveParams(request) {
|
|
|
441
541
|
size: seedreamSize(request.quality),
|
|
442
542
|
response_format: isVolcSeedream(model) ? "url" : "b64_json"
|
|
443
543
|
};
|
|
544
|
+
if (isZhipuImage(model)) return {
|
|
545
|
+
model,
|
|
546
|
+
...request.size !== "" && request.size !== "auto" && OPENAI_SIZE_BY_RATIO[request.size] !== void 0 ? { size: OPENAI_SIZE_BY_RATIO[request.size] } : {},
|
|
547
|
+
quality: isGlmImage(model) ? "hd" : "standard"
|
|
548
|
+
};
|
|
444
549
|
return {
|
|
445
550
|
model,
|
|
446
551
|
...request.size !== "" && request.size !== "auto" && OPENAI_SIZE_BY_RATIO[request.size] !== void 0 ? { size: OPENAI_SIZE_BY_RATIO[request.size] } : {},
|
|
@@ -458,9 +563,9 @@ function effectiveCount(request) {
|
|
|
458
563
|
/** Normalize one upstream data item into a base64 image. */
|
|
459
564
|
async function normalizeItem(item, upstream) {
|
|
460
565
|
const revisedPrompt = typeof item.revised_prompt === "string" ? item.revised_prompt : void 0;
|
|
461
|
-
if (typeof item.b64_json === "string") {
|
|
566
|
+
if (typeof item.b64_json === "string" && item.b64_json.trim() !== "") {
|
|
462
567
|
const b64 = bareBase64(item.b64_json);
|
|
463
|
-
return {
|
|
568
|
+
if (b64.trim() !== "") return {
|
|
464
569
|
b64,
|
|
465
570
|
mime: detectImageMime(Buffer.from(b64, "base64")) ?? "image/png",
|
|
466
571
|
revisedPrompt
|
|
@@ -481,7 +586,7 @@ async function normalizeItem(item, upstream) {
|
|
|
481
586
|
let response;
|
|
482
587
|
try {
|
|
483
588
|
response = await fetch(url, {
|
|
484
|
-
|
|
589
|
+
...isPresignedUrl(url) || upstream.apiKey === "" ? {} : { headers: { authorization: `Bearer ${upstream.apiKey}` } },
|
|
485
590
|
signal: budget.signal
|
|
486
591
|
});
|
|
487
592
|
} catch (error) {
|
|
@@ -607,6 +712,7 @@ async function generateImage(upstream, request, options = {}) {
|
|
|
607
712
|
const baseUrl = upstream.apiUrl.trim().replace(/\/+$/, "");
|
|
608
713
|
if (baseUrl === "") throw new ImageGenError("api_url 未配置:请先在「设置 → 插件 → 可配置」中填写", "config-missing");
|
|
609
714
|
if (upstream.apiKey.trim() === "") throw new ImageGenError("api_key 未配置:请先在「设置 → 插件 → 可配置」中填写", "config-missing");
|
|
715
|
+
if (request.mode === "edit" && isZhipuImage(wireModel(request))) throw new ImageGenError("智谱 GLM-Image 当前仅支持文生图,请切换到文生图模式或选择支持图生图的模型", "edit-unsupported");
|
|
610
716
|
const params = effectiveParams(request);
|
|
611
717
|
const count = effectiveCount(request);
|
|
612
718
|
return { images: (await Promise.all(Array.from({ length: count }, () => requestOneImage(baseUrl, upstream, request, params, options.signal)))).flat() };
|
|
@@ -707,7 +813,7 @@ async function writeIndex$1(entries) {
|
|
|
707
813
|
function isStoredEntry$1(value) {
|
|
708
814
|
if (value === null || typeof value !== "object") return false;
|
|
709
815
|
const entry = value;
|
|
710
|
-
return typeof entry.id === "string" && typeof entry.createdAt === "number" && (entry.mode === "text" || entry.mode === "edit") && typeof entry.prompt === "string" && Array.isArray(entry.images) && entry.images.every((image) => {
|
|
816
|
+
return typeof entry.id === "string" && typeof entry.createdAt === "number" && (entry.mode === "text" || entry.mode === "edit") && (entry.workflow === void 0 || entry.workflow === "ecommerce") && (entry.projectId === void 0 || typeof entry.projectId === "string") && (entry.projectName === void 0 || typeof entry.projectName === "string") && (entry.slotKey === void 0 || typeof entry.slotKey === "string") && (entry.slotLabel === void 0 || typeof entry.slotLabel === "string") && typeof entry.prompt === "string" && Array.isArray(entry.images) && entry.images.every((image) => {
|
|
711
817
|
if (image === null || typeof image !== "object") return false;
|
|
712
818
|
const record = image;
|
|
713
819
|
return typeof record.file === "string" && typeof record.mime === "string";
|
|
@@ -738,7 +844,14 @@ function toWire$1(entry) {
|
|
|
738
844
|
})),
|
|
739
845
|
...entry.refName === void 0 ? {} : { refName: entry.refName },
|
|
740
846
|
...entry.channel === void 0 ? {} : { channel: entry.channel },
|
|
741
|
-
...entry.channelId === void 0 ? {} : { channelId: entry.channelId }
|
|
847
|
+
...entry.channelId === void 0 ? {} : { channelId: entry.channelId },
|
|
848
|
+
...entry.comparisonId === void 0 ? {} : { comparisonId: entry.comparisonId },
|
|
849
|
+
...entry.comparisonModels === void 0 ? {} : { comparisonModels: entry.comparisonModels },
|
|
850
|
+
...entry.workflow === void 0 ? {} : { workflow: entry.workflow },
|
|
851
|
+
...entry.projectId === void 0 ? {} : { projectId: entry.projectId },
|
|
852
|
+
...entry.projectName === void 0 ? {} : { projectName: entry.projectName },
|
|
853
|
+
...entry.slotKey === void 0 ? {} : { slotKey: entry.slotKey },
|
|
854
|
+
...entry.slotLabel === void 0 ? {} : { slotLabel: entry.slotLabel }
|
|
742
855
|
};
|
|
743
856
|
}
|
|
744
857
|
/** List the persisted history, newest first, as wire entries. */
|
|
@@ -779,7 +892,14 @@ async function appendHistory(input) {
|
|
|
779
892
|
images: storedImages,
|
|
780
893
|
...input.refName === void 0 ? {} : { refName: input.refName },
|
|
781
894
|
...input.channelId === void 0 ? {} : { channelId: input.channelId },
|
|
782
|
-
...input.channel === void 0 ? {} : { channel: input.channel }
|
|
895
|
+
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
896
|
+
...input.comparisonId === void 0 ? {} : { comparisonId: input.comparisonId },
|
|
897
|
+
...input.comparisonModels === void 0 ? {} : { comparisonModels: input.comparisonModels },
|
|
898
|
+
...input.workflow === void 0 ? {} : { workflow: input.workflow },
|
|
899
|
+
...input.projectId === void 0 ? {} : { projectId: input.projectId },
|
|
900
|
+
...input.projectName === void 0 ? {} : { projectName: input.projectName },
|
|
901
|
+
...input.slotKey === void 0 ? {} : { slotKey: input.slotKey },
|
|
902
|
+
...input.slotLabel === void 0 ? {} : { slotLabel: input.slotLabel }
|
|
783
903
|
}, ...await readIndex$1()];
|
|
784
904
|
const kept = merged.slice(0, 50);
|
|
785
905
|
for (const dropped of merged.slice(50)) await removeEntryFiles$1(dropped);
|
|
@@ -824,12 +944,15 @@ async function readHistoryImage(file) {
|
|
|
824
944
|
/** In-memory, host-resident image generation queue. */
|
|
825
945
|
var GenerationTaskQueue = class {
|
|
826
946
|
run;
|
|
947
|
+
concurrency;
|
|
827
948
|
tasks = [];
|
|
828
949
|
controllers = /* @__PURE__ */ new Map();
|
|
829
950
|
listeners = /* @__PURE__ */ new Set();
|
|
830
|
-
running =
|
|
831
|
-
|
|
951
|
+
running = 0;
|
|
952
|
+
serialRunning = false;
|
|
953
|
+
constructor(run, concurrency = 1) {
|
|
832
954
|
this.run = run;
|
|
955
|
+
this.concurrency = concurrency;
|
|
833
956
|
}
|
|
834
957
|
list() {
|
|
835
958
|
return this.tasks.map((task) => this.snapshot(task));
|
|
@@ -860,45 +983,49 @@ var GenerationTaskQueue = class {
|
|
|
860
983
|
task.finishedAt = Date.now();
|
|
861
984
|
this.controllers.get(id)?.abort();
|
|
862
985
|
this.publish(task);
|
|
986
|
+
this.drain();
|
|
863
987
|
return this.snapshot(task);
|
|
864
988
|
}
|
|
865
989
|
retry(id) {
|
|
866
990
|
const previous = this.tasks.find((item) => item.id === id);
|
|
867
991
|
return previous === void 0 ? void 0 : this.submit(previous.request);
|
|
868
992
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
993
|
+
drain() {
|
|
994
|
+
while (this.running < Math.max(1, this.concurrency)) {
|
|
995
|
+
const task = this.tasks.find((item) => item.status === "queued" && (this.running === 0 || item.request.comparisonId !== void 0 && !this.serialRunning));
|
|
996
|
+
if (task === void 0) return;
|
|
997
|
+
this.running += 1;
|
|
998
|
+
if (task.request.comparisonId === void 0) this.serialRunning = true;
|
|
999
|
+
this.runTask(task).finally(() => {
|
|
1000
|
+
this.running -= 1;
|
|
1001
|
+
if (task.request.comparisonId === void 0) this.serialRunning = false;
|
|
1002
|
+
this.drain();
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async runTask(task) {
|
|
1007
|
+
task.status = "running";
|
|
1008
|
+
task.startedAt = Date.now();
|
|
1009
|
+
this.publish(task);
|
|
1010
|
+
const controller = new AbortController();
|
|
1011
|
+
this.controllers.set(task.id, controller);
|
|
872
1012
|
try {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
task.
|
|
877
|
-
task.
|
|
1013
|
+
const result = await this.run(task.request, controller.signal);
|
|
1014
|
+
if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
|
|
1015
|
+
task.status = "completed";
|
|
1016
|
+
task.result = result;
|
|
1017
|
+
task.finishedAt = Date.now();
|
|
1018
|
+
this.publish(task);
|
|
1019
|
+
}
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
|
|
1022
|
+
task.status = "failed";
|
|
1023
|
+
task.error = error instanceof Error ? error.message : String(error);
|
|
1024
|
+
task.finishedAt = Date.now();
|
|
878
1025
|
this.publish(task);
|
|
879
|
-
const controller = new AbortController();
|
|
880
|
-
this.controllers.set(task.id, controller);
|
|
881
|
-
try {
|
|
882
|
-
const result = await this.run(task.request, controller.signal);
|
|
883
|
-
if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
|
|
884
|
-
task.status = "completed";
|
|
885
|
-
task.result = result;
|
|
886
|
-
task.finishedAt = Date.now();
|
|
887
|
-
this.publish(task);
|
|
888
|
-
}
|
|
889
|
-
} catch (error) {
|
|
890
|
-
if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
|
|
891
|
-
task.status = "failed";
|
|
892
|
-
task.error = error instanceof Error ? error.message : String(error);
|
|
893
|
-
task.finishedAt = Date.now();
|
|
894
|
-
this.publish(task);
|
|
895
|
-
}
|
|
896
|
-
} finally {
|
|
897
|
-
this.controllers.delete(task.id);
|
|
898
|
-
}
|
|
899
1026
|
}
|
|
900
1027
|
} finally {
|
|
901
|
-
this.
|
|
1028
|
+
this.controllers.delete(task.id);
|
|
902
1029
|
}
|
|
903
1030
|
}
|
|
904
1031
|
publish(task) {
|
|
@@ -934,7 +1061,7 @@ var ImageGenerationRuntime = class {
|
|
|
934
1061
|
constructor(resolve, history = { append: appendHistory }) {
|
|
935
1062
|
this.resolve = resolve;
|
|
936
1063
|
this.history = history;
|
|
937
|
-
this.queue = new GenerationTaskQueue((request, signal) => this.run(request, signal));
|
|
1064
|
+
this.queue = new GenerationTaskQueue((request, signal) => this.run(request, signal), 4);
|
|
938
1065
|
}
|
|
939
1066
|
async run(request, signal) {
|
|
940
1067
|
const view = this.resolve();
|
|
@@ -958,7 +1085,14 @@ var ImageGenerationRuntime = class {
|
|
|
958
1085
|
images: result.images,
|
|
959
1086
|
...request.refName === void 0 ? {} : { refName: request.refName },
|
|
960
1087
|
...request.channelId === void 0 ? {} : { channelId: request.channelId },
|
|
961
|
-
...request.channel === void 0 ? {} : { channel: request.channel }
|
|
1088
|
+
...request.channel === void 0 ? {} : { channel: request.channel },
|
|
1089
|
+
...request.comparisonId === void 0 ? {} : { comparisonId: request.comparisonId },
|
|
1090
|
+
...request.comparisonModels === void 0 ? {} : { comparisonModels: request.comparisonModels },
|
|
1091
|
+
...request.workflow === void 0 ? {} : { workflow: request.workflow },
|
|
1092
|
+
...request.projectId === void 0 ? {} : { projectId: request.projectId },
|
|
1093
|
+
...request.projectName === void 0 ? {} : { projectName: request.projectName },
|
|
1094
|
+
...request.slotKey === void 0 ? {} : { slotKey: request.slotKey },
|
|
1095
|
+
...request.slotLabel === void 0 ? {} : { slotLabel: request.slotLabel }
|
|
962
1096
|
});
|
|
963
1097
|
return {
|
|
964
1098
|
...result,
|
|
@@ -1052,7 +1186,7 @@ async function writeIndex(entries) {
|
|
|
1052
1186
|
function isStoredEntry(value) {
|
|
1053
1187
|
if (value === null || typeof value !== "object") return false;
|
|
1054
1188
|
const entry = value;
|
|
1055
|
-
return typeof entry.id === "string" && typeof entry.createdAt === "number" && (entry.mode === "text" || entry.mode === "edit") && typeof entry.prompt === "string" && Array.isArray(entry.images) && entry.images.every((image) => {
|
|
1189
|
+
return typeof entry.id === "string" && typeof entry.createdAt === "number" && (entry.mode === "text" || entry.mode === "edit") && (entry.workflow === void 0 || entry.workflow === "ecommerce") && (entry.projectId === void 0 || typeof entry.projectId === "string") && (entry.projectName === void 0 || typeof entry.projectName === "string") && (entry.slotKey === void 0 || typeof entry.slotKey === "string") && (entry.slotLabel === void 0 || typeof entry.slotLabel === "string") && typeof entry.prompt === "string" && Array.isArray(entry.images) && entry.images.every((image) => {
|
|
1056
1190
|
if (image === null || typeof image !== "object") return false;
|
|
1057
1191
|
const record = image;
|
|
1058
1192
|
return typeof record.file === "string" && typeof record.mime === "string";
|
|
@@ -1084,7 +1218,12 @@ function toWire(entry) {
|
|
|
1084
1218
|
...entry.refName === void 0 ? {} : { refName: entry.refName },
|
|
1085
1219
|
...entry.tags === void 0 ? {} : { tags: entry.tags },
|
|
1086
1220
|
...entry.channel === void 0 ? {} : { channel: entry.channel },
|
|
1087
|
-
...entry.channelId === void 0 ? {} : { channelId: entry.channelId }
|
|
1221
|
+
...entry.channelId === void 0 ? {} : { channelId: entry.channelId },
|
|
1222
|
+
...entry.workflow === void 0 ? {} : { workflow: entry.workflow },
|
|
1223
|
+
...entry.projectId === void 0 ? {} : { projectId: entry.projectId },
|
|
1224
|
+
...entry.projectName === void 0 ? {} : { projectName: entry.projectName },
|
|
1225
|
+
...entry.slotKey === void 0 ? {} : { slotKey: entry.slotKey },
|
|
1226
|
+
...entry.slotLabel === void 0 ? {} : { slotLabel: entry.slotLabel }
|
|
1088
1227
|
};
|
|
1089
1228
|
}
|
|
1090
1229
|
/** List the persisted gallery, newest first, as wire entries. */
|
|
@@ -1136,7 +1275,12 @@ async function appendGallery(input) {
|
|
|
1136
1275
|
...hash === void 0 ? {} : { hash },
|
|
1137
1276
|
...input.refName === void 0 ? {} : { refName: input.refName },
|
|
1138
1277
|
...input.channelId === void 0 ? {} : { channelId: input.channelId },
|
|
1139
|
-
...input.channel === void 0 ? {} : { channel: input.channel }
|
|
1278
|
+
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
1279
|
+
...input.workflow === void 0 ? {} : { workflow: input.workflow },
|
|
1280
|
+
...input.projectId === void 0 ? {} : { projectId: input.projectId },
|
|
1281
|
+
...input.projectName === void 0 ? {} : { projectName: input.projectName },
|
|
1282
|
+
...input.slotKey === void 0 ? {} : { slotKey: input.slotKey },
|
|
1283
|
+
...input.slotLabel === void 0 ? {} : { slotLabel: input.slotLabel }
|
|
1140
1284
|
}, ...await readIndex()];
|
|
1141
1285
|
await writeIndex(merged);
|
|
1142
1286
|
return {
|
|
@@ -1584,13 +1728,20 @@ const IMAGE_PRESETS = [
|
|
|
1584
1728
|
id: "openai-official",
|
|
1585
1729
|
name: "OpenAI 官方",
|
|
1586
1730
|
apiUrl: "https://api.openai.com/v1",
|
|
1587
|
-
hint: "OpenAI
|
|
1731
|
+
hint: "OpenAI 官方图像生成接口",
|
|
1588
1732
|
models: [{
|
|
1589
1733
|
alias: "gpt-image-2",
|
|
1590
1734
|
id: "gpt-image-2"
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1735
|
+
}]
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
id: "zhipu-official",
|
|
1739
|
+
name: "智谱 AI 官方",
|
|
1740
|
+
apiUrl: "https://open.bigmodel.cn/api/paas/v4",
|
|
1741
|
+
hint: "智谱官方 GLM-Image 图像生成接口",
|
|
1742
|
+
models: [{
|
|
1743
|
+
alias: "glm-image",
|
|
1744
|
+
id: "glm-image"
|
|
1594
1745
|
}]
|
|
1595
1746
|
},
|
|
1596
1747
|
{
|
|
@@ -1669,6 +1820,7 @@ function messageOf(error) {
|
|
|
1669
1820
|
function parseGenerateRequest(body) {
|
|
1670
1821
|
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
1671
1822
|
if (prompt === "") return void 0;
|
|
1823
|
+
const comparisonModels = Array.isArray(body.comparisonModels) ? [...new Set(body.comparisonModels.filter((model) => typeof model === "string").map((model) => model.trim()).filter(Boolean))] : [];
|
|
1672
1824
|
return {
|
|
1673
1825
|
mode: body.mode === "edit" ? "edit" : "text",
|
|
1674
1826
|
model: typeof body.model === "string" ? body.model : "",
|
|
@@ -1679,7 +1831,14 @@ function parseGenerateRequest(body) {
|
|
|
1679
1831
|
detail: typeof body.detail === "string" ? body.detail : "",
|
|
1680
1832
|
...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {},
|
|
1681
1833
|
...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {},
|
|
1682
|
-
...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {}
|
|
1834
|
+
...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {},
|
|
1835
|
+
...typeof body.comparisonId === "string" && body.comparisonId !== "" ? { comparisonId: body.comparisonId } : {},
|
|
1836
|
+
...comparisonModels.length > 1 ? { comparisonModels } : {},
|
|
1837
|
+
...body.workflow === "ecommerce" ? { workflow: "ecommerce" } : {},
|
|
1838
|
+
...typeof body.projectId === "string" && body.projectId !== "" ? { projectId: body.projectId } : {},
|
|
1839
|
+
...typeof body.projectName === "string" && body.projectName !== "" ? { projectName: body.projectName } : {},
|
|
1840
|
+
...typeof body.slotKey === "string" && body.slotKey !== "" ? { slotKey: body.slotKey } : {},
|
|
1841
|
+
...typeof body.slotLabel === "string" && body.slotLabel !== "" ? { slotLabel: body.slotLabel } : {}
|
|
1683
1842
|
};
|
|
1684
1843
|
}
|
|
1685
1844
|
/** Validate a submitted history entry (images carry base64). */
|
|
@@ -1704,6 +1863,7 @@ function parseHistoryEntryInput(body) {
|
|
|
1704
1863
|
...typeof image.revisedPrompt === "string" ? { revisedPrompt: image.revisedPrompt } : {}
|
|
1705
1864
|
});
|
|
1706
1865
|
}
|
|
1866
|
+
const comparisonModels = Array.isArray(entry.comparisonModels) ? [...new Set(entry.comparisonModels.filter((model) => typeof model === "string").map((model) => model.trim()).filter(Boolean))] : [];
|
|
1707
1867
|
return {
|
|
1708
1868
|
id: entry.id,
|
|
1709
1869
|
createdAt: entry.createdAt,
|
|
@@ -1717,7 +1877,14 @@ function parseHistoryEntryInput(body) {
|
|
|
1717
1877
|
images,
|
|
1718
1878
|
...typeof entry.refName === "string" ? { refName: entry.refName } : {},
|
|
1719
1879
|
...typeof entry.channelId === "string" ? { channelId: entry.channelId } : {},
|
|
1720
|
-
...typeof entry.channel === "string" ? { channel: entry.channel } : {}
|
|
1880
|
+
...typeof entry.channel === "string" ? { channel: entry.channel } : {},
|
|
1881
|
+
...typeof entry.comparisonId === "string" ? { comparisonId: entry.comparisonId } : {},
|
|
1882
|
+
...comparisonModels.length > 1 ? { comparisonModels } : {},
|
|
1883
|
+
...entry.workflow === "ecommerce" ? { workflow: "ecommerce" } : {},
|
|
1884
|
+
...typeof entry.projectId === "string" ? { projectId: entry.projectId } : {},
|
|
1885
|
+
...typeof entry.projectName === "string" ? { projectName: entry.projectName } : {},
|
|
1886
|
+
...typeof entry.slotKey === "string" ? { slotKey: entry.slotKey } : {},
|
|
1887
|
+
...typeof entry.slotLabel === "string" ? { slotLabel: entry.slotLabel } : {}
|
|
1721
1888
|
};
|
|
1722
1889
|
}
|
|
1723
1890
|
/** Extract the image file name from a history-image request URL. */
|
|
@@ -1759,6 +1926,15 @@ function agentImageRefFrom(rawUrl) {
|
|
|
1759
1926
|
function isImageMediaType(value) {
|
|
1760
1927
|
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
1761
1928
|
}
|
|
1929
|
+
function imageDataUrl$1(value) {
|
|
1930
|
+
const match = /^data:(image\/(?:png|jpeg|webp|gif));base64,(.*)$/su.exec(value.trim());
|
|
1931
|
+
if (match === null || match[1] === void 0 || match[2] === void 0) return void 0;
|
|
1932
|
+
const data = Buffer.from(match[2], "base64");
|
|
1933
|
+
return data.byteLength === 0 ? void 0 : {
|
|
1934
|
+
mediaType: match[1],
|
|
1935
|
+
data
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1762
1938
|
/** Project one settings descriptor onto the bridge wire view. */
|
|
1763
1939
|
function toView(descriptor) {
|
|
1764
1940
|
return {
|
|
@@ -1910,6 +2086,39 @@ function makeRoutes(deps) {
|
|
|
1910
2086
|
return true;
|
|
1911
2087
|
};
|
|
1912
2088
|
return [
|
|
2089
|
+
...deps.attachments?.saveImage === void 0 || deps.pendingConversationImages === void 0 ? [] : [{
|
|
2090
|
+
kind: "exact",
|
|
2091
|
+
path: CONVERSATION_IMAGE_API,
|
|
2092
|
+
handler: async (req, res) => {
|
|
2093
|
+
if (!guard(req, res, "POST")) return;
|
|
2094
|
+
const body = await readJsonBody(req, MAX_JSON_BODY_BYTES);
|
|
2095
|
+
const sessionId = typeof body?.sessionId === "string" ? body.sessionId.trim() : "";
|
|
2096
|
+
const dataUrl = typeof body?.dataUrl === "string" ? imageDataUrl$1(body.dataUrl) : void 0;
|
|
2097
|
+
if (sessionId === "" || dataUrl === void 0) {
|
|
2098
|
+
writeJson(res, 200, {
|
|
2099
|
+
ok: false,
|
|
2100
|
+
code: "bad-request",
|
|
2101
|
+
message: "sessionId and image data are required"
|
|
2102
|
+
});
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
try {
|
|
2106
|
+
const ref = await deps.attachments.saveImage({
|
|
2107
|
+
data: dataUrl.data,
|
|
2108
|
+
mediaType: dataUrl.mediaType,
|
|
2109
|
+
...typeof body?.name === "string" && body.name.trim() !== "" ? { name: body.name.trim() } : {}
|
|
2110
|
+
});
|
|
2111
|
+
deps.pendingConversationImages.set(sessionId, ref);
|
|
2112
|
+
writeJson(res, 200, { ok: true });
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
writeJson(res, 200, {
|
|
2115
|
+
ok: false,
|
|
2116
|
+
code: "image-save-failed",
|
|
2117
|
+
message: messageOf(error)
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}],
|
|
1913
2122
|
...deps.attachments === void 0 ? [] : [{
|
|
1914
2123
|
kind: "prefix",
|
|
1915
2124
|
path: AGENT_IMAGE_API,
|
|
@@ -1955,7 +2164,7 @@ function makeRoutes(deps) {
|
|
|
1955
2164
|
try {
|
|
1956
2165
|
writeJson(res, 200, {
|
|
1957
2166
|
ok: true,
|
|
1958
|
-
models: await
|
|
2167
|
+
models: await listImageModels(upstream)
|
|
1959
2168
|
});
|
|
1960
2169
|
} catch (error) {
|
|
1961
2170
|
writeJson(res, 200, {
|
|
@@ -2733,6 +2942,113 @@ const taskResultSchema = {
|
|
|
2733
2942
|
};
|
|
2734
2943
|
/** Agent calls stay pending until the provider and history write settle. */
|
|
2735
2944
|
const AGENT_GENERATION_TIMEOUT_MS = 3e5;
|
|
2945
|
+
function ensureAgentImageConfigured(config) {
|
|
2946
|
+
if (!config.enabled) throw new ImageGenError("AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.", "plugin-disabled");
|
|
2947
|
+
if (!config.allowAgentImageGeneration) throw new ImageGenError("Agent image generation is disabled in Settings > Plugins > AI Image.", "agent-generation-disabled");
|
|
2948
|
+
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");
|
|
2949
|
+
}
|
|
2950
|
+
/** Resolve a configured image alias and its owning channel. */
|
|
2951
|
+
function resolveAgentImageModel(config, requested) {
|
|
2952
|
+
const entries = config.channels.flatMap((channel) => channel.models.map((model) => ({
|
|
2953
|
+
channel,
|
|
2954
|
+
alias: model.alias,
|
|
2955
|
+
upstream: model.id
|
|
2956
|
+
})));
|
|
2957
|
+
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");
|
|
2958
|
+
const wanted = typeof requested === "string" && requested.trim() !== "" ? requested.trim() : "";
|
|
2959
|
+
if (wanted === "") {
|
|
2960
|
+
if (entries.length === 1) return entries[0];
|
|
2961
|
+
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");
|
|
2962
|
+
}
|
|
2963
|
+
const hosting = entries.filter((entry) => entry.alias === wanted);
|
|
2964
|
+
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");
|
|
2965
|
+
return hosting.find((entry) => entry.channel.id === config.defaultChannelId) ?? hosting[0];
|
|
2966
|
+
}
|
|
2967
|
+
function findAgentImageTask(runtime, id) {
|
|
2968
|
+
const task = runtime.queue.list().find((candidate) => candidate.id === id);
|
|
2969
|
+
if (task === void 0) throw new ImageGenError(`Image generation task ${id} was not found.`, "task-not-found");
|
|
2970
|
+
return task;
|
|
2971
|
+
}
|
|
2972
|
+
/** Wait for a queue task without sending anything through the chat model. */
|
|
2973
|
+
function waitForAgentImageTask(runtime, id, signal) {
|
|
2974
|
+
return new Promise((resolveTask, rejectTask) => {
|
|
2975
|
+
let settled = false;
|
|
2976
|
+
let dispose = () => {};
|
|
2977
|
+
let timer;
|
|
2978
|
+
let abort = () => {};
|
|
2979
|
+
const cleanup = () => {
|
|
2980
|
+
dispose();
|
|
2981
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2982
|
+
signal?.removeEventListener("abort", abort);
|
|
2983
|
+
};
|
|
2984
|
+
const resolve = (task) => {
|
|
2985
|
+
if (settled) return;
|
|
2986
|
+
settled = true;
|
|
2987
|
+
cleanup();
|
|
2988
|
+
resolveTask(task);
|
|
2989
|
+
};
|
|
2990
|
+
const reject = (error) => {
|
|
2991
|
+
if (settled) return;
|
|
2992
|
+
settled = true;
|
|
2993
|
+
cleanup();
|
|
2994
|
+
rejectTask(error);
|
|
2995
|
+
};
|
|
2996
|
+
abort = () => {
|
|
2997
|
+
if (settled) return;
|
|
2998
|
+
const reason = signal?.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Image generation was cancelled.");
|
|
2999
|
+
settled = true;
|
|
3000
|
+
cleanup();
|
|
3001
|
+
runtime.queue.cancel(id);
|
|
3002
|
+
rejectTask(reason);
|
|
3003
|
+
};
|
|
3004
|
+
const onChange = (updated) => {
|
|
3005
|
+
if (updated.id === id && isFinalTask(updated)) resolve(updated);
|
|
3006
|
+
};
|
|
3007
|
+
if (signal?.aborted === true) {
|
|
3008
|
+
abort();
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
dispose = runtime.queue.subscribe(onChange);
|
|
3012
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
3013
|
+
timer = setTimeout(() => {
|
|
3014
|
+
if (settled) return;
|
|
3015
|
+
const timeout = new ImageGenError(`Image generation task ${id} timed out after ${AGENT_GENERATION_TIMEOUT_MS / 1e3} seconds.`, "generation-timeout");
|
|
3016
|
+
settled = true;
|
|
3017
|
+
cleanup();
|
|
3018
|
+
runtime.queue.cancel(id);
|
|
3019
|
+
rejectTask(timeout);
|
|
3020
|
+
}, AGENT_GENERATION_TIMEOUT_MS);
|
|
3021
|
+
let current;
|
|
3022
|
+
try {
|
|
3023
|
+
current = findAgentImageTask(runtime, id);
|
|
3024
|
+
} catch (error) {
|
|
3025
|
+
reject(error);
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
if (isFinalTask(current)) resolve(current);
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
/** Submit the same image-edit request used by the Agent tool. */
|
|
3032
|
+
async function submitAgentImageEdit(attachments, runtime, resolve, input) {
|
|
3033
|
+
const config = resolve();
|
|
3034
|
+
ensureAgentImageConfigured(config);
|
|
3035
|
+
const reference = await attachments.readImage(input.sourceImage, input.signal);
|
|
3036
|
+
const picked = resolveAgentImageModel(config, (config.channels.find((channel) => channel.id === config.defaultChannelId) ?? config.channels[0])?.models[0]?.alias ?? config.channels.flatMap((channel) => channel.models)[0]?.alias);
|
|
3037
|
+
return waitForAgentImageTask(runtime, runtime.queue.submit({
|
|
3038
|
+
mode: "edit",
|
|
3039
|
+
model: picked.alias,
|
|
3040
|
+
upstream: picked.upstream,
|
|
3041
|
+
channelId: picked.channel.id,
|
|
3042
|
+
channel: picked.channel.name,
|
|
3043
|
+
prompt: input.prompt.trim(),
|
|
3044
|
+
size: "auto",
|
|
3045
|
+
quality: "auto",
|
|
3046
|
+
n: 1,
|
|
3047
|
+
detail: "",
|
|
3048
|
+
image: imageDataUrl(reference),
|
|
3049
|
+
...reference.ref.name === void 0 ? {} : { refName: reference.ref.name }
|
|
3050
|
+
}).id, input.signal);
|
|
3051
|
+
}
|
|
2736
3052
|
function acceptedMediaType(value) {
|
|
2737
3053
|
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
2738
3054
|
}
|
|
@@ -2818,36 +3134,9 @@ function presentImageResult(_args, result) {
|
|
|
2818
3134
|
function registerAgentImageTools(ctx, runtime, resolve) {
|
|
2819
3135
|
const attachmentRefs = /* @__PURE__ */ new Map();
|
|
2820
3136
|
const ensureConfigured = () => {
|
|
2821
|
-
|
|
2822
|
-
if (!config.enabled) throw new ImageGenError("AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.", "plugin-disabled");
|
|
2823
|
-
if (!config.allowAgentImageGeneration) throw new ImageGenError("Agent image generation is disabled in Settings > Plugins > AI Image.", "agent-generation-disabled");
|
|
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");
|
|
2825
|
-
};
|
|
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];
|
|
3137
|
+
ensureAgentImageConfigured(resolve());
|
|
2850
3138
|
};
|
|
3139
|
+
const resolveModel = (requested) => resolveAgentImageModel(resolve(), requested);
|
|
2851
3140
|
const materializeTaskImages = (task) => {
|
|
2852
3141
|
if (task.status !== "completed") return Promise.resolve([]);
|
|
2853
3142
|
const existing = attachmentRefs.get(task.id);
|
|
@@ -2869,67 +3158,8 @@ function registerAgentImageTools(ctx, runtime, resolve) {
|
|
|
2869
3158
|
images
|
|
2870
3159
|
};
|
|
2871
3160
|
};
|
|
2872
|
-
const findTask = (id) =>
|
|
2873
|
-
|
|
2874
|
-
if (task === void 0) throw new ImageGenError(`Image generation task ${id} was not found.`, "task-not-found");
|
|
2875
|
-
return task;
|
|
2876
|
-
};
|
|
2877
|
-
const waitForTask = (id, signal) => new Promise((resolveTask, rejectTask) => {
|
|
2878
|
-
let settled = false;
|
|
2879
|
-
let dispose = () => {};
|
|
2880
|
-
let timer;
|
|
2881
|
-
let abort = () => {};
|
|
2882
|
-
const cleanup = () => {
|
|
2883
|
-
dispose();
|
|
2884
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
2885
|
-
signal?.removeEventListener("abort", abort);
|
|
2886
|
-
};
|
|
2887
|
-
const resolve = (task) => {
|
|
2888
|
-
if (settled) return;
|
|
2889
|
-
settled = true;
|
|
2890
|
-
cleanup();
|
|
2891
|
-
resolveTask(task);
|
|
2892
|
-
};
|
|
2893
|
-
const reject = (error) => {
|
|
2894
|
-
if (settled) return;
|
|
2895
|
-
settled = true;
|
|
2896
|
-
cleanup();
|
|
2897
|
-
rejectTask(error);
|
|
2898
|
-
};
|
|
2899
|
-
abort = () => {
|
|
2900
|
-
if (settled) return;
|
|
2901
|
-
const reason = signal?.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Image generation was cancelled.");
|
|
2902
|
-
settled = true;
|
|
2903
|
-
cleanup();
|
|
2904
|
-
runtime.queue.cancel(id);
|
|
2905
|
-
rejectTask(reason);
|
|
2906
|
-
};
|
|
2907
|
-
const onChange = (updated) => {
|
|
2908
|
-
if (updated.id === id && isFinalTask(updated)) resolve(updated);
|
|
2909
|
-
};
|
|
2910
|
-
if (signal?.aborted === true) {
|
|
2911
|
-
abort();
|
|
2912
|
-
return;
|
|
2913
|
-
}
|
|
2914
|
-
dispose = runtime.queue.subscribe(onChange);
|
|
2915
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
2916
|
-
timer = setTimeout(() => {
|
|
2917
|
-
if (settled) return;
|
|
2918
|
-
const timeout = new ImageGenError(`Image generation task ${id} timed out after ${AGENT_GENERATION_TIMEOUT_MS / 1e3} seconds.`, "generation-timeout");
|
|
2919
|
-
settled = true;
|
|
2920
|
-
cleanup();
|
|
2921
|
-
runtime.queue.cancel(id);
|
|
2922
|
-
rejectTask(timeout);
|
|
2923
|
-
}, AGENT_GENERATION_TIMEOUT_MS);
|
|
2924
|
-
let current;
|
|
2925
|
-
try {
|
|
2926
|
-
current = findTask(id);
|
|
2927
|
-
} catch (error) {
|
|
2928
|
-
reject(error);
|
|
2929
|
-
return;
|
|
2930
|
-
}
|
|
2931
|
-
if (isFinalTask(current)) resolve(current);
|
|
2932
|
-
});
|
|
3161
|
+
const findTask = (id) => findAgentImageTask(runtime, id);
|
|
3162
|
+
const waitForTask = (id, signal) => waitForAgentImageTask(runtime, id, signal);
|
|
2933
3163
|
const disposers = [
|
|
2934
3164
|
ctx.tools.register(defineTool({
|
|
2935
3165
|
name: "generate_image",
|
|
@@ -3114,11 +3344,107 @@ function toSaveImage(image, taskId, index) {
|
|
|
3114
3344
|
};
|
|
3115
3345
|
}
|
|
3116
3346
|
//#endregion
|
|
3347
|
+
//#region src/edit-image-command.ts
|
|
3348
|
+
function isImageReference(value) {
|
|
3349
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
3350
|
+
const ref = value;
|
|
3351
|
+
return typeof ref.attachmentId === "string" && (ref.mediaType === "image/png" || ref.mediaType === "image/jpeg" || ref.mediaType === "image/webp" || ref.mediaType === "image/gif") && Number.isInteger(ref.bytes) && ref.bytes > 0 && Number.isInteger(ref.width) && ref.width > 0 && Number.isInteger(ref.height) && ref.height > 0;
|
|
3352
|
+
}
|
|
3353
|
+
function imageInContent(value) {
|
|
3354
|
+
if (!Array.isArray(value)) return void 0;
|
|
3355
|
+
for (let index = value.length - 1; index >= 0; index -= 1) {
|
|
3356
|
+
const block = value[index];
|
|
3357
|
+
if (typeof block !== "object" || block === null || Array.isArray(block)) continue;
|
|
3358
|
+
const raw = block;
|
|
3359
|
+
if (raw.type === "image" && isImageReference(raw.attachment)) return raw.attachment;
|
|
3360
|
+
if (raw.type === "tool-result") {
|
|
3361
|
+
const nested = imageInContent(raw.content);
|
|
3362
|
+
if (nested !== void 0) return nested;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
/** Pick the newest image explicitly attached to this command invocation. */
|
|
3367
|
+
function imageInInvocation(value) {
|
|
3368
|
+
if (!Array.isArray(value)) return void 0;
|
|
3369
|
+
for (let index = value.length - 1; index >= 0; index -= 1) {
|
|
3370
|
+
const block = value[index];
|
|
3371
|
+
if (typeof block !== "object" || block === null || Array.isArray(block)) continue;
|
|
3372
|
+
const raw = block;
|
|
3373
|
+
if (raw.type === "image" && isImageReference(raw.attachment)) return raw.attachment;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
/** Find the newest durable image reference, including nested tool results. */
|
|
3377
|
+
function latestSessionImage(messages) {
|
|
3378
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
3379
|
+
const message = messages[index];
|
|
3380
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) continue;
|
|
3381
|
+
const image = imageInContent(message.content);
|
|
3382
|
+
if (image !== void 0) return image;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
function commandError(error) {
|
|
3386
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
3387
|
+
return {
|
|
3388
|
+
kind: "error",
|
|
3389
|
+
text: text.trim() === "" ? "图片编辑失败。" : text
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
/** Register the host-side command; it never sends the command line to a chat model. */
|
|
3393
|
+
function registerEditImageCommand(ctx, runtime, resolve, pendingImages) {
|
|
3394
|
+
return ctx.commands.register({
|
|
3395
|
+
name: "edit_image",
|
|
3396
|
+
description: "Edit the latest image in this conversation with the plugin image model",
|
|
3397
|
+
input: {
|
|
3398
|
+
hint: "Describe how to modify the latest image",
|
|
3399
|
+
images: true
|
|
3400
|
+
},
|
|
3401
|
+
async handler(invocation) {
|
|
3402
|
+
const prompt = invocation.rawInput.trim();
|
|
3403
|
+
if (prompt === "") return {
|
|
3404
|
+
kind: "error",
|
|
3405
|
+
text: "请提供图片修改描述,例如:/edit_image 把背景改成夜景"
|
|
3406
|
+
};
|
|
3407
|
+
const invocationImage = imageInInvocation(invocation.attachments);
|
|
3408
|
+
const pendingImage = pendingImages?.get(String(invocation.agent.id));
|
|
3409
|
+
const durableImage = latestSessionImage(invocation.agent.session.deriveMessages());
|
|
3410
|
+
const sourceImage = invocationImage ?? pendingImage ?? durableImage;
|
|
3411
|
+
if (sourceImage === void 0) return {
|
|
3412
|
+
kind: "error",
|
|
3413
|
+
text: "当前对话没有可用图片,请先上传图片或把画廊图片加入对话。"
|
|
3414
|
+
};
|
|
3415
|
+
try {
|
|
3416
|
+
const task = await submitAgentImageEdit(ctx.attachments, runtime, resolve, {
|
|
3417
|
+
prompt,
|
|
3418
|
+
sourceImage,
|
|
3419
|
+
signal: invocation.signal
|
|
3420
|
+
});
|
|
3421
|
+
if (task.status === "completed") {
|
|
3422
|
+
if (pendingImage !== void 0) pendingImages?.consume(String(invocation.agent.id), pendingImage);
|
|
3423
|
+
return {
|
|
3424
|
+
kind: "success",
|
|
3425
|
+
text: "图片编辑已完成,可在 AI 生图面板查看结果。"
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
return {
|
|
3429
|
+
kind: "error",
|
|
3430
|
+
text: task.error ?? `图片编辑${task.status === "cancelled" ? "已取消" : "失败"}。`
|
|
3431
|
+
};
|
|
3432
|
+
} catch (error) {
|
|
3433
|
+
return commandError(error);
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
//#endregion
|
|
3117
3439
|
//#region src/index.ts
|
|
3118
3440
|
/** Stable cordis plugin name. */
|
|
3119
3441
|
const name = "imagegen";
|
|
3120
3442
|
/** Services required before the surfaces can mount. */
|
|
3121
|
-
const inject = [
|
|
3443
|
+
const inject = [
|
|
3444
|
+
"webServer",
|
|
3445
|
+
"systemPrompt",
|
|
3446
|
+
"commands"
|
|
3447
|
+
];
|
|
3122
3448
|
/** The branded settings namespace of this plugin (the card edits it). */
|
|
3123
3449
|
const ImageGenSettingsNamespace = settingsNamespace(IMAGEGEN_SETTINGS_NAMESPACE);
|
|
3124
3450
|
const Config = z.object({
|
|
@@ -3151,7 +3477,7 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
|
|
|
3151
3477
|
/** Order of the announcement section within the tool-guidance band. */
|
|
3152
3478
|
const SECTION_ORDER = 150;
|
|
3153
3479
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
3154
|
-
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:通过「渠道」对接 OpenAI 兼容图像生成 API(每个渠道 = 一个 API 端点 + 各自的模型目录),支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送,nanobanana 系列按 aspect_ratio / image_size 参数协议发送;seedream 系列统一走 /images/generations,参考图以 JSON image
|
|
3480
|
+
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:通过「渠道」对接 OpenAI 兼容图像生成 API(每个渠道 = 一个 API 端点 + 各自的模型目录),支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送,nanobanana 系列按 aspect_ratio / image_size 参数协议发送;seedream 系列统一走 /images/generations,参考图以 JSON image 数组发送;智谱 `glm-image` 使用官方 `/api/paas/v4/images/generations`,当前仅支持文生图)。API 地址与密钥在 GUI 设置中按渠道配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用用户在各渠道配置目录中的模型;检测模型时会过滤聊天、Embedding 等非图片模型,但模型出现在 /models 中仍不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选与复用。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片显示在工具调用对应的左侧结果区域,模型收到状态和附件引用,不会额外伪造用户消息。用户也可以使用 `/edit_image <修改描述>`,命令会直接读取当前对话最近图片并调用插件图片模型,不经过对话模型的图片能力检查。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问 vibeui.top。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
|
|
3155
3481
|
/** Append the live channel × model table so an Agent can honor user choices. */
|
|
3156
3482
|
function guidanceFor(channels, defaultChannelId) {
|
|
3157
3483
|
if (channels.length === 0) return `${IMAGEGEN_GUIDANCE} 尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥。`;
|
|
@@ -3251,6 +3577,7 @@ function apply(ctx, config) {
|
|
|
3251
3577
|
};
|
|
3252
3578
|
};
|
|
3253
3579
|
const runtime = new ImageGenerationRuntime(channelsView);
|
|
3580
|
+
const pendingConversationImages = /* @__PURE__ */ new Map();
|
|
3254
3581
|
ctx.inject(["settings", "attachments"], (sctx) => {
|
|
3255
3582
|
const seam = sctx.get("settings");
|
|
3256
3583
|
sctx.effect(() => {
|
|
@@ -3279,6 +3606,7 @@ function apply(ctx, config) {
|
|
|
3279
3606
|
return [...new Set(value.channels.flatMap((channel) => channel.models.map((model) => model.alias)))];
|
|
3280
3607
|
},
|
|
3281
3608
|
attachments: sctx.attachments,
|
|
3609
|
+
pendingConversationImages,
|
|
3282
3610
|
runtime
|
|
3283
3611
|
}).map((route) => ctx.webServer.register(route));
|
|
3284
3612
|
return () => {
|
|
@@ -3286,16 +3614,33 @@ function apply(ctx, config) {
|
|
|
3286
3614
|
};
|
|
3287
3615
|
}, "dsh-imagegen: routes");
|
|
3288
3616
|
});
|
|
3289
|
-
ctx.inject([
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3617
|
+
ctx.inject([
|
|
3618
|
+
"tools",
|
|
3619
|
+
"attachments",
|
|
3620
|
+
"commands"
|
|
3621
|
+
], (tctx) => {
|
|
3622
|
+
tctx.effect(() => {
|
|
3623
|
+
const resolveAgentConfig = () => {
|
|
3624
|
+
const value = resolve();
|
|
3625
|
+
return {
|
|
3626
|
+
enabled: value.enabled,
|
|
3627
|
+
allowAgentImageGeneration: value.allowAgentImageGeneration,
|
|
3628
|
+
channels: value.channels,
|
|
3629
|
+
defaultChannelId: value.defaultChannelId
|
|
3630
|
+
};
|
|
3631
|
+
};
|
|
3632
|
+
const disposeTools = registerAgentImageTools(tctx, runtime, resolveAgentConfig);
|
|
3633
|
+
const disposeCommand = registerEditImageCommand(tctx, runtime, resolveAgentConfig, {
|
|
3634
|
+
get: (sessionId) => pendingConversationImages.get(sessionId),
|
|
3635
|
+
consume: (sessionId, ref) => {
|
|
3636
|
+
if (pendingConversationImages.get(sessionId)?.attachmentId === ref.attachmentId) pendingConversationImages.delete(sessionId);
|
|
3637
|
+
}
|
|
3638
|
+
});
|
|
3639
|
+
return () => {
|
|
3640
|
+
disposeCommand();
|
|
3641
|
+
disposeTools();
|
|
3297
3642
|
};
|
|
3298
|
-
}
|
|
3643
|
+
}, "dsh-imagegen: agent image tools and commands");
|
|
3299
3644
|
});
|
|
3300
3645
|
let disposeSection;
|
|
3301
3646
|
const sync = () => {
|
|
@@ -3321,4 +3666,4 @@ function apply(ctx, config) {
|
|
|
3321
3666
|
sync();
|
|
3322
3667
|
}
|
|
3323
3668
|
//#endregion
|
|
3324
|
-
export { CURRENT_VERSION, Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, ImageGenerationRuntime, appendGallery, apply, checkForUpdate, clearGallery, clearTemplateMemo, clearUpdateCache, compareVersions, generateImage, inject, installUpdate, listGallery, listTemplates, makeRoutes, name, profileFromProcess, readGalleryImage, readTemplateImage, refreshTemplates, registerAgentImageTools, removeGallery, updateGalleryTags };
|
|
3669
|
+
export { CURRENT_VERSION, Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, ImageGenerationRuntime, appendGallery, apply, checkForUpdate, clearGallery, clearTemplateMemo, clearUpdateCache, compareVersions, generateImage, inject, installUpdate, latestSessionImage, listGallery, listTemplates, makeRoutes, name, profileFromProcess, readGalleryImage, readTemplateImage, refreshTemplates, registerAgentImageTools, registerEditImageCommand, removeGallery, updateGalleryTags };
|