@dickpy/dsh-imagegen 1.0.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/LICENSE +201 -0
- package/README.md +126 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +2413 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +896 -0
- package/package.json +66 -0
- package/src/client/ImageGenPanel.tsx +687 -0
- package/src/client/SettingsCard.tsx +373 -0
- package/src/client/api.ts +89 -0
- package/src/client/controller.ts +46 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/helpers.ts +33 -0
- package/src/client/index.ts +127 -0
- package/src/client/locales.ts +204 -0
- package/src/client/mount.tsx +119 -0
- package/src/client/panel.module.css +970 -0
- package/src/client/settings-card.module.css +288 -0
- package/src/client/settings-form.ts +324 -0
- package/src/client/settings-scope.ts +227 -0
- package/src/client/sidebar-entry.ts +144 -0
- package/src/engine.ts +284 -0
- package/src/history-store.ts +217 -0
- package/src/index.ts +139 -0
- package/src/protocol.ts +118 -0
- package/src/routes.ts +373 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-persisted generation history: images are stored as individual files
|
|
3
|
+
* under ~/.dsh/dsh-imagegen/images/ and an index.json keeps the metadata +
|
|
4
|
+
* file names. This makes the history survive across browsers/devices that
|
|
5
|
+
* connect to the same DSH host, and keeps list responses small (the browser
|
|
6
|
+
* loads image bytes lazily through the history image route).
|
|
7
|
+
*
|
|
8
|
+
* Framework-free (node:fs only) so the route layer can drive it directly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { promises as fs } from 'node:fs'
|
|
12
|
+
import { homedir } from 'node:os'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { HISTORY_MAX, type GenerateMode, type HistoryEntry, type HistoryEntryInput } from './protocol.ts'
|
|
15
|
+
|
|
16
|
+
const HISTORY_DIR = path.join(homedir(), '.dsh', 'dsh-imagegen')
|
|
17
|
+
const INDEX_PATH = path.join(HISTORY_DIR, 'index.json')
|
|
18
|
+
const IMAGES_DIR = path.join(HISTORY_DIR, 'images')
|
|
19
|
+
|
|
20
|
+
/** One image's on-disk record (file name + mime, never base64). */
|
|
21
|
+
interface StoredImage {
|
|
22
|
+
file: string
|
|
23
|
+
mime: string
|
|
24
|
+
revisedPrompt?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One entry's on-disk record. */
|
|
28
|
+
interface StoredEntry {
|
|
29
|
+
id: string
|
|
30
|
+
createdAt: number
|
|
31
|
+
mode: GenerateMode
|
|
32
|
+
model: string
|
|
33
|
+
prompt: string
|
|
34
|
+
size: string
|
|
35
|
+
quality: string
|
|
36
|
+
detail: string
|
|
37
|
+
n: number
|
|
38
|
+
images: StoredImage[]
|
|
39
|
+
refName?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The index.json shape. */
|
|
43
|
+
interface IndexFile {
|
|
44
|
+
entries: StoredEntry[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** File extension for a MIME type (image file names). */
|
|
48
|
+
function extensionOf(mime: string): string {
|
|
49
|
+
switch (mime.split(';')[0]!.trim()) {
|
|
50
|
+
case 'image/jpeg': return 'jpg'
|
|
51
|
+
case 'image/webp': return 'webp'
|
|
52
|
+
case 'image/gif': return 'gif'
|
|
53
|
+
default: return 'png'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** MIME type for a stored image file name (image route responses). */
|
|
58
|
+
function mimeOfFile(file: string): string {
|
|
59
|
+
const ext = path.extname(file).toLowerCase()
|
|
60
|
+
switch (ext) {
|
|
61
|
+
case '.jpg':
|
|
62
|
+
case '.jpeg': return 'image/jpeg'
|
|
63
|
+
case '.webp': return 'image/webp'
|
|
64
|
+
case '.gif': return 'image/gif'
|
|
65
|
+
default: return 'image/png'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Sanitize an entry id for use as a file-name prefix. */
|
|
70
|
+
function safeId(id: string): string {
|
|
71
|
+
const cleaned = id.replace(/[^a-zA-Z0-9-]/g, '-')
|
|
72
|
+
return cleaned === '' ? 'entry' : cleaned
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Ensure the storage directories exist. */
|
|
76
|
+
async function ensureDirs(): Promise<void> {
|
|
77
|
+
await fs.mkdir(IMAGES_DIR, { recursive: true })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Read the index, tolerating a missing/corrupt file. */
|
|
81
|
+
async function readIndex(): Promise<StoredEntry[]> {
|
|
82
|
+
try {
|
|
83
|
+
const raw = await fs.readFile(INDEX_PATH, 'utf8')
|
|
84
|
+
const parsed: unknown = JSON.parse(raw)
|
|
85
|
+
if (parsed === null || typeof parsed !== 'object') return []
|
|
86
|
+
const entries = (parsed as { entries?: unknown }).entries
|
|
87
|
+
if (!Array.isArray(entries)) return []
|
|
88
|
+
return entries.filter(isStoredEntry)
|
|
89
|
+
} catch {
|
|
90
|
+
return []
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Persist the index. */
|
|
95
|
+
async function writeIndex(entries: StoredEntry[]): Promise<void> {
|
|
96
|
+
await ensureDirs()
|
|
97
|
+
const payload: IndexFile = { entries }
|
|
98
|
+
const tmp = `${INDEX_PATH}.tmp-${process.pid}`
|
|
99
|
+
await fs.writeFile(tmp, JSON.stringify(payload), 'utf8')
|
|
100
|
+
await fs.rename(tmp, INDEX_PATH)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Structural guard for a stored entry. */
|
|
104
|
+
function isStoredEntry(value: unknown): value is StoredEntry {
|
|
105
|
+
if (value === null || typeof value !== 'object') return false
|
|
106
|
+
const entry = value as Record<string, unknown>
|
|
107
|
+
return typeof entry.id === 'string'
|
|
108
|
+
&& typeof entry.createdAt === 'number'
|
|
109
|
+
&& (entry.mode === 'text' || entry.mode === 'edit')
|
|
110
|
+
&& typeof entry.prompt === 'string'
|
|
111
|
+
&& Array.isArray(entry.images)
|
|
112
|
+
&& entry.images.every(image => {
|
|
113
|
+
if (image === null || typeof image !== 'object') return false
|
|
114
|
+
const record = image as Record<string, unknown>
|
|
115
|
+
return typeof record.file === 'string' && typeof record.mime === 'string'
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Remove one entry's image files (best effort). */
|
|
120
|
+
async function removeEntryFiles(entry: StoredEntry): Promise<void> {
|
|
121
|
+
for (const image of entry.images) {
|
|
122
|
+
try { await fs.rm(path.join(IMAGES_DIR, image.file), { force: true }) } catch { /* ignore */ }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Project a stored entry onto the wire shape (image URLs). */
|
|
127
|
+
function toWire(entry: StoredEntry): HistoryEntry {
|
|
128
|
+
return {
|
|
129
|
+
id: entry.id,
|
|
130
|
+
createdAt: entry.createdAt,
|
|
131
|
+
mode: entry.mode,
|
|
132
|
+
model: entry.model,
|
|
133
|
+
prompt: entry.prompt,
|
|
134
|
+
size: entry.size,
|
|
135
|
+
quality: entry.quality,
|
|
136
|
+
detail: entry.detail,
|
|
137
|
+
n: entry.n,
|
|
138
|
+
images: entry.images.map(image => ({
|
|
139
|
+
url: `/api/dsh-imagegen/history/image/${image.file}`,
|
|
140
|
+
mime: image.mime,
|
|
141
|
+
...image.revisedPrompt === undefined ? {} : { revisedPrompt: image.revisedPrompt },
|
|
142
|
+
})),
|
|
143
|
+
...entry.refName === undefined ? {} : { refName: entry.refName },
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** List the persisted history, newest first, as wire entries. */
|
|
148
|
+
export async function listHistory(): Promise<HistoryEntry[]> {
|
|
149
|
+
const entries = await readIndex()
|
|
150
|
+
return entries.map(toWire)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Append one generation, evicting the oldest beyond HISTORY_MAX. */
|
|
154
|
+
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)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Remove one entry (and its image files). */
|
|
189
|
+
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)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Remove every entry (and all image files). */
|
|
199
|
+
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 []
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Read one stored image file by its (validated) file name. */
|
|
207
|
+
export async function readHistoryImage(file: string): Promise<{ data: Buffer; mime: string } | undefined> {
|
|
208
|
+
// Only accept <id>-<index>.<png|jpg|jpeg|webp|gif> — the exact names this
|
|
209
|
+
// store writes — so the route can never escape the images directory.
|
|
210
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*-[0-9]+\.(png|jpg|jpeg|webp|gif)$/.test(file)) return undefined
|
|
211
|
+
try {
|
|
212
|
+
const data = await fs.readFile(path.join(IMAGES_DIR, file))
|
|
213
|
+
return { data, mime: mimeOfFile(file) }
|
|
214
|
+
} catch {
|
|
215
|
+
return undefined
|
|
216
|
+
}
|
|
217
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-imagegen — host half. Mounts the plugin's settings section (api_url /
|
|
3
|
+
* api_key on the host settings seam), the /api/dsh-imagegen route family
|
|
4
|
+
* (loopback-only settings bridge + image-generation proxy that keeps the API
|
|
5
|
+
* key host-side), and a system-prompt announcement. The browser half
|
|
6
|
+
* (./client) renders the sidebar entry and the split-pane generation studio.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
10
|
+
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
11
|
+
import z from 'schemastery'
|
|
12
|
+
// Type-only: pulls the webServer Context merge (route registration).
|
|
13
|
+
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
14
|
+
// Type-only: pulls the systemPrompt Context merge (announcement section).
|
|
15
|
+
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
16
|
+
import { IMAGEGEN_SETTINGS_NAMESPACE } from './protocol.ts'
|
|
17
|
+
import { makeRoutes, type SettingsSeam } from './routes.ts'
|
|
18
|
+
|
|
19
|
+
/** Stable cordis plugin name. */
|
|
20
|
+
export const name = 'imagegen'
|
|
21
|
+
|
|
22
|
+
/** Services required before the surfaces can mount. */
|
|
23
|
+
export const inject = ['webServer', 'systemPrompt']
|
|
24
|
+
|
|
25
|
+
// Internals re-exported for smoke tests and host-side debugging; the plugin
|
|
26
|
+
// contract only requires name / inject / Config / apply.
|
|
27
|
+
export { makeRoutes } from './routes.ts'
|
|
28
|
+
export { generateImage, ImageGenError } from './engine.ts'
|
|
29
|
+
|
|
30
|
+
/** The branded settings namespace of this plugin (the card edits it). */
|
|
31
|
+
export const ImageGenSettingsNamespace = settingsNamespace(IMAGEGEN_SETTINGS_NAMESPACE)
|
|
32
|
+
|
|
33
|
+
/** Plugin config, validated by the same-named schemastery schema. */
|
|
34
|
+
export interface Config {
|
|
35
|
+
/** Master switch for the plugin (routes, prompt section). */
|
|
36
|
+
enabled?: boolean
|
|
37
|
+
/** Announce the plugin in every agent's system prompt. */
|
|
38
|
+
announceToAgent?: boolean
|
|
39
|
+
/** Base URL of the OpenAI-compatible endpoint, e.g. https://api.openai.com/v1 */
|
|
40
|
+
apiUrl?: string
|
|
41
|
+
/** Bearer API key (stored as a secret field on the settings seam). */
|
|
42
|
+
apiKey?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const Config: z<Config> = z.object({
|
|
46
|
+
enabled: z.boolean().default(true),
|
|
47
|
+
announceToAgent: z.boolean().default(true),
|
|
48
|
+
apiUrl: z.string().default(''),
|
|
49
|
+
apiKey: z.string().role('secret').default(''),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/** Schema defaults, re-read for hand-built contexts (the loader applies them normally). */
|
|
53
|
+
const DEFAULT_ENABLED = true
|
|
54
|
+
const DEFAULT_ANNOUNCE = true
|
|
55
|
+
|
|
56
|
+
/** Order of the announcement section within the tool-guidance band. */
|
|
57
|
+
const SECTION_ORDER = 150
|
|
58
|
+
|
|
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 / 文生图 / 图生图」时即指本插件,请据此协作。'
|
|
61
|
+
|
|
62
|
+
/** Effective config (schema defaults applied). */
|
|
63
|
+
interface EffectiveConfig {
|
|
64
|
+
enabled: boolean
|
|
65
|
+
announceToAgent: boolean
|
|
66
|
+
apiUrl: string
|
|
67
|
+
apiKey: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Mount the settings section, routes, and announcement.
|
|
72
|
+
* @param ctx - host plugin context carrying webServer/systemPrompt.
|
|
73
|
+
* @param config - resolved plugin config (schema defaults applied by the loader).
|
|
74
|
+
*/
|
|
75
|
+
export function apply(ctx: Context, config?: Config): void {
|
|
76
|
+
// The live source the surfaces read: the settings section once the settings
|
|
77
|
+
// service is attached, the composition entry otherwise.
|
|
78
|
+
let current: () => Config = () => config ?? {}
|
|
79
|
+
const resolve = (): EffectiveConfig => {
|
|
80
|
+
const value = current()
|
|
81
|
+
return {
|
|
82
|
+
enabled: value.enabled ?? DEFAULT_ENABLED,
|
|
83
|
+
announceToAgent: value.announceToAgent ?? DEFAULT_ANNOUNCE,
|
|
84
|
+
apiUrl: value.apiUrl ?? '',
|
|
85
|
+
apiKey: value.apiKey ?? '',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The route family mounts once, gated on the settings seam (the bridge
|
|
90
|
+
// serves it; without the seam there is nothing to expose). Route handlers
|
|
91
|
+
// read resolve() per request, so config edits apply live. The settings
|
|
92
|
+
// bridge deliberately keeps serving while the plugin is disabled — it is
|
|
93
|
+
// how the user re-enables the plugin from the settings card.
|
|
94
|
+
ctx.inject(['settings'], (sctx) => {
|
|
95
|
+
const seam = sctx.get('settings') as unknown as SettingsSeam
|
|
96
|
+
sctx.effect(
|
|
97
|
+
() => {
|
|
98
|
+
const routes = makeRoutes({
|
|
99
|
+
settings: seam,
|
|
100
|
+
resolve: () => {
|
|
101
|
+
const value = resolve()
|
|
102
|
+
return { apiUrl: value.apiUrl, apiKey: value.apiKey }
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
const disposers = routes.map(route => ctx.webServer.register(route))
|
|
106
|
+
return () => { for (const dispose of disposers) dispose() }
|
|
107
|
+
},
|
|
108
|
+
'dsh-imagegen: routes',
|
|
109
|
+
)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// System-prompt announcement (toggled by settings changes).
|
|
113
|
+
let disposeSection: (() => void) | undefined
|
|
114
|
+
const sync = (): void => {
|
|
115
|
+
if (disposeSection !== undefined) {
|
|
116
|
+
disposeSection()
|
|
117
|
+
disposeSection = undefined
|
|
118
|
+
}
|
|
119
|
+
const value = resolve()
|
|
120
|
+
if (!value.enabled || !value.announceToAgent) return
|
|
121
|
+
disposeSection = ctx.systemPrompt.section({
|
|
122
|
+
name: 'plugin:dsh-imagegen',
|
|
123
|
+
order: SECTION_ORDER,
|
|
124
|
+
text: IMAGEGEN_GUIDANCE,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
installSettingsSection(ctx, ImageGenSettingsNamespace, Config, config ?? {}, {
|
|
129
|
+
setSource: (source) => {
|
|
130
|
+
current = source
|
|
131
|
+
sync()
|
|
132
|
+
},
|
|
133
|
+
onChange: sync,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Initial registration from the composition entry (covers deployments with
|
|
137
|
+
// no settings service, whose installSettingsSection never fires its hooks).
|
|
138
|
+
sync()
|
|
139
|
+
}
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire contract shared by the host and client halves of dsh-imagegen: the
|
|
3
|
+
* settings namespace, the route paths, and the generate payload/result shapes.
|
|
4
|
+
* Pure types + constants — safe for the client bundle to inline.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Settings namespace this plugin owns (host settings seam + bridge). */
|
|
8
|
+
export const IMAGEGEN_SETTINGS_NAMESPACE = 'dsh-imagegen'
|
|
9
|
+
|
|
10
|
+
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
11
|
+
export const SETTINGS_API = {
|
|
12
|
+
describe: '/api/dsh-imagegen/settings/describe',
|
|
13
|
+
mutate: '/api/dsh-imagegen/settings/mutate',
|
|
14
|
+
} as const
|
|
15
|
+
|
|
16
|
+
/** The image-generation proxy route. */
|
|
17
|
+
export const GENERATE_API = '/api/dsh-imagegen/generate'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Same-origin route family for the host-persisted generation history. Images
|
|
21
|
+
* live as files under ~/.dsh/dsh-imagegen/images/ and are served back through
|
|
22
|
+
* the `image` prefix route, so list responses carry metadata only (never
|
|
23
|
+
* base64) and the browser loads thumbnails/previews lazily.
|
|
24
|
+
*/
|
|
25
|
+
export const HISTORY_API = {
|
|
26
|
+
list: '/api/dsh-imagegen/history/list',
|
|
27
|
+
append: '/api/dsh-imagegen/history/append',
|
|
28
|
+
remove: '/api/dsh-imagegen/history/remove',
|
|
29
|
+
clear: '/api/dsh-imagegen/history/clear',
|
|
30
|
+
image: '/api/dsh-imagegen/history/image',
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
/** Maximum number of history entries retained host-side (oldest evicted). */
|
|
34
|
+
export const HISTORY_MAX = 50
|
|
35
|
+
|
|
36
|
+
/** Generation modes. */
|
|
37
|
+
export type GenerateMode = 'text' | 'edit'
|
|
38
|
+
|
|
39
|
+
/** A client → host generate request (what the panel collects). */
|
|
40
|
+
export interface GenerateRequest {
|
|
41
|
+
/** text-to-image (images/generations) or image-to-image (images/edits). */
|
|
42
|
+
mode: GenerateMode
|
|
43
|
+
/** Upstream model name, e.g. gpt-image-2. */
|
|
44
|
+
model: string
|
|
45
|
+
/** The prompt (up to 2000 chars in the UI). */
|
|
46
|
+
prompt: string
|
|
47
|
+
/** Canvas size: 'auto' or a pixel size like '1024x1024'. */
|
|
48
|
+
size: string
|
|
49
|
+
/** Quality: 'auto' | 'low' | 'medium' | 'high'. */
|
|
50
|
+
quality: string
|
|
51
|
+
/** Number of images, 1-4. */
|
|
52
|
+
n: number
|
|
53
|
+
/**
|
|
54
|
+
* Passthrough detail parameter: '' (omit), 'standard', or 'high'. Some
|
|
55
|
+
* gpt-image-2 gateways expose it; official OpenAI endpoints reject unknown
|
|
56
|
+
* parameters, so the UI defaults to '' (omit).
|
|
57
|
+
*/
|
|
58
|
+
detail: string
|
|
59
|
+
/** Reference image as a data URL (edit mode only). */
|
|
60
|
+
image?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One generated image, normalized host-side to base64 so the browser never
|
|
64
|
+
* has to fetch the upstream (no CORS, no key exposure). */
|
|
65
|
+
export interface GeneratedImage {
|
|
66
|
+
/** Raw base64 payload (no data: prefix). */
|
|
67
|
+
b64: string
|
|
68
|
+
/** MIME type of the payload, e.g. image/png. */
|
|
69
|
+
mime: string
|
|
70
|
+
/** Upstream revised prompt, when provided. */
|
|
71
|
+
revisedPrompt?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Successful generate outcome. */
|
|
75
|
+
export interface GenerateResult {
|
|
76
|
+
images: GeneratedImage[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** One history image reference as the browser consumes it (a served URL). */
|
|
80
|
+
export interface HistoryImageRef {
|
|
81
|
+
/** Same-origin URL: `${HISTORY_API.image}/<file>`. */
|
|
82
|
+
url: string
|
|
83
|
+
/** MIME type, e.g. image/png. */
|
|
84
|
+
mime: string
|
|
85
|
+
/** Upstream revised prompt, when provided. */
|
|
86
|
+
revisedPrompt?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A saved generation as the browser consumes it (metadata + served images). */
|
|
90
|
+
export interface HistoryEntry {
|
|
91
|
+
id: string
|
|
92
|
+
createdAt: number
|
|
93
|
+
mode: GenerateMode
|
|
94
|
+
model: string
|
|
95
|
+
prompt: string
|
|
96
|
+
size: string
|
|
97
|
+
quality: string
|
|
98
|
+
detail: string
|
|
99
|
+
n: number
|
|
100
|
+
images: HistoryImageRef[]
|
|
101
|
+
/** Reference-image filename (edit mode), kept for display only. */
|
|
102
|
+
refName?: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A history entry the client submits for persistence (images still carry base64). */
|
|
106
|
+
export interface HistoryEntryInput {
|
|
107
|
+
id: string
|
|
108
|
+
createdAt: number
|
|
109
|
+
mode: GenerateMode
|
|
110
|
+
model: string
|
|
111
|
+
prompt: string
|
|
112
|
+
size: string
|
|
113
|
+
quality: string
|
|
114
|
+
detail: string
|
|
115
|
+
n: number
|
|
116
|
+
images: GeneratedImage[]
|
|
117
|
+
refName?: string
|
|
118
|
+
}
|