@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
|
@@ -1,144 +1,190 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Replace the shell's standalone New Session affordance with a two-tab entry:
|
|
3
|
+
* New Session and Image Generation. When image generation is active, the
|
|
4
|
+
* plugin also uses the shell's region area as a dedicated history surface;
|
|
5
|
+
* the original workspace/session tree remains underneath and is restored when
|
|
6
|
+
* the panel closes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ImageGenController } from './controller.ts'
|
|
10
|
+
import css from './panel.module.css'
|
|
11
|
+
|
|
12
|
+
/** Stable selector for the injected two-tab host. */
|
|
13
|
+
export const ENTRY_SELECTOR = '[data-dsh-imagegen-session-tabs]'
|
|
14
|
+
/** Stable selector for the history surface in the shell region area. */
|
|
15
|
+
export const HISTORY_HOST_SELECTOR = '[data-dsh-imagegen-history-host]'
|
|
16
|
+
|
|
17
|
+
/** Inline picture glyph kept deliberately small for the sidebar rail. */
|
|
18
|
+
const IMAGE_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><circle cx="5.6" cy="5.8" r="1"/><path d="M2.5 12.5l3.6-3.4 2.4 2.2 3-3 2 2.4"/></svg>'
|
|
19
|
+
|
|
20
|
+
/** Inline plus glyph for the new-session tab. */
|
|
21
|
+
const NEW_SESSION_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M8 3v10M3 8h10"/></svg>'
|
|
22
|
+
|
|
23
|
+
/** Find the sidebar shell root, or undefined while it is not mounted. */
|
|
24
|
+
function sidebarRoot(): HTMLElement | undefined {
|
|
25
|
+
const column = document.querySelector<HTMLElement>('[data-pane="sidebar"], [class*="sidebarCol"]')
|
|
26
|
+
if (column === null) return undefined
|
|
27
|
+
const logoOwner = column.querySelector<HTMLElement>('[class*="logoRow"]')?.parentElement
|
|
28
|
+
return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The shell-owned New Session button across current and legacy shells. */
|
|
32
|
+
function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
|
|
33
|
+
return root.querySelector<HTMLButtonElement>(
|
|
34
|
+
'button[data-dsh-part="new-session"], button[class*="newSession"]',
|
|
35
|
+
) ?? Array.from(root.children).find(
|
|
36
|
+
(child): child is HTMLButtonElement => child instanceof HTMLElement && child.tagName === 'BUTTON',
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Locate the shell region that normally contains workspaces and sessions. */
|
|
41
|
+
function regionArea(root: HTMLElement): HTMLElement | undefined {
|
|
42
|
+
return root.querySelector<HTMLElement>('[class*="regionArea"]') ?? undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeTab(
|
|
46
|
+
label: string,
|
|
47
|
+
tooltip: string,
|
|
48
|
+
icon: string,
|
|
49
|
+
onClick: () => void,
|
|
50
|
+
): HTMLButtonElement {
|
|
51
|
+
const tab = document.createElement('button')
|
|
52
|
+
tab.type = 'button'
|
|
53
|
+
tab.className = css.sessionTab
|
|
54
|
+
tab.setAttribute('aria-label', label)
|
|
55
|
+
tab.setAttribute('title', tooltip)
|
|
56
|
+
tab.innerHTML = `<span class="${css.sessionTabIcon}">${icon}</span><span class="${css.sessionTabLabel}">${label}</span>`
|
|
57
|
+
tab.addEventListener('click', onClick)
|
|
58
|
+
return tab
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hideShellButton(button: HTMLButtonElement): void {
|
|
62
|
+
button.dataset.dshImagegenOriginal = ''
|
|
63
|
+
button.setAttribute('aria-hidden', 'true')
|
|
64
|
+
button.tabIndex = -1
|
|
65
|
+
button.style.display = 'none'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function restoreShellButton(button: HTMLButtonElement): void {
|
|
69
|
+
button.style.removeProperty('display')
|
|
70
|
+
button.removeAttribute('aria-hidden')
|
|
71
|
+
button.removeAttribute('tabindex')
|
|
72
|
+
delete button.dataset.dshImagegenOriginal
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Mount or repair the two-tab host at the shell's New Session position. */
|
|
76
|
+
function placeTabs(
|
|
77
|
+
root: HTMLElement,
|
|
78
|
+
controller: ImageGenController,
|
|
79
|
+
newSessionLabel: string,
|
|
80
|
+
newSessionTooltip: string,
|
|
81
|
+
imageLabel: string,
|
|
82
|
+
imageTooltip: string,
|
|
83
|
+
): HTMLDivElement | undefined {
|
|
84
|
+
const button = newSessionButton(root)
|
|
85
|
+
if (button === undefined) return undefined
|
|
86
|
+
|
|
87
|
+
const existing = root.querySelector<HTMLDivElement>(ENTRY_SELECTOR)
|
|
88
|
+
if (existing !== null && existing.parentElement === button.parentElement) {
|
|
89
|
+
hideShellButton(button)
|
|
90
|
+
return existing
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
existing?.remove()
|
|
94
|
+
const tabs = document.createElement('div')
|
|
95
|
+
tabs.dataset.dshImagegenSessionTabs = ''
|
|
96
|
+
tabs.className = css.sessionTabs
|
|
97
|
+
tabs.setAttribute('role', 'tablist')
|
|
98
|
+
tabs.setAttribute('aria-label', imageTooltip)
|
|
99
|
+
|
|
100
|
+
const newSessionTab = makeTab(newSessionLabel, newSessionTooltip, NEW_SESSION_ICON, () => {
|
|
101
|
+
controller.close()
|
|
102
|
+
button.click()
|
|
103
|
+
})
|
|
104
|
+
const imageTab = makeTab(imageLabel, imageTooltip, IMAGE_ICON, () => {
|
|
105
|
+
controller.open()
|
|
106
|
+
})
|
|
107
|
+
newSessionTab.dataset.dshImagegenTab = 'new-session'
|
|
108
|
+
imageTab.dataset.dshImagegenTab = 'image'
|
|
109
|
+
tabs.append(newSessionTab, imageTab)
|
|
110
|
+
|
|
111
|
+
button.parentElement?.insertBefore(tabs, button)
|
|
112
|
+
hideShellButton(button)
|
|
113
|
+
return tabs
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Mount an overlay host over the workspace/session tree for image history. */
|
|
117
|
+
function placeHistoryHost(root: HTMLElement): HTMLDivElement | undefined {
|
|
118
|
+
const region = regionArea(root)
|
|
119
|
+
if (region === undefined) return undefined
|
|
120
|
+
const existing = region.querySelector<HTMLDivElement>(HISTORY_HOST_SELECTOR)
|
|
121
|
+
if (existing !== null) return existing
|
|
122
|
+
const host = document.createElement('div')
|
|
123
|
+
host.dataset.dshImagegenHistoryHost = ''
|
|
124
|
+
host.className = css.sidebarHistoryHost
|
|
125
|
+
region.append(host)
|
|
126
|
+
return host
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Mount the two tabs and self-heal after React rebuilds the sidebar. The
|
|
131
|
+
* shell-owned button is restored by the disposer so unloading the plugin
|
|
132
|
+
* leaves the host unchanged.
|
|
133
|
+
*/
|
|
134
|
+
export function mountSidebarEntry(
|
|
135
|
+
controller: ImageGenController,
|
|
136
|
+
newSessionLabel: string,
|
|
137
|
+
newSessionTooltip: string,
|
|
138
|
+
imageLabel: string,
|
|
139
|
+
imageTooltip: string,
|
|
140
|
+
): () => void {
|
|
141
|
+
let root: HTMLElement | undefined
|
|
142
|
+
let tabs: HTMLDivElement | undefined
|
|
143
|
+
let historyHost: HTMLDivElement | undefined
|
|
144
|
+
let originalButton: HTMLButtonElement | undefined
|
|
145
|
+
|
|
146
|
+
const syncActive = (): void => {
|
|
147
|
+
if (tabs === undefined) return
|
|
148
|
+
const newTab = tabs.querySelector<HTMLElement>('[data-dsh-imagegen-tab="new-session"]')
|
|
149
|
+
const imageTab = tabs.querySelector<HTMLElement>('[data-dsh-imagegen-tab="image"]')
|
|
150
|
+
if (controller.getSnapshot().panelOpen) {
|
|
151
|
+
if (newTab !== null) delete newTab.dataset.active
|
|
152
|
+
if (imageTab !== null) imageTab.dataset.active = ''
|
|
153
|
+
} else {
|
|
154
|
+
if (newTab !== null) newTab.dataset.active = ''
|
|
155
|
+
if (imageTab !== null) delete imageTab.dataset.active
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const ensure = (): void => {
|
|
160
|
+
if (root !== undefined && !root.isConnected) {
|
|
161
|
+
root = undefined
|
|
162
|
+
tabs = undefined
|
|
163
|
+
historyHost = undefined
|
|
164
|
+
originalButton = undefined
|
|
165
|
+
}
|
|
166
|
+
root ??= sidebarRoot()
|
|
167
|
+
if (root === undefined) return
|
|
168
|
+
root.dataset.dshImagegenSidebarRoot = ''
|
|
169
|
+
const button = newSessionButton(root)
|
|
170
|
+
if (button === undefined) return
|
|
171
|
+
originalButton ??= button
|
|
172
|
+
tabs = placeTabs(root, controller, newSessionLabel, newSessionTooltip, imageLabel, imageTooltip)
|
|
173
|
+
historyHost = placeHistoryHost(root)
|
|
174
|
+
syncActive()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const bodyObserver = new MutationObserver(ensure)
|
|
178
|
+
bodyObserver.observe(document.body, { childList: true, subtree: true })
|
|
179
|
+
const unsubscribe = controller.subscribe(syncActive)
|
|
180
|
+
ensure()
|
|
181
|
+
|
|
182
|
+
return () => {
|
|
183
|
+
bodyObserver.disconnect()
|
|
184
|
+
unsubscribe()
|
|
185
|
+
tabs?.remove()
|
|
186
|
+
historyHost?.remove()
|
|
187
|
+
if (originalButton !== undefined && originalButton.isConnected) restoreShellButton(originalButton)
|
|
188
|
+
if (root !== undefined) delete root.dataset.dshImagegenSidebarRoot
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** Direct `/edit_image` command for editing the latest image in the session. */
|
|
2
|
+
|
|
3
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
+
import type { CommandResult } from '@deepseek-ai/dsh-commands'
|
|
5
|
+
import type { AttachmentStore, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
|
6
|
+
import type {} from '@deepseek-ai/dsh-commands'
|
|
7
|
+
import type {} from '@deepseek-ai/dsh-attachment'
|
|
8
|
+
import {
|
|
9
|
+
submitAgentImageEdit,
|
|
10
|
+
type AgentImageToolConfig,
|
|
11
|
+
} from './agent-image-tools.ts'
|
|
12
|
+
import type { ImageGenerationRuntime } from './generation-runtime.ts'
|
|
13
|
+
|
|
14
|
+
function isImageReference(value: unknown): value is ImageAttachmentRef {
|
|
15
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
|
16
|
+
const ref = value as Record<string, unknown>
|
|
17
|
+
return typeof ref.attachmentId === 'string'
|
|
18
|
+
&& (ref.mediaType === 'image/png' || ref.mediaType === 'image/jpeg' || ref.mediaType === 'image/webp' || ref.mediaType === 'image/gif')
|
|
19
|
+
&& Number.isInteger(ref.bytes) && (ref.bytes as number) > 0
|
|
20
|
+
&& Number.isInteger(ref.width) && (ref.width as number) > 0
|
|
21
|
+
&& Number.isInteger(ref.height) && (ref.height as number) > 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function imageInContent(value: unknown): ImageAttachmentRef | undefined {
|
|
25
|
+
if (!Array.isArray(value)) return undefined
|
|
26
|
+
for (let index = value.length - 1; index >= 0; index -= 1) {
|
|
27
|
+
const block = value[index]
|
|
28
|
+
if (typeof block !== 'object' || block === null || Array.isArray(block)) continue
|
|
29
|
+
const raw = block as Record<string, unknown>
|
|
30
|
+
if (raw.type === 'image' && isImageReference(raw.attachment)) return raw.attachment
|
|
31
|
+
if (raw.type === 'tool-result') {
|
|
32
|
+
const nested = imageInContent(raw.content)
|
|
33
|
+
if (nested !== undefined) return nested
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Pick the newest image explicitly attached to this command invocation. */
|
|
40
|
+
function imageInInvocation(value: unknown): ImageAttachmentRef | undefined {
|
|
41
|
+
if (!Array.isArray(value)) return undefined
|
|
42
|
+
for (let index = value.length - 1; index >= 0; index -= 1) {
|
|
43
|
+
const block = value[index]
|
|
44
|
+
if (typeof block !== 'object' || block === null || Array.isArray(block)) continue
|
|
45
|
+
const raw = block as Record<string, unknown>
|
|
46
|
+
if (raw.type === 'image' && isImageReference(raw.attachment)) return raw.attachment
|
|
47
|
+
}
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Find the newest durable image reference, including nested tool results. */
|
|
52
|
+
export function latestSessionImage(messages: readonly unknown[]): ImageAttachmentRef | undefined {
|
|
53
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
54
|
+
const message = messages[index]
|
|
55
|
+
if (typeof message !== 'object' || message === null || Array.isArray(message)) continue
|
|
56
|
+
const image = imageInContent((message as { content?: unknown }).content)
|
|
57
|
+
if (image !== undefined) return image
|
|
58
|
+
}
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function commandError(error: unknown): CommandResult {
|
|
63
|
+
const text = error instanceof Error ? error.message : String(error)
|
|
64
|
+
return { kind: 'error', text: text.trim() === '' ? '图片编辑失败。' : text }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Register the host-side command; it never sends the command line to a chat model. */
|
|
68
|
+
export function registerEditImageCommand(
|
|
69
|
+
ctx: Context,
|
|
70
|
+
runtime: ImageGenerationRuntime,
|
|
71
|
+
resolve: () => AgentImageToolConfig,
|
|
72
|
+
pendingImages?: {
|
|
73
|
+
get: (sessionId: string) => ImageAttachmentRef | undefined
|
|
74
|
+
consume: (sessionId: string, ref: ImageAttachmentRef) => void
|
|
75
|
+
},
|
|
76
|
+
): () => void {
|
|
77
|
+
return ctx.commands.register({
|
|
78
|
+
name: 'edit_image',
|
|
79
|
+
description: 'Edit the latest image in this conversation with the plugin image model',
|
|
80
|
+
// `images` is understood by the newer host command protocol. Keep the
|
|
81
|
+
// source compatible with older development-only command typings.
|
|
82
|
+
input: { hint: 'Describe how to modify the latest image', images: true } as { hint: string; images: boolean },
|
|
83
|
+
async handler(invocation): Promise<CommandResult> {
|
|
84
|
+
const prompt = invocation.rawInput.trim()
|
|
85
|
+
if (prompt === '') return { kind: 'error', text: '请提供图片修改描述,例如:/edit_image 把背景改成夜景' }
|
|
86
|
+
const invocationImage = imageInInvocation((invocation as typeof invocation & { attachments?: readonly unknown[] }).attachments)
|
|
87
|
+
// A staged composer image is newer than anything already committed to
|
|
88
|
+
// the session, so it must win when the user explicitly adds a preview
|
|
89
|
+
// or gallery image before running the command.
|
|
90
|
+
const pendingImage = pendingImages?.get(String(invocation.agent.id))
|
|
91
|
+
const durableImage = latestSessionImage(invocation.agent.session.deriveMessages())
|
|
92
|
+
const sourceImage = invocationImage ?? pendingImage ?? durableImage
|
|
93
|
+
if (sourceImage === undefined) return { kind: 'error', text: '当前对话没有可用图片,请先上传图片或把画廊图片加入对话。' }
|
|
94
|
+
try {
|
|
95
|
+
const task = await submitAgentImageEdit(ctx.attachments as Pick<AttachmentStore, 'readImage'>, runtime, resolve, {
|
|
96
|
+
prompt,
|
|
97
|
+
sourceImage,
|
|
98
|
+
signal: invocation.signal,
|
|
99
|
+
})
|
|
100
|
+
if (task.status === 'completed') {
|
|
101
|
+
if (pendingImage !== undefined) pendingImages?.consume(String(invocation.agent.id), pendingImage)
|
|
102
|
+
return { kind: 'success', text: '图片编辑已完成,可在 AI 生图面板查看结果。' }
|
|
103
|
+
}
|
|
104
|
+
return { kind: 'error', text: task.error ?? `图片编辑${task.status === 'cancelled' ? '已取消' : '失败'}。` }
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return commandError(error)
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
}
|
package/src/engine.ts
CHANGED
|
@@ -79,6 +79,15 @@ function isSeedream(model: string): boolean {
|
|
|
79
79
|
return modelFamily(model) === 'seedream'
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** Whether the model uses the official Zhipu image-generation contract. */
|
|
83
|
+
function isZhipuImage(model: string): boolean {
|
|
84
|
+
return modelFamily(model) === 'zhipu'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isGlmImage(model: string): boolean {
|
|
88
|
+
return /^glm-image(?:-|$)/i.test(model.trim())
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
/** Whether this is the official Volcengine Ark model naming convention. */
|
|
83
92
|
function isVolcSeedream(model: string): boolean {
|
|
84
93
|
return /^doubao-seedream(?:-|$)/i.test(model.trim())
|
|
@@ -163,6 +172,22 @@ function bareBase64(value: string): string {
|
|
|
163
172
|
return parsed !== undefined && parsed.base64 !== undefined ? parsed.base64 : value
|
|
164
173
|
}
|
|
165
174
|
|
|
175
|
+
/** Whether a result URL carries cloud-storage signing credentials. */
|
|
176
|
+
function isPresignedUrl(value: string): boolean {
|
|
177
|
+
let url: URL
|
|
178
|
+
try {
|
|
179
|
+
url = new URL(value)
|
|
180
|
+
} catch {
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
const params = new Set(Array.from(url.searchParams.keys(), key => key.toLowerCase()))
|
|
184
|
+
if (params.has('x-goog-signature') || params.has('x-goog-credential')) return true
|
|
185
|
+
if (params.has('x-amz-signature') || params.has('x-amz-credential')) return true
|
|
186
|
+
return params.has('signature') && (
|
|
187
|
+
params.has('expires') || params.has('googleaccessid') || params.has('awsaccesskeyid')
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
166
191
|
/** Clamp the requested image count into the API-accepted range. */
|
|
167
192
|
function clampCount(n: number): number {
|
|
168
193
|
if (!Number.isFinite(n)) return 1
|
|
@@ -234,6 +259,18 @@ function effectiveParams(request: GenerateRequest): {
|
|
|
234
259
|
response_format: isVolcSeedream(model) ? 'url' : 'b64_json',
|
|
235
260
|
}
|
|
236
261
|
}
|
|
262
|
+
// Zhipu's official image API accepts OpenAI-style JSON but uses its own
|
|
263
|
+
// quality vocabulary. GLM-Image currently supports hd only; CogView uses
|
|
264
|
+
// the standard tier. Size remains a valid custom pixel size for both.
|
|
265
|
+
if (isZhipuImage(model)) {
|
|
266
|
+
return {
|
|
267
|
+
model,
|
|
268
|
+
...request.size !== '' && request.size !== 'auto' && OPENAI_SIZE_BY_RATIO[request.size] !== undefined
|
|
269
|
+
? { size: OPENAI_SIZE_BY_RATIO[request.size] }
|
|
270
|
+
: {},
|
|
271
|
+
quality: isGlmImage(model) ? 'hd' : 'standard',
|
|
272
|
+
}
|
|
273
|
+
}
|
|
237
274
|
// OpenAI-compatible endpoints: nearest pixel size, clarity tiers mapped to
|
|
238
275
|
// the quality levels (1k→low / 2k→medium / 4k→high), detail passthrough.
|
|
239
276
|
return {
|
|
@@ -261,9 +298,11 @@ async function normalizeItem(
|
|
|
261
298
|
upstream: UpstreamConfig,
|
|
262
299
|
): Promise<{ b64: string; mime: string; revisedPrompt?: string }> {
|
|
263
300
|
const revisedPrompt = typeof item.revised_prompt === 'string' ? item.revised_prompt : undefined
|
|
264
|
-
if (typeof item.b64_json === 'string') {
|
|
301
|
+
if (typeof item.b64_json === 'string' && item.b64_json.trim() !== '') {
|
|
265
302
|
const b64 = bareBase64(item.b64_json)
|
|
266
|
-
|
|
303
|
+
if (b64.trim() !== '') {
|
|
304
|
+
return { b64, mime: detectImageMime(Buffer.from(b64, 'base64')) ?? 'image/png', revisedPrompt }
|
|
305
|
+
}
|
|
267
306
|
}
|
|
268
307
|
if (typeof item.url !== 'string' || item.url === '') {
|
|
269
308
|
throw new ImageGenError('upstream image item has neither b64_json nor url')
|
|
@@ -278,9 +317,9 @@ async function normalizeItem(
|
|
|
278
317
|
let response: Response
|
|
279
318
|
try {
|
|
280
319
|
response = await fetch(url, {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
320
|
+
...isPresignedUrl(url) || upstream.apiKey === ''
|
|
321
|
+
? {}
|
|
322
|
+
: { headers: { authorization: `Bearer ${upstream.apiKey}` } },
|
|
284
323
|
signal: budget.signal,
|
|
285
324
|
})
|
|
286
325
|
} catch (error) {
|
|
@@ -443,6 +482,9 @@ export async function generateImage(upstream: UpstreamConfig, request: GenerateR
|
|
|
443
482
|
const baseUrl = upstream.apiUrl.trim().replace(/\/+$/, '')
|
|
444
483
|
if (baseUrl === '') throw new ImageGenError('api_url 未配置:请先在「设置 → 插件 → 可配置」中填写', 'config-missing')
|
|
445
484
|
if (upstream.apiKey.trim() === '') throw new ImageGenError('api_key 未配置:请先在「设置 → 插件 → 可配置」中填写', 'config-missing')
|
|
485
|
+
if (request.mode === 'edit' && isZhipuImage(wireModel(request))) {
|
|
486
|
+
throw new ImageGenError('智谱 GLM-Image 当前仅支持文生图,请切换到文生图模式或选择支持图生图的模型', 'edit-unsupported')
|
|
487
|
+
}
|
|
446
488
|
const params = effectiveParams(request)
|
|
447
489
|
const count = effectiveCount(request)
|
|
448
490
|
const batches = await Promise.all(
|
package/src/gallery-store.ts
CHANGED
|
@@ -63,6 +63,11 @@ interface StoredEntry {
|
|
|
63
63
|
tags?: string[]
|
|
64
64
|
channelId?: string
|
|
65
65
|
channel?: string
|
|
66
|
+
workflow?: 'ecommerce'
|
|
67
|
+
projectId?: string
|
|
68
|
+
projectName?: string
|
|
69
|
+
slotKey?: string
|
|
70
|
+
slotLabel?: string
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
/** The index.json shape. */
|
|
@@ -140,6 +145,11 @@ function isStoredEntry(value: unknown): value is StoredEntry {
|
|
|
140
145
|
return typeof entry.id === 'string'
|
|
141
146
|
&& typeof entry.createdAt === 'number'
|
|
142
147
|
&& (entry.mode === 'text' || entry.mode === 'edit')
|
|
148
|
+
&& (entry.workflow === undefined || entry.workflow === 'ecommerce')
|
|
149
|
+
&& (entry.projectId === undefined || typeof entry.projectId === 'string')
|
|
150
|
+
&& (entry.projectName === undefined || typeof entry.projectName === 'string')
|
|
151
|
+
&& (entry.slotKey === undefined || typeof entry.slotKey === 'string')
|
|
152
|
+
&& (entry.slotLabel === undefined || typeof entry.slotLabel === 'string')
|
|
143
153
|
&& typeof entry.prompt === 'string'
|
|
144
154
|
&& Array.isArray(entry.images)
|
|
145
155
|
&& entry.images.every(image => {
|
|
@@ -177,6 +187,11 @@ function toWire(entry: StoredEntry): HistoryEntry {
|
|
|
177
187
|
...entry.tags === undefined ? {} : { tags: entry.tags },
|
|
178
188
|
...entry.channel === undefined ? {} : { channel: entry.channel },
|
|
179
189
|
...entry.channelId === undefined ? {} : { channelId: entry.channelId },
|
|
190
|
+
...entry.workflow === undefined ? {} : { workflow: entry.workflow },
|
|
191
|
+
...entry.projectId === undefined ? {} : { projectId: entry.projectId },
|
|
192
|
+
...entry.projectName === undefined ? {} : { projectName: entry.projectName },
|
|
193
|
+
...entry.slotKey === undefined ? {} : { slotKey: entry.slotKey },
|
|
194
|
+
...entry.slotLabel === undefined ? {} : { slotLabel: entry.slotLabel },
|
|
180
195
|
}
|
|
181
196
|
}
|
|
182
197
|
|
|
@@ -231,6 +246,11 @@ export async function appendGallery(input: HistoryEntryInput): Promise<GalleryAp
|
|
|
231
246
|
...input.refName === undefined ? {} : { refName: input.refName },
|
|
232
247
|
...input.channelId === undefined ? {} : { channelId: input.channelId },
|
|
233
248
|
...input.channel === undefined ? {} : { channel: input.channel },
|
|
249
|
+
...input.workflow === undefined ? {} : { workflow: input.workflow },
|
|
250
|
+
...input.projectId === undefined ? {} : { projectId: input.projectId },
|
|
251
|
+
...input.projectName === undefined ? {} : { projectName: input.projectName },
|
|
252
|
+
...input.slotKey === undefined ? {} : { slotKey: input.slotKey },
|
|
253
|
+
...input.slotLabel === undefined ? {} : { slotLabel: input.slotLabel },
|
|
234
254
|
}
|
|
235
255
|
const merged = [entry, ...await readIndex()]
|
|
236
256
|
await writeIndex(merged)
|
|
@@ -38,7 +38,9 @@ export class ImageGenerationRuntime {
|
|
|
38
38
|
private readonly resolve: () => ChannelsView,
|
|
39
39
|
private readonly history: HistorySink = { append: appendHistory },
|
|
40
40
|
) {
|
|
41
|
-
|
|
41
|
+
// A comparison can contain up to four models; let those tasks run at the
|
|
42
|
+
// same time while still applying a small host-wide concurrency limit.
|
|
43
|
+
this.queue = new GenerationTaskQueue((request, signal) => this.run(request, signal), 4)
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
async run(request: GenerateRequest, signal?: AbortSignal): Promise<GenerateResult> {
|
|
@@ -66,10 +68,17 @@ export class ImageGenerationRuntime {
|
|
|
66
68
|
...request.refName === undefined ? {} : { refName: request.refName },
|
|
67
69
|
...request.channelId === undefined ? {} : { channelId: request.channelId },
|
|
68
70
|
...request.channel === undefined ? {} : { channel: request.channel },
|
|
71
|
+
...request.comparisonId === undefined ? {} : { comparisonId: request.comparisonId },
|
|
72
|
+
...request.comparisonModels === undefined ? {} : { comparisonModels: request.comparisonModels },
|
|
73
|
+
...request.workflow === undefined ? {} : { workflow: request.workflow },
|
|
74
|
+
...request.projectId === undefined ? {} : { projectId: request.projectId },
|
|
75
|
+
...request.projectName === undefined ? {} : { projectName: request.projectName },
|
|
76
|
+
...request.slotKey === undefined ? {} : { slotKey: request.slotKey },
|
|
77
|
+
...request.slotLabel === undefined ? {} : { slotLabel: request.slotLabel },
|
|
69
78
|
})
|
|
70
79
|
return { ...result, history }
|
|
71
80
|
} catch (error) {
|
|
72
81
|
return { ...result, historyError: error instanceof Error ? error.message : String(error) }
|
|
73
82
|
}
|
|
74
83
|
}
|
|
75
|
-
}
|
|
84
|
+
}
|