@dickpy/dsh-imagegen 1.0.20 → 1.2.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 +46 -13
- package/docs/images/agent-chat-edit.png +0 -0
- package/docs/images/agent-chat-generate.png +0 -0
- package/docs/images/multi-model-comparison.png +0 -0
- package/lib/client.js +1677 -538
- package/lib/client.js.map +1 -1
- package/lib/index.js +877 -57
- package/package.json +4 -1
- package/src/agent-image-tools.ts +289 -0
- package/src/client/ImageGenPanel.tsx +333 -46
- package/src/client/SettingsCard.tsx +293 -23
- package/src/client/api.ts +37 -1
- package/src/client/locales.ts +150 -2
- package/src/client/panel.module.css +129 -1
- package/src/client/settings-card.module.css +222 -0
- package/src/client/settings-form.ts +12 -0
- package/src/client/settings-scope.ts +24 -1
- package/src/engine.ts +32 -4
- package/src/gallery-store.ts +15 -1
- package/src/generation-runtime.ts +48 -0
- package/src/image-models.ts +19 -0
- package/src/index.ts +72 -3
- package/src/prompt-enhancer.ts +79 -0
- package/src/protocol.ts +37 -2
- package/src/routes.ts +142 -40
- package/src/task-queue.ts +103 -0
package/lib/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm/message";
|
|
10
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
9
11
|
//#region src/protocol.ts
|
|
10
12
|
/**
|
|
11
13
|
* Wire contract shared by the host and client halves of dsh-imagegen: the
|
|
@@ -15,7 +17,7 @@ import { spawn } from "node:child_process";
|
|
|
15
17
|
/** Settings namespace this plugin owns (host settings seam + bridge). */
|
|
16
18
|
const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
|
|
17
19
|
/** Published package version shared by the host updater and the client UI. */
|
|
18
|
-
const PLUGIN_VERSION = "1.0
|
|
20
|
+
const PLUGIN_VERSION = "1.2.0";
|
|
19
21
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
20
22
|
const SETTINGS_API = {
|
|
21
23
|
describe: "/api/dsh-imagegen/settings/describe",
|
|
@@ -23,6 +25,20 @@ const SETTINGS_API = {
|
|
|
23
25
|
};
|
|
24
26
|
/** The image-generation proxy route. */
|
|
25
27
|
const GENERATE_API = "/api/dsh-imagegen/generate";
|
|
28
|
+
/** Host-mediated OpenAI-compatible prompt enhancement endpoints. */
|
|
29
|
+
const PROMPT_ENHANCE_API = {
|
|
30
|
+
models: "/api/dsh-imagegen/prompt-enhance/models",
|
|
31
|
+
enhance: "/api/dsh-imagegen/prompt-enhance"
|
|
32
|
+
};
|
|
33
|
+
/** Host-mediated candidate discovery for the configured image API. */
|
|
34
|
+
const IMAGE_MODEL_API = { models: "/api/dsh-imagegen/image-models" };
|
|
35
|
+
/** Host-resident generation queue endpoints. */
|
|
36
|
+
const TASK_API = {
|
|
37
|
+
submit: "/api/dsh-imagegen/tasks/submit",
|
|
38
|
+
list: "/api/dsh-imagegen/tasks/list",
|
|
39
|
+
cancel: "/api/dsh-imagegen/tasks/cancel",
|
|
40
|
+
retry: "/api/dsh-imagegen/tasks/retry"
|
|
41
|
+
};
|
|
26
42
|
/** Host-mediated GitHub Release update routes. */
|
|
27
43
|
const UPDATE_API = {
|
|
28
44
|
check: "/api/dsh-imagegen/update/check",
|
|
@@ -51,6 +67,7 @@ const GALLERY_API = {
|
|
|
51
67
|
append: "/api/dsh-imagegen/gallery/append",
|
|
52
68
|
remove: "/api/dsh-imagegen/gallery/remove",
|
|
53
69
|
clear: "/api/dsh-imagegen/gallery/clear",
|
|
70
|
+
tags: "/api/dsh-imagegen/gallery/tags",
|
|
54
71
|
image: "/api/dsh-imagegen/gallery/image"
|
|
55
72
|
};
|
|
56
73
|
/**
|
|
@@ -65,6 +82,78 @@ const TEMPLATES_API = {
|
|
|
65
82
|
image: "/api/dsh-imagegen/templates/image"
|
|
66
83
|
};
|
|
67
84
|
//#endregion
|
|
85
|
+
//#region src/prompt-enhancer.ts
|
|
86
|
+
function endpoint(base, suffix) {
|
|
87
|
+
return `${base.replace(/\/+$/, "")}${suffix}`;
|
|
88
|
+
}
|
|
89
|
+
function headers(apiKey) {
|
|
90
|
+
return {
|
|
91
|
+
"content-type": "application/json",
|
|
92
|
+
...apiKey.trim() === "" ? {} : { authorization: `Bearer ${apiKey.trim()}` }
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async function responseJson(response) {
|
|
96
|
+
const body = await response.json().catch(() => void 0);
|
|
97
|
+
if (!response.ok || body === void 0 || body === null || typeof body !== "object") {
|
|
98
|
+
const message = body !== null && typeof body === "object" && typeof body.error?.message === "string" ? body.error.message : `HTTP ${response.status}`;
|
|
99
|
+
throw new Error(message);
|
|
100
|
+
}
|
|
101
|
+
return body;
|
|
102
|
+
}
|
|
103
|
+
/** List candidates exposed by an OpenAI-compatible endpoint. */
|
|
104
|
+
async function listOpenAIModels(config) {
|
|
105
|
+
if (config.apiUrl.trim() === "") throw new Error("API URL is required");
|
|
106
|
+
const body = await responseJson(await fetch(endpoint(config.apiUrl, "/models"), { headers: headers(config.apiKey) }));
|
|
107
|
+
const data = Array.isArray(body.data) ? body.data : [];
|
|
108
|
+
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));
|
|
109
|
+
}
|
|
110
|
+
/** List chat models exposed by an OpenAI-compatible endpoint. */
|
|
111
|
+
async function listPromptModels(config) {
|
|
112
|
+
return listOpenAIModels(config);
|
|
113
|
+
}
|
|
114
|
+
/** Expand a concise image request into a production-ready image prompt. */
|
|
115
|
+
async function enhancePrompt(config, prompt) {
|
|
116
|
+
if (config.apiUrl.trim() === "" || config.model.trim() === "") throw new Error("prompt enhancement model is not configured");
|
|
117
|
+
const body = await responseJson(await fetch(endpoint(config.apiUrl, "/chat/completions"), {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: headers(config.apiKey),
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
model: config.model.trim(),
|
|
122
|
+
temperature: .7,
|
|
123
|
+
messages: [{
|
|
124
|
+
role: "system",
|
|
125
|
+
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."
|
|
126
|
+
}, {
|
|
127
|
+
role: "user",
|
|
128
|
+
content: prompt
|
|
129
|
+
}]
|
|
130
|
+
})
|
|
131
|
+
}));
|
|
132
|
+
const choices = Array.isArray(body.choices) ? body.choices : [];
|
|
133
|
+
const content = choices[0] !== null && typeof choices[0] === "object" ? choices[0].message?.content : void 0;
|
|
134
|
+
if (typeof content !== "string" || content.trim() === "") throw new Error("chat model returned an empty prompt");
|
|
135
|
+
return content.trim();
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/image-models.ts
|
|
139
|
+
/**
|
|
140
|
+
* Image-model configuration shared by the host, panel, and Agent tools.
|
|
141
|
+
* `/models` exposes candidates only: the configured list is the explicit
|
|
142
|
+
* allow-list because OpenAI-compatible gateways rarely advertise modalities.
|
|
143
|
+
*/
|
|
144
|
+
const DEFAULT_IMAGE_MODELS = ["gpt-image-2", "grok-imagine-image"];
|
|
145
|
+
/** Normalize user-entered model identifiers and retain a usable legacy default. */
|
|
146
|
+
function normalizeImageModels(value) {
|
|
147
|
+
const candidates = Array.isArray(value) ? value : [];
|
|
148
|
+
const unique = /* @__PURE__ */ new Set();
|
|
149
|
+
for (const candidate of candidates) {
|
|
150
|
+
if (typeof candidate !== "string") continue;
|
|
151
|
+
const model = candidate.trim();
|
|
152
|
+
if (model !== "") unique.add(model);
|
|
153
|
+
}
|
|
154
|
+
return unique.size > 0 ? [...unique] : [...DEFAULT_IMAGE_MODELS];
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
68
157
|
//#region src/engine.ts
|
|
69
158
|
/** A generation failure with a user-presentable message. */
|
|
70
159
|
var ImageGenError = class extends Error {
|
|
@@ -110,6 +199,30 @@ const OPENAI_SIZE_BY_RATIO = {
|
|
|
110
199
|
/** Panel ratios that need renaming for a model's vocabulary. Grok documents
|
|
111
200
|
* 20:9 as its ultra-wide ratio, so the panel's 21:9 label is sent as 20:9. */
|
|
112
201
|
const GROK_ASPECT_ALIASES = { "21:9": "20:9" };
|
|
202
|
+
/**
|
|
203
|
+
* One request-scoped timeout that is cleared as soon as its fetch settles.
|
|
204
|
+
* AbortSignal.timeout() cannot be disposed early; using it inside a long-lived
|
|
205
|
+
* task queue leaves an otherwise idle Node process holding every timeout.
|
|
206
|
+
*/
|
|
207
|
+
function requestSignal(source, timeoutMs) {
|
|
208
|
+
const controller = new AbortController();
|
|
209
|
+
const abortFromSource = () => {
|
|
210
|
+
controller.abort(source?.reason);
|
|
211
|
+
};
|
|
212
|
+
if (source?.aborted === true) abortFromSource();
|
|
213
|
+
else source?.addEventListener("abort", abortFromSource, { once: true });
|
|
214
|
+
const timeout = setTimeout(() => {
|
|
215
|
+
controller.abort(new DOMException("The operation timed out.", "TimeoutError"));
|
|
216
|
+
}, timeoutMs);
|
|
217
|
+
timeout.unref();
|
|
218
|
+
return {
|
|
219
|
+
signal: controller.signal,
|
|
220
|
+
dispose: () => {
|
|
221
|
+
clearTimeout(timeout);
|
|
222
|
+
source?.removeEventListener("abort", abortFromSource);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
113
226
|
/** Content-type extension hints for URL-fetched images. */
|
|
114
227
|
function mimeOfExtension(path) {
|
|
115
228
|
const match = /\.([a-z0-9]+)$/i.exec(path);
|
|
@@ -194,14 +307,17 @@ async function normalizeItem(item, upstream) {
|
|
|
194
307
|
revisedPrompt
|
|
195
308
|
};
|
|
196
309
|
}
|
|
310
|
+
const budget = requestSignal(void 0, IMAGE_FETCH_TIMEOUT_MS);
|
|
197
311
|
let response;
|
|
198
312
|
try {
|
|
199
313
|
response = await fetch(url, {
|
|
200
314
|
headers: { ...upstream.apiKey === "" ? {} : { authorization: `Bearer ${upstream.apiKey}` } },
|
|
201
|
-
signal:
|
|
315
|
+
signal: budget.signal
|
|
202
316
|
});
|
|
203
317
|
} catch (error) {
|
|
204
318
|
throw new ImageGenError(`failed to fetch the generated image url: ${error instanceof Error ? error.message : String(error)}`);
|
|
319
|
+
} finally {
|
|
320
|
+
budget.dispose();
|
|
205
321
|
}
|
|
206
322
|
if (!response.ok) throw new ImageGenError(`failed to fetch the generated image url: HTTP ${response.status}`);
|
|
207
323
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
@@ -217,7 +333,7 @@ async function normalizeItem(item, upstream) {
|
|
|
217
333
|
* Issue one single-image request (never sends `n`). The response is kept as a
|
|
218
334
|
* list so a gateway that happens to return several images per call still works.
|
|
219
335
|
*/
|
|
220
|
-
async function requestOneImage(baseUrl, upstream, request, params) {
|
|
336
|
+
async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
221
337
|
const headers = { authorization: `Bearer ${upstream.apiKey.trim()}` };
|
|
222
338
|
let body;
|
|
223
339
|
if (request.mode === "edit") {
|
|
@@ -260,18 +376,21 @@ async function requestOneImage(baseUrl, upstream, request, params) {
|
|
|
260
376
|
...params
|
|
261
377
|
});
|
|
262
378
|
}
|
|
379
|
+
const budget = requestSignal(signal, UPSTREAM_TIMEOUT_MS);
|
|
263
380
|
let response;
|
|
264
381
|
try {
|
|
265
382
|
response = await fetch(`${baseUrl}/images/${request.mode === "edit" ? "edits" : "generations"}`, {
|
|
266
383
|
method: "POST",
|
|
267
384
|
headers,
|
|
268
385
|
body,
|
|
269
|
-
signal:
|
|
386
|
+
signal: budget.signal
|
|
270
387
|
});
|
|
271
388
|
} catch (error) {
|
|
272
389
|
const message = error instanceof Error ? error.message : String(error);
|
|
273
390
|
if (/aborter/i.test(message) || /timeout/i.test(message)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
|
|
274
391
|
throw new ImageGenError(`无法连接上游接口:${message}`, "upstream-unreachable");
|
|
392
|
+
} finally {
|
|
393
|
+
budget.dispose();
|
|
275
394
|
}
|
|
276
395
|
let payload;
|
|
277
396
|
try {
|
|
@@ -295,13 +414,13 @@ async function requestOneImage(baseUrl, upstream, request, params) {
|
|
|
295
414
|
* parameter is never sent, because Responses-API-based gateways reject it as
|
|
296
415
|
* `tools[0].n`), then the results are flattened in order.
|
|
297
416
|
*/
|
|
298
|
-
async function generateImage(upstream, request) {
|
|
417
|
+
async function generateImage(upstream, request, options = {}) {
|
|
299
418
|
const baseUrl = upstream.apiUrl.trim().replace(/\/+$/, "");
|
|
300
419
|
if (baseUrl === "") throw new ImageGenError("api_url 未配置:请先在「设置 → 插件 → 可配置」中填写", "config-missing");
|
|
301
420
|
if (upstream.apiKey.trim() === "") throw new ImageGenError("api_key 未配置:请先在「设置 → 插件 → 可配置」中填写", "config-missing");
|
|
302
421
|
const params = effectiveParams(request);
|
|
303
422
|
const count = effectiveCount(request);
|
|
304
|
-
return { images: (await Promise.all(Array.from({ length: count }, () => requestOneImage(baseUrl, upstream, request, params)))).flat() };
|
|
423
|
+
return { images: (await Promise.all(Array.from({ length: count }, () => requestOneImage(baseUrl, upstream, request, params, options.signal)))).flat() };
|
|
305
424
|
}
|
|
306
425
|
/** Human-readable failure message from an upstream error payload. */
|
|
307
426
|
function upstreamMessage(payload, status) {
|
|
@@ -508,6 +627,146 @@ async function readHistoryImage(file) {
|
|
|
508
627
|
}
|
|
509
628
|
}
|
|
510
629
|
//#endregion
|
|
630
|
+
//#region src/task-queue.ts
|
|
631
|
+
/** In-memory, host-resident image generation queue. */
|
|
632
|
+
var GenerationTaskQueue = class {
|
|
633
|
+
run;
|
|
634
|
+
tasks = [];
|
|
635
|
+
controllers = /* @__PURE__ */ new Map();
|
|
636
|
+
listeners = /* @__PURE__ */ new Set();
|
|
637
|
+
running = false;
|
|
638
|
+
constructor(run) {
|
|
639
|
+
this.run = run;
|
|
640
|
+
}
|
|
641
|
+
list() {
|
|
642
|
+
return this.tasks.map((task) => this.snapshot(task));
|
|
643
|
+
}
|
|
644
|
+
/** Observe queue state changes. Listener failures never disrupt generation. */
|
|
645
|
+
subscribe(listener) {
|
|
646
|
+
this.listeners.add(listener);
|
|
647
|
+
return () => {
|
|
648
|
+
this.listeners.delete(listener);
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
submit(request) {
|
|
652
|
+
const task = {
|
|
653
|
+
id: randomUUID(),
|
|
654
|
+
request: { ...request },
|
|
655
|
+
status: "queued",
|
|
656
|
+
createdAt: Date.now()
|
|
657
|
+
};
|
|
658
|
+
this.tasks.unshift(task);
|
|
659
|
+
this.publish(task);
|
|
660
|
+
this.drain();
|
|
661
|
+
return this.snapshot(task);
|
|
662
|
+
}
|
|
663
|
+
cancel(id) {
|
|
664
|
+
const task = this.tasks.find((item) => item.id === id);
|
|
665
|
+
if (task === void 0 || task.status === "completed" || task.status === "failed" || task.status === "cancelled") return task;
|
|
666
|
+
task.status = "cancelled";
|
|
667
|
+
task.finishedAt = Date.now();
|
|
668
|
+
this.controllers.get(id)?.abort();
|
|
669
|
+
this.publish(task);
|
|
670
|
+
return this.snapshot(task);
|
|
671
|
+
}
|
|
672
|
+
retry(id) {
|
|
673
|
+
const previous = this.tasks.find((item) => item.id === id);
|
|
674
|
+
return previous === void 0 ? void 0 : this.submit(previous.request);
|
|
675
|
+
}
|
|
676
|
+
async drain() {
|
|
677
|
+
if (this.running) return;
|
|
678
|
+
this.running = true;
|
|
679
|
+
try {
|
|
680
|
+
for (;;) {
|
|
681
|
+
const task = this.tasks.find((item) => item.status === "queued");
|
|
682
|
+
if (task === void 0) return;
|
|
683
|
+
task.status = "running";
|
|
684
|
+
task.startedAt = Date.now();
|
|
685
|
+
this.publish(task);
|
|
686
|
+
const controller = new AbortController();
|
|
687
|
+
this.controllers.set(task.id, controller);
|
|
688
|
+
try {
|
|
689
|
+
const result = await this.run(task.request, controller.signal);
|
|
690
|
+
if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
|
|
691
|
+
task.status = "completed";
|
|
692
|
+
task.result = result;
|
|
693
|
+
task.finishedAt = Date.now();
|
|
694
|
+
this.publish(task);
|
|
695
|
+
}
|
|
696
|
+
} catch (error) {
|
|
697
|
+
if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
|
|
698
|
+
task.status = "failed";
|
|
699
|
+
task.error = error instanceof Error ? error.message : String(error);
|
|
700
|
+
task.finishedAt = Date.now();
|
|
701
|
+
this.publish(task);
|
|
702
|
+
}
|
|
703
|
+
} finally {
|
|
704
|
+
this.controllers.delete(task.id);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
} finally {
|
|
708
|
+
this.running = false;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
publish(task) {
|
|
712
|
+
const snapshot = this.snapshot(task);
|
|
713
|
+
for (const listener of this.listeners) try {
|
|
714
|
+
listener(snapshot);
|
|
715
|
+
} catch {}
|
|
716
|
+
}
|
|
717
|
+
snapshot(task) {
|
|
718
|
+
return {
|
|
719
|
+
...task,
|
|
720
|
+
request: { ...task.request },
|
|
721
|
+
...task.result === void 0 ? {} : { result: task.result }
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/generation-runtime.ts
|
|
727
|
+
/**
|
|
728
|
+
* Shared host-side generation runtime. Both the browser routes and Agent tools
|
|
729
|
+
* submit to this one queue so persisted history and cancellation semantics stay
|
|
730
|
+
* identical regardless of where a request originated.
|
|
731
|
+
*/
|
|
732
|
+
var ImageGenerationRuntime = class {
|
|
733
|
+
resolve;
|
|
734
|
+
history;
|
|
735
|
+
queue;
|
|
736
|
+
constructor(resolve, history = { append: appendHistory }) {
|
|
737
|
+
this.resolve = resolve;
|
|
738
|
+
this.history = history;
|
|
739
|
+
this.queue = new GenerationTaskQueue((request, signal) => this.run(request, signal));
|
|
740
|
+
}
|
|
741
|
+
async run(request, signal) {
|
|
742
|
+
const result = await generateImage(this.resolve(), request, { signal });
|
|
743
|
+
try {
|
|
744
|
+
const history = await this.history.append({
|
|
745
|
+
id: randomUUID(),
|
|
746
|
+
createdAt: Date.now(),
|
|
747
|
+
mode: request.mode,
|
|
748
|
+
model: request.model,
|
|
749
|
+
prompt: request.prompt,
|
|
750
|
+
size: request.size,
|
|
751
|
+
quality: request.quality,
|
|
752
|
+
detail: request.detail,
|
|
753
|
+
n: request.n,
|
|
754
|
+
images: result.images,
|
|
755
|
+
...request.refName === void 0 ? {} : { refName: request.refName }
|
|
756
|
+
});
|
|
757
|
+
return {
|
|
758
|
+
...result,
|
|
759
|
+
history
|
|
760
|
+
};
|
|
761
|
+
} catch (error) {
|
|
762
|
+
return {
|
|
763
|
+
...result,
|
|
764
|
+
historyError: error instanceof Error ? error.message : String(error)
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
//#endregion
|
|
511
770
|
//#region src/gallery-store.ts
|
|
512
771
|
/**
|
|
513
772
|
* Host-persisted gallery (user-curated favorites): mirrors the history store
|
|
@@ -616,7 +875,8 @@ function toWire(entry) {
|
|
|
616
875
|
mime: image.mime,
|
|
617
876
|
...image.revisedPrompt === void 0 ? {} : { revisedPrompt: image.revisedPrompt }
|
|
618
877
|
})),
|
|
619
|
-
...entry.refName === void 0 ? {} : { refName: entry.refName }
|
|
878
|
+
...entry.refName === void 0 ? {} : { refName: entry.refName },
|
|
879
|
+
...entry.tags === void 0 ? {} : { tags: entry.tags }
|
|
620
880
|
};
|
|
621
881
|
}
|
|
622
882
|
/** List the persisted gallery, newest first, as wire entries. */
|
|
@@ -686,6 +946,17 @@ async function removeGallery(id) {
|
|
|
686
946
|
return kept.map(toWire);
|
|
687
947
|
});
|
|
688
948
|
}
|
|
949
|
+
/** Replace the user-managed labels for one gallery entry. */
|
|
950
|
+
async function updateGalleryTags(id, tags) {
|
|
951
|
+
return mutateGallery(async () => {
|
|
952
|
+
const normalized = [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))].slice(0, 20);
|
|
953
|
+
const entries = await readIndex();
|
|
954
|
+
const target = entries.find((entry) => entry.id === id);
|
|
955
|
+
if (target !== void 0) target.tags = normalized;
|
|
956
|
+
await writeIndex(entries);
|
|
957
|
+
return entries.map(toWire);
|
|
958
|
+
});
|
|
959
|
+
}
|
|
689
960
|
/** Remove every entry (and all image files). */
|
|
690
961
|
async function clearGallery() {
|
|
691
962
|
return mutateGallery(async () => {
|
|
@@ -1134,6 +1405,21 @@ async function readJsonBody(req, maxBytes = MAX_JSON_BODY_BYTES) {
|
|
|
1134
1405
|
function messageOf(error) {
|
|
1135
1406
|
return error instanceof Error ? error.message : String(error);
|
|
1136
1407
|
}
|
|
1408
|
+
function parseGenerateRequest(body) {
|
|
1409
|
+
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
1410
|
+
if (prompt === "") return void 0;
|
|
1411
|
+
return {
|
|
1412
|
+
mode: body.mode === "edit" ? "edit" : "text",
|
|
1413
|
+
model: typeof body.model === "string" ? body.model : "",
|
|
1414
|
+
prompt,
|
|
1415
|
+
size: typeof body.size === "string" ? body.size : "auto",
|
|
1416
|
+
quality: typeof body.quality === "string" ? body.quality : "auto",
|
|
1417
|
+
n: typeof body.n === "number" ? body.n : 1,
|
|
1418
|
+
detail: typeof body.detail === "string" ? body.detail : "",
|
|
1419
|
+
...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {},
|
|
1420
|
+
...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {}
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1137
1423
|
/** Validate a submitted history entry (images carry base64). */
|
|
1138
1424
|
function parseHistoryEntryInput(body) {
|
|
1139
1425
|
const raw = body.entry;
|
|
@@ -1228,6 +1514,7 @@ function makeRoutes(deps) {
|
|
|
1228
1514
|
append: appendGallery,
|
|
1229
1515
|
remove: removeGallery,
|
|
1230
1516
|
clear: clearGallery,
|
|
1517
|
+
updateTags: updateGalleryTags,
|
|
1231
1518
|
readImage: readGalleryImage
|
|
1232
1519
|
};
|
|
1233
1520
|
const templates = deps.templates ?? {
|
|
@@ -1235,6 +1522,24 @@ function makeRoutes(deps) {
|
|
|
1235
1522
|
refresh: refreshTemplates,
|
|
1236
1523
|
readImage: readTemplateImage
|
|
1237
1524
|
};
|
|
1525
|
+
const resolvePrompt = deps.resolvePrompt ?? (() => ({
|
|
1526
|
+
apiUrl: "",
|
|
1527
|
+
apiKey: "",
|
|
1528
|
+
model: ""
|
|
1529
|
+
}));
|
|
1530
|
+
const resolveImageModels = deps.resolveImageModels ?? (() => normalizeImageModels(void 0));
|
|
1531
|
+
const parseConfiguredRequest = (body) => {
|
|
1532
|
+
const request = parseGenerateRequest(body);
|
|
1533
|
+
if (request === void 0) return void 0;
|
|
1534
|
+
const models = normalizeImageModels(resolveImageModels());
|
|
1535
|
+
const model = request.model.trim() === "" ? models[0] : request.model.trim();
|
|
1536
|
+
if (!models.includes(model)) throw new Error(`image model "${model}" is not configured; choose one of: ${models.join(", ")}`);
|
|
1537
|
+
return {
|
|
1538
|
+
...request,
|
|
1539
|
+
model
|
|
1540
|
+
};
|
|
1541
|
+
};
|
|
1542
|
+
const runtime = deps.runtime ?? new ImageGenerationRuntime(deps.resolve, history);
|
|
1238
1543
|
const guard = (req, res, method) => {
|
|
1239
1544
|
if (!isLoopbackRequest(req)) {
|
|
1240
1545
|
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
@@ -1247,6 +1552,73 @@ function makeRoutes(deps) {
|
|
|
1247
1552
|
return true;
|
|
1248
1553
|
};
|
|
1249
1554
|
return [
|
|
1555
|
+
{
|
|
1556
|
+
kind: "exact",
|
|
1557
|
+
path: IMAGE_MODEL_API.models,
|
|
1558
|
+
handler: async (req, res) => {
|
|
1559
|
+
if (!guard(req, res, "POST")) return;
|
|
1560
|
+
try {
|
|
1561
|
+
writeJson(res, 200, {
|
|
1562
|
+
ok: true,
|
|
1563
|
+
models: await listOpenAIModels(deps.resolve())
|
|
1564
|
+
});
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
writeJson(res, 200, {
|
|
1567
|
+
ok: false,
|
|
1568
|
+
code: "image-models-failed",
|
|
1569
|
+
message: messageOf(error)
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
},
|
|
1574
|
+
{
|
|
1575
|
+
kind: "exact",
|
|
1576
|
+
path: PROMPT_ENHANCE_API.models,
|
|
1577
|
+
handler: async (req, res) => {
|
|
1578
|
+
if (!guard(req, res, "POST")) return;
|
|
1579
|
+
try {
|
|
1580
|
+
writeJson(res, 200, {
|
|
1581
|
+
ok: true,
|
|
1582
|
+
models: await listPromptModels(resolvePrompt())
|
|
1583
|
+
});
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
writeJson(res, 200, {
|
|
1586
|
+
ok: false,
|
|
1587
|
+
code: "prompt-models-failed",
|
|
1588
|
+
message: messageOf(error)
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
},
|
|
1593
|
+
{
|
|
1594
|
+
kind: "exact",
|
|
1595
|
+
path: PROMPT_ENHANCE_API.enhance,
|
|
1596
|
+
handler: async (req, res) => {
|
|
1597
|
+
if (!guard(req, res, "POST")) return;
|
|
1598
|
+
const body = await readJsonBody(req);
|
|
1599
|
+
const prompt = typeof body?.prompt === "string" ? body.prompt.trim() : "";
|
|
1600
|
+
if (prompt === "") {
|
|
1601
|
+
writeJson(res, 200, {
|
|
1602
|
+
ok: false,
|
|
1603
|
+
code: "bad-request",
|
|
1604
|
+
message: "prompt is required"
|
|
1605
|
+
});
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
try {
|
|
1609
|
+
writeJson(res, 200, {
|
|
1610
|
+
ok: true,
|
|
1611
|
+
prompt: await enhancePrompt(resolvePrompt(), prompt)
|
|
1612
|
+
});
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
writeJson(res, 200, {
|
|
1615
|
+
ok: false,
|
|
1616
|
+
code: "prompt-enhance-failed",
|
|
1617
|
+
message: messageOf(error)
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
},
|
|
1250
1622
|
{
|
|
1251
1623
|
kind: "exact",
|
|
1252
1624
|
path: SETTINGS_API.describe,
|
|
@@ -1321,62 +1693,30 @@ function makeRoutes(deps) {
|
|
|
1321
1693
|
});
|
|
1322
1694
|
return;
|
|
1323
1695
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1696
|
+
let request;
|
|
1697
|
+
try {
|
|
1698
|
+
request = parseConfiguredRequest(body);
|
|
1699
|
+
} catch (error) {
|
|
1326
1700
|
writeJson(res, 200, {
|
|
1327
1701
|
ok: false,
|
|
1328
|
-
code: "
|
|
1329
|
-
message:
|
|
1702
|
+
code: "image-model-not-configured",
|
|
1703
|
+
message: messageOf(error)
|
|
1330
1704
|
});
|
|
1331
1705
|
return;
|
|
1332
1706
|
}
|
|
1333
|
-
if (
|
|
1707
|
+
if (request === void 0) {
|
|
1334
1708
|
writeJson(res, 200, {
|
|
1335
1709
|
ok: false,
|
|
1336
1710
|
code: "bad-request",
|
|
1337
|
-
message: "prompt
|
|
1711
|
+
message: "prompt is required"
|
|
1338
1712
|
});
|
|
1339
1713
|
return;
|
|
1340
1714
|
}
|
|
1341
|
-
const request = {
|
|
1342
|
-
mode: body.mode === "edit" ? "edit" : "text",
|
|
1343
|
-
model: typeof body.model === "string" ? body.model : "gpt-image-2",
|
|
1344
|
-
prompt,
|
|
1345
|
-
size: typeof body.size === "string" ? body.size : "auto",
|
|
1346
|
-
quality: typeof body.quality === "string" ? body.quality : "auto",
|
|
1347
|
-
n: typeof body.n === "number" ? body.n : 1,
|
|
1348
|
-
detail: typeof body.detail === "string" ? body.detail : "",
|
|
1349
|
-
...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {},
|
|
1350
|
-
...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {}
|
|
1351
|
-
};
|
|
1352
1715
|
try {
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
createdAt: Date.now(),
|
|
1358
|
-
mode: request.mode,
|
|
1359
|
-
model: request.model,
|
|
1360
|
-
prompt: request.prompt,
|
|
1361
|
-
size: request.size,
|
|
1362
|
-
quality: request.quality,
|
|
1363
|
-
detail: request.detail,
|
|
1364
|
-
n: request.n,
|
|
1365
|
-
images: result.images,
|
|
1366
|
-
...request.refName === void 0 ? {} : { refName: request.refName }
|
|
1367
|
-
});
|
|
1368
|
-
writeJson(res, 200, {
|
|
1369
|
-
ok: true,
|
|
1370
|
-
...result,
|
|
1371
|
-
history: entries
|
|
1372
|
-
});
|
|
1373
|
-
} catch (error) {
|
|
1374
|
-
writeJson(res, 200, {
|
|
1375
|
-
ok: true,
|
|
1376
|
-
...result,
|
|
1377
|
-
historyError: messageOf(error)
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1716
|
+
writeJson(res, 200, {
|
|
1717
|
+
ok: true,
|
|
1718
|
+
...await runtime.run(request)
|
|
1719
|
+
});
|
|
1380
1720
|
} catch (error) {
|
|
1381
1721
|
const message = error instanceof Error ? error.message : String(error);
|
|
1382
1722
|
writeJson(res, 200, {
|
|
@@ -1387,6 +1727,90 @@ function makeRoutes(deps) {
|
|
|
1387
1727
|
}
|
|
1388
1728
|
}
|
|
1389
1729
|
},
|
|
1730
|
+
{
|
|
1731
|
+
kind: "exact",
|
|
1732
|
+
path: TASK_API.submit,
|
|
1733
|
+
handler: async (req, res) => {
|
|
1734
|
+
if (!guard(req, res, "POST")) return;
|
|
1735
|
+
const body = await readJsonBody(req);
|
|
1736
|
+
let request;
|
|
1737
|
+
try {
|
|
1738
|
+
request = body === void 0 ? void 0 : parseConfiguredRequest(body);
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
writeJson(res, 200, {
|
|
1741
|
+
ok: false,
|
|
1742
|
+
code: "image-model-not-configured",
|
|
1743
|
+
message: messageOf(error)
|
|
1744
|
+
});
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
if (request === void 0) {
|
|
1748
|
+
writeJson(res, 200, {
|
|
1749
|
+
ok: false,
|
|
1750
|
+
code: "bad-request",
|
|
1751
|
+
message: "prompt is required"
|
|
1752
|
+
});
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
writeJson(res, 200, {
|
|
1756
|
+
ok: true,
|
|
1757
|
+
task: runtime.queue.submit(request)
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
},
|
|
1761
|
+
{
|
|
1762
|
+
kind: "exact",
|
|
1763
|
+
path: TASK_API.list,
|
|
1764
|
+
handler: async (req, res) => {
|
|
1765
|
+
if (!guard(req, res, "POST")) return;
|
|
1766
|
+
writeJson(res, 200, {
|
|
1767
|
+
ok: true,
|
|
1768
|
+
tasks: runtime.queue.list()
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
},
|
|
1772
|
+
{
|
|
1773
|
+
kind: "exact",
|
|
1774
|
+
path: TASK_API.cancel,
|
|
1775
|
+
handler: async (req, res) => {
|
|
1776
|
+
if (!guard(req, res, "POST")) return;
|
|
1777
|
+
const body = await readJsonBody(req);
|
|
1778
|
+
const task = typeof body?.id === "string" ? runtime.queue.cancel(body.id) : void 0;
|
|
1779
|
+
if (task === void 0) {
|
|
1780
|
+
writeJson(res, 200, {
|
|
1781
|
+
ok: false,
|
|
1782
|
+
code: "not-found",
|
|
1783
|
+
message: "task not found"
|
|
1784
|
+
});
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
writeJson(res, 200, {
|
|
1788
|
+
ok: true,
|
|
1789
|
+
task
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
},
|
|
1793
|
+
{
|
|
1794
|
+
kind: "exact",
|
|
1795
|
+
path: TASK_API.retry,
|
|
1796
|
+
handler: async (req, res) => {
|
|
1797
|
+
if (!guard(req, res, "POST")) return;
|
|
1798
|
+
const body = await readJsonBody(req);
|
|
1799
|
+
const task = typeof body?.id === "string" ? runtime.queue.retry(body.id) : void 0;
|
|
1800
|
+
if (task === void 0) {
|
|
1801
|
+
writeJson(res, 200, {
|
|
1802
|
+
ok: false,
|
|
1803
|
+
code: "not-found",
|
|
1804
|
+
message: "task not found"
|
|
1805
|
+
});
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
writeJson(res, 200, {
|
|
1809
|
+
ok: true,
|
|
1810
|
+
task
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
},
|
|
1390
1814
|
{
|
|
1391
1815
|
kind: "exact",
|
|
1392
1816
|
path: UPDATE_API.check,
|
|
@@ -1671,6 +2095,36 @@ function makeRoutes(deps) {
|
|
|
1671
2095
|
}
|
|
1672
2096
|
}
|
|
1673
2097
|
},
|
|
2098
|
+
{
|
|
2099
|
+
kind: "exact",
|
|
2100
|
+
path: GALLERY_API.tags,
|
|
2101
|
+
handler: async (req, res) => {
|
|
2102
|
+
if (!guard(req, res, "POST")) return;
|
|
2103
|
+
const body = await readJsonBody(req);
|
|
2104
|
+
const id = typeof body?.id === "string" ? body.id : "";
|
|
2105
|
+
const tags = Array.isArray(body?.tags) ? body.tags.filter((tag) => typeof tag === "string") : void 0;
|
|
2106
|
+
if (id === "" || tags === void 0 || gallery.updateTags === void 0) {
|
|
2107
|
+
writeJson(res, 200, {
|
|
2108
|
+
ok: false,
|
|
2109
|
+
code: "bad-request",
|
|
2110
|
+
message: "gallery id and tags are required"
|
|
2111
|
+
});
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
try {
|
|
2115
|
+
writeJson(res, 200, {
|
|
2116
|
+
ok: true,
|
|
2117
|
+
entries: await gallery.updateTags(id, tags)
|
|
2118
|
+
});
|
|
2119
|
+
} catch (error) {
|
|
2120
|
+
writeJson(res, 200, {
|
|
2121
|
+
ok: false,
|
|
2122
|
+
code: "gallery-failed",
|
|
2123
|
+
message: messageOf(error)
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
},
|
|
1674
2128
|
{
|
|
1675
2129
|
kind: "exact",
|
|
1676
2130
|
path: GALLERY_API.clear,
|
|
@@ -1791,6 +2245,328 @@ function makeRoutes(deps) {
|
|
|
1791
2245
|
];
|
|
1792
2246
|
}
|
|
1793
2247
|
//#endregion
|
|
2248
|
+
//#region src/agent-image-tools.ts
|
|
2249
|
+
const imageRefSchema = {
|
|
2250
|
+
type: "object",
|
|
2251
|
+
additionalProperties: false,
|
|
2252
|
+
properties: {
|
|
2253
|
+
attachment_id: {
|
|
2254
|
+
type: "string",
|
|
2255
|
+
required: true
|
|
2256
|
+
},
|
|
2257
|
+
media_type: {
|
|
2258
|
+
type: "string",
|
|
2259
|
+
required: true
|
|
2260
|
+
},
|
|
2261
|
+
bytes: {
|
|
2262
|
+
type: "integer",
|
|
2263
|
+
required: true
|
|
2264
|
+
},
|
|
2265
|
+
width: {
|
|
2266
|
+
type: "integer",
|
|
2267
|
+
required: true
|
|
2268
|
+
},
|
|
2269
|
+
height: {
|
|
2270
|
+
type: "integer",
|
|
2271
|
+
required: true
|
|
2272
|
+
},
|
|
2273
|
+
name: { type: "string" }
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
const taskResultSchema = {
|
|
2277
|
+
type: "object",
|
|
2278
|
+
additionalProperties: false,
|
|
2279
|
+
properties: {
|
|
2280
|
+
task_id: {
|
|
2281
|
+
type: "string",
|
|
2282
|
+
required: true
|
|
2283
|
+
},
|
|
2284
|
+
status: {
|
|
2285
|
+
type: "string",
|
|
2286
|
+
required: true
|
|
2287
|
+
},
|
|
2288
|
+
message: {
|
|
2289
|
+
type: "string",
|
|
2290
|
+
required: true
|
|
2291
|
+
},
|
|
2292
|
+
error: { type: "string" },
|
|
2293
|
+
images: {
|
|
2294
|
+
type: "array",
|
|
2295
|
+
required: true,
|
|
2296
|
+
items: imageRefSchema
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
};
|
|
2300
|
+
function acceptedMediaType(value) {
|
|
2301
|
+
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
2302
|
+
}
|
|
2303
|
+
function projectRef(ref) {
|
|
2304
|
+
return {
|
|
2305
|
+
attachment_id: String(ref.attachmentId),
|
|
2306
|
+
media_type: ref.mediaType,
|
|
2307
|
+
bytes: ref.bytes,
|
|
2308
|
+
width: ref.width,
|
|
2309
|
+
height: ref.height,
|
|
2310
|
+
...ref.name === void 0 ? {} : { name: ref.name }
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
function restoreRef(value) {
|
|
2314
|
+
if (!acceptedMediaType(value.media_type)) throw new ImageGenError("source_image.media_type is not a supported image type", "bad-reference-image");
|
|
2315
|
+
if (!Number.isInteger(value.bytes) || value.bytes < 1 || !Number.isInteger(value.width) || value.width < 1 || !Number.isInteger(value.height) || value.height < 1) throw new ImageGenError("source_image metadata is invalid", "bad-reference-image");
|
|
2316
|
+
return {
|
|
2317
|
+
attachmentId: value.attachment_id,
|
|
2318
|
+
mediaType: value.media_type,
|
|
2319
|
+
bytes: value.bytes,
|
|
2320
|
+
width: value.width,
|
|
2321
|
+
height: value.height,
|
|
2322
|
+
...value.name === void 0 ? {} : { name: value.name }
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
function imageDataUrl(image) {
|
|
2326
|
+
return `data:${image.ref.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
|
|
2327
|
+
}
|
|
2328
|
+
function renderTaskResult(value) {
|
|
2329
|
+
return [{
|
|
2330
|
+
type: "text",
|
|
2331
|
+
text: JSON.stringify(value)
|
|
2332
|
+
}, ...value.images.map((image) => ({
|
|
2333
|
+
type: "image",
|
|
2334
|
+
attachment: restoreRef(image)
|
|
2335
|
+
}))];
|
|
2336
|
+
}
|
|
2337
|
+
/** Register the global Agent tools and unregister them with the plugin lifecycle. */
|
|
2338
|
+
function registerAgentImageTools(ctx, runtime, resolve) {
|
|
2339
|
+
const attachmentRefs = /* @__PURE__ */ new Map();
|
|
2340
|
+
const taskSubscriptions = /* @__PURE__ */ new Set();
|
|
2341
|
+
const ensureConfigured = () => {
|
|
2342
|
+
const config = resolve();
|
|
2343
|
+
if (!config.enabled) throw new ImageGenError("AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.", "plugin-disabled");
|
|
2344
|
+
if (!config.allowAgentImageGeneration) throw new ImageGenError("Agent image generation is disabled in Settings > Plugins > AI Image.", "agent-generation-disabled");
|
|
2345
|
+
if (config.apiUrl.trim() === "" || config.apiKey.trim() === "") throw new ImageGenError("Image API credentials are not configured. Open Settings > Plugins > AI Image and fill in API URL and API key.", "image-api-not-configured");
|
|
2346
|
+
};
|
|
2347
|
+
const selectedModel = (requested) => {
|
|
2348
|
+
const models = normalizeImageModels(resolve().imageModels);
|
|
2349
|
+
const model = typeof requested === "string" && requested.trim() !== "" ? requested.trim() : models[0];
|
|
2350
|
+
if (!models.includes(model)) throw new ImageGenError(`Image model "${model}" is not configured. Choose one of: ${models.join(", ")}.`, "image-model-not-configured");
|
|
2351
|
+
return model;
|
|
2352
|
+
};
|
|
2353
|
+
const materializeTaskImages = (task) => {
|
|
2354
|
+
if (task.status !== "completed") return Promise.resolve([]);
|
|
2355
|
+
const existing = attachmentRefs.get(task.id);
|
|
2356
|
+
if (existing !== void 0) return existing;
|
|
2357
|
+
const pending = ctx.attachments.saveImages((task.result?.images ?? []).map((image, index) => toSaveImage(image, task.id, index))).then((refs) => refs.map(projectRef));
|
|
2358
|
+
attachmentRefs.set(task.id, pending);
|
|
2359
|
+
pending.catch(() => {
|
|
2360
|
+
if (attachmentRefs.get(task.id) === pending) attachmentRefs.delete(task.id);
|
|
2361
|
+
});
|
|
2362
|
+
return pending;
|
|
2363
|
+
};
|
|
2364
|
+
const taskResult = async (task) => {
|
|
2365
|
+
const images = await materializeTaskImages(task);
|
|
2366
|
+
return {
|
|
2367
|
+
task_id: task.id,
|
|
2368
|
+
status: task.status,
|
|
2369
|
+
message: task.status === "completed" ? "Generation completed. The images are attached below 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. Completion will be delivered to the conversation automatically with image attachments.",
|
|
2370
|
+
...task.error === void 0 ? {} : { error: task.error },
|
|
2371
|
+
images
|
|
2372
|
+
};
|
|
2373
|
+
};
|
|
2374
|
+
const findTask = (id) => {
|
|
2375
|
+
const task = runtime.queue.list().find((candidate) => candidate.id === id);
|
|
2376
|
+
if (task === void 0) throw new ImageGenError(`Image generation task ${id} was not found.`, "task-not-found");
|
|
2377
|
+
return task;
|
|
2378
|
+
};
|
|
2379
|
+
const notifyCompletion = async (agent, task) => {
|
|
2380
|
+
const result = await taskResult(task);
|
|
2381
|
+
const completed = task.status === "completed";
|
|
2382
|
+
const text = completed ? `图像生成任务已完成(${task.id})。图片已附在这条消息中,可以直接查看、下载或作为后续图生图的参考。` : task.status === "cancelled" ? `图像生成任务已取消(${task.id})。` : `图像生成任务失败(${task.id}):${task.error ?? "未知错误"}`;
|
|
2383
|
+
agent.send(createUserMessage({
|
|
2384
|
+
content: [{
|
|
2385
|
+
type: "text",
|
|
2386
|
+
text
|
|
2387
|
+
}, ...completed ? result.images.map((image) => ({
|
|
2388
|
+
type: "image",
|
|
2389
|
+
attachment: restoreRef(image)
|
|
2390
|
+
})) : []],
|
|
2391
|
+
source: { kind: "user" }
|
|
2392
|
+
}), "next-turn", true);
|
|
2393
|
+
};
|
|
2394
|
+
const watchTask = (task, agent) => {
|
|
2395
|
+
if (agent === void 0) return;
|
|
2396
|
+
let dispose;
|
|
2397
|
+
const onChange = (updated) => {
|
|
2398
|
+
if (updated.id !== task.id || !isFinalTask(updated)) return;
|
|
2399
|
+
dispose?.();
|
|
2400
|
+
if (dispose !== void 0) taskSubscriptions.delete(dispose);
|
|
2401
|
+
notifyCompletion(agent, updated).catch(() => {});
|
|
2402
|
+
};
|
|
2403
|
+
dispose = runtime.queue.subscribe(onChange);
|
|
2404
|
+
taskSubscriptions.add(dispose);
|
|
2405
|
+
const current = findTask(task.id);
|
|
2406
|
+
if (isFinalTask(current)) onChange(current);
|
|
2407
|
+
};
|
|
2408
|
+
const disposers = [
|
|
2409
|
+
ctx.tools.register(defineTool({
|
|
2410
|
+
name: "generate_image",
|
|
2411
|
+
description: "Queue a text-to-image generation request. This returns immediately with a task id. When it finishes, the conversation automatically receives a visible image-attachment notification; do not repeatedly poll. Only use models configured for this plugin; omit model to use the first configured image model. Use get_image_generation_task only for an explicit status check or recovery.",
|
|
2412
|
+
parameters: {
|
|
2413
|
+
prompt: {
|
|
2414
|
+
type: "string",
|
|
2415
|
+
required: true,
|
|
2416
|
+
description: "Detailed image-generation prompt."
|
|
2417
|
+
},
|
|
2418
|
+
model: {
|
|
2419
|
+
type: "string",
|
|
2420
|
+
description: "One of the configured image models. Defaults to the first configured model."
|
|
2421
|
+
},
|
|
2422
|
+
size: {
|
|
2423
|
+
type: "string",
|
|
2424
|
+
description: "Aspect ratio such as 1:1, 16:9, 9:16, or auto."
|
|
2425
|
+
},
|
|
2426
|
+
quality: {
|
|
2427
|
+
type: "string",
|
|
2428
|
+
description: "auto, 1k, 2k, or 4k."
|
|
2429
|
+
},
|
|
2430
|
+
count: {
|
|
2431
|
+
type: "integer",
|
|
2432
|
+
description: "Number of images, 1 to 4. Defaults to 1."
|
|
2433
|
+
},
|
|
2434
|
+
detail: {
|
|
2435
|
+
type: "string",
|
|
2436
|
+
description: "Optional provider detail value, for example standard or high."
|
|
2437
|
+
}
|
|
2438
|
+
},
|
|
2439
|
+
output: {
|
|
2440
|
+
schema: taskResultSchema,
|
|
2441
|
+
render: (_args, value) => renderTaskResult(value)
|
|
2442
|
+
},
|
|
2443
|
+
async execute(args, exec) {
|
|
2444
|
+
ensureConfigured();
|
|
2445
|
+
const task = runtime.queue.submit({
|
|
2446
|
+
mode: "text",
|
|
2447
|
+
model: selectedModel(args.model),
|
|
2448
|
+
prompt: args.prompt.trim(),
|
|
2449
|
+
size: args.size ?? "auto",
|
|
2450
|
+
quality: args.quality ?? "auto",
|
|
2451
|
+
n: Math.min(4, Math.max(1, args.count ?? 1)),
|
|
2452
|
+
detail: args.detail ?? ""
|
|
2453
|
+
});
|
|
2454
|
+
watchTask(task, exec.agent);
|
|
2455
|
+
return taskResult(task);
|
|
2456
|
+
}
|
|
2457
|
+
})),
|
|
2458
|
+
ctx.tools.register(defineTool({
|
|
2459
|
+
name: "edit_image",
|
|
2460
|
+
description: "Queue an image-to-image edit. source_image must be an image reference returned by a completed generation notification or get_image_generation_task; pass that entire object unchanged. Only configured image models are allowed; omit model to use the first configured model. Completion is automatically delivered to the conversation with visible image attachments; do not repeatedly poll.",
|
|
2461
|
+
parameters: {
|
|
2462
|
+
prompt: {
|
|
2463
|
+
type: "string",
|
|
2464
|
+
required: true,
|
|
2465
|
+
description: "How to transform the source image."
|
|
2466
|
+
},
|
|
2467
|
+
source_image: {
|
|
2468
|
+
...imageRefSchema,
|
|
2469
|
+
required: true,
|
|
2470
|
+
description: "Image reference returned by get_image_generation_task."
|
|
2471
|
+
},
|
|
2472
|
+
model: {
|
|
2473
|
+
type: "string",
|
|
2474
|
+
description: "One of the configured image models. Defaults to the first configured model."
|
|
2475
|
+
},
|
|
2476
|
+
size: {
|
|
2477
|
+
type: "string",
|
|
2478
|
+
description: "Aspect ratio such as 1:1, 16:9, 9:16, or auto."
|
|
2479
|
+
},
|
|
2480
|
+
quality: {
|
|
2481
|
+
type: "string",
|
|
2482
|
+
description: "auto, 1k, 2k, or 4k."
|
|
2483
|
+
},
|
|
2484
|
+
count: {
|
|
2485
|
+
type: "integer",
|
|
2486
|
+
description: "Number of images, 1 to 4. Defaults to 1."
|
|
2487
|
+
},
|
|
2488
|
+
detail: {
|
|
2489
|
+
type: "string",
|
|
2490
|
+
description: "Optional provider detail value."
|
|
2491
|
+
}
|
|
2492
|
+
},
|
|
2493
|
+
output: {
|
|
2494
|
+
schema: taskResultSchema,
|
|
2495
|
+
render: (_args, value) => renderTaskResult(value)
|
|
2496
|
+
},
|
|
2497
|
+
async execute(args, exec) {
|
|
2498
|
+
ensureConfigured();
|
|
2499
|
+
const reference = await ctx.attachments.readImage(restoreRef(args.source_image), exec.signal);
|
|
2500
|
+
const task = runtime.queue.submit({
|
|
2501
|
+
mode: "edit",
|
|
2502
|
+
model: selectedModel(args.model),
|
|
2503
|
+
prompt: args.prompt.trim(),
|
|
2504
|
+
size: args.size ?? "auto",
|
|
2505
|
+
quality: args.quality ?? "auto",
|
|
2506
|
+
n: Math.min(4, Math.max(1, args.count ?? 1)),
|
|
2507
|
+
detail: args.detail ?? "",
|
|
2508
|
+
image: imageDataUrl(reference),
|
|
2509
|
+
...reference.ref.name === void 0 ? {} : { refName: reference.ref.name }
|
|
2510
|
+
});
|
|
2511
|
+
watchTask(task, exec.agent);
|
|
2512
|
+
return taskResult(task);
|
|
2513
|
+
}
|
|
2514
|
+
})),
|
|
2515
|
+
ctx.tools.register(defineTool({
|
|
2516
|
+
name: "get_image_generation_task",
|
|
2517
|
+
description: "Optionally check an image-generation task status. Completed tasks return image references for edit_image, but the conversation already receives a visible completion notification automatically; do not poll repeatedly.",
|
|
2518
|
+
parameters: { task_id: {
|
|
2519
|
+
type: "string",
|
|
2520
|
+
required: true,
|
|
2521
|
+
description: "Task id returned by generate_image or edit_image."
|
|
2522
|
+
} },
|
|
2523
|
+
output: {
|
|
2524
|
+
schema: taskResultSchema,
|
|
2525
|
+
render: (_args, value) => renderTaskResult(value)
|
|
2526
|
+
},
|
|
2527
|
+
async execute(args) {
|
|
2528
|
+
ensureConfigured();
|
|
2529
|
+
return taskResult(findTask(args.task_id));
|
|
2530
|
+
}
|
|
2531
|
+
})),
|
|
2532
|
+
ctx.tools.register(defineTool({
|
|
2533
|
+
name: "cancel_image_generation_task",
|
|
2534
|
+
description: "Cancel a queued or running image generation task.",
|
|
2535
|
+
parameters: { task_id: {
|
|
2536
|
+
type: "string",
|
|
2537
|
+
required: true,
|
|
2538
|
+
description: "Task id returned by generate_image or edit_image."
|
|
2539
|
+
} },
|
|
2540
|
+
output: {
|
|
2541
|
+
schema: taskResultSchema,
|
|
2542
|
+
render: (_args, value) => renderTaskResult(value)
|
|
2543
|
+
},
|
|
2544
|
+
async execute(args) {
|
|
2545
|
+
ensureConfigured();
|
|
2546
|
+
const task = runtime.queue.cancel(args.task_id);
|
|
2547
|
+
if (task === void 0) throw new ImageGenError(`Image generation task ${args.task_id} was not found.`, "task-not-found");
|
|
2548
|
+
return taskResult(task);
|
|
2549
|
+
}
|
|
2550
|
+
}))
|
|
2551
|
+
];
|
|
2552
|
+
return () => {
|
|
2553
|
+
for (const dispose of taskSubscriptions) dispose();
|
|
2554
|
+
taskSubscriptions.clear();
|
|
2555
|
+
for (const dispose of disposers) dispose();
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
function isFinalTask(task) {
|
|
2559
|
+
return task.status === "completed" || task.status === "failed" || task.status === "cancelled";
|
|
2560
|
+
}
|
|
2561
|
+
function toSaveImage(image, taskId, index) {
|
|
2562
|
+
const mediaType = acceptedMediaType(image.mime) ? image.mime : "image/png";
|
|
2563
|
+
return {
|
|
2564
|
+
data: Buffer.from(image.b64, "base64"),
|
|
2565
|
+
mediaType,
|
|
2566
|
+
name: `imagegen-${taskId}-${index + 1}.${mediaType === "image/jpeg" ? "jpg" : mediaType.slice(6)}`
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
//#endregion
|
|
1794
2570
|
//#region src/index.ts
|
|
1795
2571
|
/** Stable cordis plugin name. */
|
|
1796
2572
|
const name = "imagegen";
|
|
@@ -1801,16 +2577,26 @@ const ImageGenSettingsNamespace = settingsNamespace(IMAGEGEN_SETTINGS_NAMESPACE)
|
|
|
1801
2577
|
const Config = z.object({
|
|
1802
2578
|
enabled: z.boolean().default(true),
|
|
1803
2579
|
announceToAgent: z.boolean().default(true),
|
|
2580
|
+
allowAgentImageGeneration: z.boolean().default(true),
|
|
1804
2581
|
apiUrl: z.string().default(""),
|
|
1805
|
-
apiKey: z.string().role("secret").default("")
|
|
2582
|
+
apiKey: z.string().role("secret").default(""),
|
|
2583
|
+
imageModels: z.array(z.string()).default([...DEFAULT_IMAGE_MODELS]),
|
|
2584
|
+
promptApiUrl: z.string().default(""),
|
|
2585
|
+
promptApiKey: z.string().role("secret").default(""),
|
|
2586
|
+
promptModel: z.string().default("")
|
|
1806
2587
|
});
|
|
1807
2588
|
/** Schema defaults, re-read for hand-built contexts (the loader applies them normally). */
|
|
1808
2589
|
const DEFAULT_ENABLED = true;
|
|
1809
2590
|
const DEFAULT_ANNOUNCE = true;
|
|
2591
|
+
const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
|
|
1810
2592
|
/** Order of the announcement section within the tool-guidance band. */
|
|
1811
2593
|
const SECTION_ORDER = 150;
|
|
1812
2594
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
1813
|
-
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API
|
|
2595
|
+
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API,模型由用户在「设置 → 插件 → AI 生图」中检测或手动配置的生图模型列表决定;支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送)。API 地址与密钥在 GUI 设置中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用已配置的生图模型;模型出现在 /models 中不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选与复用。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;任务后台异步执行,完成后插件会自动唤醒原对话,并以可直接查看和复用的图片附件回贴结果,因此不要反复轮询。仅在用户明确要求进度或需要恢复任务时,才使用 `get_image_generation_task` 查询状态。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问 vibeui.top。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
|
|
2596
|
+
/** Add the live allow-list so an Agent can honor a user's model choice. */
|
|
2597
|
+
function guidanceFor(imageModels) {
|
|
2598
|
+
return `${IMAGEGEN_GUIDANCE} 当前允许调用的生图模型:${imageModels.join("、")}。用户指定其中某个模型时,工具参数 model 必须使用该精确名称;未指定时使用列表中的第一个。`;
|
|
2599
|
+
}
|
|
1814
2600
|
/**
|
|
1815
2601
|
* Mount the settings section, routes, and announcement.
|
|
1816
2602
|
* @param ctx - host plugin context carrying webServer/systemPrompt.
|
|
@@ -1823,10 +2609,22 @@ function apply(ctx, config) {
|
|
|
1823
2609
|
return {
|
|
1824
2610
|
enabled: value.enabled ?? DEFAULT_ENABLED,
|
|
1825
2611
|
announceToAgent: value.announceToAgent ?? DEFAULT_ANNOUNCE,
|
|
2612
|
+
allowAgentImageGeneration: value.allowAgentImageGeneration ?? DEFAULT_ALLOW_AGENT_IMAGE_GENERATION,
|
|
1826
2613
|
apiUrl: value.apiUrl ?? "",
|
|
1827
|
-
apiKey: value.apiKey ?? ""
|
|
2614
|
+
apiKey: value.apiKey ?? "",
|
|
2615
|
+
imageModels: normalizeImageModels(value.imageModels),
|
|
2616
|
+
promptApiUrl: value.promptApiUrl ?? "",
|
|
2617
|
+
promptApiKey: value.promptApiKey ?? "",
|
|
2618
|
+
promptModel: value.promptModel ?? ""
|
|
1828
2619
|
};
|
|
1829
2620
|
};
|
|
2621
|
+
const runtime = new ImageGenerationRuntime(() => {
|
|
2622
|
+
const value = resolve();
|
|
2623
|
+
return {
|
|
2624
|
+
apiUrl: value.apiUrl,
|
|
2625
|
+
apiKey: value.apiKey
|
|
2626
|
+
};
|
|
2627
|
+
});
|
|
1830
2628
|
ctx.inject(["settings"], (sctx) => {
|
|
1831
2629
|
const seam = sctx.get("settings");
|
|
1832
2630
|
sctx.effect(() => {
|
|
@@ -1838,13 +2636,35 @@ function apply(ctx, config) {
|
|
|
1838
2636
|
apiUrl: value.apiUrl,
|
|
1839
2637
|
apiKey: value.apiKey
|
|
1840
2638
|
};
|
|
1841
|
-
}
|
|
2639
|
+
},
|
|
2640
|
+
resolvePrompt: () => {
|
|
2641
|
+
const value = resolve();
|
|
2642
|
+
return {
|
|
2643
|
+
apiUrl: value.promptApiUrl.trim() || value.apiUrl,
|
|
2644
|
+
apiKey: value.promptApiKey.trim() || value.apiKey,
|
|
2645
|
+
model: value.promptModel
|
|
2646
|
+
};
|
|
2647
|
+
},
|
|
2648
|
+
resolveImageModels: () => resolve().imageModels,
|
|
2649
|
+
runtime
|
|
1842
2650
|
}).map((route) => ctx.webServer.register(route));
|
|
1843
2651
|
return () => {
|
|
1844
2652
|
for (const dispose of disposers) dispose();
|
|
1845
2653
|
};
|
|
1846
2654
|
}, "dsh-imagegen: routes");
|
|
1847
2655
|
});
|
|
2656
|
+
ctx.inject(["tools", "attachments"], (tctx) => {
|
|
2657
|
+
tctx.effect(() => registerAgentImageTools(tctx, runtime, () => {
|
|
2658
|
+
const value = resolve();
|
|
2659
|
+
return {
|
|
2660
|
+
enabled: value.enabled,
|
|
2661
|
+
allowAgentImageGeneration: value.allowAgentImageGeneration,
|
|
2662
|
+
apiUrl: value.apiUrl,
|
|
2663
|
+
apiKey: value.apiKey,
|
|
2664
|
+
imageModels: value.imageModels
|
|
2665
|
+
};
|
|
2666
|
+
}), "dsh-imagegen: agent image tools");
|
|
2667
|
+
});
|
|
1848
2668
|
let disposeSection;
|
|
1849
2669
|
const sync = () => {
|
|
1850
2670
|
if (disposeSection !== void 0) {
|
|
@@ -1856,7 +2676,7 @@ function apply(ctx, config) {
|
|
|
1856
2676
|
disposeSection = ctx.systemPrompt.section({
|
|
1857
2677
|
name: "plugin:dsh-imagegen",
|
|
1858
2678
|
order: SECTION_ORDER,
|
|
1859
|
-
text:
|
|
2679
|
+
text: guidanceFor(value.imageModels)
|
|
1860
2680
|
});
|
|
1861
2681
|
};
|
|
1862
2682
|
installSettingsSection(ctx, ImageGenSettingsNamespace, Config, config ?? {}, {
|
|
@@ -1869,4 +2689,4 @@ function apply(ctx, config) {
|
|
|
1869
2689
|
sync();
|
|
1870
2690
|
}
|
|
1871
2691
|
//#endregion
|
|
1872
|
-
export { CURRENT_VERSION, Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, appendGallery, apply, checkForUpdate, clearGallery, clearTemplateMemo, clearUpdateCache, compareVersions, generateImage, inject, installUpdate, listGallery, listTemplates, makeRoutes, name, profileFromProcess, readGalleryImage, readTemplateImage, refreshTemplates, removeGallery };
|
|
2692
|
+
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 };
|