@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.
- package/LICENSE +201 -201
- package/README.md +203 -181
- package/cordis.patch.yml +8 -8
- package/docs/images/multi-model-comparison.png +0 -0
- package/lib/client.js +2711 -1318
- package/lib/client.js.map +1 -1
- package/lib/index.js +830 -155
- package/package.json +70 -68
- package/src/agent-image-tools.ts +418 -316
- package/src/client/ImageGenPanel.tsx +1703 -1476
- package/src/client/SettingsCard.tsx +936 -648
- package/src/client/TemplateLibrary.tsx +336 -336
- package/src/client/api.ts +193 -193
- package/src/client/channels-form.ts +263 -0
- package/src/client/controller.ts +46 -46
- package/src/client/conversation-sync.ts +14 -0
- package/src/client/css-modules.d.ts +5 -5
- package/src/client/helpers.ts +33 -33
- package/src/client/image-toolview.module.css +73 -73
- package/src/client/image-toolview.tsx +170 -152
- package/src/client/index.ts +32 -22
- package/src/client/locales.ts +610 -484
- package/src/client/mount.tsx +185 -96
- package/src/client/panel.module.css +1713 -1445
- package/src/client/settings-card.module.css +1023 -536
- package/src/client/settings-form.ts +336 -336
- package/src/client/settings-scope.ts +298 -250
- package/src/client/sidebar-entry.ts +148 -102
- package/src/client/templates.module.css +453 -453
- package/src/engine.ts +520 -464
- package/src/gallery-store.ts +286 -280
- package/src/generation-runtime.ts +79 -48
- package/src/history-store.ts +250 -238
- package/src/image-format.ts +11 -0
- package/src/image-models.ts +19 -19
- package/src/index.ts +318 -212
- package/src/model-catalog.ts +115 -0
- package/src/presets.ts +71 -0
- package/src/prompt-enhancer.ts +137 -79
- package/src/protocol.ts +338 -253
- package/src/routes.ts +916 -738
- package/src/task-queue.ts +113 -103
- package/src/templates/cases.json +10196 -10196
- package/src/templates-store.ts +278 -278
- package/src/updater.ts +117 -117
|
@@ -1,158 +1,176 @@
|
|
|
1
|
-
/** Inline renderer for image-generation tool-result attachments. */
|
|
2
|
-
|
|
3
|
-
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
|
4
|
-
import type { ClientContext,
|
|
1
|
+
/** Inline renderer for image-generation tool-result attachments. */
|
|
2
|
+
|
|
3
|
+
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
|
4
|
+
import type { ClientContext, SessionId, ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client'
|
|
5
5
|
import { useEffect, useMemo, useState } from 'react'
|
|
6
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!disposed)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const load = async (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
6
|
+
import { AGENT_IMAGE_API } from '../protocol.ts'
|
|
7
|
+
import { CHAT_IMAGE_EVENT } from './conversation-sync.ts'
|
|
8
|
+
import css from './image-toolview.module.css'
|
|
9
|
+
|
|
10
|
+
/** Owner props supplied by the host's keyed tool-call slot. */
|
|
11
|
+
export interface ImageToolViewOwnerProps {
|
|
12
|
+
callId: string
|
|
13
|
+
toolName: string
|
|
14
|
+
block: ToolCallBlock
|
|
15
|
+
cwd?: string
|
|
16
|
+
home?: string
|
|
17
|
+
openFile: (path: string) => void
|
|
18
|
+
inspect?: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface LoadedImage {
|
|
22
|
+
ref: ImageAttachmentRef
|
|
23
|
+
src: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ImageToolViewProps extends ImageToolViewOwnerProps {
|
|
27
|
+
sessionId: SessionId
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isSettled(block: ToolCallBlock): block is Extract<ToolCallBlock, { kind: 'tool-result' }> {
|
|
31
|
+
return 'kind' in block
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function imageRefsOf(block: ToolCallBlock): ImageAttachmentRef[] {
|
|
35
|
+
if (!isSettled(block)) return []
|
|
36
|
+
const resultContent = block.resultView?.card === 'generic' ? block.resultView.content ?? [] : []
|
|
37
|
+
return [...block.content, ...resultContent]
|
|
38
|
+
.flatMap(content => content.type === 'image' ? [content.attachment] : [])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function textOf(block: ToolCallBlock): string {
|
|
42
|
+
if (!isSettled(block)) return ''
|
|
43
|
+
return block.content
|
|
44
|
+
.filter(content => content.type === 'text')
|
|
45
|
+
.map(content => content.text)
|
|
46
|
+
.join('\n')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resultInfo(block: ToolCallBlock): { status: string; message: string } {
|
|
50
|
+
if (!isSettled(block)) return { status: 'running', message: '正在生成图片…' }
|
|
51
|
+
const text = textOf(block)
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(text) as { status?: unknown; message?: unknown }
|
|
54
|
+
return {
|
|
55
|
+
status: typeof parsed.status === 'string' ? parsed.status : block.isError ? 'failed' : 'completed',
|
|
56
|
+
message: typeof parsed.message === 'string' ? parsed.message : '',
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
return { status: block.isError ? 'failed' : 'completed', message: text }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function statusLabel(status: string): string {
|
|
64
|
+
if (status === 'running' || status === 'queued') return '生成中'
|
|
65
|
+
if (status === 'failed') return '生成失败'
|
|
66
|
+
if (status === 'cancelled') return '已取消'
|
|
67
|
+
return '图片结果'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function useAttachmentImages(
|
|
71
|
+
sessionId: SessionId,
|
|
72
|
+
refs: ImageAttachmentRef[],
|
|
73
|
+
load: (sessionId: SessionId, ref: ImageAttachmentRef) => Promise<string>,
|
|
74
|
+
): { images: LoadedImage[]; error: string | null } {
|
|
75
|
+
const key = useMemo(() => refs.map(ref => String(ref.attachmentId)).join('|'), [refs])
|
|
76
|
+
const [images, setImages] = useState<LoadedImage[]>([])
|
|
77
|
+
const [error, setError] = useState<string | null>(null)
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
let disposed = false
|
|
81
|
+
const urls: string[] = []
|
|
82
|
+
const revoke = (): void => {
|
|
83
|
+
for (const url of urls) URL.revokeObjectURL(url)
|
|
84
|
+
urls.length = 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setImages([])
|
|
88
|
+
setError(null)
|
|
89
|
+
if (refs.length === 0) return () => { /* no attachments to clean up */ }
|
|
90
|
+
|
|
91
|
+
void Promise.all(refs.map(async ref => {
|
|
92
|
+
const src = await load(sessionId, ref)
|
|
93
|
+
urls.push(src)
|
|
94
|
+
return { ref, src }
|
|
95
|
+
}))
|
|
96
|
+
.then(next => {
|
|
97
|
+
if (!disposed) setImages(next)
|
|
98
|
+
})
|
|
99
|
+
.catch(errorValue => {
|
|
100
|
+
revoke()
|
|
101
|
+
if (!disposed) setError(errorValue instanceof Error ? errorValue.message : String(errorValue))
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return () => {
|
|
105
|
+
disposed = true
|
|
106
|
+
revoke()
|
|
107
|
+
}
|
|
108
|
+
}, [key, load, refs, sessionId])
|
|
109
|
+
|
|
110
|
+
return { images, error }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Register the inline image result view for all image-generation result tools. */
|
|
114
|
+
export function registerImageToolviews(ctx: ClientContext): void {
|
|
115
|
+
const load = async (_sessionId: SessionId, ref: ImageAttachmentRef): Promise<string> => {
|
|
116
|
+
// Tool-result images intentionally do not occur in model-visible session
|
|
117
|
+
// content, so session.readAttachment() rejects them. The plugin route
|
|
118
|
+
// reads the same durable attachment after validating the complete ref.
|
|
119
|
+
const query = new URLSearchParams({
|
|
120
|
+
attachment_id: String(ref.attachmentId),
|
|
121
|
+
media_type: ref.mediaType,
|
|
122
|
+
bytes: String(ref.bytes),
|
|
123
|
+
width: String(ref.width),
|
|
124
|
+
height: String(ref.height),
|
|
125
|
+
})
|
|
126
|
+
const response = await fetch(`${AGENT_IMAGE_API}?${query.toString()}`)
|
|
127
|
+
if (!response.ok) throw new Error(`无法读取图片附件(HTTP ${response.status})。`)
|
|
128
|
+
const blob = await response.blob()
|
|
129
|
+
return URL.createObjectURL(blob)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const ImageToolView = (props: ImageToolViewProps): React.JSX.Element => {
|
|
125
133
|
const refs = useMemo(() => imageRefsOf(props.block), [props.block])
|
|
126
134
|
const { status, message } = resultInfo(props.block)
|
|
127
135
|
const { images, error } = useAttachmentImages(props.sessionId, refs, load)
|
|
128
136
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
className={css.imageLink}
|
|
139
|
-
href={image.src}
|
|
140
|
-
key={String(image.ref.attachmentId)}
|
|
141
|
-
rel="noreferrer"
|
|
142
|
-
target="_blank"
|
|
143
|
-
title="打开原图"
|
|
144
|
-
>
|
|
145
|
-
<img className={css.image} src={image.src} alt={image.ref.name ?? '生成图片'} />
|
|
146
|
-
</a>)}
|
|
147
|
-
</div>}
|
|
148
|
-
{refs.length > 0 && images.length === 0 && error === null && <p className={css.loading}>正在加载图片…</p>}
|
|
149
|
-
{error !== null && <p className={css.error}>{error}</p>}
|
|
150
|
-
</section>
|
|
151
|
-
}
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (images.length === 0) return
|
|
139
|
+
document.dispatchEvent(new CustomEvent(CHAT_IMAGE_EVENT, {
|
|
140
|
+
detail: {
|
|
141
|
+
sessionId: props.sessionId,
|
|
142
|
+
refs: images.map(image => image.ref),
|
|
143
|
+
},
|
|
144
|
+
}))
|
|
145
|
+
}, [images, props.sessionId])
|
|
152
146
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
147
|
+
return <section className={css.root} data-state={status} data-tool={props.toolName}>
|
|
148
|
+
<header className={css.header}>
|
|
149
|
+
<span className={css.icon} aria-hidden="true">▧</span>
|
|
150
|
+
<strong>{props.toolName}</strong>
|
|
151
|
+
<span className={css.status}>{statusLabel(status)}</span>
|
|
152
|
+
</header>
|
|
153
|
+
{message !== '' && <p className={css.message}>{message}</p>}
|
|
154
|
+
{images.length > 0 && <div className={css.images}>
|
|
155
|
+
{images.map(image => <a
|
|
156
|
+
className={css.imageLink}
|
|
157
|
+
href={image.src}
|
|
158
|
+
key={String(image.ref.attachmentId)}
|
|
159
|
+
rel="noreferrer"
|
|
160
|
+
target="_blank"
|
|
161
|
+
title="打开原图"
|
|
162
|
+
>
|
|
163
|
+
<img className={css.image} src={image.src} alt={image.ref.name ?? '生成图片'} />
|
|
164
|
+
</a>)}
|
|
165
|
+
</div>}
|
|
166
|
+
{refs.length > 0 && images.length === 0 && error === null && <p className={css.loading}>正在加载图片…</p>}
|
|
167
|
+
{error !== null && <p className={css.error}>{error}</p>}
|
|
168
|
+
</section>
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
ctx.slots.inject('tool.call.toolview', function* () {
|
|
172
|
+
for (const key of ['generate_image', 'edit_image', 'get_image_generation_task']) {
|
|
173
|
+
yield ctx.slots.register({ name: 'tool.call.toolview', key }, ImageToolView)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* whole boot when a plugin apply throws, and an external plugin must not take
|
|
12
12
|
* the GUI down.
|
|
13
13
|
*/
|
|
14
|
-
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
15
|
-
import type {
|
|
14
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
15
|
+
import type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'
|
|
16
|
+
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
|
16
17
|
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
|
17
18
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
18
19
|
// Type-only: pulls the LocaleNamespaceMap merge table.
|
|
@@ -23,9 +24,10 @@ import { tt } from './helpers.ts'
|
|
|
23
24
|
import { en, zh, type ImageGenKey } from './locales.ts'
|
|
24
25
|
import { mountPanel } from './mount.tsx'
|
|
25
26
|
import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
26
|
-
import { ImageGenSettingsCard, ImageGenSettingsCardController } from './SettingsCard.tsx'
|
|
27
|
-
import { bindImageGenScope, type ImageGenScope } from './settings-scope.ts'
|
|
27
|
+
import { ImageGenSettingsCard, ImageGenSettingsCardController } from './SettingsCard.tsx'
|
|
28
|
+
import { bindImageGenScope, type ImageGenScope } from './settings-scope.ts'
|
|
28
29
|
import { registerImageToolviews, type ImageToolViewOwnerProps } from './image-toolview.tsx'
|
|
30
|
+
import type { ConversationService } from './conversation-sync.ts'
|
|
29
31
|
|
|
30
32
|
/** Locale namespace this plugin owns. */
|
|
31
33
|
const NS = 'dsh-imagegen'
|
|
@@ -36,7 +38,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
36
38
|
'dsh-imagegen': ImageGenKey
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
interface SlotMap {
|
|
41
|
+
interface SlotMap {
|
|
40
42
|
/**
|
|
41
43
|
* The official plugin-configuration slot the Settings → Plugins →
|
|
42
44
|
* Configurable tab declares and renders. This card registers there as its
|
|
@@ -45,11 +47,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
45
47
|
* same shape so this package can register without depending on the
|
|
46
48
|
* sibling UI package.
|
|
47
49
|
*/
|
|
48
|
-
'settings.plugin.item': { kind: 'keyed'; scope: 'root'; owner: ImageGenPluginItemOwnerProps }
|
|
49
|
-
/** Image-generation results render their durable image blocks inline. */
|
|
50
|
-
'tool.call.toolview': { kind: 'keyed'; scope: 'session'; owner: ImageToolViewOwnerProps }
|
|
51
|
-
}
|
|
52
|
-
}
|
|
50
|
+
'settings.plugin.item': { kind: 'keyed'; scope: 'root'; owner: ImageGenPluginItemOwnerProps }
|
|
51
|
+
/** Image-generation results render their durable image blocks inline. */
|
|
52
|
+
'tool.call.toolview': { kind: 'keyed'; scope: 'session'; owner: ImageToolViewOwnerProps }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
53
55
|
|
|
54
56
|
/** Owner share of a plugin card (the section supplies nothing). */
|
|
55
57
|
export interface ImageGenPluginItemOwnerProps {
|
|
@@ -58,15 +60,15 @@ export interface ImageGenPluginItemOwnerProps {
|
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/** Required services (fiber inject waiting — the runtime must be up first). */
|
|
61
|
-
export const inject = ['slots', 'locale', 'connection', 'sessions']
|
|
63
|
+
export const inject = ['slots', 'locale', 'connection', 'sessions', 'conversation']
|
|
62
64
|
|
|
63
65
|
/**
|
|
64
66
|
* Mount the studio, its sidebar entry, and the settings card.
|
|
65
67
|
* @param ctx - client root context (services: slots, locale, connection).
|
|
66
68
|
*/
|
|
67
|
-
export function apply(ctx: ClientContext): void {
|
|
68
|
-
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-imagegen: dictionaries')
|
|
69
|
-
registerImageToolviews(ctx)
|
|
69
|
+
export function apply(ctx: ClientContext): void {
|
|
70
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-imagegen: dictionaries')
|
|
71
|
+
registerImageToolviews(ctx)
|
|
70
72
|
|
|
71
73
|
const connection = ctx.get('connection') as ConnectionHandle | undefined
|
|
72
74
|
const loopback = connection?.isLoopback === true
|
|
@@ -100,14 +102,22 @@ export function apply(ctx: ClientContext): void {
|
|
|
100
102
|
// the scope is still loading, the composition default is unknown, so nothing
|
|
101
103
|
// mounts yet. Only an unavailable scope falls back to the default (enabled).
|
|
102
104
|
let uiDisposer: (() => void) | undefined
|
|
103
|
-
const mountUi = (): void => {
|
|
104
|
-
if (uiDisposer !== undefined) return
|
|
105
|
-
const controller = new ImageGenController()
|
|
106
|
-
const api = new ImageGenApi()
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
const mountUi = (): void => {
|
|
106
|
+
if (uiDisposer !== undefined) return
|
|
107
|
+
const controller = new ImageGenController()
|
|
108
|
+
const api = new ImageGenApi()
|
|
109
|
+
const sessions = ctx.get('sessions') as ISessions | undefined
|
|
110
|
+
const conversation = ctx.get('conversation') as ConversationService | undefined
|
|
111
|
+
const disposers: Array<() => void> = []
|
|
112
|
+
try {
|
|
113
|
+
disposers.push(mountSidebarEntry(
|
|
114
|
+
controller,
|
|
115
|
+
tt('entry.newSession'),
|
|
116
|
+
tt('entry.newSessionTooltip'),
|
|
117
|
+
tt('entry.image'),
|
|
118
|
+
tt('entry.tooltip'),
|
|
119
|
+
))
|
|
120
|
+
disposers.push(mountPanel(controller, api, scope, { sessions, conversation }))
|
|
111
121
|
} catch (error) {
|
|
112
122
|
// DOM failures degrade the studio, never the GUI.
|
|
113
123
|
console.warn('[dsh-imagegen] mount failed:', error)
|