@dickpy/dsh-imagegen 1.5.5 → 1.5.6
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 +1 -0
- package/lib/client.js +994 -462
- package/lib/client.js.map +1 -1
- package/lib/index.js +80 -50
- package/package.json +1 -1
- package/src/canvas-store.ts +15 -16
- package/src/client/CanvasWorkspace.tsx +273 -30
- package/src/client/ImageGenPanel.tsx +2737 -2679
- package/src/client/SettingsCard.tsx +14 -0
- package/src/client/canvas-workspace.module.css +144 -8
- package/src/client/locales.ts +79 -25
- package/src/client/panel.module.css +20 -26
- package/src/engine.ts +31 -14
- package/src/gallery-store.ts +13 -13
- package/src/generation-runtime.ts +2 -2
- package/src/history-store.ts +12 -12
- package/src/image-storage-path.ts +12 -0
- package/src/index.ts +5 -0
- package/src/protocol.ts +8 -2
- package/src/routes.ts +3 -0
- package/src/task-queue.ts +4 -5
|
@@ -33,6 +33,7 @@ export interface ImageGenSettings {
|
|
|
33
33
|
promptApiUrl?: string
|
|
34
34
|
promptApiKey?: string
|
|
35
35
|
promptModel?: string
|
|
36
|
+
localStoragePath?: string
|
|
36
37
|
storageEnabled?: boolean
|
|
37
38
|
storageEndpoint?: string
|
|
38
39
|
storageRegion?: string
|
|
@@ -55,6 +56,7 @@ export interface ImageGenSettingsCardState extends CardShell {
|
|
|
55
56
|
promptApiUrl: CardFieldState
|
|
56
57
|
promptApiKey: CardFieldState
|
|
57
58
|
promptModel: CardFieldState
|
|
59
|
+
localStoragePath: CardFieldState
|
|
58
60
|
storageEnabled: CardFieldState
|
|
59
61
|
storageEndpoint: CardFieldState
|
|
60
62
|
storageRegion: CardFieldState
|
|
@@ -98,6 +100,7 @@ export class ImageGenSettingsCardController {
|
|
|
98
100
|
textField('promptApiUrl'),
|
|
99
101
|
secretField('promptApiKey'),
|
|
100
102
|
textField('promptModel'),
|
|
103
|
+
textField('localStoragePath'),
|
|
101
104
|
booleanField('storageEnabled'),
|
|
102
105
|
textField('storageEndpoint'),
|
|
103
106
|
textField('storageRegion'),
|
|
@@ -124,6 +127,7 @@ export class ImageGenSettingsCardController {
|
|
|
124
127
|
promptApiUrl: this.form.field('promptApiUrl'),
|
|
125
128
|
promptApiKey: this.form.field('promptApiKey'),
|
|
126
129
|
promptModel: this.form.field('promptModel'),
|
|
130
|
+
localStoragePath: this.form.field('localStoragePath'),
|
|
127
131
|
storageEnabled: this.form.field('storageEnabled'),
|
|
128
132
|
storageEndpoint: this.form.field('storageEndpoint'),
|
|
129
133
|
storageRegion: this.form.field('storageRegion'),
|
|
@@ -475,6 +479,16 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
475
479
|
<span aria-hidden="true">{storageOpen ? '⌃' : '⌄'}</span>
|
|
476
480
|
</button>
|
|
477
481
|
{storageOpen ? <div className={css.optionalContent}>
|
|
482
|
+
<ValueField
|
|
483
|
+
id="dsh-imagegen-settings-local-storage-path"
|
|
484
|
+
label={t('settings.localStoragePath')}
|
|
485
|
+
hint={t('settings.localStoragePathHint')}
|
|
486
|
+
placeholder="E:\\dsh-imagegen-data"
|
|
487
|
+
{...fieldProps}
|
|
488
|
+
{...state.localStoragePath}
|
|
489
|
+
onEdit={(text) => { props.edit('localStoragePath', text) }}
|
|
490
|
+
onReset={() => { props.resetField('localStoragePath') }}
|
|
491
|
+
/>
|
|
478
492
|
<BooleanField
|
|
479
493
|
id="dsh-imagegen-settings-storage-enabled"
|
|
480
494
|
label={t('settings.storageEnabled')}
|
|
@@ -142,6 +142,68 @@
|
|
|
142
142
|
|
|
143
143
|
.grid[data-mode="blank"] { background-image: none; }
|
|
144
144
|
|
|
145
|
+
.grid[data-mode="diagonal"] {
|
|
146
|
+
background-image: repeating-linear-gradient(45deg, color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent) 0 1px, transparent 1px 16px);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.grid[data-mode="checker"] {
|
|
150
|
+
background-image:
|
|
151
|
+
conic-gradient(color-mix(in srgb, var(--dsw-alias-label-primary) 7%, transparent) 90deg, transparent 90deg 180deg, color-mix(in srgb, var(--dsw-alias-label-primary) 7%, transparent) 180deg 270deg, transparent 270deg);
|
|
152
|
+
background-size: 48px 48px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.grid[data-mode="image"] { background-image: none; }
|
|
156
|
+
|
|
157
|
+
.gridScrim {
|
|
158
|
+
position: absolute;
|
|
159
|
+
inset: 0;
|
|
160
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
161
|
+
opacity: 0.6;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* ------------------------------------------------ background menu */
|
|
165
|
+
|
|
166
|
+
.backgroundMenu {
|
|
167
|
+
position: absolute;
|
|
168
|
+
transform: translate(-50%, -100%);
|
|
169
|
+
z-index: 45;
|
|
170
|
+
min-width: 168px;
|
|
171
|
+
padding: 5px;
|
|
172
|
+
border-radius: 11px;
|
|
173
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
174
|
+
background: color-mix(in srgb, var(--dsw-alias-bg-layer-1) 96%, transparent);
|
|
175
|
+
box-shadow: 0 16px 40px rgb(15 23 42 / 20%);
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-direction: column;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.backgroundMenu button {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: space-between;
|
|
184
|
+
border: none;
|
|
185
|
+
border-radius: 7px;
|
|
186
|
+
background: transparent;
|
|
187
|
+
color: var(--dsw-alias-label-primary);
|
|
188
|
+
font-size: 12.5px;
|
|
189
|
+
padding: 7px 10px;
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
text-align: left;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.backgroundMenu button:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
195
|
+
|
|
196
|
+
.backgroundMenu button[data-active] {
|
|
197
|
+
color: var(--dsw-alias-brand-primary);
|
|
198
|
+
background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.backgroundMenuDivider {
|
|
202
|
+
height: 1px;
|
|
203
|
+
margin: 4px 6px;
|
|
204
|
+
background: var(--dsw-alias-border-l1);
|
|
205
|
+
}
|
|
206
|
+
|
|
145
207
|
.world {
|
|
146
208
|
position: absolute;
|
|
147
209
|
left: 0;
|
|
@@ -256,6 +318,33 @@
|
|
|
256
318
|
display: block;
|
|
257
319
|
}
|
|
258
320
|
|
|
321
|
+
.imageInfo {
|
|
322
|
+
position: absolute;
|
|
323
|
+
top: 6px;
|
|
324
|
+
z-index: 2;
|
|
325
|
+
padding: 3px 8px;
|
|
326
|
+
border-radius: 6px;
|
|
327
|
+
background: color-mix(in srgb, var(--dsw-alias-label-primary) 62%, transparent);
|
|
328
|
+
color: var(--dsw-alias-bg-layer-1);
|
|
329
|
+
font-size: 10.5px;
|
|
330
|
+
line-height: 1.4;
|
|
331
|
+
pointer-events: none;
|
|
332
|
+
backdrop-filter: blur(3px);
|
|
333
|
+
max-width: calc(100% - 90px);
|
|
334
|
+
overflow: hidden;
|
|
335
|
+
text-overflow: ellipsis;
|
|
336
|
+
white-space: nowrap;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.imageInfoLeft { left: 6px; font-weight: 600; }
|
|
340
|
+
|
|
341
|
+
.imageInfoRight {
|
|
342
|
+
right: 6px;
|
|
343
|
+
left: auto;
|
|
344
|
+
font-variant-numeric: tabular-nums;
|
|
345
|
+
opacity: 0.85;
|
|
346
|
+
}
|
|
347
|
+
|
|
259
348
|
.nodeState,
|
|
260
349
|
.nodeStateError {
|
|
261
350
|
display: flex;
|
|
@@ -343,7 +432,53 @@
|
|
|
343
432
|
backdrop-filter: blur(10px);
|
|
344
433
|
}
|
|
345
434
|
|
|
346
|
-
.dock .iconButton {
|
|
435
|
+
.dock .iconButton {
|
|
436
|
+
position: relative;
|
|
437
|
+
overflow: hidden;
|
|
438
|
+
width: 40px;
|
|
439
|
+
height: 40px;
|
|
440
|
+
border-radius: 11px;
|
|
441
|
+
transition: transform 0.18s cubic-bezier(0.34, 1.9, 0.64, 1), background 0.15s ease, color 0.15s ease, box-shadow 0.18s ease;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* water-wave sheen sweeping across + gentle lift on hover */
|
|
445
|
+
.dock .iconButton::before {
|
|
446
|
+
content: '';
|
|
447
|
+
position: absolute;
|
|
448
|
+
top: -4px;
|
|
449
|
+
bottom: -4px;
|
|
450
|
+
left: -150%;
|
|
451
|
+
width: 65%;
|
|
452
|
+
background: linear-gradient(105deg, transparent, color-mix(in srgb, currentColor 26%, transparent), transparent);
|
|
453
|
+
transform: skewX(-18deg);
|
|
454
|
+
opacity: 0;
|
|
455
|
+
pointer-events: none;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.dock .iconButton:not(:disabled):hover {
|
|
459
|
+
transform: translateY(calc(-3px - var(--dock-lift, 0px)));
|
|
460
|
+
box-shadow: 0 6px 14px color-mix(in srgb, var(--dsw-alias-label-primary) 20%, transparent);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.dock .iconButton:not(:disabled):hover::before {
|
|
464
|
+
animation: dockShine 0.55s ease forwards;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.dock .iconButton:not(:disabled):active {
|
|
468
|
+
transform: translateY(-1px) scale(0.96);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
@keyframes dockShine {
|
|
472
|
+
0% { left: -150%; opacity: 1; }
|
|
473
|
+
100% { left: 170%; opacity: 1; }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.dock .iconButton[data-active] {
|
|
477
|
+
background: var(--dsw-alias-label-primary);
|
|
478
|
+
color: var(--dsw-alias-bg-layer-1);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.dock .iconButton[data-active]:hover { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-layer-1); }
|
|
347
482
|
|
|
348
483
|
.dockDivider {
|
|
349
484
|
width: 1px;
|
|
@@ -439,29 +574,30 @@
|
|
|
439
574
|
/* ------------------------------------------------------ config node */
|
|
440
575
|
|
|
441
576
|
.configNode {
|
|
442
|
-
padding: 0
|
|
577
|
+
padding: 0 14px 14px;
|
|
443
578
|
cursor: grab;
|
|
444
579
|
}
|
|
445
580
|
|
|
446
|
-
.configNode .nodeHeader { padding:
|
|
581
|
+
.configNode .nodeHeader { padding: 10px 2px 6px; font-size: 12px; }
|
|
447
582
|
|
|
448
583
|
.configLinks {
|
|
449
584
|
display: flex;
|
|
450
|
-
|
|
585
|
+
align-items: center;
|
|
586
|
+
margin: 4px 0 10px;
|
|
451
587
|
flex: none;
|
|
452
588
|
}
|
|
453
589
|
|
|
454
590
|
.configLinks .composerChip {
|
|
455
|
-
font-size:
|
|
456
|
-
padding:
|
|
591
|
+
font-size: 11px;
|
|
592
|
+
padding: 3px 10px;
|
|
457
593
|
}
|
|
458
594
|
|
|
459
595
|
.configHint {
|
|
460
596
|
flex: 1;
|
|
461
597
|
min-height: 0;
|
|
462
598
|
margin: 0;
|
|
463
|
-
font-size:
|
|
464
|
-
line-height: 1.
|
|
599
|
+
font-size: 11.5px;
|
|
600
|
+
line-height: 1.7;
|
|
465
601
|
color: var(--dsw-alias-label-tertiary);
|
|
466
602
|
overflow: auto;
|
|
467
603
|
}
|
package/src/client/locales.ts
CHANGED
|
@@ -31,7 +31,7 @@ export const zh = {
|
|
|
31
31
|
'canvas.untitled': '未命名画布',
|
|
32
32
|
'canvas.imageNode': '图片节点',
|
|
33
33
|
'canvas.textNode': '文本节点',
|
|
34
|
-
'canvas.fromGallery': '
|
|
34
|
+
'canvas.fromGallery': '素材库图片',
|
|
35
35
|
'canvas.fromHistory': '历史图片',
|
|
36
36
|
'canvas.project': '画布项目',
|
|
37
37
|
'canvas.newCanvas': '新建画布',
|
|
@@ -39,7 +39,15 @@ export const zh = {
|
|
|
39
39
|
'canvas.deleteCanvasConfirm': '再点一次确认删除',
|
|
40
40
|
'canvas.rename': '重命名画布',
|
|
41
41
|
'canvas.renameHint': '双击重命名',
|
|
42
|
-
'canvas.background': '
|
|
42
|
+
'canvas.background': '画布背景',
|
|
43
|
+
'canvas.backgroundDots': '点阵',
|
|
44
|
+
'canvas.backgroundLines': '网格线',
|
|
45
|
+
'canvas.backgroundDiagonal': '斜线',
|
|
46
|
+
'canvas.backgroundChecker': '棋盘格',
|
|
47
|
+
'canvas.backgroundBlank': '空白',
|
|
48
|
+
'canvas.backgroundUpload': '上传自定义背景…',
|
|
49
|
+
'canvas.backgroundRemove': '移除自定义背景',
|
|
50
|
+
'canvas.templateLibrary': '模板库(选择提示词生成文本+配置节点)',
|
|
43
51
|
'canvas.undo': '撤销 (Ctrl+Z)',
|
|
44
52
|
'canvas.redo': '重做 (Ctrl+Shift+Z)',
|
|
45
53
|
'canvas.saving': '保存中…',
|
|
@@ -89,7 +97,11 @@ export const zh = {
|
|
|
89
97
|
'canvas.close': '关闭',
|
|
90
98
|
'canvas.tabUpload': '上传',
|
|
91
99
|
'canvas.tabHistory': '历史记录',
|
|
92
|
-
'canvas.tabGallery': '
|
|
100
|
+
'canvas.tabGallery': '素材库',
|
|
101
|
+
'canvas.imageMenuUpload': '上传本地图片',
|
|
102
|
+
'canvas.imageMenuAssets': '从素材库选择',
|
|
103
|
+
'canvas.imageMenuHistory': '从历史记录选择',
|
|
104
|
+
'canvas.imageMenuGenerate': 'AI 生成新图',
|
|
93
105
|
'canvas.tabGenerate': 'AI 生成',
|
|
94
106
|
'canvas.dropHint': '图片拖动到此处即可添加',
|
|
95
107
|
'canvas.dropSub': '支持多选,PNG / JPG / WebP / GIF',
|
|
@@ -241,14 +253,18 @@ export const zh = {
|
|
|
241
253
|
'history.allModels': '全部模型',
|
|
242
254
|
'history.allRatios': '全部比例',
|
|
243
255
|
// gallery
|
|
244
|
-
'gallery.title': '
|
|
256
|
+
'gallery.title': '素材库',
|
|
245
257
|
'gallery.categories': '分类',
|
|
246
|
-
'gallery.all': '
|
|
258
|
+
'gallery.all': '全部素材',
|
|
259
|
+
'gallery.upload': '上传素材',
|
|
260
|
+
'gallery.uploadModel': '本地上传',
|
|
261
|
+
'gallery.uploaded': '素材已入库',
|
|
262
|
+
'gallery.uploadHint': '把本地图保存进素材库,画布随时可用',
|
|
247
263
|
'gallery.gpt': 'gpt-image-2',
|
|
248
264
|
'gallery.grok': 'grok-imagine-image',
|
|
249
265
|
'gallery.ratio': '画面比例',
|
|
250
266
|
'gallery.tags': '标签',
|
|
251
|
-
'gallery.filterHint': '
|
|
267
|
+
'gallery.filterHint': '按生成模式、模型和比例筛选素材',
|
|
252
268
|
'gallery.count': '· 共 {count} 幅',
|
|
253
269
|
'gallery.viewMode': '视图模式',
|
|
254
270
|
'gallery.masonry': '瀑布流',
|
|
@@ -270,16 +286,16 @@ export const zh = {
|
|
|
270
286
|
'gallery.downloadSelected': '下载所选',
|
|
271
287
|
'gallery.exportJson': '导出 JSON',
|
|
272
288
|
'gallery.select': '选择作品',
|
|
273
|
-
'gallery.add': '
|
|
274
|
-
'gallery.added': '
|
|
275
|
-
'gallery.already': '
|
|
276
|
-
'gallery.delete': '
|
|
289
|
+
'gallery.add': '加入素材库',
|
|
290
|
+
'gallery.added': '已加入素材库',
|
|
291
|
+
'gallery.already': '已在素材库中',
|
|
292
|
+
'gallery.delete': '移出素材库',
|
|
277
293
|
'gallery.openFolder': '打开文件夹',
|
|
278
|
-
'gallery.clear': '
|
|
294
|
+
'gallery.clear': '清空素材库',
|
|
279
295
|
'gallery.openFolderHint': '在系统的文件管理器中打开图片保存目录(~/.dsh/dsh-imagegen)',
|
|
280
|
-
'gallery.clearConfirm': '
|
|
281
|
-
'gallery.empty': '
|
|
282
|
-
'gallery.viewing': '
|
|
296
|
+
'gallery.clearConfirm': '确定要清空整个素材库吗?此操作不可撤销。',
|
|
297
|
+
'gallery.empty': '素材库还是空的,把喜欢的图片加入进来吧',
|
|
298
|
+
'gallery.viewing': '素材库 · {time}',
|
|
283
299
|
// preview
|
|
284
300
|
'preview.title': '图片预览',
|
|
285
301
|
'preview.open': '点击预览',
|
|
@@ -340,6 +356,8 @@ export const zh = {
|
|
|
340
356
|
'settings.removeModel': '移除模型',
|
|
341
357
|
'settings.optional': '可选',
|
|
342
358
|
'settings.moreOptions': '更多设置',
|
|
359
|
+
'settings.localStoragePath': '本地图片存储目录',
|
|
360
|
+
'settings.localStoragePathHint': '生成历史、素材库和无限画布图片统一保存到此目录;留空使用默认的 ~/.dsh/dsh-imagegen。',
|
|
343
361
|
'settings.storageTitle': '对象存储同步',
|
|
344
362
|
'settings.storageEnabled': '启用对象存储同步',
|
|
345
363
|
'settings.storageHint': '图片保存到本地后,异步上传一份到 S3 兼容对象存储(腾讯云 COS / 阿里云 OSS / 七牛云 S3 / MinIO / R2 等)',
|
|
@@ -353,8 +371,8 @@ export const zh = {
|
|
|
353
371
|
'settings.storageAccessKeyHint': 'S3 兼容密钥 ID(COS SecretId / OSS AccessKeyId / 七牛 AK)',
|
|
354
372
|
'settings.storageSecretKey': 'Secret Key',
|
|
355
373
|
'settings.storageSecretKeyHint': 'S3 兼容密钥,仅保存在本机设置文档',
|
|
356
|
-
'settings.storageSyncGallery': '
|
|
357
|
-
'settings.storageSyncGalleryHint': '
|
|
374
|
+
'settings.storageSyncGallery': '同步素材库图片',
|
|
375
|
+
'settings.storageSyncGalleryHint': '加入素材库的图片自动上传',
|
|
358
376
|
'settings.storageSyncHistory': '同步历史记录图片',
|
|
359
377
|
'settings.storageSyncHistoryHint': '所有生成结果也一并上传(数量可能较大)',
|
|
360
378
|
'settings.storageTest': '测试连接',
|
|
@@ -458,7 +476,7 @@ export const zh = {
|
|
|
458
476
|
'channels.statusIncomplete': '配置未完成',
|
|
459
477
|
'channels.edit': '编辑',
|
|
460
478
|
'channels.delete': '删除',
|
|
461
|
-
'channels.deleteConfirmTitle': '删除渠道「{name}
|
|
479
|
+
'channels.deleteConfirmTitle': '删除渠道「{name}」?该渠道的密钥将被移除;历史与素材库中的记录仍会保留。',
|
|
462
480
|
'channels.confirm': '确认删除',
|
|
463
481
|
'channels.cancel': '取消',
|
|
464
482
|
'channels.editorTitle': '渠道',
|
|
@@ -530,7 +548,15 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
530
548
|
'canvas.deleteCanvasConfirm': 'Click again to confirm',
|
|
531
549
|
'canvas.rename': 'Rename canvas',
|
|
532
550
|
'canvas.renameHint': 'Double-click to rename',
|
|
533
|
-
'canvas.background': '
|
|
551
|
+
'canvas.background': 'Canvas background',
|
|
552
|
+
'canvas.backgroundDots': 'Dots',
|
|
553
|
+
'canvas.backgroundLines': 'Grid lines',
|
|
554
|
+
'canvas.backgroundDiagonal': 'Diagonal',
|
|
555
|
+
'canvas.backgroundChecker': 'Checkerboard',
|
|
556
|
+
'canvas.backgroundBlank': 'Blank',
|
|
557
|
+
'canvas.backgroundUpload': 'Upload custom background…',
|
|
558
|
+
'canvas.backgroundRemove': 'Remove custom background',
|
|
559
|
+
'canvas.templateLibrary': 'Template library (pick a prompt to create text + config nodes)',
|
|
534
560
|
'canvas.undo': 'Undo (Ctrl+Z)',
|
|
535
561
|
'canvas.redo': 'Redo (Ctrl+Shift+Z)',
|
|
536
562
|
'canvas.saving': 'Saving…',
|
|
@@ -580,7 +606,11 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
580
606
|
'canvas.close': 'Close',
|
|
581
607
|
'canvas.tabUpload': 'Upload',
|
|
582
608
|
'canvas.tabHistory': 'History',
|
|
583
|
-
'canvas.tabGallery': '
|
|
609
|
+
'canvas.tabGallery': 'Assets',
|
|
610
|
+
'canvas.imageMenuUpload': 'Upload local image',
|
|
611
|
+
'canvas.imageMenuAssets': 'Pick from assets',
|
|
612
|
+
'canvas.imageMenuHistory': 'Pick from history',
|
|
613
|
+
'canvas.imageMenuGenerate': 'Generate with AI',
|
|
584
614
|
'canvas.tabGenerate': 'AI generate',
|
|
585
615
|
'canvas.dropHint': 'Drop images here to add them',
|
|
586
616
|
'canvas.dropSub': 'Multiple files, PNG / JPG / WebP / GIF',
|
|
@@ -725,9 +755,13 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
725
755
|
'history.ratio': 'Ratio filter',
|
|
726
756
|
'history.allModels': 'All models',
|
|
727
757
|
'history.allRatios': 'All ratios',
|
|
728
|
-
'gallery.title': '
|
|
758
|
+
'gallery.title': 'Assets',
|
|
729
759
|
'gallery.categories': 'Categories',
|
|
730
|
-
'gallery.all': 'All
|
|
760
|
+
'gallery.all': 'All assets',
|
|
761
|
+
'gallery.upload': 'Upload',
|
|
762
|
+
'gallery.uploadModel': 'Local upload',
|
|
763
|
+
'gallery.uploaded': 'Asset saved',
|
|
764
|
+
'gallery.uploadHint': 'Save local images for reuse on the canvas',
|
|
731
765
|
'gallery.gpt': 'gpt-image-2',
|
|
732
766
|
'gallery.grok': 'grok-imagine-image',
|
|
733
767
|
'gallery.ratio': 'Aspect ratio',
|
|
@@ -820,6 +854,8 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
820
854
|
'settings.removeModel': 'Remove model',
|
|
821
855
|
'settings.optional': 'Optional',
|
|
822
856
|
'settings.moreOptions': 'More settings',
|
|
857
|
+
'settings.localStoragePath': 'Local image storage directory',
|
|
858
|
+
'settings.localStoragePathHint': 'History, assets and canvas images share this directory; leave empty for the default ~/.dsh/dsh-imagegen.',
|
|
823
859
|
'settings.storageTitle': 'Object storage sync',
|
|
824
860
|
'settings.storageEnabled': 'Enable object storage sync',
|
|
825
861
|
'settings.storageHint': 'After images land locally, mirror them to an S3-compatible store (Tencent COS / Alibaba OSS / Qiniu S3 / MinIO / R2 …)',
|
|
@@ -1013,7 +1049,15 @@ export const ru: Record<keyof typeof zh, string> = {
|
|
|
1013
1049
|
'canvas.deleteCanvasConfirm': 'Нажмите ещё раз для подтверждения',
|
|
1014
1050
|
'canvas.rename': 'Переименовать холст',
|
|
1015
1051
|
'canvas.renameHint': 'Двойной клик — переименовать',
|
|
1016
|
-
'canvas.background': '
|
|
1052
|
+
'canvas.background': 'Фон холста',
|
|
1053
|
+
'canvas.backgroundDots': 'Точки',
|
|
1054
|
+
'canvas.backgroundLines': 'Сетка',
|
|
1055
|
+
'canvas.backgroundDiagonal': 'Диагональ',
|
|
1056
|
+
'canvas.backgroundChecker': 'Шахматка',
|
|
1057
|
+
'canvas.backgroundBlank': 'Пустой',
|
|
1058
|
+
'canvas.backgroundUpload': 'Загрузить свой фон…',
|
|
1059
|
+
'canvas.backgroundRemove': 'Убрать свой фон',
|
|
1060
|
+
'canvas.templateLibrary': 'Библиотека шаблонов (выберите промпт — создадутся текстовый узел и узел конфигурации)',
|
|
1017
1061
|
'canvas.undo': 'Отменить (Ctrl+Z)',
|
|
1018
1062
|
'canvas.redo': 'Повторить (Ctrl+Shift+Z)',
|
|
1019
1063
|
'canvas.saving': 'Сохранение…',
|
|
@@ -1063,7 +1107,11 @@ export const ru: Record<keyof typeof zh, string> = {
|
|
|
1063
1107
|
'canvas.close': 'Закрыть',
|
|
1064
1108
|
'canvas.tabUpload': 'Загрузка',
|
|
1065
1109
|
'canvas.tabHistory': 'История',
|
|
1066
|
-
'canvas.tabGallery': '
|
|
1110
|
+
'canvas.tabGallery': 'Материалы',
|
|
1111
|
+
'canvas.imageMenuUpload': 'Загрузить локальное изображение',
|
|
1112
|
+
'canvas.imageMenuAssets': 'Выбрать из материалов',
|
|
1113
|
+
'canvas.imageMenuHistory': 'Выбрать из истории',
|
|
1114
|
+
'canvas.imageMenuGenerate': 'Создать через AI',
|
|
1067
1115
|
'canvas.tabGenerate': 'AI-генерация',
|
|
1068
1116
|
'canvas.dropHint': 'Перетащите изображения сюда',
|
|
1069
1117
|
'canvas.dropSub': 'Несколько файлов, PNG / JPG / WebP / GIF',
|
|
@@ -1208,9 +1256,13 @@ export const ru: Record<keyof typeof zh, string> = {
|
|
|
1208
1256
|
'history.ratio': 'Фильтр по пропорциям',
|
|
1209
1257
|
'history.allModels': 'Все модели',
|
|
1210
1258
|
'history.allRatios': 'Все пропорции',
|
|
1211
|
-
'gallery.title': '
|
|
1259
|
+
'gallery.title': 'Материалы',
|
|
1212
1260
|
'gallery.categories': 'Категории',
|
|
1213
|
-
'gallery.all': 'Все
|
|
1261
|
+
'gallery.all': 'Все материалы',
|
|
1262
|
+
'gallery.upload': 'Загрузить',
|
|
1263
|
+
'gallery.uploadModel': 'Локальный файл',
|
|
1264
|
+
'gallery.uploaded': 'Материал добавлен',
|
|
1265
|
+
'gallery.uploadHint': 'Сохраните локальные изображения для холста',
|
|
1214
1266
|
'gallery.gpt': 'gpt-image-2',
|
|
1215
1267
|
'gallery.grok': 'grok-imagine-image',
|
|
1216
1268
|
'gallery.ratio': 'Пропорции',
|
|
@@ -1303,6 +1355,8 @@ export const ru: Record<keyof typeof zh, string> = {
|
|
|
1303
1355
|
'settings.removeModel': 'Удалить модель',
|
|
1304
1356
|
'settings.optional': 'необязательно',
|
|
1305
1357
|
'settings.moreOptions': 'Дополнительно',
|
|
1358
|
+
'settings.localStoragePath': 'Локальная папка изображений',
|
|
1359
|
+
'settings.localStoragePathHint': 'История, материалы и холст используют эту папку; пусто — ~/.dsh/dsh-imagegen.',
|
|
1306
1360
|
'settings.storageTitle': 'Синхронизация с объектным хранилищем',
|
|
1307
1361
|
'settings.storageEnabled': 'Включить синхронизацию',
|
|
1308
1362
|
'settings.storageHint': 'После сохранения локально изображения выгружаются в S3-совместимое хранилище (Tencent COS / Alibaba OSS / Qiniu S3 / MinIO / R2…)',
|
|
@@ -488,26 +488,26 @@ html[data-dsh-imagegen-active] .sidebarHistoryHost {
|
|
|
488
488
|
flex: 1;
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
.generation {
|
|
492
|
-
display: flex;
|
|
493
|
-
width: 100%;
|
|
494
|
-
gap: 14px;
|
|
495
|
-
min-width: 0;
|
|
496
|
-
min-height: 0;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/* The canvas workspace owns the full generation surface. Keep the classic
|
|
500
|
-
config/result DOM mounted so switching tabs remains instantaneous, but hide
|
|
501
|
-
those siblings while the scene graph is active. */
|
|
502
|
-
.generation[data-workspace='canvas'] {
|
|
503
|
-
display: flex;
|
|
504
|
-
height: 100%;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
.generation[data-workspace='canvas'] > .config,
|
|
508
|
-
.generation[data-workspace='canvas'] > .canvas {
|
|
509
|
-
display: none !important;
|
|
510
|
-
}
|
|
491
|
+
.generation {
|
|
492
|
+
display: flex;
|
|
493
|
+
width: 100%;
|
|
494
|
+
gap: 14px;
|
|
495
|
+
min-width: 0;
|
|
496
|
+
min-height: 0;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* The canvas workspace owns the full generation surface. Keep the classic
|
|
500
|
+
config/result DOM mounted so switching tabs remains instantaneous, but hide
|
|
501
|
+
those siblings while the scene graph is active. */
|
|
502
|
+
.generation[data-workspace='canvas'] {
|
|
503
|
+
display: flex;
|
|
504
|
+
height: 100%;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.generation[data-workspace='canvas'] > .config,
|
|
508
|
+
.generation[data-workspace='canvas'] > .canvas {
|
|
509
|
+
display: none !important;
|
|
510
|
+
}
|
|
511
511
|
|
|
512
512
|
.config {
|
|
513
513
|
display: flex;
|
|
@@ -2534,12 +2534,6 @@ html[data-dsh-imagegen-chat-collapsed] [data-dsh-imagegen-chat-resizer] {
|
|
|
2534
2534
|
}
|
|
2535
2535
|
|
|
2536
2536
|
/* --- gallery workspace -------------------------------------------------------- */
|
|
2537
|
-
.config[data-gallery='true'] .configScroll > *:not(:first-child),
|
|
2538
|
-
.config[data-gallery='true'] .footer,
|
|
2539
|
-
.canvas[data-gallery='true'] > .canvasState,
|
|
2540
|
-
.canvas[data-gallery='true'] > .canvasError,
|
|
2541
|
-
.canvas[data-gallery='true'] > .canvasBody,
|
|
2542
|
-
.studio:has(.config[data-gallery='true']) > .history { display: none; }
|
|
2543
2537
|
.config[data-gallery='true'] .configScroll { display: flex; flex: none; overflow: visible; order: 1; }
|
|
2544
2538
|
.config[data-gallery='true'] .galleryFilters { order: 2; flex: 1; min-height: 0; }
|
|
2545
2539
|
.galleryFilters { padding: 18px 14px; overflow-y: auto; overflow-x: hidden; }
|
package/src/engine.ts
CHANGED
|
@@ -559,17 +559,25 @@ async function requestOneImage(
|
|
|
559
559
|
if (typeof request.image !== 'string' || request.image === '') {
|
|
560
560
|
throw new ImageGenError('图生图需要上传参考图片', 'edit-image-missing')
|
|
561
561
|
}
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
562
|
+
const decodeReference = (dataUrl: string): { bytes: Buffer; mime: string; filename: string } => {
|
|
563
|
+
const parsed = parseDataUrl(dataUrl)
|
|
564
|
+
if (parsed === undefined) throw new ImageGenError('参考图片格式无效', 'edit-image-invalid')
|
|
565
|
+
let bytes: Buffer
|
|
566
|
+
try {
|
|
567
|
+
bytes = Buffer.from(parsed.base64, 'base64')
|
|
568
|
+
} catch {
|
|
569
|
+
throw new ImageGenError('参考图片数据无法解码', 'edit-image-invalid')
|
|
570
|
+
}
|
|
571
|
+
if (bytes.byteLength > MAX_EDIT_IMAGE_BYTES) {
|
|
572
|
+
throw new ImageGenError('参考图片超过 10MB 上限', 'edit-image-too-large')
|
|
573
|
+
}
|
|
574
|
+
return { bytes, mime: parsed.mime, filename: `reference.${extensionOf(parsed.mime)}` }
|
|
572
575
|
}
|
|
576
|
+
const primary = decodeReference(request.image)
|
|
577
|
+
const extras = (request.images ?? [])
|
|
578
|
+
.filter(img => typeof img === 'string' && img !== '')
|
|
579
|
+
.slice(0, 4)
|
|
580
|
+
.map(decodeReference)
|
|
573
581
|
// Grok Imagine /images/edits takes a JSON image_url object (a base64 data
|
|
574
582
|
// URI is accepted) instead of OpenAI's multipart form-data upload.
|
|
575
583
|
if (isGrokImagine(params.model)) {
|
|
@@ -585,7 +593,7 @@ async function requestOneImage(
|
|
|
585
593
|
// Nano Banana OpenAI-compatible gateways accept the standard multipart
|
|
586
594
|
// edit upload, with the family's own aspect_ratio / image_size knobs.
|
|
587
595
|
const form = new FormData()
|
|
588
|
-
form.append('image', new Blob([bytes], { type:
|
|
596
|
+
form.append('image', new Blob([primary.bytes], { type: primary.mime }), primary.filename)
|
|
589
597
|
form.append('prompt', request.prompt)
|
|
590
598
|
form.append('model', params.model)
|
|
591
599
|
if (params.aspect_ratio !== undefined) form.append('aspect_ratio', params.aspect_ratio)
|
|
@@ -593,19 +601,28 @@ async function requestOneImage(
|
|
|
593
601
|
body = form
|
|
594
602
|
} else if (isSeedream(params.model)) {
|
|
595
603
|
// Seedream unifies generation and editing on /images/generations; the
|
|
596
|
-
// reference image is a JSON URL / data-URL array, never multipart
|
|
604
|
+
// reference image is a JSON URL / data-URL array, never multipart, and
|
|
605
|
+
// the protocol natively accepts several references.
|
|
597
606
|
headers['content-type'] = 'application/json'
|
|
598
607
|
body = JSON.stringify({
|
|
599
608
|
model: params.model,
|
|
600
609
|
prompt: request.prompt,
|
|
601
|
-
image: [request.image],
|
|
610
|
+
image: [request.image, ...(request.images ?? []).filter(img => typeof img === 'string' && img !== '').slice(0, 4)],
|
|
602
611
|
...params.size !== undefined ? { size: params.size } : {},
|
|
603
612
|
...params.resolution !== undefined ? { resolution: params.resolution } : {},
|
|
604
613
|
response_format: isVolcSeedream(params.model) ? 'url' : 'b64_json',
|
|
605
614
|
})
|
|
606
615
|
} else {
|
|
607
616
|
const form = new FormData()
|
|
608
|
-
|
|
617
|
+
if (extras.length > 0) {
|
|
618
|
+
// OpenAI-style multi-reference upload: repeat the image[] field so
|
|
619
|
+
// every connected canvas reference reaches the gateway.
|
|
620
|
+
for (const [index, reference] of [primary, ...extras].entries()) {
|
|
621
|
+
form.append('image[]', new Blob([reference.bytes], { type: reference.mime }), `reference-${index}.${extensionOf(reference.mime)}`)
|
|
622
|
+
}
|
|
623
|
+
} else {
|
|
624
|
+
form.append('image', new Blob([primary.bytes], { type: primary.mime }), primary.filename)
|
|
625
|
+
}
|
|
609
626
|
form.append('prompt', request.prompt)
|
|
610
627
|
form.append('model', params.model)
|
|
611
628
|
if (params.size !== undefined) form.append('size', params.size)
|