@dickpy/dsh-imagegen 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -10
- package/docs/images/agent-chat-edit.png +0 -0
- package/docs/images/agent-chat-generate.png +0 -0
- package/lib/client.js +687 -355
- package/lib/client.js.map +1 -1
- package/lib/index.js +664 -164
- package/package.json +4 -1
- package/src/agent-image-tools.ts +289 -0
- package/src/client/ImageGenPanel.tsx +30 -22
- package/src/client/SettingsCard.tsx +261 -70
- package/src/client/locales.ts +40 -0
- package/src/client/settings-card.module.css +180 -0
- package/src/client/settings-form.ts +12 -0
- package/src/client/settings-scope.ts +2 -0
- package/src/engine.ts +29 -2
- package/src/generation-runtime.ts +48 -0
- package/src/image-models.ts +19 -0
- package/src/index.ts +48 -2
- package/src/prompt-enhancer.ts +17 -5
- package/src/protocol.ts +6 -1
- package/src/routes.ts +41 -33
- package/src/task-queue.ts +36 -3
package/src/routes.ts
CHANGED
|
@@ -9,14 +9,15 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
|
9
9
|
import { randomUUID } from 'node:crypto'
|
|
10
10
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
11
11
|
import { SettingsConflictError, settingsNamespace, type SettingsDescriptor } from '@deepseek-ai/dsh-settings'
|
|
12
|
-
import {
|
|
13
|
-
import { enhancePrompt, listPromptModels, type PromptModelConfig } from './prompt-enhancer.ts'
|
|
14
|
-
import {
|
|
12
|
+
import type { UpstreamConfig } from './engine.ts'
|
|
13
|
+
import { enhancePrompt, listOpenAIModels, listPromptModels, type PromptModelConfig } from './prompt-enhancer.ts'
|
|
14
|
+
import { normalizeImageModels } from './image-models.ts'
|
|
15
|
+
import { ImageGenerationRuntime } from './generation-runtime.ts'
|
|
15
16
|
import { appendHistory, clearHistory, listHistory, readHistoryImage, removeHistory } from './history-store.ts'
|
|
16
17
|
import { appendGallery, clearGallery, listGallery, readGalleryImage, removeGallery, updateGalleryTags } from './gallery-store.ts'
|
|
17
18
|
import { listTemplates, readTemplateImage, refreshTemplates } from './templates-store.ts'
|
|
18
19
|
import { checkForUpdate, CURRENT_VERSION, installUpdate } from './updater.ts'
|
|
19
|
-
import { GALLERY_API, GENERATE_API, HISTORY_API, IMAGEGEN_SETTINGS_NAMESPACE, PROMPT_ENHANCE_API, SETTINGS_API, TASK_API, TEMPLATES_API, UPDATE_API, type GeneratedImage, type GenerateRequest, type HistoryEntry, type HistoryEntryInput, type TemplateListResult, type TemplateRefreshResult } from './protocol.ts'
|
|
20
|
+
import { GALLERY_API, GENERATE_API, HISTORY_API, IMAGEGEN_SETTINGS_NAMESPACE, IMAGE_MODEL_API, PROMPT_ENHANCE_API, SETTINGS_API, TASK_API, TEMPLATES_API, UPDATE_API, type GeneratedImage, type GenerateRequest, type HistoryEntry, type HistoryEntryInput, type TemplateListResult, type TemplateRefreshResult } from './protocol.ts'
|
|
20
21
|
|
|
21
22
|
/** Cap on JSON request bodies (settings ops and generate payloads are small). */
|
|
22
23
|
const MAX_JSON_BODY_BYTES = 24 * 1024 * 1024
|
|
@@ -39,6 +40,8 @@ export interface ImageGenRoutesDeps {
|
|
|
39
40
|
resolve: () => UpstreamConfig
|
|
40
41
|
/** Resolve the optional chat-model configuration for prompt enhancement. */
|
|
41
42
|
resolvePrompt?: () => PromptModelConfig
|
|
43
|
+
/** Models explicitly selected for this image API endpoint. */
|
|
44
|
+
resolveImageModels?: () => string[]
|
|
42
45
|
/** Overrideable history backend, primarily for host integration tests. */
|
|
43
46
|
history?: {
|
|
44
47
|
list: () => Promise<HistoryEntry[]>
|
|
@@ -62,6 +65,8 @@ export interface ImageGenRoutesDeps {
|
|
|
62
65
|
refresh: () => Promise<TemplateRefreshResult>
|
|
63
66
|
readImage: (file: string) => Promise<{ data: Buffer; mime: string } | undefined>
|
|
64
67
|
}
|
|
68
|
+
/** Shared host queue, used by Agent tools and browser task endpoints. */
|
|
69
|
+
runtime?: ImageGenerationRuntime
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/** Loopback literal check plus browser same-origin markers (mirrors dsh-ssh). */
|
|
@@ -122,7 +127,7 @@ function parseGenerateRequest(body: Record<string, unknown>): GenerateRequest |
|
|
|
122
127
|
if (prompt === '') return undefined
|
|
123
128
|
return {
|
|
124
129
|
mode: body.mode === 'edit' ? 'edit' : 'text',
|
|
125
|
-
model: typeof body.model === 'string' ? body.model : '
|
|
130
|
+
model: typeof body.model === 'string' ? body.model : '',
|
|
126
131
|
prompt,
|
|
127
132
|
size: typeof body.size === 'string' ? body.size : 'auto',
|
|
128
133
|
quality: typeof body.quality === 'string' ? body.quality : 'auto',
|
|
@@ -234,28 +239,16 @@ export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
|
234
239
|
readImage: readTemplateImage,
|
|
235
240
|
}
|
|
236
241
|
const resolvePrompt = deps.resolvePrompt ?? (() => ({ apiUrl: '', apiKey: '', model: '' }))
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
prompt: request.prompt,
|
|
246
|
-
size: request.size,
|
|
247
|
-
quality: request.quality,
|
|
248
|
-
detail: request.detail,
|
|
249
|
-
n: request.n,
|
|
250
|
-
images: result.images,
|
|
251
|
-
...request.refName === undefined ? {} : { refName: request.refName },
|
|
252
|
-
})
|
|
253
|
-
return { ...result, history: entries }
|
|
254
|
-
} catch (error) {
|
|
255
|
-
return { ...result, historyError: messageOf(error) }
|
|
256
|
-
}
|
|
242
|
+
const resolveImageModels = deps.resolveImageModels ?? (() => normalizeImageModels(undefined))
|
|
243
|
+
const parseConfiguredRequest = (body: Record<string, unknown>): GenerateRequest | undefined => {
|
|
244
|
+
const request = parseGenerateRequest(body)
|
|
245
|
+
if (request === undefined) return undefined
|
|
246
|
+
const models = normalizeImageModels(resolveImageModels())
|
|
247
|
+
const model = request.model.trim() === '' ? models[0] : request.model.trim()
|
|
248
|
+
if (!models.includes(model)) throw new Error(`image model "${model}" is not configured; choose one of: ${models.join(', ')}`)
|
|
249
|
+
return { ...request, model }
|
|
257
250
|
}
|
|
258
|
-
const
|
|
251
|
+
const runtime = deps.runtime ?? new ImageGenerationRuntime(deps.resolve, history)
|
|
259
252
|
const guard = (req: IncomingMessage, res: ServerResponse, method: string): boolean => {
|
|
260
253
|
if (!isLoopbackRequest(req)) {
|
|
261
254
|
writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
@@ -269,6 +262,19 @@ export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
|
269
262
|
}
|
|
270
263
|
|
|
271
264
|
return [
|
|
265
|
+
// -------------------------------------------- image model discovery
|
|
266
|
+
{
|
|
267
|
+
kind: 'exact',
|
|
268
|
+
path: IMAGE_MODEL_API.models,
|
|
269
|
+
handler: async (req, res) => {
|
|
270
|
+
if (!guard(req, res, 'POST')) return
|
|
271
|
+
try {
|
|
272
|
+
writeJson(res, 200, { ok: true, models: await listOpenAIModels(deps.resolve()) })
|
|
273
|
+
} catch (error) {
|
|
274
|
+
writeJson(res, 200, { ok: false, code: 'image-models-failed', message: messageOf(error) })
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
},
|
|
272
278
|
// ----------------------------------------------- prompt enhancement
|
|
273
279
|
{
|
|
274
280
|
kind: 'exact',
|
|
@@ -360,13 +366,14 @@ export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
|
360
366
|
writeJson(res, 200, { ok: false, code: 'bad-request', message: 'unreadable JSON body' })
|
|
361
367
|
return
|
|
362
368
|
}
|
|
363
|
-
|
|
369
|
+
let request: GenerateRequest | undefined
|
|
370
|
+
try { request = parseConfiguredRequest(body) } catch (error) { writeJson(res, 200, { ok: false, code: 'image-model-not-configured', message: messageOf(error) }); return }
|
|
364
371
|
if (request === undefined) {
|
|
365
372
|
writeJson(res, 200, { ok: false, code: 'bad-request', message: 'prompt is required' })
|
|
366
373
|
return
|
|
367
374
|
}
|
|
368
375
|
try {
|
|
369
|
-
writeJson(res, 200, { ok: true, ...await
|
|
376
|
+
writeJson(res, 200, { ok: true, ...await runtime.run(request) })
|
|
370
377
|
} catch (error) {
|
|
371
378
|
const message = error instanceof Error ? error.message : String(error)
|
|
372
379
|
const code = error instanceof Error && 'code' in error && typeof (error as { code?: unknown }).code === 'string'
|
|
@@ -382,21 +389,22 @@ export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
|
382
389
|
handler: async (req, res) => {
|
|
383
390
|
if (!guard(req, res, 'POST')) return
|
|
384
391
|
const body = await readJsonBody(req)
|
|
385
|
-
|
|
392
|
+
let request: GenerateRequest | undefined
|
|
393
|
+
try { request = body === undefined ? undefined : parseConfiguredRequest(body) } catch (error) { writeJson(res, 200, { ok: false, code: 'image-model-not-configured', message: messageOf(error) }); return }
|
|
386
394
|
if (request === undefined) { writeJson(res, 200, { ok: false, code: 'bad-request', message: 'prompt is required' }); return }
|
|
387
|
-
writeJson(res, 200, { ok: true, task:
|
|
395
|
+
writeJson(res, 200, { ok: true, task: runtime.queue.submit(request) })
|
|
388
396
|
},
|
|
389
397
|
},
|
|
390
398
|
{
|
|
391
399
|
kind: 'exact', path: TASK_API.list,
|
|
392
|
-
handler: async (req, res) => { if (!guard(req, res, 'POST')) return; writeJson(res, 200, { ok: true, tasks:
|
|
400
|
+
handler: async (req, res) => { if (!guard(req, res, 'POST')) return; writeJson(res, 200, { ok: true, tasks: runtime.queue.list() }) },
|
|
393
401
|
},
|
|
394
402
|
{
|
|
395
403
|
kind: 'exact', path: TASK_API.cancel,
|
|
396
404
|
handler: async (req, res) => {
|
|
397
405
|
if (!guard(req, res, 'POST')) return
|
|
398
406
|
const body = await readJsonBody(req)
|
|
399
|
-
const task = typeof body?.id === 'string' ?
|
|
407
|
+
const task = typeof body?.id === 'string' ? runtime.queue.cancel(body.id) : undefined
|
|
400
408
|
if (task === undefined) { writeJson(res, 200, { ok: false, code: 'not-found', message: 'task not found' }); return }
|
|
401
409
|
writeJson(res, 200, { ok: true, task })
|
|
402
410
|
},
|
|
@@ -406,7 +414,7 @@ export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
|
406
414
|
handler: async (req, res) => {
|
|
407
415
|
if (!guard(req, res, 'POST')) return
|
|
408
416
|
const body = await readJsonBody(req)
|
|
409
|
-
const task = typeof body?.id === 'string' ?
|
|
417
|
+
const task = typeof body?.id === 'string' ? runtime.queue.retry(body.id) : undefined
|
|
410
418
|
if (task === undefined) { writeJson(res, 200, { ok: false, code: 'not-found', message: 'task not found' }); return }
|
|
411
419
|
writeJson(res, 200, { ok: true, task })
|
|
412
420
|
},
|
package/src/task-queue.ts
CHANGED
|
@@ -3,22 +3,32 @@
|
|
|
3
3
|
import { randomUUID } from 'node:crypto'
|
|
4
4
|
import type { GenerateRequest, GenerateResult, GenerationTask } from './protocol.ts'
|
|
5
5
|
|
|
6
|
+
export type GenerationTaskListener = (task: GenerationTask) => void
|
|
7
|
+
|
|
6
8
|
export class GenerationTaskQueue {
|
|
7
9
|
private readonly tasks: GenerationTask[] = []
|
|
8
10
|
private readonly controllers = new Map<string, AbortController>()
|
|
11
|
+
private readonly listeners = new Set<GenerationTaskListener>()
|
|
9
12
|
private running = false
|
|
10
13
|
|
|
11
14
|
constructor(private readonly run: (request: GenerateRequest, signal: AbortSignal) => Promise<GenerateResult>) {}
|
|
12
15
|
|
|
13
16
|
list(): GenerationTask[] {
|
|
14
|
-
return this.tasks.map(task =>
|
|
17
|
+
return this.tasks.map(task => this.snapshot(task))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Observe queue state changes. Listener failures never disrupt generation. */
|
|
21
|
+
subscribe(listener: GenerationTaskListener): () => void {
|
|
22
|
+
this.listeners.add(listener)
|
|
23
|
+
return () => { this.listeners.delete(listener) }
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
submit(request: GenerateRequest): GenerationTask {
|
|
18
27
|
const task: GenerationTask = { id: randomUUID(), request: { ...request }, status: 'queued', createdAt: Date.now() }
|
|
19
28
|
this.tasks.unshift(task)
|
|
29
|
+
this.publish(task)
|
|
20
30
|
void this.drain()
|
|
21
|
-
return task
|
|
31
|
+
return this.snapshot(task)
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
cancel(id: string): GenerationTask | undefined {
|
|
@@ -27,7 +37,8 @@ export class GenerationTaskQueue {
|
|
|
27
37
|
task.status = 'cancelled'
|
|
28
38
|
task.finishedAt = Date.now()
|
|
29
39
|
this.controllers.get(id)?.abort()
|
|
30
|
-
|
|
40
|
+
this.publish(task)
|
|
41
|
+
return this.snapshot(task)
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
retry(id: string): GenerationTask | undefined {
|
|
@@ -44,6 +55,7 @@ export class GenerationTaskQueue {
|
|
|
44
55
|
if (task === undefined) return
|
|
45
56
|
task.status = 'running'
|
|
46
57
|
task.startedAt = Date.now()
|
|
58
|
+
this.publish(task)
|
|
47
59
|
const controller = new AbortController()
|
|
48
60
|
this.controllers.set(task.id, controller)
|
|
49
61
|
try {
|
|
@@ -52,12 +64,14 @@ export class GenerationTaskQueue {
|
|
|
52
64
|
task.status = 'completed'
|
|
53
65
|
task.result = result
|
|
54
66
|
task.finishedAt = Date.now()
|
|
67
|
+
this.publish(task)
|
|
55
68
|
}
|
|
56
69
|
} catch (error) {
|
|
57
70
|
if (this.tasks.find(item => item.id === task.id)?.status !== 'cancelled') {
|
|
58
71
|
task.status = 'failed'
|
|
59
72
|
task.error = error instanceof Error ? error.message : String(error)
|
|
60
73
|
task.finishedAt = Date.now()
|
|
74
|
+
this.publish(task)
|
|
61
75
|
}
|
|
62
76
|
} finally {
|
|
63
77
|
this.controllers.delete(task.id)
|
|
@@ -67,4 +81,23 @@ export class GenerationTaskQueue {
|
|
|
67
81
|
this.running = false
|
|
68
82
|
}
|
|
69
83
|
}
|
|
84
|
+
|
|
85
|
+
private publish(task: GenerationTask): void {
|
|
86
|
+
const snapshot = this.snapshot(task)
|
|
87
|
+
for (const listener of this.listeners) {
|
|
88
|
+
try {
|
|
89
|
+
listener(snapshot)
|
|
90
|
+
} catch {
|
|
91
|
+
// Observers must not be able to interrupt the queue pump.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private snapshot(task: GenerationTask): GenerationTask {
|
|
97
|
+
return {
|
|
98
|
+
...task,
|
|
99
|
+
request: { ...task.request },
|
|
100
|
+
...task.result === undefined ? {} : { result: task.result },
|
|
101
|
+
}
|
|
102
|
+
}
|
|
70
103
|
}
|