@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
@@ -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, ISessions, SessionId, ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client'
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 css from './image-toolview.module.css'
7
-
8
- /** Owner props supplied by the host's keyed tool-call slot. */
9
- export interface ImageToolViewOwnerProps {
10
- callId: string
11
- toolName: string
12
- block: ToolCallBlock
13
- cwd?: string
14
- home?: string
15
- openFile: (path: string) => void
16
- inspect?: () => void
17
- }
18
-
19
- interface LoadedImage {
20
- ref: ImageAttachmentRef
21
- src: string
22
- }
23
-
24
- interface ImageToolViewProps extends ImageToolViewOwnerProps {
25
- sessionId: SessionId
26
- }
27
-
28
- function isSettled(block: ToolCallBlock): block is Extract<ToolCallBlock, { kind: 'tool-result' }> {
29
- return 'kind' in block
30
- }
31
-
32
- function imageRefsOf(block: ToolCallBlock): ImageAttachmentRef[] {
33
- if (!isSettled(block)) return []
34
- return block.content.flatMap(content => content.type === 'image' ? [content.attachment] : [])
35
- }
36
-
37
- function textOf(block: ToolCallBlock): string {
38
- if (!isSettled(block)) return ''
39
- return block.content
40
- .filter(content => content.type === 'text')
41
- .map(content => content.text)
42
- .join('\n')
43
- }
44
-
45
- function resultInfo(block: ToolCallBlock): { status: string; message: string } {
46
- if (!isSettled(block)) return { status: 'running', message: '正在生成图片…' }
47
- const text = textOf(block)
48
- try {
49
- const parsed = JSON.parse(text) as { status?: unknown; message?: unknown }
50
- return {
51
- status: typeof parsed.status === 'string' ? parsed.status : block.isError ? 'failed' : 'completed',
52
- message: typeof parsed.message === 'string' ? parsed.message : '',
53
- }
54
- } catch {
55
- return { status: block.isError ? 'failed' : 'completed', message: text }
56
- }
57
- }
58
-
59
- function statusLabel(status: string): string {
60
- if (status === 'running' || status === 'queued') return '生成中'
61
- if (status === 'failed') return '生成失败'
62
- if (status === 'cancelled') return '已取消'
63
- return '图片结果'
64
- }
65
-
66
- function useAttachmentImages(
67
- sessionId: SessionId,
68
- refs: ImageAttachmentRef[],
69
- load: (sessionId: SessionId, ref: ImageAttachmentRef) => Promise<string>,
70
- ): { images: LoadedImage[]; error: string | null } {
71
- const key = useMemo(() => refs.map(ref => String(ref.attachmentId)).join('|'), [refs])
72
- const [images, setImages] = useState<LoadedImage[]>([])
73
- const [error, setError] = useState<string | null>(null)
74
-
75
- useEffect(() => {
76
- let disposed = false
77
- const urls: string[] = []
78
- const revoke = (): void => {
79
- for (const url of urls) URL.revokeObjectURL(url)
80
- urls.length = 0
81
- }
82
-
83
- setImages([])
84
- setError(null)
85
- if (refs.length === 0) return () => { /* no attachments to clean up */ }
86
-
87
- void Promise.all(refs.map(async ref => {
88
- const src = await load(sessionId, ref)
89
- urls.push(src)
90
- return { ref, src }
91
- }))
92
- .then(next => {
93
- if (!disposed) setImages(next)
94
- })
95
- .catch(errorValue => {
96
- revoke()
97
- if (!disposed) setError(errorValue instanceof Error ? errorValue.message : String(errorValue))
98
- })
99
-
100
- return () => {
101
- disposed = true
102
- revoke()
103
- }
104
- }, [key, load, refs, sessionId])
105
-
106
- return { images, error }
107
- }
108
-
109
- /** Register the inline image result view for all image-generation result tools. */
110
- export function registerImageToolviews(ctx: ClientContext): void {
111
- // Older client typings still expose the host-side SessionStore on the
112
- // generic Context property. The runtime service itself provides the newer
113
- // binding/session face, so resolve it through Cordis and narrow locally.
114
- const sessions = ctx.get('sessions') as unknown as ISessions | undefined
115
- const load = async (sessionId: SessionId, ref: ImageAttachmentRef): Promise<string> => {
116
- const session = sessions?.binding(sessionId)?.session
117
- if (session === undefined) throw new Error('当前会话不可用,无法读取图片附件。')
118
- const result = await session.readAttachment(ref.attachmentId)
119
- if (!result.ok) throw new Error(result.error.message)
120
- const blob = new Blob([new Uint8Array(result.value.data)], { type: result.value.attachment.mediaType })
121
- return URL.createObjectURL(blob)
122
- }
123
-
124
- const ImageToolView = (props: ImageToolViewProps): React.JSX.Element => {
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
- return <section className={css.root} data-state={status} data-tool={props.toolName}>
130
- <header className={css.header}>
131
- <span className={css.icon} aria-hidden="true">▧</span>
132
- <strong>{props.toolName}</strong>
133
- <span className={css.status}>{statusLabel(status)}</span>
134
- </header>
135
- {message !== '' && <p className={css.message}>{message}</p>}
136
- {images.length > 0 && <div className={css.images}>
137
- {images.map(image => <a
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
- ctx.slots.inject('tool.call.toolview', function* () {
154
- for (const key of ['generate_image', 'edit_image', 'get_image_generation_task']) {
155
- yield ctx.slots.register({ name: 'tool.call.toolview', key }, ImageToolView)
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
+ }
@@ -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 { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
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 disposers: Array<() => void> = []
108
- try {
109
- disposers.push(mountSidebarEntry(controller, tt('entry.label'), tt('entry.tooltip')))
110
- disposers.push(mountPanel(controller, api, scope))
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)