@dickpy/dsh-imagegen 1.2.3 → 1.4.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.
Files changed (45) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +203 -181
  3. package/cordis.patch.yml +8 -8
  4. package/docs/images/multi-model-comparison.png +0 -0
  5. package/lib/client.js +2711 -1318
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +830 -155
  8. package/package.json +70 -68
  9. package/src/agent-image-tools.ts +418 -316
  10. package/src/client/ImageGenPanel.tsx +1703 -1476
  11. package/src/client/SettingsCard.tsx +936 -648
  12. package/src/client/TemplateLibrary.tsx +336 -336
  13. package/src/client/api.ts +193 -193
  14. package/src/client/channels-form.ts +263 -0
  15. package/src/client/controller.ts +46 -46
  16. package/src/client/conversation-sync.ts +14 -0
  17. package/src/client/css-modules.d.ts +5 -5
  18. package/src/client/helpers.ts +33 -33
  19. package/src/client/image-toolview.module.css +73 -73
  20. package/src/client/image-toolview.tsx +170 -152
  21. package/src/client/index.ts +32 -22
  22. package/src/client/locales.ts +610 -484
  23. package/src/client/mount.tsx +185 -96
  24. package/src/client/panel.module.css +1713 -1445
  25. package/src/client/settings-card.module.css +1023 -536
  26. package/src/client/settings-form.ts +336 -336
  27. package/src/client/settings-scope.ts +298 -250
  28. package/src/client/sidebar-entry.ts +148 -102
  29. package/src/client/templates.module.css +453 -453
  30. package/src/engine.ts +520 -464
  31. package/src/gallery-store.ts +286 -280
  32. package/src/generation-runtime.ts +79 -48
  33. package/src/history-store.ts +250 -238
  34. package/src/image-format.ts +11 -0
  35. package/src/image-models.ts +19 -19
  36. package/src/index.ts +318 -212
  37. package/src/model-catalog.ts +115 -0
  38. package/src/presets.ts +71 -0
  39. package/src/prompt-enhancer.ts +137 -79
  40. package/src/protocol.ts +338 -253
  41. package/src/routes.ts +916 -738
  42. package/src/task-queue.ts +113 -103
  43. package/src/templates/cases.json +10196 -10196
  44. package/src/templates-store.ts +278 -278
  45. package/src/updater.ts +117 -117
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Model catalog: protocol-family detection and capability annotation shared by
3
+ * the engine, the settings card, and the panel. Known id patterns map onto the
4
+ * OpenAI-compatible image protocol families this plugin shapes requests for;
5
+ * anything unrecognized falls back to the generic OpenAI protocol (best
6
+ * effort) and is flagged as unknown so the UI can warn without blocking.
7
+ *
8
+ * Framework-free (pure data + regex), safe for the client bundle to inline.
9
+ */
10
+
11
+ export type ModelFamily = 'gpt-image' | 'dall-e' | 'grok' | 'nanobanana' | 'seedream' | 'zhipu' | 'unknown'
12
+
13
+ /** Capability/identity annotation for one model id. */
14
+ export interface ModelCatalogEntry {
15
+ /** Family the request is shaped for. */
16
+ family: ModelFamily
17
+ /** Short badge label (en). */
18
+ label: string
19
+ /** Short badge label (zh). */
20
+ labelZh: string
21
+ /** Whether the family is known (false = best-effort OpenAI protocol). */
22
+ known: boolean
23
+ /** Whether the family supports image-to-image edits. */
24
+ supportsEdit: boolean
25
+ /** Whether the family natively takes aspect-ratio dials. */
26
+ supportsAspectRatio: boolean
27
+ /** Quality tiers the family interprets natively. */
28
+ qualityTiers: string[]
29
+ }
30
+
31
+ const ENTRIES: Record<Exclude<ModelFamily, 'unknown'>, Omit<ModelCatalogEntry, 'family'>> = {
32
+ 'gpt-image': {
33
+ label: 'gpt-image',
34
+ labelZh: 'GPT 图像',
35
+ known: true,
36
+ supportsEdit: true,
37
+ supportsAspectRatio: false,
38
+ qualityTiers: ['1K', '2K', '4K'],
39
+ },
40
+ 'dall-e': {
41
+ label: 'DALL·E',
42
+ labelZh: 'DALL·E',
43
+ known: true,
44
+ supportsEdit: true,
45
+ supportsAspectRatio: false,
46
+ qualityTiers: ['auto'],
47
+ },
48
+ grok: {
49
+ label: 'grok',
50
+ labelZh: 'Grok',
51
+ known: true,
52
+ supportsEdit: true,
53
+ supportsAspectRatio: true,
54
+ qualityTiers: ['1K', '2K'],
55
+ },
56
+ nanobanana: {
57
+ label: 'nanobanana',
58
+ labelZh: 'Nano Banana',
59
+ known: true,
60
+ supportsEdit: true,
61
+ supportsAspectRatio: true,
62
+ qualityTiers: ['1K', '2K', '4K'],
63
+ },
64
+ seedream: {
65
+ label: 'seedream',
66
+ labelZh: 'Seedream',
67
+ known: true,
68
+ supportsEdit: true,
69
+ supportsAspectRatio: true,
70
+ qualityTiers: ['1K', '2K'],
71
+ },
72
+ zhipu: {
73
+ label: 'GLM-Image',
74
+ labelZh: '智谱图像',
75
+ known: true,
76
+ supportsEdit: false,
77
+ supportsAspectRatio: false,
78
+ qualityTiers: ['HD'],
79
+ },
80
+ }
81
+
82
+ /** Official Gemini image ids served by Nano Banana gateways. */
83
+ const NANOBANANA_GEMINI_IDS = new Set([
84
+ 'gemini-3-pro-image',
85
+ 'gemini-3-pro-image-preview',
86
+ 'gemini-3.1-flash-image',
87
+ 'gemini-3.1-flash-image-preview',
88
+ 'gemini-3.1-flash-lite-image',
89
+ 'gemini-2.5-flash-image',
90
+ ])
91
+
92
+ /** Classify one upstream model id into its request-shaping family. */
93
+ export function describeModel(model: string): ModelCatalogEntry {
94
+ const id = model.trim()
95
+ if (/^gpt-image/i.test(id)) return { family: 'gpt-image', ...ENTRIES['gpt-image'] }
96
+ if (/^dall-e/i.test(id)) return { family: 'dall-e', ...ENTRIES['dall-e'] }
97
+ if (/^grok-imagine(?:-|$)/.test(id)) return { family: 'grok', ...ENTRIES.grok }
98
+ if (/^nanobanana/i.test(id) || NANOBANANA_GEMINI_IDS.has(id)) return { family: 'nanobanana', ...ENTRIES.nanobanana }
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 }
101
+ return { family: 'unknown', label: 'unknown', labelZh: '未知协议', known: false, supportsEdit: true, supportsAspectRatio: false, qualityTiers: [] }
102
+ }
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
+
112
+ /** The family a model id routes its request through. */
113
+ export function modelFamily(model: string): ModelFamily {
114
+ return describeModel(model).family
115
+ }
package/src/presets.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Built-in provider catalog (presets). A preset is an official or well-known
3
+ * OpenAI-compatible endpoint with its known model list, so the user only fills
4
+ * in the API key. The list ships with the package and is served to the
5
+ * settings card through a host route, so it can later be refreshed online like
6
+ * the template library.
7
+ *
8
+ * Framework-free (pure data), safe for the host routes to serve directly.
9
+ */
10
+
11
+ import type { ModelMapping } from './protocol.ts'
12
+
13
+ /** One built-in provider the settings card can instantiate a channel from. */
14
+ export interface PresetProvider {
15
+ /** Stable preset id stored on channels created from it ('' = custom). */
16
+ id: string
17
+ /** Display name shown in the picker (also the channel's default name). */
18
+ name: string
19
+ /** Official base URL prefilled into the channel. */
20
+ apiUrl: string
21
+ /** One-line description shown in the picker. */
22
+ hint: string
23
+ /** Known model list prefilled into the channel's model catalog. */
24
+ models: ModelMapping[]
25
+ }
26
+
27
+ export const IMAGE_PRESETS: PresetProvider[] = [
28
+ {
29
+ id: 'volc-ark-seedream',
30
+ name: '字节 · 火山方舟(Seedream)',
31
+ apiUrl: 'https://ark.cn-beijing.volces.com/api/v3',
32
+ hint: '字节跳动官方 Seedream 文生图/图生图入口',
33
+ models: [
34
+ { alias: 'seedream-5.0-pro', id: 'seedream-5.0-pro' },
35
+ { alias: 'seedream-5.0', id: 'seedream-5.0' },
36
+ { alias: 'seedream-4.0', id: 'seedream-4.0' },
37
+ ],
38
+ },
39
+ {
40
+ id: 'openai-official',
41
+ name: 'OpenAI 官方',
42
+ apiUrl: 'https://api.openai.com/v1',
43
+ hint: 'OpenAI 官方图像生成接口',
44
+ models: [
45
+ { alias: 'gpt-image-2', id: 'gpt-image-2' },
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' },
55
+ ],
56
+ },
57
+ {
58
+ id: 'xai-grok',
59
+ name: 'xAI(Grok)',
60
+ apiUrl: 'https://api.x.ai/v1',
61
+ hint: 'xAI 官方接口:Grok Imagine 系列',
62
+ models: [
63
+ { alias: 'grok-imagine-image', id: 'grok-imagine-image' },
64
+ ],
65
+ },
66
+ ]
67
+
68
+ /** Look up one built-in provider by id. */
69
+ export function presetById(id: string): PresetProvider | undefined {
70
+ return IMAGE_PRESETS.find(preset => preset.id === id)
71
+ }
@@ -1,79 +1,137 @@
1
- /** OpenAI-compatible chat helpers used by the optional prompt-enhancement UI. */
2
-
3
- export interface PromptModelConfig {
4
- apiUrl: string
5
- apiKey: string
6
- model: string
7
- }
8
-
9
- /** Credentials shared by OpenAI-compatible `/models` discovery. */
10
- export interface ModelListConfig {
11
- apiUrl: string
12
- apiKey: string
13
- }
14
-
15
- function endpoint(base: string, suffix: string): string {
16
- return `${base.replace(/\/+$/, '')}${suffix}`
17
- }
18
-
19
- function headers(apiKey: string): HeadersInit {
20
- return {
21
- 'content-type': 'application/json',
22
- ...apiKey.trim() === '' ? {} : { authorization: `Bearer ${apiKey.trim()}` },
23
- }
24
- }
25
-
26
- async function responseJson(response: Response): Promise<Record<string, unknown>> {
27
- const body: unknown = await response.json().catch(() => undefined)
28
- if (!response.ok || body === undefined || body === null || typeof body !== 'object') {
29
- const message = body !== null && typeof body === 'object' && typeof (body as { error?: { message?: unknown } }).error?.message === 'string'
30
- ? (body as { error: { message: string } }).error.message
31
- : `HTTP ${response.status}`
32
- throw new Error(message)
33
- }
34
- return body as Record<string, unknown>
35
- }
36
-
37
- /** List candidates exposed by an OpenAI-compatible endpoint. */
38
- export async function listOpenAIModels(config: ModelListConfig): Promise<string[]> {
39
- if (config.apiUrl.trim() === '') throw new Error('API URL is required')
40
- const response = await fetch(endpoint(config.apiUrl, '/models'), { headers: headers(config.apiKey) })
41
- const body = await responseJson(response)
42
- const data = Array.isArray(body.data) ? body.data : []
43
- return [...new Set(data
44
- .flatMap(item => item !== null && typeof item === 'object' && typeof (item as { id?: unknown }).id === 'string' ? [(item as { id: string }).id.trim()] : [])
45
- .filter(Boolean))]
46
- .sort((a, b) => a.localeCompare(b))
47
- }
48
-
49
- /** List chat models exposed by an OpenAI-compatible endpoint. */
50
- export async function listPromptModels(config: PromptModelConfig): Promise<string[]> {
51
- return listOpenAIModels(config)
52
- }
53
-
54
- /** Expand a concise image request into a production-ready image prompt. */
55
- export async function enhancePrompt(config: PromptModelConfig, prompt: string): Promise<string> {
56
- if (config.apiUrl.trim() === '' || config.model.trim() === '') throw new Error('prompt enhancement model is not configured')
57
- const response = await fetch(endpoint(config.apiUrl, '/chat/completions'), {
58
- method: 'POST',
59
- headers: headers(config.apiKey),
60
- body: JSON.stringify({
61
- model: config.model.trim(),
62
- temperature: 0.7,
63
- messages: [
64
- {
65
- role: 'system',
66
- content: 'You are an expert image-prompt editor. Expand the user request into one vivid, specific image-generation prompt. Preserve intent and language. Add only useful visual detail: subject, composition, lighting, materials, color, camera/style and quality. Return only the finished prompt, with no preface or markdown.',
67
- },
68
- { role: 'user', content: prompt },
69
- ],
70
- }),
71
- })
72
- const body = await responseJson(response)
73
- const choices = Array.isArray(body.choices) ? body.choices : []
74
- const content = choices[0] !== null && typeof choices[0] === 'object'
75
- ? (choices[0] as { message?: { content?: unknown } }).message?.content
76
- : undefined
77
- if (typeof content !== 'string' || content.trim() === '') throw new Error('chat model returned an empty prompt')
78
- return content.trim()
79
- }
1
+ /** OpenAI-compatible chat helpers used by the optional prompt-enhancement UI. */
2
+
3
+ import { isLikelyImageModelId } from './model-catalog.ts'
4
+
5
+ export interface PromptModelConfig {
6
+ apiUrl: string
7
+ apiKey: string
8
+ model: string
9
+ }
10
+
11
+ /** Credentials shared by OpenAI-compatible `/models` discovery. */
12
+ export interface ModelListConfig {
13
+ apiUrl: string
14
+ apiKey: string
15
+ }
16
+
17
+ function endpoint(base: string, suffix: string): string {
18
+ return `${base.replace(/\/+$/, '')}${suffix}`
19
+ }
20
+
21
+ function headers(apiKey: string): HeadersInit {
22
+ return {
23
+ 'content-type': 'application/json',
24
+ ...apiKey.trim() === '' ? {} : { authorization: `Bearer ${apiKey.trim()}` },
25
+ }
26
+ }
27
+
28
+ async function responseJson(response: Response): Promise<Record<string, unknown>> {
29
+ const body: unknown = await response.json().catch(() => undefined)
30
+ if (!response.ok || body === undefined || body === null || typeof body !== 'object') {
31
+ const message = body !== null && typeof body === 'object' && typeof (body as { error?: { message?: unknown } }).error?.message === 'string'
32
+ ? (body as { error: { message: string } }).error.message
33
+ : `HTTP ${response.status}`
34
+ throw new Error(message)
35
+ }
36
+ return body as Record<string, unknown>
37
+ }
38
+
39
+ type ModelRecord = Record<string, unknown> & { id: string }
40
+
41
+ async function listModelRecords(config: ModelListConfig): Promise<ModelRecord[]> {
42
+ if (config.apiUrl.trim() === '') throw new Error('API URL is required')
43
+ const response = await fetch(endpoint(config.apiUrl, '/models'), { headers: headers(config.apiKey) })
44
+ const body = await responseJson(response)
45
+ const data = Array.isArray(body.data) ? body.data : []
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))]
104
+ .sort((a, b) => a.localeCompare(b))
105
+ }
106
+
107
+ /** List chat models exposed by an OpenAI-compatible endpoint. */
108
+ export async function listPromptModels(config: PromptModelConfig): Promise<string[]> {
109
+ return listOpenAIModels(config)
110
+ }
111
+
112
+ /** Expand a concise image request into a production-ready image prompt. */
113
+ export async function enhancePrompt(config: PromptModelConfig, prompt: string): Promise<string> {
114
+ if (config.apiUrl.trim() === '' || config.model.trim() === '') throw new Error('prompt enhancement model is not configured')
115
+ const response = await fetch(endpoint(config.apiUrl, '/chat/completions'), {
116
+ method: 'POST',
117
+ headers: headers(config.apiKey),
118
+ body: JSON.stringify({
119
+ model: config.model.trim(),
120
+ temperature: 0.7,
121
+ messages: [
122
+ {
123
+ role: 'system',
124
+ content: 'You are an expert image-prompt editor. Expand the user request into one vivid, specific image-generation prompt. Preserve intent and language. Add only useful visual detail: subject, composition, lighting, materials, color, camera/style and quality. Return only the finished prompt, with no preface or markdown.',
125
+ },
126
+ { role: 'user', content: prompt },
127
+ ],
128
+ }),
129
+ })
130
+ const body = await responseJson(response)
131
+ const choices = Array.isArray(body.choices) ? body.choices : []
132
+ const content = choices[0] !== null && typeof choices[0] === 'object'
133
+ ? (choices[0] as { message?: { content?: unknown } }).message?.content
134
+ : undefined
135
+ if (typeof content !== 'string' || content.trim() === '') throw new Error('chat model returned an empty prompt')
136
+ return content.trim()
137
+ }