@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/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
- await ensureDirs();
355
- const prefix = safeId(input.id);
356
- const storedImages = [];
357
- for (let index = 0; index < input.images.length; index++) {
358
- const image = input.images[index];
359
- const file = `${prefix}-${index}.${extensionOf(image.mime)}`;
360
- await promises.writeFile(path.join(IMAGES_DIR, file), Buffer.from(image.b64, "base64"));
361
- storedImages.push({
362
- file,
363
- mime: image.mime,
364
- ...image.revisedPrompt === void 0 ? {} : { revisedPrompt: image.revisedPrompt }
365
- });
366
- }
367
- const merged = [{
368
- id: input.id,
369
- createdAt: input.createdAt,
370
- mode: input.mode,
371
- model: input.model,
372
- prompt: input.prompt,
373
- size: input.size,
374
- quality: input.quality,
375
- detail: input.detail,
376
- n: input.n,
377
- images: storedImages,
378
- ...input.refName === void 0 ? {} : { refName: input.refName }
379
- }, ...await readIndex()];
380
- const kept = merged.slice(0, 50);
381
- for (const dropped of merged.slice(50)) await removeEntryFiles(dropped);
382
- await writeIndex(kept);
383
- return kept.map(toWire);
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
- const previous = await readIndex();
388
- const target = previous.find((entry) => entry.id === id);
389
- if (target !== void 0) await removeEntryFiles(target);
390
- const kept = previous.filter((entry) => entry.id !== id);
391
- await writeIndex(kept);
392
- return kept.map(toWire);
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
- const previous = await readIndex();
397
- for (const entry of previous) await removeEntryFiles(entry);
398
- await writeIndex([]);
399
- return [];
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
- writeJson(res, 200, {
668
- ok: true,
669
- ...await generateImage(deps.resolve(), request)
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 listHistory()
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 appendHistory(entry)
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 removeHistory(id)
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 clearHistory()
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 readHistoryImage(file);
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 生图」入口;本地插件(源码位于 E:\\dsh-plugin,独立于 dsh-web-ui 插件全家桶)。能力:对接 OpenAI 兼容图像生成 API(模型 gpt-image-2),支持文生图(/images/generations)与图生图(/images/edits,上传参考图);API 地址与密钥在 GUI「设置 → 插件 → 可配置」中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务。用户提到「生图 / 绘画 / 生成图片 / gpt-image-2 / 文生图 / 图生图」时即指本插件,请据此协作。";
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.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, HistoryEntryInput, HistoryImageRef } from '../protocol.ts'
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
- const entry: HistoryEntryInput = {
190
- id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
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, type HistoryEntryInput } from '../protocol.ts'
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 { images: body.images }
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, {
@@ -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 → Web UI Plugins" and fill in api_url and api_key for AI Image.',
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)',
@@ -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
- await ensureDirs()
156
- const prefix = safeId(input.id)
157
- const storedImages: StoredImage[] = []
158
- for (let index = 0; index < input.images.length; index++) {
159
- const image = input.images[index]!
160
- const file = `${prefix}-${index}.${extensionOf(image.mime)}`
161
- await fs.writeFile(path.join(IMAGES_DIR, file), Buffer.from(image.b64, 'base64'))
162
- storedImages.push({
163
- file,
164
- mime: image.mime,
165
- ...image.revisedPrompt === undefined ? {} : { revisedPrompt: image.revisedPrompt },
166
- })
167
- }
168
- const entry: StoredEntry = {
169
- id: input.id,
170
- createdAt: input.createdAt,
171
- mode: input.mode,
172
- model: input.model,
173
- prompt: input.prompt,
174
- size: input.size,
175
- quality: input.quality,
176
- detail: input.detail,
177
- n: input.n,
178
- images: storedImages,
179
- ...input.refName === undefined ? {} : { refName: input.refName },
180
- }
181
- const merged = [entry, ...await readIndex()]
182
- const kept = merged.slice(0, HISTORY_MAX)
183
- for (const dropped of merged.slice(HISTORY_MAX)) await removeEntryFiles(dropped)
184
- await writeIndex(kept)
185
- return kept.map(toWire)
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
- const previous = await readIndex()
191
- const target = previous.find(entry => entry.id === id)
192
- if (target !== undefined) await removeEntryFiles(target)
193
- const kept = previous.filter(entry => entry.id !== id)
194
- await writeIndex(kept)
195
- return kept.map(toWire)
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
- const previous = await readIndex()
201
- for (const entry of previous) await removeEntryFiles(entry)
202
- await writeIndex([])
203
- return []
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 生图」入口;本地插件(源码位于 E:\\dsh-plugin,独立于 dsh-web-ui 插件全家桶)。能力:对接 OpenAI 兼容图像生成 API(模型 gpt-image-2),支持文生图(/images/generations)与图生图(/images/edits,上传参考图);API 地址与密钥在 GUI「设置 → 插件 → 可配置」中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务。用户提到「生图 / 绘画 / 生成图片 / gpt-image-2 / 文生图 / 图生图」时即指本插件,请据此协作。'
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
- writeJson(res, 200, { ok: true, ...result })
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 listHistory() })
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 appendHistory(entry) })
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 removeHistory(id) })
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 clearHistory() })
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 readHistoryImage(file)
393
+ const found = await history.readImage(file)
360
394
  if (found === undefined) {
361
395
  writeJson(res, 404, { error: 'not found' })
362
396
  return