@dickpy/dsh-imagegen 1.0.9 → 1.0.19
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 +19 -4
- package/docs/images/prompt-template-library.png +0 -0
- package/lib/client.js +1151 -464
- package/lib/client.js.map +1 -1
- package/lib/index.js +441 -36
- package/package.json +3 -3
- package/src/client/ImageGenPanel.tsx +452 -62
- package/src/client/api.ts +38 -1
- package/src/client/locales.ts +71 -22
- package/src/client/mount.tsx +11 -6
- package/src/client/panel.module.css +209 -7
- package/src/engine.ts +78 -11
- package/src/gallery-store.ts +266 -0
- package/src/index.ts +2 -1
- package/src/protocol.ts +21 -5
- package/src/routes.ts +119 -1
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SettingsConflictError, installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
2
2
|
import z from "schemastery";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { promises } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
@@ -10,12 +10,12 @@ import { spawn } from "node:child_process";
|
|
|
10
10
|
/**
|
|
11
11
|
* Wire contract shared by the host and client halves of dsh-imagegen: the
|
|
12
12
|
* settings namespace, the route paths, and the generate payload/result shapes.
|
|
13
|
-
* Pure types + constants
|
|
13
|
+
* Pure types + constants 鈥?safe for the client bundle to inline.
|
|
14
14
|
*/
|
|
15
15
|
/** Settings namespace this plugin owns (host settings seam + bridge). */
|
|
16
16
|
const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
|
|
17
17
|
/** Published package version shared by the host updater and the client UI. */
|
|
18
|
-
const PLUGIN_VERSION = "1.0.
|
|
18
|
+
const PLUGIN_VERSION = "1.0.19";
|
|
19
19
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
20
20
|
const SETTINGS_API = {
|
|
21
21
|
describe: "/api/dsh-imagegen/settings/describe",
|
|
@@ -42,6 +42,18 @@ const HISTORY_API = {
|
|
|
42
42
|
image: "/api/dsh-imagegen/history/image"
|
|
43
43
|
};
|
|
44
44
|
/**
|
|
45
|
+
* Same-origin route family for the user-curated gallery (favorites). Entries
|
|
46
|
+
* reuse the history wire shape and persist under ~/.dsh/dsh-imagegen/gallery/;
|
|
47
|
+
* unlike history there is no size cap 鈥?the user adds images on purpose.
|
|
48
|
+
*/
|
|
49
|
+
const GALLERY_API = {
|
|
50
|
+
list: "/api/dsh-imagegen/gallery/list",
|
|
51
|
+
append: "/api/dsh-imagegen/gallery/append",
|
|
52
|
+
remove: "/api/dsh-imagegen/gallery/remove",
|
|
53
|
+
clear: "/api/dsh-imagegen/gallery/clear",
|
|
54
|
+
image: "/api/dsh-imagegen/gallery/image"
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
45
57
|
* Same-origin route family for the bundled prompt-template library
|
|
46
58
|
* (awesome-gpt-image-2 mirror). The case list ships inside the package and is
|
|
47
59
|
* served by the host; reference images are proxied through the `image` prefix
|
|
@@ -76,6 +88,28 @@ const DALLE3_SIZES = /* @__PURE__ */ new Set([
|
|
|
76
88
|
"1792x1024",
|
|
77
89
|
"1024x1792"
|
|
78
90
|
]);
|
|
91
|
+
/** Whether the model is an xAI Grok Imagine model (grok-imagine-image,
|
|
92
|
+
* grok-imagine-image-2.0, …). Grok Imagine speaks JSON on both endpoints
|
|
93
|
+
* and exposes its own aspect-ratio / response-format knobs instead of the
|
|
94
|
+
* OpenAI size/quality/detail passthrough. */
|
|
95
|
+
function isGrokImagine(model) {
|
|
96
|
+
return /^grok-imagine(?:-|$)/.test(model);
|
|
97
|
+
}
|
|
98
|
+
/** The panel's aspect ratios mapped to the closest OpenAI pixel size
|
|
99
|
+
* (gpt-image-2 / generic OpenAI-compatible endpoints). */
|
|
100
|
+
const OPENAI_SIZE_BY_RATIO = {
|
|
101
|
+
"1:1": "1024x1024",
|
|
102
|
+
"3:4": "1024x1536",
|
|
103
|
+
"4:3": "1536x1024",
|
|
104
|
+
"9:16": "1024x1792",
|
|
105
|
+
"2:3": "1024x1536",
|
|
106
|
+
"3:2": "1536x1024",
|
|
107
|
+
"16:9": "1792x1024",
|
|
108
|
+
"21:9": "1792x1024"
|
|
109
|
+
};
|
|
110
|
+
/** Panel ratios that need renaming for a model's vocabulary. Grok documents
|
|
111
|
+
* 20:9 as its ultra-wide ratio, so the panel's 21:9 label is sent as 20:9. */
|
|
112
|
+
const GROK_ASPECT_ALIASES = { "21:9": "20:9" };
|
|
79
113
|
/** Content-type extension hints for URL-fetched images. */
|
|
80
114
|
function mimeOfExtension(path) {
|
|
81
115
|
const match = /\.([a-z0-9]+)$/i.exec(path);
|
|
@@ -114,14 +148,25 @@ function clampCount(n) {
|
|
|
114
148
|
* so the count is satisfied by parallel single-image requests instead. */
|
|
115
149
|
function effectiveParams(request) {
|
|
116
150
|
const model = request.model.trim() === "" ? "gpt-image-2" : request.model.trim();
|
|
117
|
-
if (model === "dall-e-3")
|
|
151
|
+
if (model === "dall-e-3") {
|
|
152
|
+
const pixel = OPENAI_SIZE_BY_RATIO[request.size];
|
|
153
|
+
return {
|
|
154
|
+
model,
|
|
155
|
+
size: pixel !== void 0 && DALLE3_SIZES.has(pixel) ? pixel : "1024x1024"
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (isGrokImagine(model)) return {
|
|
118
159
|
model,
|
|
119
|
-
size
|
|
160
|
+
...request.size !== "" && request.size !== "auto" ? { aspect_ratio: GROK_ASPECT_ALIASES[request.size] ?? request.size } : {},
|
|
161
|
+
...request.quality !== "" && request.quality !== "auto" ? { resolution: request.quality === "4k" ? "2k" : request.quality } : {},
|
|
162
|
+
response_format: "b64_json"
|
|
120
163
|
};
|
|
121
164
|
return {
|
|
122
165
|
model,
|
|
123
|
-
...request.size !== "" && request.size !== "auto" ? { size: request.size } : {},
|
|
124
|
-
...request.quality
|
|
166
|
+
...request.size !== "" && request.size !== "auto" && OPENAI_SIZE_BY_RATIO[request.size] !== void 0 ? { size: OPENAI_SIZE_BY_RATIO[request.size] } : {},
|
|
167
|
+
...request.quality === "1k" ? { quality: "low" } : {},
|
|
168
|
+
...request.quality === "2k" ? { quality: "medium" } : {},
|
|
169
|
+
...request.quality === "4k" ? { quality: "high" } : {},
|
|
125
170
|
...request.detail !== "" ? { detail: request.detail } : {}
|
|
126
171
|
};
|
|
127
172
|
}
|
|
@@ -186,14 +231,28 @@ async function requestOneImage(baseUrl, upstream, request, params) {
|
|
|
186
231
|
throw new ImageGenError("参考图片数据无法解码", "edit-image-invalid");
|
|
187
232
|
}
|
|
188
233
|
if (bytes.byteLength > MAX_EDIT_IMAGE_BYTES) throw new ImageGenError("参考图片超过 10MB 上限", "edit-image-too-large");
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
234
|
+
if (isGrokImagine(params.model)) {
|
|
235
|
+
headers["content-type"] = "application/json";
|
|
236
|
+
body = JSON.stringify({
|
|
237
|
+
model: params.model,
|
|
238
|
+
prompt: request.prompt,
|
|
239
|
+
image: {
|
|
240
|
+
url: request.image,
|
|
241
|
+
type: "image_url"
|
|
242
|
+
},
|
|
243
|
+
...params.aspect_ratio !== void 0 ? { aspect_ratio: params.aspect_ratio } : {},
|
|
244
|
+
response_format: "b64_json"
|
|
245
|
+
});
|
|
246
|
+
} else {
|
|
247
|
+
const form = new FormData();
|
|
248
|
+
form.append("image", new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf$2(parsed.mime)}`);
|
|
249
|
+
form.append("prompt", request.prompt);
|
|
250
|
+
form.append("model", params.model);
|
|
251
|
+
if (params.size !== void 0) form.append("size", params.size);
|
|
252
|
+
if (params.quality !== void 0) form.append("quality", params.quality);
|
|
253
|
+
if (params.detail !== void 0) form.append("detail", params.detail);
|
|
254
|
+
body = form;
|
|
255
|
+
}
|
|
197
256
|
} else {
|
|
198
257
|
headers["content-type"] = "application/json";
|
|
199
258
|
body = JSON.stringify({
|
|
@@ -259,7 +318,7 @@ function upstreamMessage(payload, status) {
|
|
|
259
318
|
return `上游接口拒绝请求(HTTP ${status})`;
|
|
260
319
|
}
|
|
261
320
|
/** File extension for a MIME type (multipart reference image). */
|
|
262
|
-
function extensionOf$
|
|
321
|
+
function extensionOf$2(mime) {
|
|
263
322
|
switch (mime.split(";")[0].trim()) {
|
|
264
323
|
case "image/jpeg": return "jpg";
|
|
265
324
|
case "image/webp": return "webp";
|
|
@@ -278,11 +337,193 @@ function extensionOf$1(mime) {
|
|
|
278
337
|
*
|
|
279
338
|
* Framework-free (node:fs only) so the route layer can drive it directly.
|
|
280
339
|
*/
|
|
340
|
+
const HISTORY_DIR$1 = path.join(homedir(), ".dsh", "dsh-imagegen");
|
|
341
|
+
const INDEX_PATH$1 = path.join(HISTORY_DIR$1, "index.json");
|
|
342
|
+
const IMAGES_DIR$1 = path.join(HISTORY_DIR$1, "images");
|
|
343
|
+
let pendingMutation$1 = Promise.resolve();
|
|
344
|
+
function mutateHistory(operation) {
|
|
345
|
+
const next = pendingMutation$1.then(operation, operation);
|
|
346
|
+
pendingMutation$1 = next.then(() => void 0, () => void 0);
|
|
347
|
+
return next;
|
|
348
|
+
}
|
|
349
|
+
/** File extension for a MIME type (image file names). */
|
|
350
|
+
function extensionOf$1(mime) {
|
|
351
|
+
switch (mime.split(";")[0].trim()) {
|
|
352
|
+
case "image/jpeg": return "jpg";
|
|
353
|
+
case "image/webp": return "webp";
|
|
354
|
+
case "image/gif": return "gif";
|
|
355
|
+
default: return "png";
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** MIME type for a stored image file name (image route responses). */
|
|
359
|
+
function mimeOfFile$2(file) {
|
|
360
|
+
switch (path.extname(file).toLowerCase()) {
|
|
361
|
+
case ".jpg":
|
|
362
|
+
case ".jpeg": return "image/jpeg";
|
|
363
|
+
case ".webp": return "image/webp";
|
|
364
|
+
case ".gif": return "image/gif";
|
|
365
|
+
default: return "image/png";
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/** Sanitize an entry id for use as a file-name prefix. */
|
|
369
|
+
function safeId$1(id) {
|
|
370
|
+
const cleaned = id.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
371
|
+
return cleaned === "" ? "entry" : cleaned;
|
|
372
|
+
}
|
|
373
|
+
/** Ensure the storage directories exist. */
|
|
374
|
+
async function ensureDirs$1() {
|
|
375
|
+
await promises.mkdir(IMAGES_DIR$1, { recursive: true });
|
|
376
|
+
}
|
|
377
|
+
/** Read the index, tolerating a missing/corrupt file. */
|
|
378
|
+
async function readIndex$1() {
|
|
379
|
+
try {
|
|
380
|
+
const raw = await promises.readFile(INDEX_PATH$1, "utf8");
|
|
381
|
+
const parsed = JSON.parse(raw);
|
|
382
|
+
if (parsed === null || typeof parsed !== "object") return [];
|
|
383
|
+
const entries = parsed.entries;
|
|
384
|
+
if (!Array.isArray(entries)) return [];
|
|
385
|
+
return entries.filter(isStoredEntry$1);
|
|
386
|
+
} catch {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/** Persist the index. */
|
|
391
|
+
async function writeIndex$1(entries) {
|
|
392
|
+
await ensureDirs$1();
|
|
393
|
+
const payload = { entries };
|
|
394
|
+
const tmp = `${INDEX_PATH$1}.tmp-${process.pid}`;
|
|
395
|
+
await promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
396
|
+
await promises.rename(tmp, INDEX_PATH$1);
|
|
397
|
+
}
|
|
398
|
+
/** Structural guard for a stored entry. */
|
|
399
|
+
function isStoredEntry$1(value) {
|
|
400
|
+
if (value === null || typeof value !== "object") return false;
|
|
401
|
+
const entry = value;
|
|
402
|
+
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) => {
|
|
403
|
+
if (image === null || typeof image !== "object") return false;
|
|
404
|
+
const record = image;
|
|
405
|
+
return typeof record.file === "string" && typeof record.mime === "string";
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/** Remove one entry's image files (best effort). */
|
|
409
|
+
async function removeEntryFiles$1(entry) {
|
|
410
|
+
for (const image of entry.images) try {
|
|
411
|
+
await promises.rm(path.join(IMAGES_DIR$1, image.file), { force: true });
|
|
412
|
+
} catch {}
|
|
413
|
+
}
|
|
414
|
+
/** Project a stored entry onto the wire shape (image URLs). */
|
|
415
|
+
function toWire$1(entry) {
|
|
416
|
+
return {
|
|
417
|
+
id: entry.id,
|
|
418
|
+
createdAt: entry.createdAt,
|
|
419
|
+
mode: entry.mode,
|
|
420
|
+
model: entry.model,
|
|
421
|
+
prompt: entry.prompt,
|
|
422
|
+
size: entry.size,
|
|
423
|
+
quality: entry.quality,
|
|
424
|
+
detail: entry.detail,
|
|
425
|
+
n: entry.n,
|
|
426
|
+
images: entry.images.map((image) => ({
|
|
427
|
+
url: `/api/dsh-imagegen/history/image/${image.file}`,
|
|
428
|
+
mime: image.mime,
|
|
429
|
+
...image.revisedPrompt === void 0 ? {} : { revisedPrompt: image.revisedPrompt }
|
|
430
|
+
})),
|
|
431
|
+
...entry.refName === void 0 ? {} : { refName: entry.refName }
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
/** List the persisted history, newest first, as wire entries. */
|
|
435
|
+
async function listHistory() {
|
|
436
|
+
return (await readIndex$1()).map(toWire$1);
|
|
437
|
+
}
|
|
438
|
+
/** Append one generation, evicting the oldest beyond HISTORY_MAX. */
|
|
439
|
+
async function appendHistory(input) {
|
|
440
|
+
return mutateHistory(async () => {
|
|
441
|
+
await ensureDirs$1();
|
|
442
|
+
const prefix = safeId$1(input.id);
|
|
443
|
+
const storedImages = [];
|
|
444
|
+
try {
|
|
445
|
+
for (let index = 0; index < input.images.length; index++) {
|
|
446
|
+
const image = input.images[index];
|
|
447
|
+
const file = `${prefix}-${index}.${extensionOf$1(image.mime)}`;
|
|
448
|
+
await promises.writeFile(path.join(IMAGES_DIR$1, file), Buffer.from(image.b64, "base64"));
|
|
449
|
+
storedImages.push({
|
|
450
|
+
file,
|
|
451
|
+
mime: image.mime,
|
|
452
|
+
...image.revisedPrompt === void 0 ? {} : { revisedPrompt: image.revisedPrompt }
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
await removeEntryFiles$1({ images: storedImages });
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
const merged = [{
|
|
460
|
+
id: input.id,
|
|
461
|
+
createdAt: input.createdAt,
|
|
462
|
+
mode: input.mode,
|
|
463
|
+
model: input.model,
|
|
464
|
+
prompt: input.prompt,
|
|
465
|
+
size: input.size,
|
|
466
|
+
quality: input.quality,
|
|
467
|
+
detail: input.detail,
|
|
468
|
+
n: input.n,
|
|
469
|
+
images: storedImages,
|
|
470
|
+
...input.refName === void 0 ? {} : { refName: input.refName }
|
|
471
|
+
}, ...await readIndex$1()];
|
|
472
|
+
const kept = merged.slice(0, 50);
|
|
473
|
+
for (const dropped of merged.slice(50)) await removeEntryFiles$1(dropped);
|
|
474
|
+
await writeIndex$1(kept);
|
|
475
|
+
return kept.map(toWire$1);
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
/** Remove one entry (and its image files). */
|
|
479
|
+
async function removeHistory(id) {
|
|
480
|
+
return mutateHistory(async () => {
|
|
481
|
+
const previous = await readIndex$1();
|
|
482
|
+
const target = previous.find((entry) => entry.id === id);
|
|
483
|
+
if (target !== void 0) await removeEntryFiles$1(target);
|
|
484
|
+
const kept = previous.filter((entry) => entry.id !== id);
|
|
485
|
+
await writeIndex$1(kept);
|
|
486
|
+
return kept.map(toWire$1);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
/** Remove every entry (and all image files). */
|
|
490
|
+
async function clearHistory() {
|
|
491
|
+
return mutateHistory(async () => {
|
|
492
|
+
const previous = await readIndex$1();
|
|
493
|
+
for (const entry of previous) await removeEntryFiles$1(entry);
|
|
494
|
+
await writeIndex$1([]);
|
|
495
|
+
return [];
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/** Read one stored image file by its (validated) file name. */
|
|
499
|
+
async function readHistoryImage(file) {
|
|
500
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*-[0-9]+\.(png|jpg|jpeg|webp|gif)$/.test(file)) return void 0;
|
|
501
|
+
try {
|
|
502
|
+
return {
|
|
503
|
+
data: await promises.readFile(path.join(IMAGES_DIR$1, file)),
|
|
504
|
+
mime: mimeOfFile$2(file)
|
|
505
|
+
};
|
|
506
|
+
} catch {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/gallery-store.ts
|
|
512
|
+
/**
|
|
513
|
+
* Host-persisted gallery (user-curated favorites): mirrors the history store
|
|
514
|
+
* (image files + an index.json under ~/.dsh/dsh-imagegen/gallery/) but with no
|
|
515
|
+
* size cap — every entry is an explicit user choice. Appends are deduplicated
|
|
516
|
+
* by image content so adding the same generated image twice is a no-op.
|
|
517
|
+
*
|
|
518
|
+
* Framework-free (node:fs + node:crypto only) so the route layer can drive it
|
|
519
|
+
* directly.
|
|
520
|
+
*/
|
|
281
521
|
const HISTORY_DIR = path.join(homedir(), ".dsh", "dsh-imagegen");
|
|
282
|
-
const
|
|
283
|
-
const
|
|
522
|
+
const GALLERY_DIR = path.join(HISTORY_DIR, "gallery");
|
|
523
|
+
const INDEX_PATH = path.join(GALLERY_DIR, "index.json");
|
|
524
|
+
const IMAGES_DIR = path.join(GALLERY_DIR, "images");
|
|
284
525
|
let pendingMutation = Promise.resolve();
|
|
285
|
-
function
|
|
526
|
+
function mutateGallery(operation) {
|
|
286
527
|
const next = pendingMutation.then(operation, operation);
|
|
287
528
|
pendingMutation = next.then(() => void 0, () => void 0);
|
|
288
529
|
return next;
|
|
@@ -311,6 +552,12 @@ function safeId(id) {
|
|
|
311
552
|
const cleaned = id.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
312
553
|
return cleaned === "" ? "entry" : cleaned;
|
|
313
554
|
}
|
|
555
|
+
/** Short content fingerprint of the entry's first image. */
|
|
556
|
+
function fingerprint(input) {
|
|
557
|
+
const first = input.images[0];
|
|
558
|
+
if (first === void 0) return void 0;
|
|
559
|
+
return createHash("sha1").update(first.b64).digest("hex");
|
|
560
|
+
}
|
|
314
561
|
/** Ensure the storage directories exist. */
|
|
315
562
|
async function ensureDirs() {
|
|
316
563
|
await promises.mkdir(IMAGES_DIR, { recursive: true });
|
|
@@ -365,21 +612,31 @@ function toWire(entry) {
|
|
|
365
612
|
detail: entry.detail,
|
|
366
613
|
n: entry.n,
|
|
367
614
|
images: entry.images.map((image) => ({
|
|
368
|
-
url: `/api/dsh-imagegen/
|
|
615
|
+
url: `/api/dsh-imagegen/gallery/image/${image.file}`,
|
|
369
616
|
mime: image.mime,
|
|
370
617
|
...image.revisedPrompt === void 0 ? {} : { revisedPrompt: image.revisedPrompt }
|
|
371
618
|
})),
|
|
372
619
|
...entry.refName === void 0 ? {} : { refName: entry.refName }
|
|
373
620
|
};
|
|
374
621
|
}
|
|
375
|
-
/** List the persisted
|
|
376
|
-
async function
|
|
622
|
+
/** List the persisted gallery, newest first, as wire entries. */
|
|
623
|
+
async function listGallery() {
|
|
377
624
|
return (await readIndex()).map(toWire);
|
|
378
625
|
}
|
|
379
|
-
/** Append one
|
|
380
|
-
|
|
381
|
-
|
|
626
|
+
/** Append one image to the gallery. Deduplicates by first-image content —
|
|
627
|
+
* appending an image already in the gallery returns `added: false` with the
|
|
628
|
+
* list unchanged. No size cap: every entry is an explicit user choice. */
|
|
629
|
+
async function appendGallery(input) {
|
|
630
|
+
return mutateGallery(async () => {
|
|
382
631
|
await ensureDirs();
|
|
632
|
+
const hash = fingerprint(input);
|
|
633
|
+
if (hash !== void 0) {
|
|
634
|
+
const existing = await readIndex();
|
|
635
|
+
if (existing.some((entry) => entry.hash === hash)) return {
|
|
636
|
+
entries: existing.map(toWire),
|
|
637
|
+
added: false
|
|
638
|
+
};
|
|
639
|
+
}
|
|
383
640
|
const prefix = safeId(input.id);
|
|
384
641
|
const storedImages = [];
|
|
385
642
|
try {
|
|
@@ -408,17 +665,19 @@ async function appendHistory(input) {
|
|
|
408
665
|
detail: input.detail,
|
|
409
666
|
n: input.n,
|
|
410
667
|
images: storedImages,
|
|
668
|
+
...hash === void 0 ? {} : { hash },
|
|
411
669
|
...input.refName === void 0 ? {} : { refName: input.refName }
|
|
412
670
|
}, ...await readIndex()];
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
671
|
+
await writeIndex(merged);
|
|
672
|
+
return {
|
|
673
|
+
entries: merged.map(toWire),
|
|
674
|
+
added: true
|
|
675
|
+
};
|
|
417
676
|
});
|
|
418
677
|
}
|
|
419
678
|
/** Remove one entry (and its image files). */
|
|
420
|
-
async function
|
|
421
|
-
return
|
|
679
|
+
async function removeGallery(id) {
|
|
680
|
+
return mutateGallery(async () => {
|
|
422
681
|
const previous = await readIndex();
|
|
423
682
|
const target = previous.find((entry) => entry.id === id);
|
|
424
683
|
if (target !== void 0) await removeEntryFiles(target);
|
|
@@ -428,8 +687,8 @@ async function removeHistory(id) {
|
|
|
428
687
|
});
|
|
429
688
|
}
|
|
430
689
|
/** Remove every entry (and all image files). */
|
|
431
|
-
async function
|
|
432
|
-
return
|
|
690
|
+
async function clearGallery() {
|
|
691
|
+
return mutateGallery(async () => {
|
|
433
692
|
const previous = await readIndex();
|
|
434
693
|
for (const entry of previous) await removeEntryFiles(entry);
|
|
435
694
|
await writeIndex([]);
|
|
@@ -437,7 +696,7 @@ async function clearHistory() {
|
|
|
437
696
|
});
|
|
438
697
|
}
|
|
439
698
|
/** Read one stored image file by its (validated) file name. */
|
|
440
|
-
async function
|
|
699
|
+
async function readGalleryImage(file) {
|
|
441
700
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*-[0-9]+\.(png|jpg|jpeg|webp|gif)$/.test(file)) return void 0;
|
|
442
701
|
try {
|
|
443
702
|
return {
|
|
@@ -964,6 +1223,13 @@ function makeRoutes(deps) {
|
|
|
964
1223
|
clear: clearHistory,
|
|
965
1224
|
readImage: readHistoryImage
|
|
966
1225
|
};
|
|
1226
|
+
const gallery = deps.gallery ?? {
|
|
1227
|
+
list: listGallery,
|
|
1228
|
+
append: appendGallery,
|
|
1229
|
+
remove: removeGallery,
|
|
1230
|
+
clear: clearGallery,
|
|
1231
|
+
readImage: readGalleryImage
|
|
1232
|
+
};
|
|
967
1233
|
const templates = deps.templates ?? {
|
|
968
1234
|
list: listTemplates,
|
|
969
1235
|
refresh: refreshTemplates,
|
|
@@ -1315,6 +1581,145 @@ function makeRoutes(deps) {
|
|
|
1315
1581
|
res.end(found.data);
|
|
1316
1582
|
}
|
|
1317
1583
|
},
|
|
1584
|
+
{
|
|
1585
|
+
kind: "exact",
|
|
1586
|
+
path: GALLERY_API.list,
|
|
1587
|
+
handler: async (req, res) => {
|
|
1588
|
+
if (!guard(req, res, "POST")) return;
|
|
1589
|
+
try {
|
|
1590
|
+
writeJson(res, 200, {
|
|
1591
|
+
ok: true,
|
|
1592
|
+
entries: await gallery.list()
|
|
1593
|
+
});
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
writeJson(res, 200, {
|
|
1596
|
+
ok: false,
|
|
1597
|
+
code: "gallery-failed",
|
|
1598
|
+
message: messageOf(error)
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
kind: "exact",
|
|
1605
|
+
path: GALLERY_API.append,
|
|
1606
|
+
handler: async (req, res) => {
|
|
1607
|
+
if (!guard(req, res, "POST")) return;
|
|
1608
|
+
const body = await readJsonBody(req, MAX_HISTORY_BODY_BYTES);
|
|
1609
|
+
if (body === void 0) {
|
|
1610
|
+
writeJson(res, 200, {
|
|
1611
|
+
ok: false,
|
|
1612
|
+
code: "bad-request",
|
|
1613
|
+
message: "unreadable JSON body"
|
|
1614
|
+
});
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
const entry = parseHistoryEntryInput(body);
|
|
1618
|
+
if (entry === void 0) {
|
|
1619
|
+
writeJson(res, 200, {
|
|
1620
|
+
ok: false,
|
|
1621
|
+
code: "bad-request",
|
|
1622
|
+
message: "malformed gallery entry"
|
|
1623
|
+
});
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
try {
|
|
1627
|
+
const result = await gallery.append({
|
|
1628
|
+
...entry,
|
|
1629
|
+
id: randomUUID()
|
|
1630
|
+
});
|
|
1631
|
+
writeJson(res, 200, {
|
|
1632
|
+
ok: true,
|
|
1633
|
+
entries: result.entries,
|
|
1634
|
+
added: result.added
|
|
1635
|
+
});
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
writeJson(res, 200, {
|
|
1638
|
+
ok: false,
|
|
1639
|
+
code: "gallery-failed",
|
|
1640
|
+
message: messageOf(error)
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
},
|
|
1645
|
+
{
|
|
1646
|
+
kind: "exact",
|
|
1647
|
+
path: GALLERY_API.remove,
|
|
1648
|
+
handler: async (req, res) => {
|
|
1649
|
+
if (!guard(req, res, "POST")) return;
|
|
1650
|
+
const body = await readJsonBody(req);
|
|
1651
|
+
const id = body !== void 0 && typeof body.id === "string" ? body.id : "";
|
|
1652
|
+
if (id === "") {
|
|
1653
|
+
writeJson(res, 200, {
|
|
1654
|
+
ok: false,
|
|
1655
|
+
code: "bad-request",
|
|
1656
|
+
message: "gallery id is required"
|
|
1657
|
+
});
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
try {
|
|
1661
|
+
writeJson(res, 200, {
|
|
1662
|
+
ok: true,
|
|
1663
|
+
entries: await gallery.remove(id)
|
|
1664
|
+
});
|
|
1665
|
+
} catch (error) {
|
|
1666
|
+
writeJson(res, 200, {
|
|
1667
|
+
ok: false,
|
|
1668
|
+
code: "gallery-failed",
|
|
1669
|
+
message: messageOf(error)
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
},
|
|
1674
|
+
{
|
|
1675
|
+
kind: "exact",
|
|
1676
|
+
path: GALLERY_API.clear,
|
|
1677
|
+
handler: async (req, res) => {
|
|
1678
|
+
if (!guard(req, res, "POST")) return;
|
|
1679
|
+
try {
|
|
1680
|
+
writeJson(res, 200, {
|
|
1681
|
+
ok: true,
|
|
1682
|
+
entries: await gallery.clear()
|
|
1683
|
+
});
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
writeJson(res, 200, {
|
|
1686
|
+
ok: false,
|
|
1687
|
+
code: "gallery-failed",
|
|
1688
|
+
message: messageOf(error)
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
},
|
|
1693
|
+
{
|
|
1694
|
+
kind: "prefix",
|
|
1695
|
+
path: GALLERY_API.image,
|
|
1696
|
+
handler: async (req, res) => {
|
|
1697
|
+
if (!isLoopbackRequest(req)) {
|
|
1698
|
+
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
if (req.method !== "GET") {
|
|
1702
|
+
writeJson(res, 405, { error: `method not allowed: ${req.method}` });
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
const file = imageFileFrom(req.url, GALLERY_API.image);
|
|
1706
|
+
if (file === void 0) {
|
|
1707
|
+
writeJson(res, 404, { error: "not found" });
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
const found = await gallery.readImage(file);
|
|
1711
|
+
if (found === void 0) {
|
|
1712
|
+
writeJson(res, 404, { error: "not found" });
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
res.writeHead(200, {
|
|
1716
|
+
"content-type": found.mime,
|
|
1717
|
+
"content-length": found.data.length,
|
|
1718
|
+
"cache-control": "private, max-age=3600"
|
|
1719
|
+
});
|
|
1720
|
+
res.end(found.data);
|
|
1721
|
+
}
|
|
1722
|
+
},
|
|
1318
1723
|
{
|
|
1319
1724
|
kind: "exact",
|
|
1320
1725
|
path: TEMPLATES_API.list,
|
|
@@ -1405,7 +1810,7 @@ const DEFAULT_ANNOUNCE = true;
|
|
|
1405
1810
|
/** Order of the announcement section within the tool-guidance band. */
|
|
1406
1811
|
const SECTION_ORDER = 150;
|
|
1407
1812
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
1408
|
-
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API(模型 gpt-image-2),支持文生图(/images/generations)与图生图(/images/edits
|
|
1813
|
+
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API(模型 gpt-image-2 / grok-imagine-image),支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok 模型按官方 JSON image_url 协议发送);API 地址与密钥在 GUI「设置 → 插件 → 可配置」中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载;可一键把满意的图片加入「画廊」(工作台顶部 文生图 / 图生图 / 画廊 三个标签页,画廊在右侧展示,收藏持久化在本地 ~/.dsh/dsh-imagegen/gallery/)。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条 gpt-image-2 提示词案例(含中文标题、分类、参考图,可搜索/按分类筛选),参考图经宿主代理按需缓存到本地;用户可一键把模板提示词填入提示词框再生成。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问 vibeui.top。用户提到「生图 / 绘画 / 生成图片 / gpt-image-2 / grok-imagine-image / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
|
|
1409
1814
|
/**
|
|
1410
1815
|
* Mount the settings section, routes, and announcement.
|
|
1411
1816
|
* @param ctx - host plugin context carrying webServer/systemPrompt.
|
|
@@ -1464,4 +1869,4 @@ function apply(ctx, config) {
|
|
|
1464
1869
|
sync();
|
|
1465
1870
|
}
|
|
1466
1871
|
//#endregion
|
|
1467
|
-
export { CURRENT_VERSION, Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, apply, checkForUpdate, clearTemplateMemo, clearUpdateCache, compareVersions, generateImage, inject, installUpdate, listTemplates, makeRoutes, name, profileFromProcess, readTemplateImage, refreshTemplates };
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dickpy/dsh-imagegen",
|
|
3
|
-
"description": "AI
|
|
4
|
-
"version": "1.0.
|
|
3
|
+
"description": "AI 鐢熷浘 (image generation) plugin for the dsh web GUI: text-to-image and image-to-image through a configurable OpenAI-compatible endpoint (gpt-image-2 / grok-imagine-image / dall-e-3, with native xAI Grok Imagine request shaping), with a settings card for api_url / api_key and a sidebar entry opening a split-pane generation studio.",
|
|
4
|
+
"version": "1.0.19",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -65,4 +65,4 @@
|
|
|
65
65
|
"typecheck": "tsc --noEmit",
|
|
66
66
|
"fetch-templates": "node scripts/fetch-templates.mjs"
|
|
67
67
|
}
|
|
68
|
-
}
|
|
68
|
+
}
|