@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/src/history-store.ts
CHANGED
|
@@ -49,6 +49,13 @@ interface StoredEntry {
|
|
|
49
49
|
refName?: string
|
|
50
50
|
channelId?: string
|
|
51
51
|
channel?: string
|
|
52
|
+
comparisonId?: string
|
|
53
|
+
comparisonModels?: string[]
|
|
54
|
+
workflow?: 'ecommerce'
|
|
55
|
+
projectId?: string
|
|
56
|
+
projectName?: string
|
|
57
|
+
slotKey?: string
|
|
58
|
+
slotLabel?: string
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
/** The index.json shape. */
|
|
@@ -119,6 +126,11 @@ function isStoredEntry(value: unknown): value is StoredEntry {
|
|
|
119
126
|
return typeof entry.id === 'string'
|
|
120
127
|
&& typeof entry.createdAt === 'number'
|
|
121
128
|
&& (entry.mode === 'text' || entry.mode === 'edit')
|
|
129
|
+
&& (entry.workflow === undefined || entry.workflow === 'ecommerce')
|
|
130
|
+
&& (entry.projectId === undefined || typeof entry.projectId === 'string')
|
|
131
|
+
&& (entry.projectName === undefined || typeof entry.projectName === 'string')
|
|
132
|
+
&& (entry.slotKey === undefined || typeof entry.slotKey === 'string')
|
|
133
|
+
&& (entry.slotLabel === undefined || typeof entry.slotLabel === 'string')
|
|
122
134
|
&& typeof entry.prompt === 'string'
|
|
123
135
|
&& Array.isArray(entry.images)
|
|
124
136
|
&& entry.images.every(image => {
|
|
@@ -155,6 +167,13 @@ function toWire(entry: StoredEntry): HistoryEntry {
|
|
|
155
167
|
...entry.refName === undefined ? {} : { refName: entry.refName },
|
|
156
168
|
...entry.channel === undefined ? {} : { channel: entry.channel },
|
|
157
169
|
...entry.channelId === undefined ? {} : { channelId: entry.channelId },
|
|
170
|
+
...entry.comparisonId === undefined ? {} : { comparisonId: entry.comparisonId },
|
|
171
|
+
...entry.comparisonModels === undefined ? {} : { comparisonModels: entry.comparisonModels },
|
|
172
|
+
...entry.workflow === undefined ? {} : { workflow: entry.workflow },
|
|
173
|
+
...entry.projectId === undefined ? {} : { projectId: entry.projectId },
|
|
174
|
+
...entry.projectName === undefined ? {} : { projectName: entry.projectName },
|
|
175
|
+
...entry.slotKey === undefined ? {} : { slotKey: entry.slotKey },
|
|
176
|
+
...entry.slotLabel === undefined ? {} : { slotLabel: entry.slotLabel },
|
|
158
177
|
}
|
|
159
178
|
}
|
|
160
179
|
|
|
@@ -199,6 +218,13 @@ export async function appendHistory(input: HistoryEntryInput): Promise<HistoryEn
|
|
|
199
218
|
...input.refName === undefined ? {} : { refName: input.refName },
|
|
200
219
|
...input.channelId === undefined ? {} : { channelId: input.channelId },
|
|
201
220
|
...input.channel === undefined ? {} : { channel: input.channel },
|
|
221
|
+
...input.comparisonId === undefined ? {} : { comparisonId: input.comparisonId },
|
|
222
|
+
...input.comparisonModels === undefined ? {} : { comparisonModels: input.comparisonModels },
|
|
223
|
+
...input.workflow === undefined ? {} : { workflow: input.workflow },
|
|
224
|
+
...input.projectId === undefined ? {} : { projectId: input.projectId },
|
|
225
|
+
...input.projectName === undefined ? {} : { projectName: input.projectName },
|
|
226
|
+
...input.slotKey === undefined ? {} : { slotKey: input.slotKey },
|
|
227
|
+
...input.slotLabel === undefined ? {} : { slotLabel: input.slotLabel },
|
|
202
228
|
}
|
|
203
229
|
const merged = [entry, ...await readIndex()]
|
|
204
230
|
const kept = merged.slice(0, HISTORY_MAX)
|
package/src/image-models.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* allow-list because OpenAI-compatible gateways rarely advertise modalities.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export const DEFAULT_IMAGE_MODELS = ['gpt-image-2', 'grok-imagine-image', 'nanobanana2', 'nanobanana2-lite', 'nanobanana-pro', 'seedream-5.0-pro'] as const
|
|
7
|
+
export const DEFAULT_IMAGE_MODELS = ['gpt-image-2', 'grok-imagine-image', 'nanobanana2', 'nanobanana2-lite', 'nanobanana-pro', 'seedream-5.0-pro', 'glm-image'] as const
|
|
8
8
|
|
|
9
9
|
/** Normalize user-entered model identifiers and retain a usable legacy default. */
|
|
10
10
|
export function normalizeImageModels(value: unknown): string[] {
|
package/src/index.ts
CHANGED
|
@@ -14,19 +14,22 @@ import z from 'schemastery'
|
|
|
14
14
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
15
15
|
// Type-only: pulls the systemPrompt Context merge (announcement section).
|
|
16
16
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
17
|
+
// Type-only: pulls the human slash-command registry Context merge.
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-commands'
|
|
17
19
|
import type {} from '@deepseek-ai/dsh-tools'
|
|
18
|
-
import type {} from '@deepseek-ai/dsh-attachment'
|
|
20
|
+
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
|
19
21
|
import { IMAGEGEN_SETTINGS_NAMESPACE, type ChannelConfig, type ModelMapping } from './protocol.ts'
|
|
20
22
|
import { makeRoutes, type SettingsSeam } from './routes.ts'
|
|
21
23
|
import { ImageGenerationRuntime, type ChannelsView, type RuntimeChannel } from './generation-runtime.ts'
|
|
22
24
|
import { registerAgentImageTools } from './agent-image-tools.ts'
|
|
25
|
+
import { registerEditImageCommand } from './edit-image-command.ts'
|
|
23
26
|
import { presetById } from './presets.ts'
|
|
24
27
|
|
|
25
28
|
/** Stable cordis plugin name. */
|
|
26
29
|
export const name = 'imagegen'
|
|
27
30
|
|
|
28
31
|
/** Services required before the surfaces can mount. */
|
|
29
|
-
export const inject = ['webServer', 'systemPrompt']
|
|
32
|
+
export const inject = ['webServer', 'systemPrompt', 'commands']
|
|
30
33
|
|
|
31
34
|
// Internals re-exported for smoke tests and host-side debugging; the plugin
|
|
32
35
|
// contract only requires name / inject / Config / apply.
|
|
@@ -34,6 +37,7 @@ export { makeRoutes } from './routes.ts'
|
|
|
34
37
|
export { generateImage, ImageGenError } from './engine.ts'
|
|
35
38
|
export { ImageGenerationRuntime } from './generation-runtime.ts'
|
|
36
39
|
export { registerAgentImageTools } from './agent-image-tools.ts'
|
|
40
|
+
export { latestSessionImage, registerEditImageCommand } from './edit-image-command.ts'
|
|
37
41
|
export { appendGallery, clearGallery, listGallery, readGalleryImage, removeGallery, updateGalleryTags } from './gallery-store.ts'
|
|
38
42
|
export { listTemplates, readTemplateImage, refreshTemplates, clearTemplateMemo } from './templates-store.ts'
|
|
39
43
|
export { checkForUpdate, clearUpdateCache, compareVersions, CURRENT_VERSION, installUpdate, profileFromProcess } from './updater.ts'
|
|
@@ -111,7 +115,7 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true
|
|
|
111
115
|
const SECTION_ORDER = 150
|
|
112
116
|
|
|
113
117
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
114
|
-
export const IMAGEGEN_GUIDANCE = '本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:通过「渠道」对接 OpenAI 兼容图像生成 API(每个渠道 = 一个 API 端点 + 各自的模型目录),支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送,nanobanana 系列按 aspect_ratio / image_size 参数协议发送;seedream 系列统一走 /images/generations,参考图以 JSON image
|
|
118
|
+
export const IMAGEGEN_GUIDANCE = '本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:通过「渠道」对接 OpenAI 兼容图像生成 API(每个渠道 = 一个 API 端点 + 各自的模型目录),支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送,nanobanana 系列按 aspect_ratio / image_size 参数协议发送;seedream 系列统一走 /images/generations,参考图以 JSON image 数组发送;智谱 `glm-image` 使用官方 `/api/paas/v4/images/generations`,当前仅支持文生图)。API 地址与密钥在 GUI 设置中按渠道配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用用户在各渠道配置目录中的模型;检测模型时会过滤聊天、Embedding 等非图片模型,但模型出现在 /models 中仍不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选与复用。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片显示在工具调用对应的左侧结果区域,模型收到状态和附件引用,不会额外伪造用户消息。用户也可以使用 `/edit_image <修改描述>`,命令会直接读取当前对话最近图片并调用插件图片模型,不经过对话模型的图片能力检查。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问 vibeui.top。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。'
|
|
115
119
|
|
|
116
120
|
/** Append the live channel × model table so an Agent can honor user choices. */
|
|
117
121
|
function guidanceFor(channels: RuntimeChannel[], defaultChannelId: string): string {
|
|
@@ -235,6 +239,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
235
239
|
// entry points; Agent tools wait for their task result by default and render
|
|
236
240
|
// images in the tool result instead of injecting a synthetic user message.
|
|
237
241
|
const runtime = new ImageGenerationRuntime(channelsView)
|
|
242
|
+
const pendingConversationImages = new Map<string, ImageAttachmentRef>()
|
|
238
243
|
|
|
239
244
|
// The route family mounts once, gated on the settings seam (the bridge
|
|
240
245
|
// serves it; without the seam there is nothing to expose). Route handlers
|
|
@@ -267,6 +272,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
267
272
|
return [...new Set(value.channels.flatMap(channel => channel.models.map(model => model.alias)))]
|
|
268
273
|
},
|
|
269
274
|
attachments: sctx.attachments,
|
|
275
|
+
pendingConversationImages,
|
|
270
276
|
runtime,
|
|
271
277
|
})
|
|
272
278
|
const disposers = routes.map(route => ctx.webServer.register(route))
|
|
@@ -276,16 +282,29 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
276
282
|
)
|
|
277
283
|
})
|
|
278
284
|
|
|
279
|
-
ctx.inject(['tools', 'attachments'], (tctx) => {
|
|
280
|
-
tctx.effect(() =>
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
ctx.inject(['tools', 'attachments', 'commands'], (tctx) => {
|
|
286
|
+
tctx.effect(() => {
|
|
287
|
+
const resolveAgentConfig = () => {
|
|
288
|
+
const value = resolve()
|
|
289
|
+
return {
|
|
290
|
+
enabled: value.enabled,
|
|
291
|
+
allowAgentImageGeneration: value.allowAgentImageGeneration,
|
|
292
|
+
channels: value.channels,
|
|
293
|
+
defaultChannelId: value.defaultChannelId,
|
|
294
|
+
}
|
|
287
295
|
}
|
|
288
|
-
|
|
296
|
+
const disposeTools = registerAgentImageTools(tctx, runtime, resolveAgentConfig)
|
|
297
|
+
const disposeCommand = registerEditImageCommand(tctx, runtime, resolveAgentConfig, {
|
|
298
|
+
get: sessionId => pendingConversationImages.get(sessionId),
|
|
299
|
+
consume: (sessionId, ref) => {
|
|
300
|
+
if (pendingConversationImages.get(sessionId)?.attachmentId === ref.attachmentId) pendingConversationImages.delete(sessionId)
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
return () => {
|
|
304
|
+
disposeCommand()
|
|
305
|
+
disposeTools()
|
|
306
|
+
}
|
|
307
|
+
}, 'dsh-imagegen: agent image tools and commands')
|
|
289
308
|
})
|
|
290
309
|
|
|
291
310
|
// System-prompt announcement (toggled by settings changes).
|
package/src/model-catalog.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Framework-free (pure data + regex), safe for the client bundle to inline.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export type ModelFamily = 'gpt-image' | 'dall-e' | 'grok' | 'nanobanana' | 'seedream' | 'unknown'
|
|
11
|
+
export type ModelFamily = 'gpt-image' | 'dall-e' | 'grok' | 'nanobanana' | 'seedream' | 'zhipu' | 'unknown'
|
|
12
12
|
|
|
13
13
|
/** Capability/identity annotation for one model id. */
|
|
14
14
|
export interface ModelCatalogEntry {
|
|
@@ -69,6 +69,14 @@ const ENTRIES: Record<Exclude<ModelFamily, 'unknown'>, Omit<ModelCatalogEntry, '
|
|
|
69
69
|
supportsAspectRatio: true,
|
|
70
70
|
qualityTiers: ['1K', '2K'],
|
|
71
71
|
},
|
|
72
|
+
zhipu: {
|
|
73
|
+
label: 'GLM-Image',
|
|
74
|
+
labelZh: '智谱图像',
|
|
75
|
+
known: true,
|
|
76
|
+
supportsEdit: false,
|
|
77
|
+
supportsAspectRatio: false,
|
|
78
|
+
qualityTiers: ['HD'],
|
|
79
|
+
},
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
/** Official Gemini image ids served by Nano Banana gateways. */
|
|
@@ -89,10 +97,19 @@ export function describeModel(model: string): ModelCatalogEntry {
|
|
|
89
97
|
if (/^grok-imagine(?:-|$)/.test(id)) return { family: 'grok', ...ENTRIES.grok }
|
|
90
98
|
if (/^nanobanana/i.test(id) || NANOBANANA_GEMINI_IDS.has(id)) return { family: 'nanobanana', ...ENTRIES.nanobanana }
|
|
91
99
|
if (/^(?:doubao-)?seedream/i.test(id)) return { family: 'seedream', ...ENTRIES.seedream }
|
|
100
|
+
if (/^(?:glm-image|cogview(?:-|$))/i.test(id)) return { family: 'zhipu', ...ENTRIES.zhipu }
|
|
92
101
|
return { family: 'unknown', label: 'unknown', labelZh: '未知协议', known: false, supportsEdit: true, supportsAspectRatio: false, qualityTiers: [] }
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
/** Conservative fallback for providers whose /models response only has ids.
|
|
105
|
+
* Metadata-aware filtering lives in prompt-enhancer.ts; this catches common
|
|
106
|
+
* image model naming conventions without treating every unknown model as an
|
|
107
|
+
* image model. */
|
|
108
|
+
export function isLikelyImageModelId(model: string): boolean {
|
|
109
|
+
return /(?:^|[-_.])(?:image|img|diffusion|flux|cogview|imagen|seedream|nanobanana|grok-imagine|dall-e|stable-diffusion|sdxl|pixart|kolors|ideogram|midjourney|recraft|hunyuan|jimeng|wanx|hidream|playground)(?:$|[-_.])/i.test(model.trim())
|
|
110
|
+
}
|
|
111
|
+
|
|
95
112
|
/** The family a model id routes its request through. */
|
|
96
113
|
export function modelFamily(model: string): ModelFamily {
|
|
97
114
|
return describeModel(model).family
|
|
98
|
-
}
|
|
115
|
+
}
|
package/src/presets.ts
CHANGED
|
@@ -40,10 +40,18 @@ export const IMAGE_PRESETS: PresetProvider[] = [
|
|
|
40
40
|
id: 'openai-official',
|
|
41
41
|
name: 'OpenAI 官方',
|
|
42
42
|
apiUrl: 'https://api.openai.com/v1',
|
|
43
|
-
hint: 'OpenAI
|
|
43
|
+
hint: 'OpenAI 官方图像生成接口',
|
|
44
44
|
models: [
|
|
45
45
|
{ alias: 'gpt-image-2', id: 'gpt-image-2' },
|
|
46
|
-
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'zhipu-official',
|
|
50
|
+
name: '智谱 AI 官方',
|
|
51
|
+
apiUrl: 'https://open.bigmodel.cn/api/paas/v4',
|
|
52
|
+
hint: '智谱官方 GLM-Image 图像生成接口',
|
|
53
|
+
models: [
|
|
54
|
+
{ alias: 'glm-image', id: 'glm-image' },
|
|
47
55
|
],
|
|
48
56
|
},
|
|
49
57
|
{
|
|
@@ -60,4 +68,4 @@ export const IMAGE_PRESETS: PresetProvider[] = [
|
|
|
60
68
|
/** Look up one built-in provider by id. */
|
|
61
69
|
export function presetById(id: string): PresetProvider | undefined {
|
|
62
70
|
return IMAGE_PRESETS.find(preset => preset.id === id)
|
|
63
|
-
}
|
|
71
|
+
}
|
package/src/prompt-enhancer.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** OpenAI-compatible chat helpers used by the optional prompt-enhancement UI. */
|
|
2
2
|
|
|
3
|
+
import { isLikelyImageModelId } from './model-catalog.ts'
|
|
4
|
+
|
|
3
5
|
export interface PromptModelConfig {
|
|
4
6
|
apiUrl: string
|
|
5
7
|
apiKey: string
|
|
@@ -34,15 +36,71 @@ async function responseJson(response: Response): Promise<Record<string, unknown>
|
|
|
34
36
|
return body as Record<string, unknown>
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
type ModelRecord = Record<string, unknown> & { id: string }
|
|
40
|
+
|
|
41
|
+
async function listModelRecords(config: ModelListConfig): Promise<ModelRecord[]> {
|
|
39
42
|
if (config.apiUrl.trim() === '') throw new Error('API URL is required')
|
|
40
43
|
const response = await fetch(endpoint(config.apiUrl, '/models'), { headers: headers(config.apiKey) })
|
|
41
44
|
const body = await responseJson(response)
|
|
42
45
|
const data = Array.isArray(body.data) ? body.data : []
|
|
43
|
-
return
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
+
return data.flatMap(item => {
|
|
47
|
+
if (item === null || typeof item !== 'object' || typeof (item as { id?: unknown }).id !== 'string') return []
|
|
48
|
+
const id = (item as { id: string }).id.trim()
|
|
49
|
+
return id === '' ? [] : [{ ...(item as Record<string, unknown>), id }]
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function textOf(value: unknown): string[] {
|
|
54
|
+
if (typeof value === 'string') return [value]
|
|
55
|
+
if (!Array.isArray(value)) return []
|
|
56
|
+
return value.filter((item): item is string => typeof item === 'string')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hasImageGenerationCapability(record: ModelRecord): boolean | undefined {
|
|
60
|
+
const capability = record.capabilities
|
|
61
|
+
if (capability !== null && typeof capability === 'object') {
|
|
62
|
+
const values = capability as Record<string, unknown>
|
|
63
|
+
for (const key of ['image_generation', 'imageGeneration', 'text_to_image', 'textToImage', 'image_gen']) {
|
|
64
|
+
if (typeof values[key] === 'boolean') return values[key]
|
|
65
|
+
}
|
|
66
|
+
const serialized = JSON.stringify(values).toLowerCase()
|
|
67
|
+
if (/image[ _-]?generation|text[ _-]?to[ _-]?image/.test(serialized)) return true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const taskText = [
|
|
71
|
+
...textOf(record.task),
|
|
72
|
+
...textOf(record.task_type),
|
|
73
|
+
...textOf(record.taskType),
|
|
74
|
+
...textOf(record.type),
|
|
75
|
+
...textOf(record.model_type),
|
|
76
|
+
...textOf(record.modelType),
|
|
77
|
+
...textOf(record.tasks),
|
|
78
|
+
...textOf(record.description),
|
|
79
|
+
].join(' ').toLowerCase()
|
|
80
|
+
if (/image[ _-]?generation|text[ _-]?to[ _-]?image|image[ _-]?gen/.test(taskText)) return true
|
|
81
|
+
if (/^image(?:[ _-]?generation)?$/.test(taskText.trim())) return true
|
|
82
|
+
if (/embedding|rerank|moderation|transcri|speech|audio|video|chat[ _-]?completion/.test(taskText)) return false
|
|
83
|
+
|
|
84
|
+
for (const key of ['output_modalities', 'outputModalities', 'supported_output_modalities']) {
|
|
85
|
+
const modalities = textOf(record[key]).map(value => value.toLowerCase())
|
|
86
|
+
if (modalities.length > 0) return modalities.includes('image')
|
|
87
|
+
}
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isImageModelRecord(record: ModelRecord): boolean {
|
|
92
|
+
return hasImageGenerationCapability(record) ?? isLikelyImageModelId(record.id)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** List candidates exposed by an OpenAI-compatible endpoint. */
|
|
96
|
+
export async function listOpenAIModels(config: ModelListConfig): Promise<string[]> {
|
|
97
|
+
return [...new Set((await listModelRecords(config)).map(record => record.id))]
|
|
98
|
+
.sort((a, b) => a.localeCompare(b))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** List only models that advertise or conventionally represent image generation. */
|
|
102
|
+
export async function listImageModels(config: ModelListConfig): Promise<string[]> {
|
|
103
|
+
return [...new Set((await listModelRecords(config)).filter(isImageModelRecord).map(record => record.id))]
|
|
46
104
|
.sort((a, b) => a.localeCompare(b))
|
|
47
105
|
}
|
|
48
106
|
|
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.
|
|
11
|
+
export const PLUGIN_VERSION = '1.5.0'
|
|
12
12
|
|
|
13
13
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
14
14
|
export const SETTINGS_API = {
|
|
@@ -36,6 +36,9 @@ export const PRESETS_API = '/api/dsh-imagegen/presets' as const
|
|
|
36
36
|
/** Loopback-only image reader for Agent tool-result previews. */
|
|
37
37
|
export const AGENT_IMAGE_API = '/api/dsh-imagegen/agent-image' as const
|
|
38
38
|
|
|
39
|
+
/** Store the current composer image for the direct edit_image command. */
|
|
40
|
+
export const CONVERSATION_IMAGE_API = '/api/dsh-imagegen/conversation-image' as const
|
|
41
|
+
|
|
39
42
|
/**
|
|
40
43
|
* Host-computed per-channel usage counters (generation-count badges in the
|
|
41
44
|
* settings card): entries are tallied from the persisted history and gallery
|
|
@@ -149,8 +152,47 @@ export interface TemplateRefreshResult {
|
|
|
149
152
|
/** Generation modes. */
|
|
150
153
|
export type GenerateMode = 'text' | 'edit'
|
|
151
154
|
|
|
155
|
+
/** Metadata shared by the ecommerce product-set workflow. */
|
|
156
|
+
export interface EcommerceTaskMeta {
|
|
157
|
+
workflow?: 'ecommerce'
|
|
158
|
+
projectId?: string
|
|
159
|
+
projectName?: string
|
|
160
|
+
slotKey?: string
|
|
161
|
+
slotLabel?: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Role an uploaded product asset plays in the ecommerce workflow. 'none' is
|
|
165
|
+
* only used as a slot selection meaning "generate without a reference". */
|
|
166
|
+
export type EcommerceRefRole = 'none' | 'product' | 'packaging' | 'detail' | 'style'
|
|
167
|
+
|
|
168
|
+
/** One planned image slot in a product set. */
|
|
169
|
+
export interface ProductSetSlot {
|
|
170
|
+
key: string
|
|
171
|
+
label: string
|
|
172
|
+
description: string
|
|
173
|
+
count: number
|
|
174
|
+
enabled: boolean
|
|
175
|
+
/** Which uploaded asset role this slot uses as its edit reference. */
|
|
176
|
+
refRole?: EcommerceRefRole
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A browser-local ecommerce product-set draft. */
|
|
180
|
+
export interface ProductSetDraft {
|
|
181
|
+
projectId: string
|
|
182
|
+
projectName: string
|
|
183
|
+
category: string
|
|
184
|
+
platform: string
|
|
185
|
+
language: string
|
|
186
|
+
size: string
|
|
187
|
+
productName: string
|
|
188
|
+
sellingPoints: string
|
|
189
|
+
protectedFeatures: string
|
|
190
|
+
styleHint: string
|
|
191
|
+
slots: ProductSetSlot[]
|
|
192
|
+
}
|
|
193
|
+
|
|
152
194
|
/** A client → host generate request (what the panel collects). */
|
|
153
|
-
export interface GenerateRequest {
|
|
195
|
+
export interface GenerateRequest extends EcommerceTaskMeta {
|
|
154
196
|
/** text-to-image (images/generations) or image-to-image (images/edits). */
|
|
155
197
|
mode: GenerateMode
|
|
156
198
|
/**
|
|
@@ -191,6 +233,10 @@ export interface GenerateRequest {
|
|
|
191
233
|
/** Upstream model id actually sent to the gateway (host-filled from the
|
|
192
234
|
* alias mapping; defaults to `model` when absent). */
|
|
193
235
|
upstream?: string
|
|
236
|
+
/** Stable client-created id shared by the tasks in one comparison run. */
|
|
237
|
+
comparisonId?: string
|
|
238
|
+
/** All model aliases selected for one comparison run. */
|
|
239
|
+
comparisonModels?: string[]
|
|
194
240
|
}
|
|
195
241
|
|
|
196
242
|
/** One generated image, normalized host-side to base64 so the browser never
|
|
@@ -254,7 +300,7 @@ export interface PresetProviderView {
|
|
|
254
300
|
|
|
255
301
|
export type GenerationTaskStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
256
302
|
|
|
257
|
-
export interface GenerationTask {
|
|
303
|
+
export interface GenerationTask extends EcommerceTaskMeta {
|
|
258
304
|
id: string
|
|
259
305
|
request: GenerateRequest
|
|
260
306
|
status: GenerationTaskStatus
|
|
@@ -285,7 +331,7 @@ export interface HistoryImageRef {
|
|
|
285
331
|
}
|
|
286
332
|
|
|
287
333
|
/** A saved generation as the browser consumes it (metadata + served images). */
|
|
288
|
-
export interface HistoryEntry {
|
|
334
|
+
export interface HistoryEntry extends EcommerceTaskMeta {
|
|
289
335
|
id: string
|
|
290
336
|
createdAt: number
|
|
291
337
|
mode: GenerateMode
|
|
@@ -304,10 +350,14 @@ export interface HistoryEntry {
|
|
|
304
350
|
channelId?: string
|
|
305
351
|
/** Channel display name snapshot (survives channel deletion). */
|
|
306
352
|
channel?: string
|
|
353
|
+
/** Stable id shared by the history entries in one comparison run. */
|
|
354
|
+
comparisonId?: string
|
|
355
|
+
/** Model aliases included in the comparison run. */
|
|
356
|
+
comparisonModels?: string[]
|
|
307
357
|
}
|
|
308
358
|
|
|
309
359
|
/** A history entry the client submits for persistence (images still carry base64). */
|
|
310
|
-
export interface HistoryEntryInput {
|
|
360
|
+
export interface HistoryEntryInput extends EcommerceTaskMeta {
|
|
311
361
|
id: string
|
|
312
362
|
createdAt: number
|
|
313
363
|
mode: GenerateMode
|
|
@@ -323,4 +373,8 @@ export interface HistoryEntryInput {
|
|
|
323
373
|
channelId?: string
|
|
324
374
|
/** Channel display name snapshot (survives channel deletion). */
|
|
325
375
|
channel?: string
|
|
376
|
+
/** Stable id shared by the history entries in one comparison run. */
|
|
377
|
+
comparisonId?: string
|
|
378
|
+
/** Model aliases included in the comparison run. */
|
|
379
|
+
comparisonModels?: string[]
|
|
326
380
|
}
|
package/src/routes.ts
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
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
|
-
import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
|
|
11
|
+
import type { ImageAttachmentRef, ImageMediaType, SaveImageAttachment } from '@deepseek-ai/dsh-attachment'
|
|
12
12
|
import { SettingsConflictError, settingsNamespace, type SettingsDescriptor } from '@deepseek-ai/dsh-settings'
|
|
13
13
|
import type { UpstreamConfig } from './engine.ts'
|
|
14
|
-
import { enhancePrompt,
|
|
14
|
+
import { enhancePrompt, listImageModels, listPromptModels, type PromptModelConfig } from './prompt-enhancer.ts'
|
|
15
15
|
import { normalizeImageModels } from './image-models.ts'
|
|
16
16
|
import { ImageGenerationRuntime, type ChannelsView } from './generation-runtime.ts'
|
|
17
17
|
import { appendHistory, clearHistory, listHistory, readHistoryImage, removeHistory } from './history-store.ts'
|
|
@@ -19,7 +19,7 @@ import { appendGallery, clearGallery, listGallery, readGalleryImage, removeGalle
|
|
|
19
19
|
import { listTemplates, readTemplateImage, refreshTemplates } from './templates-store.ts'
|
|
20
20
|
import { checkForUpdate, CURRENT_VERSION, installUpdate } from './updater.ts'
|
|
21
21
|
import { IMAGE_PRESETS } from './presets.ts'
|
|
22
|
-
import { AGENT_IMAGE_API, GALLERY_API, GENERATE_API, HISTORY_API, IMAGEGEN_SETTINGS_NAMESPACE, IMAGE_MODEL_API, PRESETS_API, PROMPT_ENHANCE_API, SETTINGS_API, TASK_API, TEMPLATES_API, UPDATE_API, USAGE_API, type GeneratedImage, type GenerateRequest, type HistoryEntry, type HistoryEntryInput, type ModelMapping, type PresetProviderView, type TemplateListResult, type TemplateRefreshResult } from './protocol.ts'
|
|
22
|
+
import { AGENT_IMAGE_API, CONVERSATION_IMAGE_API, GALLERY_API, GENERATE_API, HISTORY_API, IMAGEGEN_SETTINGS_NAMESPACE, IMAGE_MODEL_API, PRESETS_API, PROMPT_ENHANCE_API, SETTINGS_API, TASK_API, TEMPLATES_API, UPDATE_API, USAGE_API, type GeneratedImage, type GenerateRequest, type HistoryEntry, type HistoryEntryInput, type ModelMapping, type PresetProviderView, type TemplateListResult, type TemplateRefreshResult } from './protocol.ts'
|
|
23
23
|
|
|
24
24
|
/** Cap on JSON request bodies (settings ops and generate payloads are small). */
|
|
25
25
|
const MAX_JSON_BODY_BYTES = 24 * 1024 * 1024
|
|
@@ -49,6 +49,11 @@ export interface ImageGenRoutesDeps {
|
|
|
49
49
|
/** Host attachment storage used by Agent tool-result previews. */
|
|
50
50
|
attachments?: {
|
|
51
51
|
readImage: (ref: ImageAttachmentRef) => Promise<{ ref: ImageAttachmentRef; data: Uint8Array }>
|
|
52
|
+
saveImage?: (input: SaveImageAttachment) => Promise<ImageAttachmentRef>
|
|
53
|
+
}
|
|
54
|
+
/** Latest composer image staged for the direct edit_image command. */
|
|
55
|
+
pendingConversationImages?: {
|
|
56
|
+
set: (sessionId: string, ref: ImageAttachmentRef) => void
|
|
52
57
|
}
|
|
53
58
|
/** Overrideable history backend, primarily for host integration tests. */
|
|
54
59
|
history?: {
|
|
@@ -133,6 +138,9 @@ function messageOf(error: unknown): string {
|
|
|
133
138
|
function parseGenerateRequest(body: Record<string, unknown>): GenerateRequest | undefined {
|
|
134
139
|
const prompt = typeof body.prompt === 'string' ? body.prompt.trim() : ''
|
|
135
140
|
if (prompt === '') return undefined
|
|
141
|
+
const comparisonModels = Array.isArray(body.comparisonModels)
|
|
142
|
+
? [...new Set(body.comparisonModels.filter((model): model is string => typeof model === 'string').map(model => model.trim()).filter(Boolean))]
|
|
143
|
+
: []
|
|
136
144
|
return {
|
|
137
145
|
mode: body.mode === 'edit' ? 'edit' : 'text',
|
|
138
146
|
model: typeof body.model === 'string' ? body.model : '',
|
|
@@ -144,6 +152,13 @@ function parseGenerateRequest(body: Record<string, unknown>): GenerateRequest |
|
|
|
144
152
|
...typeof body.image === 'string' && body.image !== '' ? { image: body.image } : {},
|
|
145
153
|
...typeof body.refName === 'string' && body.refName !== '' ? { refName: body.refName } : {},
|
|
146
154
|
...typeof body.channelId === 'string' && body.channelId !== '' ? { channelId: body.channelId } : {},
|
|
155
|
+
...typeof body.comparisonId === 'string' && body.comparisonId !== '' ? { comparisonId: body.comparisonId } : {},
|
|
156
|
+
...comparisonModels.length > 1 ? { comparisonModels } : {},
|
|
157
|
+
...body.workflow === 'ecommerce' ? { workflow: 'ecommerce' as const } : {},
|
|
158
|
+
...typeof body.projectId === 'string' && body.projectId !== '' ? { projectId: body.projectId } : {},
|
|
159
|
+
...typeof body.projectName === 'string' && body.projectName !== '' ? { projectName: body.projectName } : {},
|
|
160
|
+
...typeof body.slotKey === 'string' && body.slotKey !== '' ? { slotKey: body.slotKey } : {},
|
|
161
|
+
...typeof body.slotLabel === 'string' && body.slotLabel !== '' ? { slotLabel: body.slotLabel } : {},
|
|
147
162
|
}
|
|
148
163
|
}
|
|
149
164
|
|
|
@@ -169,6 +184,9 @@ function parseHistoryEntryInput(body: Record<string, unknown>): HistoryEntryInpu
|
|
|
169
184
|
...typeof image.revisedPrompt === 'string' ? { revisedPrompt: image.revisedPrompt } : {},
|
|
170
185
|
})
|
|
171
186
|
}
|
|
187
|
+
const comparisonModels = Array.isArray(entry.comparisonModels)
|
|
188
|
+
? [...new Set(entry.comparisonModels.filter((model): model is string => typeof model === 'string').map(model => model.trim()).filter(Boolean))]
|
|
189
|
+
: []
|
|
172
190
|
return {
|
|
173
191
|
id: entry.id,
|
|
174
192
|
createdAt: entry.createdAt,
|
|
@@ -183,6 +201,13 @@ function parseHistoryEntryInput(body: Record<string, unknown>): HistoryEntryInpu
|
|
|
183
201
|
...typeof entry.refName === 'string' ? { refName: entry.refName } : {},
|
|
184
202
|
...typeof entry.channelId === 'string' ? { channelId: entry.channelId } : {},
|
|
185
203
|
...typeof entry.channel === 'string' ? { channel: entry.channel } : {},
|
|
204
|
+
...typeof entry.comparisonId === 'string' ? { comparisonId: entry.comparisonId } : {},
|
|
205
|
+
...comparisonModels.length > 1 ? { comparisonModels } : {},
|
|
206
|
+
...entry.workflow === 'ecommerce' ? { workflow: 'ecommerce' as const } : {},
|
|
207
|
+
...typeof entry.projectId === 'string' ? { projectId: entry.projectId } : {},
|
|
208
|
+
...typeof entry.projectName === 'string' ? { projectName: entry.projectName } : {},
|
|
209
|
+
...typeof entry.slotKey === 'string' ? { slotKey: entry.slotKey } : {},
|
|
210
|
+
...typeof entry.slotLabel === 'string' ? { slotLabel: entry.slotLabel } : {},
|
|
186
211
|
}
|
|
187
212
|
}
|
|
188
213
|
|
|
@@ -231,6 +256,13 @@ function isImageMediaType(value: string): value is ImageMediaType {
|
|
|
231
256
|
return value === 'image/png' || value === 'image/jpeg' || value === 'image/webp' || value === 'image/gif'
|
|
232
257
|
}
|
|
233
258
|
|
|
259
|
+
function imageDataUrl(value: string): { mediaType: ImageMediaType; data: Uint8Array } | undefined {
|
|
260
|
+
const match = /^data:(image\/(?:png|jpeg|webp|gif));base64,(.*)$/su.exec(value.trim())
|
|
261
|
+
if (match === null || match[1] === undefined || match[2] === undefined) return undefined
|
|
262
|
+
const data = Buffer.from(match[2], 'base64')
|
|
263
|
+
return data.byteLength === 0 ? undefined : { mediaType: match[1] as ImageMediaType, data }
|
|
264
|
+
}
|
|
265
|
+
|
|
234
266
|
/** Project one settings descriptor onto the bridge wire view. */
|
|
235
267
|
function toView(descriptor: SettingsDescriptor): Record<string, unknown> {
|
|
236
268
|
return {
|
|
@@ -341,6 +373,32 @@ export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
|
341
373
|
}
|
|
342
374
|
|
|
343
375
|
return [
|
|
376
|
+
// ---------------------------- composer image for /edit_image (exact)
|
|
377
|
+
...(deps.attachments?.saveImage === undefined || deps.pendingConversationImages === undefined ? [] : [{
|
|
378
|
+
kind: 'exact' as const,
|
|
379
|
+
path: CONVERSATION_IMAGE_API,
|
|
380
|
+
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
381
|
+
if (!guard(req, res, 'POST')) return
|
|
382
|
+
const body = await readJsonBody(req, MAX_JSON_BODY_BYTES)
|
|
383
|
+
const sessionId = typeof body?.sessionId === 'string' ? body.sessionId.trim() : ''
|
|
384
|
+
const dataUrl = typeof body?.dataUrl === 'string' ? imageDataUrl(body.dataUrl) : undefined
|
|
385
|
+
if (sessionId === '' || dataUrl === undefined) {
|
|
386
|
+
writeJson(res, 200, { ok: false, code: 'bad-request', message: 'sessionId and image data are required' })
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const ref = await deps.attachments!.saveImage!({
|
|
391
|
+
data: dataUrl.data,
|
|
392
|
+
mediaType: dataUrl.mediaType,
|
|
393
|
+
...typeof body?.name === 'string' && body.name.trim() !== '' ? { name: body.name.trim() } : {},
|
|
394
|
+
})
|
|
395
|
+
deps.pendingConversationImages!.set(sessionId, ref)
|
|
396
|
+
writeJson(res, 200, { ok: true })
|
|
397
|
+
} catch (error) {
|
|
398
|
+
writeJson(res, 200, { ok: false, code: 'image-save-failed', message: messageOf(error) })
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
} satisfies WebRoute]),
|
|
344
402
|
// ------------------------------------ Agent tool-result image (prefix)
|
|
345
403
|
...(deps.attachments === undefined ? [] : [{
|
|
346
404
|
kind: 'prefix' as const,
|
|
@@ -393,7 +451,7 @@ export function makeRoutes(deps: ImageGenRoutesDeps): WebRoute[] {
|
|
|
393
451
|
apiKey: typeof body?.apiKey === 'string' && body.apiKey.trim() !== '' ? body.apiKey.trim() : (stored?.apiKey ?? ''),
|
|
394
452
|
}
|
|
395
453
|
try {
|
|
396
|
-
writeJson(res, 200, { ok: true, models: await
|
|
454
|
+
writeJson(res, 200, { ok: true, models: await listImageModels(upstream) })
|
|
397
455
|
} catch (error) {
|
|
398
456
|
writeJson(res, 200, { ok: false, code: 'image-models-failed', message: messageOf(error) })
|
|
399
457
|
}
|