@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.
@@ -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 { width: 34px; height: 34px; }
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 10px 10px;
577
+ padding: 0 14px 14px;
443
578
  cursor: grab;
444
579
  }
445
580
 
446
- .configNode .nodeHeader { padding: 7px 2px 5px; }
581
+ .configNode .nodeHeader { padding: 10px 2px 6px; font-size: 12px; }
447
582
 
448
583
  .configLinks {
449
584
  display: flex;
450
- margin: 2px 0 8px;
585
+ align-items: center;
586
+ margin: 4px 0 10px;
451
587
  flex: none;
452
588
  }
453
589
 
454
590
  .configLinks .composerChip {
455
- font-size: 10.5px;
456
- padding: 2px 8px;
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: 11px;
464
- line-height: 1.55;
599
+ font-size: 11.5px;
600
+ line-height: 1.7;
465
601
  color: var(--dsw-alias-label-tertiary);
466
602
  overflow: auto;
467
603
  }
@@ -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': '画廊 · {time}',
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': 'Cycle 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': 'Gallery',
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': 'Gallery',
758
+ 'gallery.title': 'Assets',
729
759
  'gallery.categories': 'Categories',
730
- 'gallery.all': 'All works',
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 parsed = parseDataUrl(request.image)
563
- if (parsed === undefined) throw new ImageGenError('参考图片格式无效', 'edit-image-invalid')
564
- let bytes: Buffer
565
- try {
566
- bytes = Buffer.from(parsed.base64, 'base64')
567
- } catch {
568
- throw new ImageGenError('参考图片数据无法解码', 'edit-image-invalid')
569
- }
570
- if (bytes.byteLength > MAX_EDIT_IMAGE_BYTES) {
571
- throw new ImageGenError('参考图片超过 10MB 上限', 'edit-image-too-large')
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: parsed.mime }), `reference.${extensionOf(parsed.mime)}`)
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
- form.append('image', new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf(parsed.mime)}`)
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)