@dickpy/dsh-imagegen 1.0.0 → 1.0.1
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 +35 -22
- package/docs/images/image-generation-studio.png +0 -0
- package/docs/images/plugin-settings.png +0 -0
- package/lib/client.js +92 -108
- package/lib/client.js.map +1 -1
- package/lib/index.js +100 -51
- package/package.json +3 -2
- package/src/client/ImageGenPanel.tsx +4 -19
- package/src/client/api.ts +7 -14
- package/src/client/locales.ts +1 -1
- package/src/history-store.ts +62 -41
- package/src/index.ts +1 -1
- package/src/protocol.ts +6 -0
- package/src/routes.ts +41 -7
package/lib/index.js
CHANGED
|
@@ -1,5 +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
4
|
import { promises } from "node:fs";
|
|
4
5
|
import { homedir } from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
@@ -260,6 +261,12 @@ function extensionOf$1(mime) {
|
|
|
260
261
|
const HISTORY_DIR = path.join(homedir(), ".dsh", "dsh-imagegen");
|
|
261
262
|
const INDEX_PATH = path.join(HISTORY_DIR, "index.json");
|
|
262
263
|
const IMAGES_DIR = path.join(HISTORY_DIR, "images");
|
|
264
|
+
let pendingMutation = Promise.resolve();
|
|
265
|
+
function mutateHistory(operation) {
|
|
266
|
+
const next = pendingMutation.then(operation, operation);
|
|
267
|
+
pendingMutation = next.then(() => void 0, () => void 0);
|
|
268
|
+
return next;
|
|
269
|
+
}
|
|
263
270
|
/** File extension for a MIME type (image file names). */
|
|
264
271
|
function extensionOf(mime) {
|
|
265
272
|
switch (mime.split(";")[0].trim()) {
|
|
@@ -351,52 +358,63 @@ async function listHistory() {
|
|
|
351
358
|
}
|
|
352
359
|
/** Append one generation, evicting the oldest beyond HISTORY_MAX. */
|
|
353
360
|
async function appendHistory(input) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
361
|
+
return mutateHistory(async () => {
|
|
362
|
+
await ensureDirs();
|
|
363
|
+
const prefix = safeId(input.id);
|
|
364
|
+
const storedImages = [];
|
|
365
|
+
try {
|
|
366
|
+
for (let index = 0; index < input.images.length; index++) {
|
|
367
|
+
const image = input.images[index];
|
|
368
|
+
const file = `${prefix}-${index}.${extensionOf(image.mime)}`;
|
|
369
|
+
await promises.writeFile(path.join(IMAGES_DIR, file), Buffer.from(image.b64, "base64"));
|
|
370
|
+
storedImages.push({
|
|
371
|
+
file,
|
|
372
|
+
mime: image.mime,
|
|
373
|
+
...image.revisedPrompt === void 0 ? {} : { revisedPrompt: image.revisedPrompt }
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
} catch (error) {
|
|
377
|
+
await removeEntryFiles({ images: storedImages });
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
const merged = [{
|
|
381
|
+
id: input.id,
|
|
382
|
+
createdAt: input.createdAt,
|
|
383
|
+
mode: input.mode,
|
|
384
|
+
model: input.model,
|
|
385
|
+
prompt: input.prompt,
|
|
386
|
+
size: input.size,
|
|
387
|
+
quality: input.quality,
|
|
388
|
+
detail: input.detail,
|
|
389
|
+
n: input.n,
|
|
390
|
+
images: storedImages,
|
|
391
|
+
...input.refName === void 0 ? {} : { refName: input.refName }
|
|
392
|
+
}, ...await readIndex()];
|
|
393
|
+
const kept = merged.slice(0, 50);
|
|
394
|
+
for (const dropped of merged.slice(50)) await removeEntryFiles(dropped);
|
|
395
|
+
await writeIndex(kept);
|
|
396
|
+
return kept.map(toWire);
|
|
397
|
+
});
|
|
384
398
|
}
|
|
385
399
|
/** Remove one entry (and its image files). */
|
|
386
400
|
async function removeHistory(id) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
401
|
+
return mutateHistory(async () => {
|
|
402
|
+
const previous = await readIndex();
|
|
403
|
+
const target = previous.find((entry) => entry.id === id);
|
|
404
|
+
if (target !== void 0) await removeEntryFiles(target);
|
|
405
|
+
const kept = previous.filter((entry) => entry.id !== id);
|
|
406
|
+
await writeIndex(kept);
|
|
407
|
+
return kept.map(toWire);
|
|
408
|
+
});
|
|
393
409
|
}
|
|
394
410
|
/** Remove every entry (and all image files). */
|
|
395
411
|
async function clearHistory() {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
412
|
+
return mutateHistory(async () => {
|
|
413
|
+
const previous = await readIndex();
|
|
414
|
+
for (const entry of previous) await removeEntryFiles(entry);
|
|
415
|
+
await writeIndex([]);
|
|
416
|
+
return [];
|
|
417
|
+
});
|
|
400
418
|
}
|
|
401
419
|
/** Read one stored image file by its (validated) file name. */
|
|
402
420
|
async function readHistoryImage(file) {
|
|
@@ -550,6 +568,13 @@ function failureOf(error) {
|
|
|
550
568
|
* @returns the route registrations.
|
|
551
569
|
*/
|
|
552
570
|
function makeRoutes(deps) {
|
|
571
|
+
const history = deps.history ?? {
|
|
572
|
+
list: listHistory,
|
|
573
|
+
append: appendHistory,
|
|
574
|
+
remove: removeHistory,
|
|
575
|
+
clear: clearHistory,
|
|
576
|
+
readImage: readHistoryImage
|
|
577
|
+
};
|
|
553
578
|
const guard = (req, res, method) => {
|
|
554
579
|
if (!isLoopbackRequest(req)) {
|
|
555
580
|
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
@@ -661,13 +686,37 @@ function makeRoutes(deps) {
|
|
|
661
686
|
quality: typeof body.quality === "string" ? body.quality : "auto",
|
|
662
687
|
n: typeof body.n === "number" ? body.n : 1,
|
|
663
688
|
detail: typeof body.detail === "string" ? body.detail : "",
|
|
664
|
-
...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {}
|
|
689
|
+
...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {},
|
|
690
|
+
...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {}
|
|
665
691
|
};
|
|
666
692
|
try {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
693
|
+
const result = await generateImage(deps.resolve(), request);
|
|
694
|
+
try {
|
|
695
|
+
const entries = await history.append({
|
|
696
|
+
id: randomUUID(),
|
|
697
|
+
createdAt: Date.now(),
|
|
698
|
+
mode: request.mode,
|
|
699
|
+
model: request.model,
|
|
700
|
+
prompt: request.prompt,
|
|
701
|
+
size: request.size,
|
|
702
|
+
quality: request.quality,
|
|
703
|
+
detail: request.detail,
|
|
704
|
+
n: request.n,
|
|
705
|
+
images: result.images,
|
|
706
|
+
...request.refName === void 0 ? {} : { refName: request.refName }
|
|
707
|
+
});
|
|
708
|
+
writeJson(res, 200, {
|
|
709
|
+
ok: true,
|
|
710
|
+
...result,
|
|
711
|
+
history: entries
|
|
712
|
+
});
|
|
713
|
+
} catch (error) {
|
|
714
|
+
writeJson(res, 200, {
|
|
715
|
+
ok: true,
|
|
716
|
+
...result,
|
|
717
|
+
historyError: messageOf(error)
|
|
718
|
+
});
|
|
719
|
+
}
|
|
671
720
|
} catch (error) {
|
|
672
721
|
const message = error instanceof Error ? error.message : String(error);
|
|
673
722
|
writeJson(res, 200, {
|
|
@@ -686,7 +735,7 @@ function makeRoutes(deps) {
|
|
|
686
735
|
try {
|
|
687
736
|
writeJson(res, 200, {
|
|
688
737
|
ok: true,
|
|
689
|
-
entries: await
|
|
738
|
+
entries: await history.list()
|
|
690
739
|
});
|
|
691
740
|
} catch (error) {
|
|
692
741
|
writeJson(res, 200, {
|
|
@@ -723,7 +772,7 @@ function makeRoutes(deps) {
|
|
|
723
772
|
try {
|
|
724
773
|
writeJson(res, 200, {
|
|
725
774
|
ok: true,
|
|
726
|
-
entries: await
|
|
775
|
+
entries: await history.append(entry)
|
|
727
776
|
});
|
|
728
777
|
} catch (error) {
|
|
729
778
|
writeJson(res, 200, {
|
|
@@ -752,7 +801,7 @@ function makeRoutes(deps) {
|
|
|
752
801
|
try {
|
|
753
802
|
writeJson(res, 200, {
|
|
754
803
|
ok: true,
|
|
755
|
-
entries: await
|
|
804
|
+
entries: await history.remove(id)
|
|
756
805
|
});
|
|
757
806
|
} catch (error) {
|
|
758
807
|
writeJson(res, 200, {
|
|
@@ -771,7 +820,7 @@ function makeRoutes(deps) {
|
|
|
771
820
|
try {
|
|
772
821
|
writeJson(res, 200, {
|
|
773
822
|
ok: true,
|
|
774
|
-
entries: await
|
|
823
|
+
entries: await history.clear()
|
|
775
824
|
});
|
|
776
825
|
} catch (error) {
|
|
777
826
|
writeJson(res, 200, {
|
|
@@ -799,7 +848,7 @@ function makeRoutes(deps) {
|
|
|
799
848
|
writeJson(res, 404, { error: "not found" });
|
|
800
849
|
return;
|
|
801
850
|
}
|
|
802
|
-
const found = await
|
|
851
|
+
const found = await history.readImage(file);
|
|
803
852
|
if (found === void 0) {
|
|
804
853
|
writeJson(res, 404, { error: "not found" });
|
|
805
854
|
return;
|
|
@@ -834,7 +883,7 @@ const DEFAULT_ANNOUNCE = true;
|
|
|
834
883
|
/** Order of the announcement section within the tool-guidance band. */
|
|
835
884
|
const SECTION_ORDER = 150;
|
|
836
885
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
837
|
-
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI
|
|
886
|
+
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API(模型 gpt-image-2),支持文生图(/images/generations)与图生图(/images/edits,上传参考图);API 地址与密钥在 GUI「设置 → 插件 → 可配置」中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务。用户提到「生图 / 绘画 / 生成图片 / gpt-image-2 / 文生图 / 图生图」时即指本插件,请据此协作。";
|
|
838
887
|
/**
|
|
839
888
|
* Mount the settings section, routes, and announcement.
|
|
840
889
|
* @param ctx - host plugin context carrying webServer/systemPrompt.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dickpy/dsh-imagegen",
|
|
3
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 / gpt-image-1 / dall-e-3), with a settings card for api_url / api_key and a sidebar entry opening a split-pane generation studio.",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"lib",
|
|
52
52
|
"src",
|
|
53
53
|
"cordis.patch.yml",
|
|
54
|
+
"docs",
|
|
54
55
|
"README.md"
|
|
55
56
|
],
|
|
56
57
|
"license": "Apache-2.0",
|
|
@@ -63,4 +64,4 @@
|
|
|
63
64
|
"watch": "tsdown --watch",
|
|
64
65
|
"typecheck": "tsc --noEmit"
|
|
65
66
|
}
|
|
66
|
-
}
|
|
67
|
+
}
|
|
@@ -13,7 +13,7 @@ import { createPortal } from 'react-dom'
|
|
|
13
13
|
import { Button, Pill } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
14
14
|
import type { ImageGenApi } from './api.ts'
|
|
15
15
|
import { errorMessage, tt } from './helpers.ts'
|
|
16
|
-
import type { GeneratedImage, GenerateMode, GenerateRequest, HistoryEntry,
|
|
16
|
+
import type { GeneratedImage, GenerateMode, GenerateRequest, HistoryEntry, HistoryImageRef } from '../protocol.ts'
|
|
17
17
|
import type { ImageGenConfig, ImageGenScope } from './settings-scope.ts'
|
|
18
18
|
import css from './panel.module.css'
|
|
19
19
|
|
|
@@ -177,6 +177,7 @@ export function ImageGenPanel(props: {
|
|
|
177
177
|
n: count,
|
|
178
178
|
detail,
|
|
179
179
|
...mode === 'edit' && refImage !== null ? { image: refImage.dataUrl } : {},
|
|
180
|
+
...mode === 'edit' && refImage !== null ? { refName: refImage.name } : {},
|
|
180
181
|
}
|
|
181
182
|
setGenerating(true)
|
|
182
183
|
setError(null)
|
|
@@ -186,24 +187,8 @@ export function ImageGenPanel(props: {
|
|
|
186
187
|
const result = await api.generate(request)
|
|
187
188
|
setImages(result.images)
|
|
188
189
|
setViewingHistoryId(null)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
createdAt: Date.now(),
|
|
192
|
-
mode,
|
|
193
|
-
model,
|
|
194
|
-
prompt: promptText,
|
|
195
|
-
size,
|
|
196
|
-
quality,
|
|
197
|
-
detail,
|
|
198
|
-
n: count,
|
|
199
|
-
images: result.images,
|
|
200
|
-
...mode === 'edit' && refImage !== null ? { refName: refImage.name } : {},
|
|
201
|
-
}
|
|
202
|
-
try {
|
|
203
|
-
setHistory(await api.historyAppend(entry))
|
|
204
|
-
} catch {
|
|
205
|
-
// Persisting history is best-effort; the images stay on the canvas.
|
|
206
|
-
}
|
|
190
|
+
if (result.history !== undefined) setHistory(result.history)
|
|
191
|
+
if (result.historyError !== undefined) setError(result.historyError)
|
|
207
192
|
} catch (caught) {
|
|
208
193
|
setError(errorMessage(caught))
|
|
209
194
|
} finally {
|
package/src/client/api.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* data access path the panel uses — plain fetch, same origin.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { GENERATE_API, HISTORY_API, type GenerateRequest, type GenerateResult, type HistoryEntry
|
|
6
|
+
import { GENERATE_API, HISTORY_API, type GenerateRequest, type GenerateResult, type HistoryEntry } from '../protocol.ts'
|
|
7
7
|
|
|
8
8
|
/** Error carrying the route's JSON error message. */
|
|
9
9
|
export class ImageGenApiError extends Error {
|
|
@@ -47,8 +47,12 @@ export class ImageGenApi {
|
|
|
47
47
|
headers: { 'content-type': 'application/json' },
|
|
48
48
|
body: JSON.stringify(request),
|
|
49
49
|
})
|
|
50
|
-
const body = await readEnvelope<{ ok: true; images: GenerateResult['images'] }>(response)
|
|
51
|
-
return {
|
|
50
|
+
const body = await readEnvelope<{ ok: true; images: GenerateResult['images']; history?: HistoryEntry[]; historyError?: string }>(response)
|
|
51
|
+
return {
|
|
52
|
+
images: body.images,
|
|
53
|
+
...body.history === undefined ? {} : { history: body.history },
|
|
54
|
+
...body.historyError === undefined ? {} : { historyError: body.historyError },
|
|
55
|
+
}
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/** List the host-persisted history (newest first). */
|
|
@@ -58,17 +62,6 @@ export class ImageGenApi {
|
|
|
58
62
|
return body.entries
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
/** Append one generation to the host-persisted history. */
|
|
62
|
-
async historyAppend(entry: HistoryEntryInput): Promise<HistoryEntry[]> {
|
|
63
|
-
const response = await fetch(HISTORY_API.append, {
|
|
64
|
-
method: 'POST',
|
|
65
|
-
headers: { 'content-type': 'application/json' },
|
|
66
|
-
body: JSON.stringify({ entry }),
|
|
67
|
-
})
|
|
68
|
-
const body = await readEnvelope<{ ok: true; entries: HistoryEntry[] }>(response)
|
|
69
|
-
return body.entries
|
|
70
|
-
}
|
|
71
|
-
|
|
72
65
|
/** Remove one history entry by id. */
|
|
73
66
|
async historyRemove(id: string): Promise<HistoryEntry[]> {
|
|
74
67
|
const response = await fetch(HISTORY_API.remove, {
|
package/src/client/locales.ts
CHANGED
|
@@ -168,7 +168,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
168
168
|
'preview.prev': 'Previous',
|
|
169
169
|
'preview.next': 'Next',
|
|
170
170
|
'preview.index': '{index} / {total}',
|
|
171
|
-
'config.missing': 'API not configured: open "Settings →
|
|
171
|
+
'config.missing': 'API not configured: open "Settings → Plugins → Configurable" and fill in api_url and api_key for AI Image.',
|
|
172
172
|
'config.configured': 'Connected to {url}',
|
|
173
173
|
'config.disabled': 'The plugin is disabled — re-enable it in Settings.',
|
|
174
174
|
'settings.title': 'AI Image (dsh-imagegen)',
|
package/src/history-store.ts
CHANGED
|
@@ -17,6 +17,16 @@ const HISTORY_DIR = path.join(homedir(), '.dsh', 'dsh-imagegen')
|
|
|
17
17
|
const INDEX_PATH = path.join(HISTORY_DIR, 'index.json')
|
|
18
18
|
const IMAGES_DIR = path.join(HISTORY_DIR, 'images')
|
|
19
19
|
|
|
20
|
+
// History mutations read and replace one shared index. Serialize them so
|
|
21
|
+
// overlapping requests cannot each read an old index and lose the other's row.
|
|
22
|
+
let pendingMutation: Promise<void> = Promise.resolve()
|
|
23
|
+
|
|
24
|
+
function mutateHistory<T>(operation: () => Promise<T>): Promise<T> {
|
|
25
|
+
const next = pendingMutation.then(operation, operation)
|
|
26
|
+
pendingMutation = next.then(() => undefined, () => undefined)
|
|
27
|
+
return next
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
/** One image's on-disk record (file name + mime, never base64). */
|
|
21
31
|
interface StoredImage {
|
|
22
32
|
file: string
|
|
@@ -152,55 +162,66 @@ export async function listHistory(): Promise<HistoryEntry[]> {
|
|
|
152
162
|
|
|
153
163
|
/** Append one generation, evicting the oldest beyond HISTORY_MAX. */
|
|
154
164
|
export async function appendHistory(input: HistoryEntryInput): Promise<HistoryEntry[]> {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
165
|
+
return mutateHistory(async () => {
|
|
166
|
+
await ensureDirs()
|
|
167
|
+
const prefix = safeId(input.id)
|
|
168
|
+
const storedImages: StoredImage[] = []
|
|
169
|
+
try {
|
|
170
|
+
for (let index = 0; index < input.images.length; index++) {
|
|
171
|
+
const image = input.images[index]!
|
|
172
|
+
const file = `${prefix}-${index}.${extensionOf(image.mime)}`
|
|
173
|
+
await fs.writeFile(path.join(IMAGES_DIR, file), Buffer.from(image.b64, 'base64'))
|
|
174
|
+
storedImages.push({
|
|
175
|
+
file,
|
|
176
|
+
mime: image.mime,
|
|
177
|
+
...image.revisedPrompt === undefined ? {} : { revisedPrompt: image.revisedPrompt },
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
await removeEntryFiles({ images: storedImages } as StoredEntry)
|
|
182
|
+
throw error
|
|
183
|
+
}
|
|
184
|
+
const entry: StoredEntry = {
|
|
185
|
+
id: input.id,
|
|
186
|
+
createdAt: input.createdAt,
|
|
187
|
+
mode: input.mode,
|
|
188
|
+
model: input.model,
|
|
189
|
+
prompt: input.prompt,
|
|
190
|
+
size: input.size,
|
|
191
|
+
quality: input.quality,
|
|
192
|
+
detail: input.detail,
|
|
193
|
+
n: input.n,
|
|
194
|
+
images: storedImages,
|
|
195
|
+
...input.refName === undefined ? {} : { refName: input.refName },
|
|
196
|
+
}
|
|
197
|
+
const merged = [entry, ...await readIndex()]
|
|
198
|
+
const kept = merged.slice(0, HISTORY_MAX)
|
|
199
|
+
for (const dropped of merged.slice(HISTORY_MAX)) await removeEntryFiles(dropped)
|
|
200
|
+
await writeIndex(kept)
|
|
201
|
+
return kept.map(toWire)
|
|
202
|
+
})
|
|
186
203
|
}
|
|
187
204
|
|
|
188
205
|
/** Remove one entry (and its image files). */
|
|
189
206
|
export async function removeHistory(id: string): Promise<HistoryEntry[]> {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
207
|
+
return mutateHistory(async () => {
|
|
208
|
+
const previous = await readIndex()
|
|
209
|
+
const target = previous.find(entry => entry.id === id)
|
|
210
|
+
if (target !== undefined) await removeEntryFiles(target)
|
|
211
|
+
const kept = previous.filter(entry => entry.id !== id)
|
|
212
|
+
await writeIndex(kept)
|
|
213
|
+
return kept.map(toWire)
|
|
214
|
+
})
|
|
196
215
|
}
|
|
197
216
|
|
|
198
217
|
/** Remove every entry (and all image files). */
|
|
199
218
|
export async function clearHistory(): Promise<HistoryEntry[]> {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
219
|
+
return mutateHistory(async () => {
|
|
220
|
+
const previous = await readIndex()
|
|
221
|
+
for (const entry of previous) await removeEntryFiles(entry)
|
|
222
|
+
await writeIndex([])
|
|
223
|
+
return []
|
|
224
|
+
})
|
|
204
225
|
}
|
|
205
226
|
|
|
206
227
|
/** Read one stored image file by its (validated) file name. */
|
package/src/index.ts
CHANGED
|
@@ -57,7 +57,7 @@ const DEFAULT_ANNOUNCE = true
|
|
|
57
57
|
const SECTION_ORDER = 150
|
|
58
58
|
|
|
59
59
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
60
|
-
export const IMAGEGEN_GUIDANCE = '本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI
|
|
60
|
+
export const IMAGEGEN_GUIDANCE = '本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API(模型 gpt-image-2),支持文生图(/images/generations)与图生图(/images/edits,上传参考图);API 地址与密钥在 GUI「设置 → 插件 → 可配置」中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务。用户提到「生图 / 绘画 / 生成图片 / gpt-image-2 / 文生图 / 图生图」时即指本插件,请据此协作。'
|
|
61
61
|
|
|
62
62
|
/** Effective config (schema defaults applied). */
|
|
63
63
|
interface EffectiveConfig {
|
package/src/protocol.ts
CHANGED
|
@@ -58,6 +58,8 @@ export interface GenerateRequest {
|
|
|
58
58
|
detail: string
|
|
59
59
|
/** Reference image as a data URL (edit mode only). */
|
|
60
60
|
image?: string
|
|
61
|
+
/** Original reference-image name, retained in the history entry. */
|
|
62
|
+
refName?: string
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
/** One generated image, normalized host-side to base64 so the browser never
|
|
@@ -74,6 +76,10 @@ export interface GeneratedImage {
|
|
|
74
76
|
/** Successful generate outcome. */
|
|
75
77
|
export interface GenerateResult {
|
|
76
78
|
images: GeneratedImage[]
|
|
79
|
+
/** Updated host-persisted history, when returned by the generate route. */
|
|
80
|
+
history?: HistoryEntry[]
|
|
81
|
+
/** Persistence failure after images were successfully generated. */
|
|
82
|
+
historyError?: string
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
/** One history image reference as the browser consumes it (a served URL). */
|
package/src/routes.ts
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
9
|
+
import { randomUUID } from 'node:crypto'
|
|
9
10
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
10
11
|
import { SettingsConflictError, settingsNamespace, type SettingsDescriptor } from '@deepseek-ai/dsh-settings'
|
|
11
12
|
import { generateImage, type UpstreamConfig } from './engine.ts'
|
|
12
13
|
import { appendHistory, clearHistory, listHistory, readHistoryImage, removeHistory } from './history-store.ts'
|
|
13
|
-
import { GENERATE_API, HISTORY_API, IMAGEGEN_SETTINGS_NAMESPACE, SETTINGS_API, type GeneratedImage, type GenerateRequest, type HistoryEntryInput } from './protocol.ts'
|
|
14
|
+
import { GENERATE_API, HISTORY_API, IMAGEGEN_SETTINGS_NAMESPACE, SETTINGS_API, type GeneratedImage, type GenerateRequest, type HistoryEntry, type HistoryEntryInput } from './protocol.ts'
|
|
14
15
|
|
|
15
16
|
/** Cap on JSON request bodies (settings ops and generate payloads are small). */
|
|
16
17
|
const MAX_JSON_BODY_BYTES = 24 * 1024 * 1024
|
|
@@ -31,6 +32,14 @@ export interface ImageGenRoutesDeps {
|
|
|
31
32
|
settings: SettingsSeam
|
|
32
33
|
/** Resolve the current upstream config (composition entry + settings). */
|
|
33
34
|
resolve: () => UpstreamConfig
|
|
35
|
+
/** Overrideable history backend, primarily for host integration tests. */
|
|
36
|
+
history?: {
|
|
37
|
+
list: () => Promise<HistoryEntry[]>
|
|
38
|
+
append: (entry: HistoryEntryInput) => Promise<HistoryEntry[]>
|
|
39
|
+
remove: (id: string) => Promise<HistoryEntry[]>
|
|
40
|
+
clear: () => Promise<HistoryEntry[]>
|
|
41
|
+
readImage: (file: string) => Promise<{ data: Buffer; mime: string } | undefined>
|
|
42
|
+
}
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
/** Loopback literal check plus browser same-origin markers (mirrors dsh-ssh). */
|
|
@@ -166,6 +175,13 @@ function failureOf(error: unknown): { ok: false; code: string; message: string }
|
|
|
166
175
|
* @returns the route registrations.
|
|
167
176
|
*/
|
|
168
177
|
export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
178
|
+
const history = deps.history ?? {
|
|
179
|
+
list: listHistory,
|
|
180
|
+
append: appendHistory,
|
|
181
|
+
remove: removeHistory,
|
|
182
|
+
clear: clearHistory,
|
|
183
|
+
readImage: readHistoryImage,
|
|
184
|
+
}
|
|
169
185
|
const guard = (req: IncomingMessage, res: ServerResponse, method: string): boolean => {
|
|
170
186
|
if (!isLoopbackRequest(req)) {
|
|
171
187
|
writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
@@ -257,10 +273,28 @@ export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
|
257
273
|
n: typeof body.n === 'number' ? body.n : 1,
|
|
258
274
|
detail: typeof body.detail === 'string' ? body.detail : '',
|
|
259
275
|
...typeof body.image === 'string' && body.image !== '' ? { image: body.image } : {},
|
|
276
|
+
...typeof body.refName === 'string' && body.refName !== '' ? { refName: body.refName } : {},
|
|
260
277
|
}
|
|
261
278
|
try {
|
|
262
279
|
const result = await generateImage(deps.resolve(), request)
|
|
263
|
-
|
|
280
|
+
try {
|
|
281
|
+
const entries = await history.append({
|
|
282
|
+
id: randomUUID(),
|
|
283
|
+
createdAt: Date.now(),
|
|
284
|
+
mode: request.mode,
|
|
285
|
+
model: request.model,
|
|
286
|
+
prompt: request.prompt,
|
|
287
|
+
size: request.size,
|
|
288
|
+
quality: request.quality,
|
|
289
|
+
detail: request.detail,
|
|
290
|
+
n: request.n,
|
|
291
|
+
images: result.images,
|
|
292
|
+
...request.refName === undefined ? {} : { refName: request.refName },
|
|
293
|
+
})
|
|
294
|
+
writeJson(res, 200, { ok: true, ...result, history: entries })
|
|
295
|
+
} catch (error) {
|
|
296
|
+
writeJson(res, 200, { ok: true, ...result, historyError: messageOf(error) })
|
|
297
|
+
}
|
|
264
298
|
} catch (error) {
|
|
265
299
|
const message = error instanceof Error ? error.message : String(error)
|
|
266
300
|
const code = error instanceof Error && 'code' in error && typeof (error as { code?: unknown }).code === 'string'
|
|
@@ -277,7 +311,7 @@ export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
|
277
311
|
handler: async (req, res) => {
|
|
278
312
|
if (!guard(req, res, 'POST')) return
|
|
279
313
|
try {
|
|
280
|
-
writeJson(res, 200, { ok: true, entries: await
|
|
314
|
+
writeJson(res, 200, { ok: true, entries: await history.list() })
|
|
281
315
|
} catch (error) {
|
|
282
316
|
writeJson(res, 200, { ok: false, code: 'history-failed', message: messageOf(error) })
|
|
283
317
|
}
|
|
@@ -300,7 +334,7 @@ export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
|
300
334
|
return
|
|
301
335
|
}
|
|
302
336
|
try {
|
|
303
|
-
writeJson(res, 200, { ok: true, entries: await
|
|
337
|
+
writeJson(res, 200, { ok: true, entries: await history.append(entry) })
|
|
304
338
|
} catch (error) {
|
|
305
339
|
writeJson(res, 200, { ok: false, code: 'history-failed', message: messageOf(error) })
|
|
306
340
|
}
|
|
@@ -319,7 +353,7 @@ export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
|
319
353
|
return
|
|
320
354
|
}
|
|
321
355
|
try {
|
|
322
|
-
writeJson(res, 200, { ok: true, entries: await
|
|
356
|
+
writeJson(res, 200, { ok: true, entries: await history.remove(id) })
|
|
323
357
|
} catch (error) {
|
|
324
358
|
writeJson(res, 200, { ok: false, code: 'history-failed', message: messageOf(error) })
|
|
325
359
|
}
|
|
@@ -332,7 +366,7 @@ export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
|
332
366
|
handler: async (req, res) => {
|
|
333
367
|
if (!guard(req, res, 'POST')) return
|
|
334
368
|
try {
|
|
335
|
-
writeJson(res, 200, { ok: true, entries: await
|
|
369
|
+
writeJson(res, 200, { ok: true, entries: await history.clear() })
|
|
336
370
|
} catch (error) {
|
|
337
371
|
writeJson(res, 200, { ok: false, code: 'history-failed', message: messageOf(error) })
|
|
338
372
|
}
|
|
@@ -356,7 +390,7 @@ export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
|
356
390
|
writeJson(res, 404, { error: 'not found' })
|
|
357
391
|
return
|
|
358
392
|
}
|
|
359
|
-
const found = await
|
|
393
|
+
const found = await history.readImage(file)
|
|
360
394
|
if (found === undefined) {
|
|
361
395
|
writeJson(res, 404, { error: 'not found' })
|
|
362
396
|
return
|