@dickpy/dsh-imagegen 1.5.0 → 1.5.2
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 +11 -8
- package/docs/images/ecommerce-mode.png +0 -0
- package/docs/images/gallery-workspace.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/plugin-settings.png +0 -0
- package/docs/images/prompt-template-library.png +0 -0
- package/lib/client.js +824 -449
- package/lib/client.js.map +1 -1
- package/lib/index.js +496 -80
- package/package.json +23 -17
- package/src/client/ImageGenPanel.tsx +10 -8
- package/src/client/InspirationGallery.tsx +106 -0
- package/src/client/SettingsCard.tsx +1 -1
- package/src/client/TemplateLibrary.tsx +159 -48
- package/src/client/api.ts +41 -8
- package/src/client/channels-form.ts +1 -1
- package/src/client/conversation-sync.ts +1 -1
- package/src/client/image-toolview.tsx +16 -10
- package/src/client/index.ts +5 -4
- package/src/client/inspiration.module.css +148 -0
- package/src/client/locales.ts +34 -8
- package/src/client/mount.tsx +1 -1
- package/src/client/settings-form.ts +2 -1
- package/src/client/settings-scope.ts +9 -5
- package/src/client/templates.module.css +95 -0
- package/src/index.ts +26 -6
- package/src/protocol.ts +81 -7
- package/src/routes.ts +128 -12
- package/src/settings-compat.ts +60 -0
- package/src/template-favorites.ts +108 -0
- package/src/templates/canghe-cases.json +11126 -0
- package/src/templates-store.ts +179 -68
- package/docs/images/image-generation-studio-single.png +0 -0
- package/docs/images/image-generation-studio.png +0 -0
- package/docs/images/poster-features-16x9.png +0 -0
package/src/client/api.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* data access path the panel uses — plain fetch, same origin.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { CONVERSATION_IMAGE_API, GALLERY_API, GENERATE_API, HISTORY_API, PROMPT_ENHANCE_API, TASK_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type GenerationTask, type HistoryEntry, type HistoryEntryInput, type TemplateListResult, type TemplateRefreshResult, type UpdateInfo } from '../protocol.ts'
|
|
6
|
+
import { CONVERSATION_IMAGE_API, GALLERY_API, GENERATE_API, HISTORY_API, PROMPT_ENHANCE_API, TASK_API, TEMPLATE_FAVORITES_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type GenerationTask, type HistoryEntry, type HistoryEntryInput, type TemplateCase, type TemplateFavorite, type TemplateListResult, type TemplateRefreshResult, type TemplateSample, type UpdateInfo } from '../protocol.ts'
|
|
7
7
|
|
|
8
8
|
/** Error carrying the route's JSON error message. */
|
|
9
9
|
export class ImageGenApiError extends Error {
|
|
@@ -181,11 +181,12 @@ export class ImageGenApi {
|
|
|
181
181
|
return (await readEnvelope<{ ok: true; entries: HistoryEntry[] }>(response)).entries
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/** Fetch
|
|
185
|
-
async templatesList(): Promise<TemplateListResult> {
|
|
186
|
-
const response = await fetch(TEMPLATES_API.list, { method: 'POST' })
|
|
184
|
+
/** Fetch one template source's list (bundled snapshot or refreshed copy). */
|
|
185
|
+
async templatesList(sourceId: string): Promise<TemplateListResult> {
|
|
186
|
+
const response = await fetch(TEMPLATES_API.list, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ source: sourceId }) })
|
|
187
187
|
const body = await readEnvelope<TemplateListResult & { ok: true }>(response)
|
|
188
188
|
return {
|
|
189
|
+
sourceId: body.sourceId,
|
|
189
190
|
cases: body.cases,
|
|
190
191
|
total: body.total,
|
|
191
192
|
origin: body.origin,
|
|
@@ -194,10 +195,42 @@ export class ImageGenApi {
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
/** Re-download
|
|
198
|
-
async templatesRefresh(): Promise<TemplateRefreshResult> {
|
|
199
|
-
const response = await fetch(TEMPLATES_API.refresh, { method: 'POST' })
|
|
198
|
+
/** Re-download one template source's list from its upstream mirror (host-side). */
|
|
199
|
+
async templatesRefresh(sourceId: string): Promise<TemplateRefreshResult> {
|
|
200
|
+
const response = await fetch(TEMPLATES_API.refresh, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ source: sourceId }) })
|
|
200
201
|
const body = await readEnvelope<TemplateRefreshResult & { ok: true }>(response)
|
|
201
|
-
return { total: body.total, fetchedAt: body.fetchedAt }
|
|
202
|
+
return { sourceId: body.sourceId, total: body.total, fetchedAt: body.fetchedAt }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Draw random cases across every source (studio inspiration wall). */
|
|
206
|
+
async templatesSample(count: number): Promise<TemplateSample[]> {
|
|
207
|
+
const response = await fetch(TEMPLATES_API.sample, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ count }) })
|
|
208
|
+
return (await readEnvelope<{ ok: true; samples: TemplateSample[] }>(response)).samples
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** List the host-persisted template favorites. */
|
|
212
|
+
async favoritesList(): Promise<TemplateFavorite[]> {
|
|
213
|
+
const response = await fetch(TEMPLATE_FAVORITES_API.list, { method: 'POST' })
|
|
214
|
+
return (await readEnvelope<{ ok: true; favorites: TemplateFavorite[] }>(response)).favorites
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Star one template (the host keeps a full case snapshot). */
|
|
218
|
+
async favoritesAdd(sourceId: string, item: TemplateCase): Promise<TemplateFavorite[]> {
|
|
219
|
+
const response = await fetch(TEMPLATE_FAVORITES_API.add, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'content-type': 'application/json' },
|
|
222
|
+
body: JSON.stringify({ source: sourceId, case: item }),
|
|
223
|
+
})
|
|
224
|
+
return (await readEnvelope<{ ok: true; favorites: TemplateFavorite[] }>(response)).favorites
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Unstar one template by its favorites key. */
|
|
228
|
+
async favoritesRemove(key: string): Promise<TemplateFavorite[]> {
|
|
229
|
+
const response = await fetch(TEMPLATE_FAVORITES_API.remove, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: { 'content-type': 'application/json' },
|
|
232
|
+
body: JSON.stringify({ key }),
|
|
233
|
+
})
|
|
234
|
+
return (await readEnvelope<{ ok: true; favorites: TemplateFavorite[] }>(response)).favorites
|
|
202
235
|
}
|
|
203
236
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - path ops never navigate *inside* the channels array.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-
|
|
15
|
+
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
16
16
|
import type { ChannelConfig, ModelMapping } from '../protocol.ts'
|
|
17
17
|
import type { ImageGenScope, SettingsOp } from './settings-scope.ts'
|
|
18
18
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
|
2
2
|
import type { ConversationController, IConversation } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
3
|
-
import type { SessionId } from '@deepseek-ai/dsh-
|
|
3
|
+
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
|
4
4
|
|
|
5
5
|
/** Document event used to bridge chat tool results into the image workspace. */
|
|
6
6
|
export const CHAT_IMAGE_EVENT = 'dsh-imagegen:chat-images'
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/** Inline renderer for image-generation tool-result attachments. */
|
|
2
2
|
|
|
3
3
|
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
|
4
|
-
import type {
|
|
4
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
5
|
+
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
|
6
|
+
import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
7
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
5
8
|
import { useEffect, useMemo, useState } from 'react'
|
|
6
9
|
import { AGENT_IMAGE_API } from '../protocol.ts'
|
|
7
10
|
import { CHAT_IMAGE_EVENT } from './conversation-sync.ts'
|
|
@@ -31,10 +34,9 @@ function isSettled(block: ToolCallBlock): block is Extract<ToolCallBlock, { kind
|
|
|
31
34
|
return 'kind' in block
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
function imageRefsOf(block: ToolCallBlock): ImageAttachmentRef[] {
|
|
35
|
-
if (!isSettled(block)) return []
|
|
36
|
-
|
|
37
|
-
return [...block.content, ...resultContent]
|
|
37
|
+
function imageRefsOf(block: ToolCallBlock): ImageAttachmentRef[] {
|
|
38
|
+
if (!isSettled(block)) return []
|
|
39
|
+
return block.content
|
|
38
40
|
.flatMap(content => content.type === 'image' ? [content.attachment] : [])
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -169,8 +171,12 @@ export function registerImageToolviews(ctx: ClientContext): void {
|
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
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({
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
174
|
+
for (const key of ['generate_image', 'edit_image', 'get_image_generation_task']) {
|
|
175
|
+
yield ctx.slots.register({
|
|
176
|
+
name: 'tool.call.toolview',
|
|
177
|
+
key,
|
|
178
|
+
inject: (sessionId: string) => ({ sessionId: sessionId as SessionId }),
|
|
179
|
+
}, ImageToolView)
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -11,12 +11,13 @@
|
|
|
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/
|
|
15
|
-
import type { ISessions } from '@deepseek-ai/dsh-
|
|
14
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
15
|
+
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
16
16
|
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
|
17
17
|
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
|
18
|
-
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
19
|
-
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
19
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
20
|
+
// Type-only: pulls the LocaleNamespaceMap merge table.
|
|
20
21
|
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
21
22
|
import { ImageGenApi } from './api.ts'
|
|
22
23
|
import { ImageGenController } from './controller.ts'
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inspiration-wall styles for the studio's empty canvas. Same conventions as
|
|
3
|
+
* panel.module.css: dsh --dsw-* tokens, scoped to this component's subtree.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
.wrap {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: 16px;
|
|
11
|
+
width: min(880px, 100%);
|
|
12
|
+
margin: auto;
|
|
13
|
+
padding: 24px;
|
|
14
|
+
font-family: var(--dsw-font-family);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.title {
|
|
18
|
+
font-size: 15px;
|
|
19
|
+
font-weight: 650;
|
|
20
|
+
color: var(--dsw-alias-label-primary);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Fallback (sample failed / libraries empty): mirrors the panel's plain
|
|
24
|
+
canvas empty state so the swap is invisible. */
|
|
25
|
+
.emptyIcon {
|
|
26
|
+
display: inline-flex;
|
|
27
|
+
color: var(--dsw-alias-label-dimmed);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.emptyTitle {
|
|
31
|
+
margin-top: -6px;
|
|
32
|
+
font-size: 15px;
|
|
33
|
+
font-weight: 650;
|
|
34
|
+
color: var(--dsw-alias-label-primary);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.emptyHint {
|
|
38
|
+
font-size: 12.5px;
|
|
39
|
+
color: var(--dsw-alias-label-tertiary);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.grid {
|
|
43
|
+
display: grid;
|
|
44
|
+
grid-template-columns: repeat(4, 1fr);
|
|
45
|
+
gap: 12px;
|
|
46
|
+
width: 100%;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Narrow canvases: fold the wall down instead of shrinking tiles into slivers. */
|
|
50
|
+
@media (max-width: 900px) {
|
|
51
|
+
.grid {
|
|
52
|
+
grid-template-columns: repeat(3, 1fr);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@media (max-width: 560px) {
|
|
57
|
+
.grid {
|
|
58
|
+
grid-template-columns: repeat(2, 1fr);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.tile {
|
|
63
|
+
position: relative;
|
|
64
|
+
display: block;
|
|
65
|
+
width: 100%;
|
|
66
|
+
aspect-ratio: 1 / 1;
|
|
67
|
+
padding: 0;
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
70
|
+
border-radius: 12px;
|
|
71
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.tile:hover {
|
|
77
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
78
|
+
transform: translateY(-2px);
|
|
79
|
+
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.thumbWrap {
|
|
83
|
+
position: absolute;
|
|
84
|
+
inset: 0;
|
|
85
|
+
display: block;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.thumb {
|
|
89
|
+
display: block;
|
|
90
|
+
width: 100%;
|
|
91
|
+
height: 100%;
|
|
92
|
+
object-fit: cover;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.thumbFallback {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
width: 100%;
|
|
100
|
+
height: 100%;
|
|
101
|
+
padding: 6px;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
104
|
+
color: var(--dsw-alias-label-tertiary);
|
|
105
|
+
font-size: 11px;
|
|
106
|
+
line-height: 1.4;
|
|
107
|
+
text-align: center;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.thumbTitle {
|
|
111
|
+
position: absolute;
|
|
112
|
+
inset: auto 0 0 0;
|
|
113
|
+
padding: 14px 8px 6px;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
text-overflow: ellipsis;
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
background: linear-gradient(transparent, rgba(0, 0, 0, 0.55));
|
|
118
|
+
color: #ffffff;
|
|
119
|
+
font-size: 10.5px;
|
|
120
|
+
text-align: center;
|
|
121
|
+
opacity: 0;
|
|
122
|
+
transition: opacity 0.15s ease;
|
|
123
|
+
pointer-events: none;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.tile:hover .thumbTitle {
|
|
127
|
+
opacity: 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.spinner {
|
|
131
|
+
width: 20px;
|
|
132
|
+
height: 20px;
|
|
133
|
+
border: 2px solid var(--dsw-alias-border-l2);
|
|
134
|
+
border-top-color: var(--dsw-alias-brand-primary);
|
|
135
|
+
border-radius: 50%;
|
|
136
|
+
animation: dsh-imagegen-inspiration-spin 0.9s linear infinite;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@keyframes dsh-imagegen-inspiration-spin {
|
|
140
|
+
to { transform: rotate(360deg); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Touch shells have no hover: keep titles visible. */
|
|
144
|
+
@media (hover: none) {
|
|
145
|
+
.thumbTitle {
|
|
146
|
+
opacity: 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
package/src/client/locales.ts
CHANGED
|
@@ -301,6 +301,7 @@ export const zh = {
|
|
|
301
301
|
// template library
|
|
302
302
|
'templates.open': '模板库',
|
|
303
303
|
'templates.title': '提示词模板库',
|
|
304
|
+
'templates.sources': '模板库来源',
|
|
304
305
|
'templates.meta': '共 {count} 个模板 · {origin}',
|
|
305
306
|
'templates.origin.bundled': '内置快照',
|
|
306
307
|
'templates.origin.refreshed': '在线刷新',
|
|
@@ -311,21 +312,33 @@ export const zh = {
|
|
|
311
312
|
'templates.use': '使用此提示词',
|
|
312
313
|
'templates.copy': '复制提示词',
|
|
313
314
|
'templates.copied': '已复制',
|
|
314
|
-
'templates.refresh': '
|
|
315
|
+
'templates.refresh': '刷新本库',
|
|
315
316
|
'templates.refreshing': '刷新中…',
|
|
316
317
|
'templates.refreshed': '已刷新,共 {count} 个模板',
|
|
317
318
|
'templates.refreshFailed': '刷新失败:{error}',
|
|
319
|
+
'templates.favorites': '收藏',
|
|
320
|
+
'templates.favoritesHint': '只看已收藏的模板',
|
|
321
|
+
'templates.favoritesEmpty': '还没有收藏,点击模板卡片右上角的星标即可收藏',
|
|
322
|
+
'templates.favoriteAdd': '收藏此模板',
|
|
323
|
+
'templates.favoriteRemove': '取消收藏',
|
|
324
|
+
'templates.favorite': '收藏',
|
|
325
|
+
'templates.unfavorite': '已收藏',
|
|
318
326
|
'templates.cacheAll': '缓存全部图片',
|
|
319
|
-
'templates.cacheAllHint': '
|
|
327
|
+
'templates.cacheAllHint': '通过本机代理把当前模板库的全部参考图缓存到本地磁盘,之后离线也能浏览',
|
|
320
328
|
'templates.caching': '缓存中 {done}/{total}…',
|
|
321
329
|
'templates.cached': '图片已全部缓存',
|
|
322
330
|
'templates.empty': '没有匹配的模板',
|
|
323
331
|
'templates.loading': '正在加载模板库…',
|
|
324
332
|
'templates.loadFailed': '模板库加载失败:{error}',
|
|
325
333
|
'templates.retry': '重试',
|
|
326
|
-
'templates.attribution': '
|
|
327
|
-
'templates.source': '来源:
|
|
334
|
+
'templates.attribution': '模板与图片来自各来源站点,作者链接见模板详情',
|
|
335
|
+
'templates.source': '来源:{label}',
|
|
328
336
|
'templates.featured': '精选',
|
|
337
|
+
// inspiration wall (studio empty state)
|
|
338
|
+
'inspiration.title': '灵感案例',
|
|
339
|
+
'inspiration.shuffle': '随机',
|
|
340
|
+
'inspiration.shuffling': '换一批…',
|
|
341
|
+
'inspiration.useHint': '点击使用该提示词',
|
|
329
342
|
// channel management
|
|
330
343
|
'channels.title': '渠道',
|
|
331
344
|
'channels.hint': '填写各渠道的 API 地址与密钥即可使用对应模型',
|
|
@@ -665,6 +678,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
665
678
|
// template library
|
|
666
679
|
'templates.open': 'Templates',
|
|
667
680
|
'templates.title': 'Prompt Template Library',
|
|
681
|
+
'templates.sources': 'Template sources',
|
|
668
682
|
'templates.meta': '{count} templates · {origin}',
|
|
669
683
|
'templates.origin.bundled': 'bundled snapshot',
|
|
670
684
|
'templates.origin.refreshed': 'refreshed online',
|
|
@@ -675,21 +689,33 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
675
689
|
'templates.use': 'Use this prompt',
|
|
676
690
|
'templates.copy': 'Copy prompt',
|
|
677
691
|
'templates.copied': 'Copied',
|
|
678
|
-
'templates.refresh': 'Refresh library',
|
|
692
|
+
'templates.refresh': 'Refresh this library',
|
|
679
693
|
'templates.refreshing': 'Refreshing…',
|
|
680
694
|
'templates.refreshed': 'Refreshed — {count} templates',
|
|
681
695
|
'templates.refreshFailed': 'Refresh failed: {error}',
|
|
696
|
+
'templates.favorites': 'Favorites',
|
|
697
|
+
'templates.favoritesHint': 'Show favorited templates only',
|
|
698
|
+
'templates.favoritesEmpty': 'No favorites yet — tap the star in a card\'s top-right corner to save one',
|
|
699
|
+
'templates.favoriteAdd': 'Favorite this template',
|
|
700
|
+
'templates.favoriteRemove': 'Remove from favorites',
|
|
701
|
+
'templates.favorite': 'Favorite',
|
|
702
|
+
'templates.unfavorite': 'Favorited',
|
|
682
703
|
'templates.cacheAll': 'Cache all images',
|
|
683
|
-
'templates.cacheAllHint': 'Mirror every reference image to local disk through the host proxy, for offline browsing',
|
|
704
|
+
'templates.cacheAllHint': 'Mirror every reference image of the current library to local disk through the host proxy, for offline browsing',
|
|
684
705
|
'templates.caching': 'Caching {done}/{total}…',
|
|
685
706
|
'templates.cached': 'All images cached',
|
|
686
707
|
'templates.empty': 'No matching templates',
|
|
687
708
|
'templates.loading': 'Loading the template library…',
|
|
688
709
|
'templates.loadFailed': 'Failed to load the library: {error}',
|
|
689
710
|
'templates.retry': 'Retry',
|
|
690
|
-
'templates.attribution': 'Templates and images come from
|
|
691
|
-
'templates.source': 'Source:
|
|
711
|
+
'templates.attribution': 'Templates and images come from each source site; author links are on each template',
|
|
712
|
+
'templates.source': 'Source: {label}',
|
|
692
713
|
'templates.featured': 'Featured',
|
|
714
|
+
// inspiration wall (studio empty state)
|
|
715
|
+
'inspiration.title': 'Inspiration',
|
|
716
|
+
'inspiration.shuffle': 'Shuffle',
|
|
717
|
+
'inspiration.shuffling': 'Shuffling…',
|
|
718
|
+
'inspiration.useHint': 'Click to use this prompt',
|
|
693
719
|
// channel management
|
|
694
720
|
'channels.title': 'Channels',
|
|
695
721
|
'channels.hint': 'Fill in each channel\'s API URL and key to use its models',
|
package/src/client/mount.tsx
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* and both get the `position: relative` base in panel.module.css.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type { ISessions } from '@deepseek-ai/dsh-
|
|
18
|
+
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
19
19
|
import { createRoot, type Root } from 'react-dom/client'
|
|
20
20
|
import type { ImageGenApi } from './api.ts'
|
|
21
21
|
import type { ImageGenController } from './controller.ts'
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* sibling UI package).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { createSnapshotStore, type
|
|
10
|
+
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
11
|
+
import type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
11
12
|
|
|
12
13
|
/** The write one field's staged text performs when the card is saved. */
|
|
13
14
|
export type FieldWrite =
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
createSnapshotStore,
|
|
13
|
-
type SettingsScope,
|
|
14
|
-
type SettingsScopeSnapshot,
|
|
15
13
|
type SnapshotStore,
|
|
16
|
-
} from '@deepseek-ai/dsh-client-
|
|
14
|
+
} from '@deepseek-ai/dsh-client-store'
|
|
15
|
+
import type { SettingsPathOpView } from '@deepseek-ai/dsh-api-remotes/client'
|
|
16
|
+
import type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
17
17
|
import { SETTINGS_API, type ChannelConfig } from '../protocol.ts'
|
|
18
18
|
|
|
19
19
|
/** The fields this plugin's settings card edits. */
|
|
@@ -158,6 +158,10 @@ class BridgeScopeController<T> implements SettingsScope<T> {
|
|
|
158
158
|
return this.enqueue(() => this.writeOps([{ op: 'unset', path: [field] }]))
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
mutate(ops: readonly SettingsPathOpView[], expectedRevision?: number): Promise<void> {
|
|
162
|
+
return this.enqueue(() => this.writeOps([...ops] as SettingsOp[], expectedRevision))
|
|
163
|
+
}
|
|
164
|
+
|
|
161
165
|
/** Apply several path ops in one revision-fenced mutate call (atomic save).
|
|
162
166
|
* Path ops may address plain-object fields (e.g. `channelSecrets.<id>`),
|
|
163
167
|
* but never navigate *inside* arrays — write array fields wholesale. */
|
|
@@ -206,8 +210,8 @@ class BridgeScopeController<T> implements SettingsScope<T> {
|
|
|
206
210
|
this.accept(view, writable)
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
private async writeOps(ops: SettingsOp[]): Promise<void> {
|
|
210
|
-
const revision = this.getSnapshot().revision
|
|
213
|
+
private async writeOps(ops: SettingsOp[], expectedRevision?: number): Promise<void> {
|
|
214
|
+
const revision = expectedRevision ?? this.getSnapshot().revision
|
|
211
215
|
let response
|
|
212
216
|
try {
|
|
213
217
|
response = await this.api.mutate({
|
|
@@ -92,6 +92,60 @@
|
|
|
92
92
|
color: var(--dsw-alias-label-primary);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/* --- source tabs --------------------------------------------------------- */
|
|
96
|
+
|
|
97
|
+
.sourceTabs {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 6px;
|
|
101
|
+
padding: 0 18px 10px;
|
|
102
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1);
|
|
103
|
+
flex: none;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.sourceTab {
|
|
107
|
+
display: inline-flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
gap: 6px;
|
|
110
|
+
height: 30px;
|
|
111
|
+
padding: 0 14px;
|
|
112
|
+
border: 1px solid transparent;
|
|
113
|
+
border-radius: 9px 9px 0 0;
|
|
114
|
+
border-bottom: none;
|
|
115
|
+
background: transparent;
|
|
116
|
+
color: var(--dsw-alias-label-secondary);
|
|
117
|
+
font-size: 13px;
|
|
118
|
+
font-weight: 600;
|
|
119
|
+
font-family: inherit;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
white-space: nowrap;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.sourceTab:hover {
|
|
125
|
+
color: var(--dsw-alias-label-primary);
|
|
126
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.sourceTab[data-active] {
|
|
130
|
+
border-color: var(--dsw-alias-border-l1);
|
|
131
|
+
border-radius: 9px;
|
|
132
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
133
|
+
color: var(--dsw-alias-brand-primary);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.sourceTabCount {
|
|
137
|
+
padding: 0 6px;
|
|
138
|
+
border-radius: 999px;
|
|
139
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
140
|
+
color: var(--dsw-alias-label-tertiary);
|
|
141
|
+
font-size: 11px;
|
|
142
|
+
font-weight: 500;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.sourceTab[data-active] .sourceTabCount {
|
|
146
|
+
color: var(--dsw-alias-label-secondary);
|
|
147
|
+
}
|
|
148
|
+
|
|
95
149
|
/* --- toolbar ----------------------------------------------------------- */
|
|
96
150
|
|
|
97
151
|
.toolbar {
|
|
@@ -263,6 +317,47 @@
|
|
|
263
317
|
font-weight: 600;
|
|
264
318
|
}
|
|
265
319
|
|
|
320
|
+
.favStar {
|
|
321
|
+
position: absolute;
|
|
322
|
+
top: 6px;
|
|
323
|
+
right: 6px;
|
|
324
|
+
display: inline-flex;
|
|
325
|
+
align-items: center;
|
|
326
|
+
justify-content: center;
|
|
327
|
+
width: 26px;
|
|
328
|
+
height: 26px;
|
|
329
|
+
padding: 0;
|
|
330
|
+
border: none;
|
|
331
|
+
border-radius: 8px;
|
|
332
|
+
background: rgba(0, 0, 0, 0.35);
|
|
333
|
+
color: rgba(255, 255, 255, 0.85);
|
|
334
|
+
cursor: pointer;
|
|
335
|
+
opacity: 0;
|
|
336
|
+
transition: opacity 0.15s ease;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.card:hover .favStar,
|
|
340
|
+
.favStar:focus-visible,
|
|
341
|
+
.favStar[data-active] {
|
|
342
|
+
opacity: 1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.favStar:hover {
|
|
346
|
+
background: rgba(0, 0, 0, 0.55);
|
|
347
|
+
color: #ffffff;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.favStar[data-active] {
|
|
351
|
+
color: #ffb020;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* Touch shells have no hover: keep the star reachable. */
|
|
355
|
+
@media (hover: none) {
|
|
356
|
+
.favStar {
|
|
357
|
+
opacity: 1;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
266
361
|
.cardBody {
|
|
267
362
|
display: flex;
|
|
268
363
|
flex-direction: column;
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis'
|
|
11
|
-
import {
|
|
11
|
+
import { installSettingsSectionCompat, settingsNamespaceCompat } from './settings-compat.ts'
|
|
12
12
|
import z from 'schemastery'
|
|
13
13
|
// Type-only: pulls the webServer Context merge (route registration).
|
|
14
14
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
@@ -20,6 +20,7 @@ import type {} from '@deepseek-ai/dsh-tools'
|
|
|
20
20
|
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
|
21
21
|
import { IMAGEGEN_SETTINGS_NAMESPACE, type ChannelConfig, type ModelMapping } from './protocol.ts'
|
|
22
22
|
import { makeRoutes, type SettingsSeam } from './routes.ts'
|
|
23
|
+
import { syncAllTemplates } from './templates-store.ts'
|
|
23
24
|
import { ImageGenerationRuntime, type ChannelsView, type RuntimeChannel } from './generation-runtime.ts'
|
|
24
25
|
import { registerAgentImageTools } from './agent-image-tools.ts'
|
|
25
26
|
import { registerEditImageCommand } from './edit-image-command.ts'
|
|
@@ -39,11 +40,12 @@ export { ImageGenerationRuntime } from './generation-runtime.ts'
|
|
|
39
40
|
export { registerAgentImageTools } from './agent-image-tools.ts'
|
|
40
41
|
export { latestSessionImage, registerEditImageCommand } from './edit-image-command.ts'
|
|
41
42
|
export { appendGallery, clearGallery, listGallery, readGalleryImage, removeGallery, updateGalleryTags } from './gallery-store.ts'
|
|
42
|
-
export { listTemplates, readTemplateImage, refreshTemplates, clearTemplateMemo } from './templates-store.ts'
|
|
43
|
+
export { listTemplates, readTemplateImage, refreshTemplates, sampleTemplates, syncAllTemplates, clearTemplateMemo } from './templates-store.ts'
|
|
44
|
+
export { addTemplateFavorite, clearTemplateFavoritesMemo, listTemplateFavorites, removeTemplateFavorite } from './template-favorites.ts'
|
|
43
45
|
export { checkForUpdate, clearUpdateCache, compareVersions, CURRENT_VERSION, installUpdate, profileFromProcess } from './updater.ts'
|
|
44
46
|
|
|
45
47
|
/** The branded settings namespace of this plugin (the card edits it). */
|
|
46
|
-
export const ImageGenSettingsNamespace =
|
|
48
|
+
export const ImageGenSettingsNamespace = settingsNamespaceCompat(IMAGEGEN_SETTINGS_NAMESPACE)
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
51
|
* Plugin config, validated by the same-named schemastery schema.
|
|
@@ -115,7 +117,7 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true
|
|
|
115
117
|
const SECTION_ORDER = 150
|
|
116
118
|
|
|
117
119
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
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
|
|
120
|
+
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 的数百条提示词案例,可搜索、筛选、收藏(星标,宿主持久化)与复用;各来源列表独立刷新,宿主每 12 小时后台自动同步一次。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片显示在工具调用对应的左侧结果区域,模型收到状态和附件引用,不会额外伪造用户消息。用户也可以使用 `/edit_image <修改描述>`,命令会直接读取当前对话最近图片并调用插件图片模型,不经过对话模型的图片能力检查。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问对应来源站点(vibeui.top / gpt-image2.canghe.ai)。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。'
|
|
119
121
|
|
|
120
122
|
/** Append the live channel × model table so an Agent can honor user choices. */
|
|
121
123
|
function guidanceFor(channels: RuntimeChannel[], defaultChannelId: string): string {
|
|
@@ -276,7 +278,25 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
276
278
|
runtime,
|
|
277
279
|
})
|
|
278
280
|
const disposers = routes.map(route => ctx.webServer.register(route))
|
|
279
|
-
|
|
281
|
+
// Background template sync: the upstream sources update on their own
|
|
282
|
+
// schedule, so pull every one of them shortly after start and then
|
|
283
|
+
// twice a day while the plugin stays enabled. Best-effort: failures
|
|
284
|
+
// keep the last good snapshot (bundled or previously refreshed).
|
|
285
|
+
const TEMPLATE_SYNC_INITIAL_DELAY_MS = 30_000
|
|
286
|
+
const TEMPLATE_SYNC_INTERVAL_MS = 12 * 60 * 60 * 1000
|
|
287
|
+
let syncTimer: NodeJS.Timeout | undefined
|
|
288
|
+
const runSync = (): void => {
|
|
289
|
+
if (!resolve().enabled) return
|
|
290
|
+
void syncAllTemplates().catch(() => { /* keep the last good snapshot */ })
|
|
291
|
+
}
|
|
292
|
+
const startTimer = setTimeout(runSync, TEMPLATE_SYNC_INITIAL_DELAY_MS)
|
|
293
|
+
syncTimer = setInterval(runSync, TEMPLATE_SYNC_INTERVAL_MS)
|
|
294
|
+
syncTimer.unref?.()
|
|
295
|
+
return () => {
|
|
296
|
+
clearTimeout(startTimer)
|
|
297
|
+
clearInterval(syncTimer)
|
|
298
|
+
for (const dispose of disposers) dispose()
|
|
299
|
+
}
|
|
280
300
|
},
|
|
281
301
|
'dsh-imagegen: routes',
|
|
282
302
|
)
|
|
@@ -323,7 +343,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
323
343
|
})
|
|
324
344
|
}
|
|
325
345
|
|
|
326
|
-
|
|
346
|
+
installSettingsSectionCompat(ctx, ImageGenSettingsNamespace, Config, config ?? {}, {
|
|
327
347
|
setSource: (source) => {
|
|
328
348
|
current = source
|
|
329
349
|
sync()
|