@dickpy/dsh-imagegen 1.5.5 → 1.5.6
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 +1 -0
- package/lib/client.js +994 -462
- package/lib/client.js.map +1 -1
- package/lib/index.js +80 -50
- package/package.json +1 -1
- package/src/canvas-store.ts +15 -16
- package/src/client/CanvasWorkspace.tsx +273 -30
- package/src/client/ImageGenPanel.tsx +2737 -2679
- package/src/client/SettingsCard.tsx +14 -0
- package/src/client/canvas-workspace.module.css +144 -8
- package/src/client/locales.ts +79 -25
- package/src/client/panel.module.css +20 -26
- package/src/engine.ts +31 -14
- package/src/gallery-store.ts +13 -13
- package/src/generation-runtime.ts +2 -2
- package/src/history-store.ts +12 -12
- package/src/image-storage-path.ts +12 -0
- package/src/index.ts +5 -0
- package/src/protocol.ts +8 -2
- package/src/routes.ts +3 -0
- package/src/task-queue.ts +4 -5
package/src/gallery-store.ts
CHANGED
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
import { promises as fs } from 'node:fs'
|
|
12
12
|
import { createHash } from 'node:crypto'
|
|
13
|
-
import { homedir } from 'node:os'
|
|
14
13
|
import path from 'node:path'
|
|
15
14
|
import type { GenerateMode, HistoryEntry, HistoryEntryInput } from './protocol.ts'
|
|
16
15
|
import { notifyImageSaved } from './storage-sync.ts'
|
|
16
|
+
import { imageDataRoot } from './image-storage-path.ts'
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
function historyDir(): string { return imageDataRoot() }
|
|
19
|
+
function galleryDir(): string { return path.join(imageDataRoot(), 'gallery') }
|
|
20
|
+
function indexPath(): string { return path.join(galleryDir(), 'index.json') }
|
|
21
|
+
function imagesDir(): string { return path.join(galleryDir(), 'images') }
|
|
22
22
|
|
|
23
23
|
/** One gallery entry carries the same wire shape as a history entry. */
|
|
24
24
|
export interface GalleryAppendResult {
|
|
@@ -114,13 +114,13 @@ function fingerprint(input: HistoryEntryInput): string | undefined {
|
|
|
114
114
|
|
|
115
115
|
/** Ensure the storage directories exist. */
|
|
116
116
|
async function ensureDirs(): Promise<void> {
|
|
117
|
-
await fs.mkdir(
|
|
117
|
+
await fs.mkdir(imagesDir(), { recursive: true })
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/** Read the index, tolerating a missing/corrupt file. */
|
|
121
121
|
async function readIndex(): Promise<StoredEntry[]> {
|
|
122
122
|
try {
|
|
123
|
-
const raw = await fs.readFile(
|
|
123
|
+
const raw = await fs.readFile(indexPath(), 'utf8')
|
|
124
124
|
const parsed: unknown = JSON.parse(raw)
|
|
125
125
|
if (parsed === null || typeof parsed !== 'object') return []
|
|
126
126
|
const entries = (parsed as { entries?: unknown }).entries
|
|
@@ -135,9 +135,9 @@ async function readIndex(): Promise<StoredEntry[]> {
|
|
|
135
135
|
async function writeIndex(entries: StoredEntry[]): Promise<void> {
|
|
136
136
|
await ensureDirs()
|
|
137
137
|
const payload: IndexFile = { entries }
|
|
138
|
-
const tmp = `${
|
|
138
|
+
const tmp = `${indexPath()}.tmp-${process.pid}`
|
|
139
139
|
await fs.writeFile(tmp, JSON.stringify(payload), 'utf8')
|
|
140
|
-
await fs.rename(tmp,
|
|
140
|
+
await fs.rename(tmp, indexPath())
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
/** Structural guard for a stored entry. */
|
|
@@ -164,7 +164,7 @@ function isStoredEntry(value: unknown): value is StoredEntry {
|
|
|
164
164
|
/** Remove one entry's image files (best effort). */
|
|
165
165
|
async function removeEntryFiles(entry: StoredEntry): Promise<void> {
|
|
166
166
|
for (const image of entry.images) {
|
|
167
|
-
try { await fs.rm(path.join(
|
|
167
|
+
try { await fs.rm(path.join(imagesDir(), image.file), { force: true }) } catch { /* ignore */ }
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -223,8 +223,8 @@ export async function appendGallery(input: HistoryEntryInput): Promise<GalleryAp
|
|
|
223
223
|
for (let index = 0; index < input.images.length; index++) {
|
|
224
224
|
const image = input.images[index]!
|
|
225
225
|
const file = `${prefix}-${index}.${extensionOf(image.mime)}`
|
|
226
|
-
await fs.writeFile(path.join(
|
|
227
|
-
notifyImageSaved('gallery', path.join(
|
|
226
|
+
await fs.writeFile(path.join(imagesDir(), file), Buffer.from(image.b64, 'base64'))
|
|
227
|
+
notifyImageSaved('gallery', path.join(imagesDir(), file))
|
|
228
228
|
storedImages.push({
|
|
229
229
|
file,
|
|
230
230
|
mime: image.mime,
|
|
@@ -303,7 +303,7 @@ export async function readGalleryImage(file: string): Promise<{ data: Buffer; mi
|
|
|
303
303
|
// store writes — so the route can never escape the images directory.
|
|
304
304
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*-[0-9]+\.(png|jpg|jpeg|webp|gif)$/.test(file)) return undefined
|
|
305
305
|
try {
|
|
306
|
-
const data = await fs.readFile(path.join(
|
|
306
|
+
const data = await fs.readFile(path.join(imagesDir(), file))
|
|
307
307
|
return { data, mime: mimeOfFile(file) }
|
|
308
308
|
} catch {
|
|
309
309
|
return undefined
|
|
@@ -38,8 +38,8 @@ export class ImageGenerationRuntime {
|
|
|
38
38
|
private readonly resolve: () => ChannelsView,
|
|
39
39
|
private readonly history: HistorySink = { append: appendHistory },
|
|
40
40
|
) {
|
|
41
|
-
//
|
|
42
|
-
//
|
|
41
|
+
// Every task runs in parallel up to this small host-wide limit; a
|
|
42
|
+
// four-model comparison fits within it in a single wave.
|
|
43
43
|
this.queue = new GenerationTaskQueue((request, signal) => this.run(request, signal), 4)
|
|
44
44
|
}
|
|
45
45
|
|
package/src/history-store.ts
CHANGED
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { promises as fs } from 'node:fs'
|
|
12
|
-
import { homedir } from 'node:os'
|
|
13
12
|
import path from 'node:path'
|
|
14
13
|
import { HISTORY_MAX, type GenerateMode, type HistoryEntry, type HistoryEntryInput } from './protocol.ts'
|
|
15
14
|
import { notifyImageSaved } from './storage-sync.ts'
|
|
15
|
+
import { imageDataRoot } from './image-storage-path.ts'
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
function historyDir(): string { return imageDataRoot() }
|
|
18
|
+
function indexPath(): string { return path.join(historyDir(), 'index.json') }
|
|
19
|
+
function imagesDir(): string { return path.join(historyDir(), 'images') }
|
|
20
20
|
|
|
21
21
|
// History mutations read and replace one shared index. Serialize them so
|
|
22
22
|
// overlapping requests cannot each read an old index and lose the other's row.
|
|
@@ -95,13 +95,13 @@ function safeId(id: string): string {
|
|
|
95
95
|
|
|
96
96
|
/** Ensure the storage directories exist. */
|
|
97
97
|
async function ensureDirs(): Promise<void> {
|
|
98
|
-
await fs.mkdir(
|
|
98
|
+
await fs.mkdir(imagesDir(), { recursive: true })
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/** Read the index, tolerating a missing/corrupt file. */
|
|
102
102
|
async function readIndex(): Promise<StoredEntry[]> {
|
|
103
103
|
try {
|
|
104
|
-
const raw = await fs.readFile(
|
|
104
|
+
const raw = await fs.readFile(indexPath(), 'utf8')
|
|
105
105
|
const parsed: unknown = JSON.parse(raw)
|
|
106
106
|
if (parsed === null || typeof parsed !== 'object') return []
|
|
107
107
|
const entries = (parsed as { entries?: unknown }).entries
|
|
@@ -116,9 +116,9 @@ async function readIndex(): Promise<StoredEntry[]> {
|
|
|
116
116
|
async function writeIndex(entries: StoredEntry[]): Promise<void> {
|
|
117
117
|
await ensureDirs()
|
|
118
118
|
const payload: IndexFile = { entries }
|
|
119
|
-
const tmp = `${
|
|
119
|
+
const tmp = `${indexPath()}.tmp-${process.pid}`
|
|
120
120
|
await fs.writeFile(tmp, JSON.stringify(payload), 'utf8')
|
|
121
|
-
await fs.rename(tmp,
|
|
121
|
+
await fs.rename(tmp, indexPath())
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/** Structural guard for a stored entry. */
|
|
@@ -145,7 +145,7 @@ function isStoredEntry(value: unknown): value is StoredEntry {
|
|
|
145
145
|
/** Remove one entry's image files (best effort). */
|
|
146
146
|
async function removeEntryFiles(entry: StoredEntry): Promise<void> {
|
|
147
147
|
for (const image of entry.images) {
|
|
148
|
-
try { await fs.rm(path.join(
|
|
148
|
+
try { await fs.rm(path.join(imagesDir(), image.file), { force: true }) } catch { /* ignore */ }
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -196,8 +196,8 @@ export async function appendHistory(input: HistoryEntryInput): Promise<HistoryEn
|
|
|
196
196
|
for (let index = 0; index < input.images.length; index++) {
|
|
197
197
|
const image = input.images[index]!
|
|
198
198
|
const file = `${prefix}-${index}.${extensionOf(image.mime)}`
|
|
199
|
-
await fs.writeFile(path.join(
|
|
200
|
-
notifyImageSaved('history', path.join(
|
|
199
|
+
await fs.writeFile(path.join(imagesDir(), file), Buffer.from(image.b64, 'base64'))
|
|
200
|
+
notifyImageSaved('history', path.join(imagesDir(), file))
|
|
201
201
|
storedImages.push({
|
|
202
202
|
file,
|
|
203
203
|
mime: image.mime,
|
|
@@ -267,7 +267,7 @@ export async function readHistoryImage(file: string): Promise<{ data: Buffer; mi
|
|
|
267
267
|
// store writes — so the route can never escape the images directory.
|
|
268
268
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*-[0-9]+\.(png|jpg|jpeg|webp|gif)$/.test(file)) return undefined
|
|
269
269
|
try {
|
|
270
|
-
const data = await fs.readFile(path.join(
|
|
270
|
+
const data = await fs.readFile(path.join(imagesDir(), file))
|
|
271
271
|
return { data, mime: mimeOfFile(file) }
|
|
272
272
|
} catch {
|
|
273
273
|
return undefined
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
const DEFAULT_ROOT = path.join(process.env.DSH_HOME?.trim() || path.join(homedir(), '.dsh'), 'dsh-imagegen')
|
|
5
|
+
let root = DEFAULT_ROOT
|
|
6
|
+
|
|
7
|
+
export function imageDataRoot(): string { return root }
|
|
8
|
+
|
|
9
|
+
export function setImageDataRoot(value: string | undefined): void {
|
|
10
|
+
const trimmed = value?.trim()
|
|
11
|
+
root = trimmed === undefined || trimmed === '' ? DEFAULT_ROOT : path.resolve(trimmed)
|
|
12
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ function mimeOfPath(filePath: string): string {
|
|
|
37
37
|
import { ImageGenerationRuntime, type ChannelsView, type RuntimeChannel } from './generation-runtime.ts'
|
|
38
38
|
import { registerAgentImageTools } from './agent-image-tools.ts'
|
|
39
39
|
import { registerEditImageCommand } from './edit-image-command.ts'
|
|
40
|
+
import { setImageDataRoot } from './image-storage-path.ts'
|
|
40
41
|
import { presetById } from './presets.ts'
|
|
41
42
|
|
|
42
43
|
/** Stable cordis plugin name. */
|
|
@@ -89,6 +90,8 @@ export interface Config {
|
|
|
89
90
|
promptApiKey?: string
|
|
90
91
|
/** Chat model used to expand short image prompts. */
|
|
91
92
|
promptModel?: string
|
|
93
|
+
/** Local root for generated/history/gallery/canvas images. Empty keeps the default under DSH_HOME. */
|
|
94
|
+
localStoragePath?: string
|
|
92
95
|
/** Sync saved images to an S3-compatible object store (COS / OSS / Qiniu S3 …). */
|
|
93
96
|
storageEnabled?: boolean
|
|
94
97
|
/** S3-compatible endpoint URL including the bucket (virtual-hosted or path style). */
|
|
@@ -133,6 +136,7 @@ export const Config: z<Config> = z.object({
|
|
|
133
136
|
promptApiUrl: z.string().default(''),
|
|
134
137
|
promptApiKey: z.string().role('secret').default(''),
|
|
135
138
|
promptModel: z.string().default(''),
|
|
139
|
+
localStoragePath: z.string().default(''),
|
|
136
140
|
storageEnabled: z.boolean().default(false),
|
|
137
141
|
storageEndpoint: z.string().default(''),
|
|
138
142
|
storageRegion: z.string().default(''),
|
|
@@ -227,6 +231,7 @@ export function apply(ctx: Context, config?: Config): (() => void) | void {
|
|
|
227
231
|
let current: () => Config = () => config ?? {}
|
|
228
232
|
const resolve = (): EffectiveConfig => {
|
|
229
233
|
const value = current() ?? {}
|
|
234
|
+
setImageDataRoot(value.localStoragePath)
|
|
230
235
|
let channels = normalizeChannels(value.channels)
|
|
231
236
|
// Settings scopes are deep-frozen by the host. Legacy migration adds the
|
|
232
237
|
// synthesized default-channel secret, so always work on a detached copy.
|
package/src/protocol.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export const IMAGEGEN_SETTINGS_NAMESPACE = 'dsh-imagegen'
|
|
9
9
|
|
|
10
10
|
/** Published package version shared by the host updater and the client UI. */
|
|
11
|
-
export const PLUGIN_VERSION = '1.5.
|
|
11
|
+
export const PLUGIN_VERSION = '1.5.6'
|
|
12
12
|
|
|
13
13
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
14
14
|
export const SETTINGS_API = {
|
|
@@ -324,7 +324,9 @@ export interface CanvasDocument {
|
|
|
324
324
|
title: string
|
|
325
325
|
revision: number
|
|
326
326
|
viewport: CanvasViewport
|
|
327
|
-
background: 'dots' | 'lines' | 'blank'
|
|
327
|
+
background: 'dots' | 'lines' | 'diagonal' | 'checker' | 'blank' | 'image'
|
|
328
|
+
/** Custom background image URL (a canvas asset) when background is 'image'. */
|
|
329
|
+
backgroundImage?: string
|
|
328
330
|
nodes: CanvasNode[]
|
|
329
331
|
connections: CanvasConnection[]
|
|
330
332
|
createdAt: number
|
|
@@ -413,6 +415,10 @@ export interface GenerateRequest extends EcommerceTaskMeta {
|
|
|
413
415
|
detail: string
|
|
414
416
|
/** Reference image as a data URL (edit mode only). */
|
|
415
417
|
image?: string
|
|
418
|
+
/** Additional reference images as data URLs (edit mode only). The first
|
|
419
|
+
* image stays in `image`; providers that accept several references get them
|
|
420
|
+
* all, single-reference providers see `image` alone. */
|
|
421
|
+
images?: string[]
|
|
416
422
|
/** Original reference-image name, retained in the history entry. */
|
|
417
423
|
refName?: string
|
|
418
424
|
/** Channel this request targets (the host falls back to the default when
|
package/src/routes.ts
CHANGED
|
@@ -187,6 +187,9 @@ function parseGenerateRequest(body: Record<string, unknown>): GenerateRequest |
|
|
|
187
187
|
n: typeof body.n === 'number' ? body.n : 1,
|
|
188
188
|
detail: typeof body.detail === 'string' ? body.detail : '',
|
|
189
189
|
...typeof body.image === 'string' && body.image !== '' ? { image: body.image } : {},
|
|
190
|
+
...Array.isArray(body.images)
|
|
191
|
+
? { images: body.images.filter((item): item is string => typeof item === 'string' && item !== '').slice(0, 4) }
|
|
192
|
+
: {},
|
|
190
193
|
...typeof body.refName === 'string' && body.refName !== '' ? { refName: body.refName } : {},
|
|
191
194
|
...typeof body.channelId === 'string' && body.channelId !== '' ? { channelId: body.channelId } : {},
|
|
192
195
|
...typeof body.comparisonId === 'string' && body.comparisonId !== '' ? { comparisonId: body.comparisonId } : {},
|
package/src/task-queue.ts
CHANGED
|
@@ -10,7 +10,6 @@ export class GenerationTaskQueue {
|
|
|
10
10
|
private readonly controllers = new Map<string, AbortController>()
|
|
11
11
|
private readonly listeners = new Set<GenerationTaskListener>()
|
|
12
12
|
private running = 0
|
|
13
|
-
private serialRunning = false
|
|
14
13
|
|
|
15
14
|
constructor(
|
|
16
15
|
private readonly run: (request: GenerateRequest, signal: AbortSignal) => Promise<GenerateResult>,
|
|
@@ -51,16 +50,16 @@ export class GenerationTaskQueue {
|
|
|
51
50
|
return previous === undefined ? undefined : this.submit(previous.request)
|
|
52
51
|
}
|
|
53
52
|
|
|
53
|
+
/** Start queued tasks while capacity remains. Plain submissions and
|
|
54
|
+
* comparison batches alike run in parallel up to the host-wide limit, so one
|
|
55
|
+
* slow upstream can no longer hold back unrelated generations. */
|
|
54
56
|
private drain(): void {
|
|
55
57
|
while (this.running < Math.max(1, this.concurrency)) {
|
|
56
|
-
const task = this.tasks.find(item => item.status === 'queued'
|
|
57
|
-
&& (this.running === 0 || (item.request.comparisonId !== undefined && !this.serialRunning)))
|
|
58
|
+
const task = this.tasks.find(item => item.status === 'queued')
|
|
58
59
|
if (task === undefined) return
|
|
59
60
|
this.running += 1
|
|
60
|
-
if (task.request.comparisonId === undefined) this.serialRunning = true
|
|
61
61
|
void this.runTask(task).finally(() => {
|
|
62
62
|
this.running -= 1
|
|
63
|
-
if (task.request.comparisonId === undefined) this.serialRunning = false
|
|
64
63
|
this.drain()
|
|
65
64
|
})
|
|
66
65
|
}
|