@dickpy/dsh-imagegen 1.3.0 → 1.5.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 +363 -196
- package/docs/images/ecommerce-mode.png +0 -0
- package/docs/images/image-generation-studio-three-column.png +0 -0
- package/docs/images/imagegen-overview.png +0 -0
- package/docs/images/multi-model-comparison.png +0 -0
- package/docs/videos/agent-chat-edit.gif +0 -0
- package/docs/videos/agent-chat-edit.mp4 +0 -0
- package/lib/client.js +2589 -884
- package/lib/client.js.map +1 -1
- package/lib/index.js +585 -240
- package/package.json +5 -2
- package/src/agent-image-tools.ts +131 -102
- package/src/client/ImageGenPanel.tsx +2679 -1594
- package/src/client/SettingsCard.tsx +6 -27
- package/src/client/api.ts +11 -1
- package/src/client/conversation-sync.ts +14 -0
- package/src/client/image-toolview.tsx +176 -165
- package/src/client/index.ts +25 -15
- package/src/client/locales.ts +746 -602
- package/src/client/mount.tsx +213 -124
- package/src/client/panel.module.css +2619 -1563
- package/src/client/sidebar-entry.ts +190 -144
- package/src/edit-image-command.ts +110 -0
- package/src/engine.ts +47 -5
- package/src/gallery-store.ts +20 -0
- package/src/generation-runtime.ts +11 -2
- package/src/history-store.ts +26 -0
- package/src/image-models.ts +1 -1
- package/src/index.ts +31 -12
- package/src/model-catalog.ts +19 -2
- package/src/presets.ts +11 -3
- package/src/prompt-enhancer.ts +63 -5
- package/src/protocol.ts +59 -5
- package/src/routes.ts +62 -4
- package/src/task-queue.ts +42 -32
- package/docs/images/agent-chat-edit.png +0 -0
- package/docs/images/agent-chat-generate.png +0 -0
- package/docs/images/agent-chat-poster-workflow.png +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dickpy/dsh-imagegen",
|
|
3
|
-
"description": "AI image generation plugin for the dsh web GUI: text-to-image and image-to-image through configurable provider channels (gpt-image-2 / grok-imagine-image / nanobanana series / seedream-5.0-pro / dall-e-3, with native xAI Grok Imagine, Google Nano Banana and ByteDance Seedream request shaping), with per-channel model catalogs and a
|
|
4
|
-
"version": "1.
|
|
3
|
+
"description": "AI image generation plugin for the dsh web GUI: text-to-image and image-to-image through configurable provider channels (gpt-image-2 / grok-imagine-image / nanobanana series / seedream-5.0-pro / dall-e-3, with native xAI Grok Imagine, Google Nano Banana and ByteDance Seedream request shaping), with per-channel model catalogs and a New Session / Image Generation tab entry opening a three-column studio beside the native conversation.",
|
|
4
|
+
"version": "1.5.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"inject": [
|
|
18
18
|
"@deepseek-ai/dsh-client-runtime",
|
|
19
19
|
"@deepseek-ai/dsh-client-connection",
|
|
20
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
20
21
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
21
22
|
"@deepseek-ai/dsh-client-ui-tool"
|
|
22
23
|
],
|
|
@@ -32,9 +33,11 @@
|
|
|
32
33
|
"@deepseek-ai/dsh-client-connection": "0.1.0-rc.7",
|
|
33
34
|
"@deepseek-ai/dsh-client-locale": "0.1.0-rc.7",
|
|
34
35
|
"@deepseek-ai/dsh-client-runtime": "0.1.0-rc.7",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.7",
|
|
35
37
|
"@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.7",
|
|
36
38
|
"@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.7",
|
|
37
39
|
"@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.7",
|
|
40
|
+
"@deepseek-ai/dsh-commands": "0.1.0-rc.7",
|
|
38
41
|
"@deepseek-ai/dsh-host-webserver": "0.1.0-rc.7",
|
|
39
42
|
"@deepseek-ai/dsh-llm": "0.1.0-rc.7",
|
|
40
43
|
"@deepseek-ai/dsh-settings": "0.1.0-rc.7",
|
package/src/agent-image-tools.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { Context } from '@deepseek-ai/cordis'
|
|
4
4
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
5
5
|
import type { ToolResult, ToolResultView } from '@deepseek-ai/dsh-tools'
|
|
6
|
-
import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
|
|
6
|
+
import type { AttachmentStore, ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
|
|
7
7
|
import type {} from '@deepseek-ai/dsh-attachment'
|
|
8
8
|
import type {} from '@deepseek-ai/dsh-tools'
|
|
9
9
|
import { ImageGenError } from './engine.ts'
|
|
@@ -63,6 +63,132 @@ const taskResultSchema = {
|
|
|
63
63
|
/** Agent calls stay pending until the provider and history write settle. */
|
|
64
64
|
const AGENT_GENERATION_TIMEOUT_MS = 300_000
|
|
65
65
|
|
|
66
|
+
export function ensureAgentImageConfigured(config: AgentImageToolConfig): void {
|
|
67
|
+
if (!config.enabled) throw new ImageGenError('AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.', 'plugin-disabled')
|
|
68
|
+
if (!config.allowAgentImageGeneration) throw new ImageGenError('Agent image generation is disabled in Settings > Plugins > AI Image.', 'agent-generation-disabled')
|
|
69
|
+
const usable = config.channels.some(channel => channel.apiUrl.trim() !== '' && channel.apiKey.trim() !== '')
|
|
70
|
+
if (!usable) throw new ImageGenError('Image API credentials are not configured. Open Settings > Plugins > AI Image, add a channel and fill in its API URL and API key.', 'image-api-not-configured')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Resolve a configured image alias and its owning channel. */
|
|
74
|
+
export function resolveAgentImageModel(config: AgentImageToolConfig, requested: unknown): { channel: RuntimeChannel; alias: string; upstream: string } {
|
|
75
|
+
const entries = config.channels.flatMap(channel => channel.models.map(model => ({ channel, alias: model.alias, upstream: model.id })))
|
|
76
|
+
if (entries.length === 0) {
|
|
77
|
+
throw new ImageGenError('No image models are configured. Open Settings > Plugins > AI Image and add a channel with at least one model.', 'no-models-configured')
|
|
78
|
+
}
|
|
79
|
+
const wanted = typeof requested === 'string' && requested.trim() !== '' ? requested.trim() : ''
|
|
80
|
+
if (wanted === '') {
|
|
81
|
+
if (entries.length === 1) return entries[0]!
|
|
82
|
+
const options = config.channels.flatMap(channel => channel.models.map(model => `"${channel.name} · ${model.alias}"`)).join(', ')
|
|
83
|
+
throw new ImageGenError(`Multiple image models are available — ask the user which channel and model to use, then call this tool again with that exact model name. Options: ${options}.`, 'model-choice-required')
|
|
84
|
+
}
|
|
85
|
+
const hosting = entries.filter(entry => entry.alias === wanted)
|
|
86
|
+
if (hosting.length === 0) {
|
|
87
|
+
const available = [...new Set(entries.map(entry => entry.alias))].join(', ')
|
|
88
|
+
throw new ImageGenError(`Image model "${wanted}" is not configured in any channel. Choose one of: ${available}.`, 'image-model-not-configured')
|
|
89
|
+
}
|
|
90
|
+
const preferred = hosting.find(entry => entry.channel.id === config.defaultChannelId)
|
|
91
|
+
return preferred ?? hosting[0]!
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function findAgentImageTask(runtime: ImageGenerationRuntime, id: string): GenerationTask {
|
|
95
|
+
const task = runtime.queue.list().find(candidate => candidate.id === id)
|
|
96
|
+
if (task === undefined) throw new ImageGenError(`Image generation task ${id} was not found.`, 'task-not-found')
|
|
97
|
+
return task
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Wait for a queue task without sending anything through the chat model. */
|
|
101
|
+
export function waitForAgentImageTask(runtime: ImageGenerationRuntime, id: string, signal: AbortSignal | undefined): Promise<GenerationTask> {
|
|
102
|
+
return new Promise<GenerationTask>((resolveTask, rejectTask) => {
|
|
103
|
+
let settled = false
|
|
104
|
+
let dispose = (): void => {}
|
|
105
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
106
|
+
let abort = (): void => {}
|
|
107
|
+
|
|
108
|
+
const cleanup = (): void => {
|
|
109
|
+
dispose()
|
|
110
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
111
|
+
signal?.removeEventListener('abort', abort)
|
|
112
|
+
}
|
|
113
|
+
const resolve = (task: GenerationTask): void => {
|
|
114
|
+
if (settled) return
|
|
115
|
+
settled = true
|
|
116
|
+
cleanup()
|
|
117
|
+
resolveTask(task)
|
|
118
|
+
}
|
|
119
|
+
const reject = (error: unknown): void => {
|
|
120
|
+
if (settled) return
|
|
121
|
+
settled = true
|
|
122
|
+
cleanup()
|
|
123
|
+
rejectTask(error)
|
|
124
|
+
}
|
|
125
|
+
abort = (): void => {
|
|
126
|
+
if (settled) return
|
|
127
|
+
const reason = signal?.reason instanceof Error ? signal.reason : new Error('Image generation was cancelled.')
|
|
128
|
+
settled = true
|
|
129
|
+
cleanup()
|
|
130
|
+
runtime.queue.cancel(id)
|
|
131
|
+
rejectTask(reason)
|
|
132
|
+
}
|
|
133
|
+
const onChange = (updated: GenerationTask): void => {
|
|
134
|
+
if (updated.id === id && isFinalTask(updated)) resolve(updated)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (signal?.aborted === true) {
|
|
138
|
+
abort()
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
dispose = runtime.queue.subscribe(onChange)
|
|
142
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
143
|
+
timer = setTimeout(() => {
|
|
144
|
+
if (settled) return
|
|
145
|
+
const timeout = new ImageGenError(`Image generation task ${id} timed out after ${AGENT_GENERATION_TIMEOUT_MS / 1000} seconds.`, 'generation-timeout')
|
|
146
|
+
settled = true
|
|
147
|
+
cleanup()
|
|
148
|
+
runtime.queue.cancel(id)
|
|
149
|
+
rejectTask(timeout)
|
|
150
|
+
}, AGENT_GENERATION_TIMEOUT_MS)
|
|
151
|
+
let current: GenerationTask
|
|
152
|
+
try {
|
|
153
|
+
current = findAgentImageTask(runtime, id)
|
|
154
|
+
} catch (error) {
|
|
155
|
+
reject(error)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
if (isFinalTask(current)) resolve(current)
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Submit the same image-edit request used by the Agent tool. */
|
|
163
|
+
export async function submitAgentImageEdit(
|
|
164
|
+
attachments: Pick<AttachmentStore, 'readImage'>,
|
|
165
|
+
runtime: ImageGenerationRuntime,
|
|
166
|
+
resolve: () => AgentImageToolConfig,
|
|
167
|
+
input: { prompt: string; sourceImage: ImageAttachmentRef; signal?: AbortSignal },
|
|
168
|
+
): Promise<GenerationTask> {
|
|
169
|
+
const config = resolve()
|
|
170
|
+
ensureAgentImageConfigured(config)
|
|
171
|
+
const reference = await attachments.readImage(input.sourceImage, input.signal)
|
|
172
|
+
const defaultChannel = config.channels.find(channel => channel.id === config.defaultChannelId) ?? config.channels[0]
|
|
173
|
+
const defaultModel = defaultChannel?.models[0]?.alias ?? config.channels.flatMap(channel => channel.models)[0]?.alias
|
|
174
|
+
const picked = resolveAgentImageModel(config, defaultModel)
|
|
175
|
+
const task = runtime.queue.submit({
|
|
176
|
+
mode: 'edit',
|
|
177
|
+
model: picked.alias,
|
|
178
|
+
upstream: picked.upstream,
|
|
179
|
+
channelId: picked.channel.id,
|
|
180
|
+
channel: picked.channel.name,
|
|
181
|
+
prompt: input.prompt.trim(),
|
|
182
|
+
size: 'auto',
|
|
183
|
+
quality: 'auto',
|
|
184
|
+
n: 1,
|
|
185
|
+
detail: '',
|
|
186
|
+
image: imageDataUrl(reference),
|
|
187
|
+
...reference.ref.name === undefined ? {} : { refName: reference.ref.name },
|
|
188
|
+
})
|
|
189
|
+
return waitForAgentImageTask(runtime, task.id, input.signal)
|
|
190
|
+
}
|
|
191
|
+
|
|
66
192
|
function acceptedMediaType(value: string): value is ImageMediaType {
|
|
67
193
|
return value === 'image/png' || value === 'image/jpeg' || value === 'image/webp' || value === 'image/gif'
|
|
68
194
|
}
|
|
@@ -158,42 +284,8 @@ function presentImageResult(_args: unknown, result: ToolResult): ToolResultView
|
|
|
158
284
|
/** Register the global Agent tools and unregister them with the plugin lifecycle. */
|
|
159
285
|
export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRuntime, resolve: () => AgentImageToolConfig): () => void {
|
|
160
286
|
const attachmentRefs = new Map<string, Promise<AgentImageRef[]>>()
|
|
161
|
-
const ensureConfigured = (): void => {
|
|
162
|
-
|
|
163
|
-
if (!config.enabled) throw new ImageGenError('AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.', 'plugin-disabled')
|
|
164
|
-
if (!config.allowAgentImageGeneration) throw new ImageGenError('Agent image generation is disabled in Settings > Plugins > AI Image.', 'agent-generation-disabled')
|
|
165
|
-
const usable = config.channels.some(channel => channel.apiUrl.trim() !== '' && channel.apiKey.trim() !== '')
|
|
166
|
-
if (!usable) throw new ImageGenError('Image API credentials are not configured. Open Settings > Plugins > AI Image, add a channel and fill in its API URL and API key.', 'image-api-not-configured')
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Resolve the requested model alias onto a channel. Rules:
|
|
171
|
-
* - a named alias must exist in some channel's catalog (several channels
|
|
172
|
-
* may host it; the default channel wins);
|
|
173
|
-
* - with no alias, a single configured model is used directly, while
|
|
174
|
-
* multiple models require the Agent to ask the user first.
|
|
175
|
-
* @returns the channel plus the alias and its upstream id.
|
|
176
|
-
*/
|
|
177
|
-
const resolveModel = (requested: unknown): { channel: RuntimeChannel; alias: string; upstream: string } => {
|
|
178
|
-
const config = resolve()
|
|
179
|
-
const entries = config.channels.flatMap(channel => channel.models.map(model => ({ channel, alias: model.alias, upstream: model.id })))
|
|
180
|
-
if (entries.length === 0) {
|
|
181
|
-
throw new ImageGenError('No image models are configured. Open Settings > Plugins > AI Image and add a channel with at least one model.', 'no-models-configured')
|
|
182
|
-
}
|
|
183
|
-
const wanted = typeof requested === 'string' && requested.trim() !== '' ? requested.trim() : ''
|
|
184
|
-
if (wanted === '') {
|
|
185
|
-
if (entries.length === 1) return entries[0]!
|
|
186
|
-
const options = config.channels.flatMap(channel => channel.models.map(model => `"${channel.name} · ${model.alias}"`)).join(', ')
|
|
187
|
-
throw new ImageGenError(`Multiple image models are available — ask the user which channel and model to use, then call this tool again with that exact model name. Options: ${options}.`, 'model-choice-required')
|
|
188
|
-
}
|
|
189
|
-
const hosting = entries.filter(entry => entry.alias === wanted)
|
|
190
|
-
if (hosting.length === 0) {
|
|
191
|
-
const available = [...new Set(entries.map(entry => entry.alias))].join(', ')
|
|
192
|
-
throw new ImageGenError(`Image model "${wanted}" is not configured in any channel. Choose one of: ${available}.`, 'image-model-not-configured')
|
|
193
|
-
}
|
|
194
|
-
const preferred = hosting.find(entry => entry.channel.id === config.defaultChannelId)
|
|
195
|
-
return preferred ?? hosting[0]!
|
|
196
|
-
}
|
|
287
|
+
const ensureConfigured = (): void => { ensureAgentImageConfigured(resolve()) }
|
|
288
|
+
const resolveModel = (requested: unknown): { channel: RuntimeChannel; alias: string; upstream: string } => resolveAgentImageModel(resolve(), requested)
|
|
197
289
|
const materializeTaskImages = (task: GenerationTask): Promise<AgentImageRef[]> => {
|
|
198
290
|
if (task.status !== 'completed') return Promise.resolve([])
|
|
199
291
|
const existing = attachmentRefs.get(task.id)
|
|
@@ -222,71 +314,8 @@ export function registerAgentImageTools(ctx: Context, runtime: ImageGenerationRu
|
|
|
222
314
|
images,
|
|
223
315
|
}
|
|
224
316
|
}
|
|
225
|
-
const findTask = (id: string): GenerationTask =>
|
|
226
|
-
|
|
227
|
-
if (task === undefined) throw new ImageGenError(`Image generation task ${id} was not found.`, 'task-not-found')
|
|
228
|
-
return task
|
|
229
|
-
}
|
|
230
|
-
const waitForTask = (id: string, signal: AbortSignal | undefined): Promise<GenerationTask> => new Promise((resolveTask, rejectTask) => {
|
|
231
|
-
let settled = false
|
|
232
|
-
let dispose = (): void => {}
|
|
233
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
234
|
-
let abort = (): void => {}
|
|
235
|
-
|
|
236
|
-
const cleanup = (): void => {
|
|
237
|
-
dispose()
|
|
238
|
-
if (timer !== undefined) clearTimeout(timer)
|
|
239
|
-
signal?.removeEventListener('abort', abort)
|
|
240
|
-
}
|
|
241
|
-
const resolve = (task: GenerationTask): void => {
|
|
242
|
-
if (settled) return
|
|
243
|
-
settled = true
|
|
244
|
-
cleanup()
|
|
245
|
-
resolveTask(task)
|
|
246
|
-
}
|
|
247
|
-
const reject = (error: unknown): void => {
|
|
248
|
-
if (settled) return
|
|
249
|
-
settled = true
|
|
250
|
-
cleanup()
|
|
251
|
-
rejectTask(error)
|
|
252
|
-
}
|
|
253
|
-
abort = (): void => {
|
|
254
|
-
if (settled) return
|
|
255
|
-
const reason = signal?.reason instanceof Error ? signal.reason : new Error('Image generation was cancelled.')
|
|
256
|
-
// Remove the listener before publishing cancellation so the queue event
|
|
257
|
-
// cannot turn an execution abort into a successful cancelled result.
|
|
258
|
-
settled = true
|
|
259
|
-
cleanup()
|
|
260
|
-
runtime.queue.cancel(id)
|
|
261
|
-
rejectTask(reason)
|
|
262
|
-
}
|
|
263
|
-
const onChange = (updated: GenerationTask): void => {
|
|
264
|
-
if (updated.id === id && isFinalTask(updated)) resolve(updated)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (signal?.aborted === true) {
|
|
268
|
-
abort()
|
|
269
|
-
return
|
|
270
|
-
}
|
|
271
|
-
dispose = runtime.queue.subscribe(onChange)
|
|
272
|
-
signal?.addEventListener('abort', abort, { once: true })
|
|
273
|
-
timer = setTimeout(() => {
|
|
274
|
-
if (settled) return
|
|
275
|
-
const timeout = new ImageGenError(`Image generation task ${id} timed out after ${AGENT_GENERATION_TIMEOUT_MS / 1000} seconds.`, 'generation-timeout')
|
|
276
|
-
settled = true
|
|
277
|
-
cleanup()
|
|
278
|
-
runtime.queue.cancel(id)
|
|
279
|
-
rejectTask(timeout)
|
|
280
|
-
}, AGENT_GENERATION_TIMEOUT_MS)
|
|
281
|
-
let current: GenerationTask
|
|
282
|
-
try {
|
|
283
|
-
current = findTask(id)
|
|
284
|
-
} catch (error) {
|
|
285
|
-
reject(error)
|
|
286
|
-
return
|
|
287
|
-
}
|
|
288
|
-
if (isFinalTask(current)) resolve(current)
|
|
289
|
-
})
|
|
317
|
+
const findTask = (id: string): GenerationTask => findAgentImageTask(runtime, id)
|
|
318
|
+
const waitForTask = (id: string, signal: AbortSignal | undefined): Promise<GenerationTask> => waitForAgentImageTask(runtime, id, signal)
|
|
290
319
|
const disposers = [
|
|
291
320
|
ctx.tools.register(defineTool({
|
|
292
321
|
name: 'generate_image',
|