dsh-image-gen 0.1.3 → 0.1.5

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/lib/client.js CHANGED
@@ -16,23 +16,745 @@ window.__ModuleLoader__.load({
16
16
  const DEFAULT_GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/interactions";
17
17
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
18
18
  const DEFAULT_SEEDREAM_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
19
+ const DEFAULT_DASHSCOPE_ENDPOINT = "https://dashscope.aliyuncs.com/api/v1";
19
20
  const DEFAULT_MODELS = {
20
21
  google: "gemini-3.1-flash-image",
21
22
  openai: "gpt-image-2",
22
- seedream: "doubao-seedream-5-0-260128"
23
+ seedream: "doubao-seedream-5-0-260128",
24
+ dashscope: "wanx2.1-t2i-turbo"
23
25
  };
24
26
  const DEFAULT_BASE_URLS = {
25
27
  google: DEFAULT_GOOGLE_ENDPOINT,
26
28
  openai: DEFAULT_OPENAI_BASE_URL,
27
- seedream: DEFAULT_SEEDREAM_BASE_URL
29
+ seedream: DEFAULT_SEEDREAM_BASE_URL,
30
+ dashscope: DEFAULT_DASHSCOPE_ENDPOINT
28
31
  };
29
32
  //#endregion
33
+ //#region lib/types/client/gallery-store.js
34
+ const DB_NAME = "dsh_image_gen_db";
35
+ const DB_VERSION = 1;
36
+ const STORE_NAME = "gallery_history";
37
+ let dbPromise = null;
38
+ function getDB() {
39
+ if (dbPromise) return dbPromise;
40
+ dbPromise = new Promise((resolve, reject) => {
41
+ if (typeof indexedDB === "undefined") {
42
+ reject(/* @__PURE__ */ new Error("IndexedDB is not supported in this environment."));
43
+ return;
44
+ }
45
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
46
+ request.onupgradeneeded = (event) => {
47
+ const db = event.target.result;
48
+ if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME, { keyPath: "id" }).createIndex("createdAt", "createdAt", { unique: false });
49
+ };
50
+ request.onsuccess = () => {
51
+ resolve(request.result);
52
+ };
53
+ request.onerror = () => {
54
+ reject(request.error);
55
+ };
56
+ });
57
+ return dbPromise;
58
+ }
59
+ const listeners = /* @__PURE__ */ new Set();
60
+ function notifyListeners() {
61
+ for (const listener of listeners) try {
62
+ listener();
63
+ } catch (err) {
64
+ console.error("[dsh-image-gen] Gallery listener error:", err);
65
+ }
66
+ }
67
+ /**
68
+ * Subscribe to gallery mutations (insert/delete/clear).
69
+ */
70
+ function subscribeGallery(listener) {
71
+ listeners.add(listener);
72
+ return () => {
73
+ listeners.delete(listener);
74
+ };
75
+ }
76
+ /**
77
+ * Save or update a gallery record by attachmentId.
78
+ */
79
+ async function saveGalleryItem(item) {
80
+ try {
81
+ const db = await getDB();
82
+ const record = {
83
+ ...item,
84
+ createdAt: item.createdAt ?? Date.now()
85
+ };
86
+ await new Promise((resolve, reject) => {
87
+ const req = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).put(record);
88
+ req.onsuccess = () => resolve();
89
+ req.onerror = () => reject(req.error);
90
+ });
91
+ notifyListeners();
92
+ } catch (err) {
93
+ console.warn("[dsh-image-gen] Failed to save gallery item to IndexedDB:", err);
94
+ }
95
+ }
96
+ /**
97
+ * Retrieve all gallery records sorted by createdAt descending.
98
+ */
99
+ async function getGalleryItems() {
100
+ try {
101
+ const db = await getDB();
102
+ return await new Promise((resolve, reject) => {
103
+ const req = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).index("createdAt").openCursor(null, "prev");
104
+ const items = [];
105
+ req.onsuccess = (event) => {
106
+ const cursor = event.target.result;
107
+ if (cursor) {
108
+ items.push(cursor.value);
109
+ cursor.continue();
110
+ } else resolve(items);
111
+ };
112
+ req.onerror = () => reject(req.error);
113
+ });
114
+ } catch (err) {
115
+ console.warn("[dsh-image-gen] Failed to read gallery items from IndexedDB:", err);
116
+ return [];
117
+ }
118
+ }
119
+ //#endregion
120
+ //#region lib/types/client/gallery-view.js
121
+ /**
122
+ * Native Workspace Gallery View Component for DSH `conversation.view` slot.
123
+ * Fully i18n-reactive (Chinese & English).
124
+ */
125
+ const DICT$1 = {
126
+ zh: {
127
+ totalCount: "共 {count} 张生成图片",
128
+ searchPlaceholder: "搜索 Prompt 关键词…",
129
+ filterAll: "全部厂商",
130
+ filterGoogle: "Google Gemini",
131
+ filterOpenAI: "OpenAI / 中转站",
132
+ filterSeedream: "字节 Seedream",
133
+ emptyTitle: "暂无生图记录",
134
+ emptyDesc: "在对话中让 Agent 生图后,生成的图片会自动收录到这里。",
135
+ noMatchTitle: "未找到匹配结果",
136
+ noMatchDesc: "尝试更换搜索关键词或选择其他厂商。",
137
+ copiedPrompt: "已复制 Prompt",
138
+ copiedImage: "已复制图片",
139
+ copyFailed: "复制失败",
140
+ preview: "查看大图",
141
+ download: "下载图片",
142
+ copyImg: "复制图片",
143
+ copyPpt: "复制 Prompt",
144
+ model: "模型",
145
+ prompt: "Prompt",
146
+ close: "关闭 (Esc)"
147
+ },
148
+ en: {
149
+ totalCount: "{count} images total",
150
+ searchPlaceholder: "Search prompt keywords…",
151
+ filterAll: "All Providers",
152
+ filterGoogle: "Google Gemini",
153
+ filterOpenAI: "OpenAI / Relay",
154
+ filterSeedream: "ByteDance Seedream",
155
+ emptyTitle: "No images generated yet",
156
+ emptyDesc: "Images generated during conversations will automatically appear here.",
157
+ noMatchTitle: "No matching images",
158
+ noMatchDesc: "Try a different search keyword or provider filter.",
159
+ copiedPrompt: "Prompt copied",
160
+ copiedImage: "Image copied",
161
+ copyFailed: "Copy failed",
162
+ preview: "Full Preview",
163
+ download: "Download",
164
+ copyImg: "Copy Image",
165
+ copyPpt: "Copy Prompt",
166
+ model: "Model",
167
+ prompt: "Prompt",
168
+ close: "Close (Esc)"
169
+ }
170
+ };
171
+ const GalleryViewTab = ({ locale }) => {
172
+ const [items, setItems] = (0, react.useState)([]);
173
+ const [search, setSearch] = (0, react.useState)("");
174
+ const [selectedProvider, setSelectedProvider] = (0, react.useState)("all");
175
+ const [previewItem, setPreviewItem] = (0, react.useState)(null);
176
+ const [previewUrl, setPreviewUrl] = (0, react.useState)(null);
177
+ const [previewBlob, setPreviewBlob] = (0, react.useState)(null);
178
+ const [toast, setToast] = (0, react.useState)(null);
179
+ const [lang, setLang] = (0, react.useState)(() => {
180
+ return (locale?.getSnapshot?.()?.active)?.startsWith("en") ? "en" : "zh";
181
+ });
182
+ (0, react.useEffect)(() => {
183
+ if (!locale?.subscribe) return;
184
+ return locale.subscribe(() => {
185
+ const active = locale.getSnapshot?.()?.active;
186
+ setLang(active?.startsWith("en") ? "en" : "zh");
187
+ });
188
+ }, [locale]);
189
+ const dict = lang === "en" ? DICT$1.en : DICT$1.zh;
190
+ const t = (key, params) => {
191
+ let text = dict[key] || DICT$1.zh[key] || key;
192
+ if (params) for (const [k, v] of Object.entries(params)) text = text.replace(`{${k}}`, v);
193
+ return text;
194
+ };
195
+ const showToast = (msg) => {
196
+ setToast(msg);
197
+ setTimeout(() => {
198
+ setToast(null);
199
+ }, 2e3);
200
+ };
201
+ (0, react.useEffect)(() => {
202
+ const seat = document.querySelector("[data-composer-seat]");
203
+ if (seat) {
204
+ const prevDisplay = seat.style.display;
205
+ seat.style.display = "none";
206
+ return () => {
207
+ seat.style.display = prevDisplay;
208
+ };
209
+ }
210
+ }, []);
211
+ (0, react.useEffect)(() => {
212
+ let active = true;
213
+ const load = () => {
214
+ getGalleryItems().then((res) => {
215
+ if (active) setItems(res);
216
+ });
217
+ };
218
+ load();
219
+ const unsubscribe = subscribeGallery(load);
220
+ return () => {
221
+ active = false;
222
+ unsubscribe();
223
+ };
224
+ }, []);
225
+ (0, react.useEffect)(() => {
226
+ if (!previewItem) return;
227
+ const onKeyDown = (e) => {
228
+ if (e.key === "Escape") {
229
+ setPreviewItem(null);
230
+ setPreviewUrl(null);
231
+ setPreviewBlob(null);
232
+ }
233
+ };
234
+ window.addEventListener("keydown", onKeyDown);
235
+ return () => window.removeEventListener("keydown", onKeyDown);
236
+ }, [previewItem]);
237
+ const filteredItems = (0, react.useMemo)(() => {
238
+ return items.filter((item) => {
239
+ if (selectedProvider !== "all" && item.provider !== selectedProvider) return false;
240
+ if (search.trim().length > 0) {
241
+ const q = search.trim().toLowerCase();
242
+ const matchPrompt = item.prompt?.toLowerCase().includes(q);
243
+ const matchModel = item.model?.toLowerCase().includes(q);
244
+ if (!matchPrompt && !matchModel) return false;
245
+ }
246
+ return true;
247
+ });
248
+ }, [
249
+ items,
250
+ search,
251
+ selectedProvider
252
+ ]);
253
+ return (0, react_jsx_runtime.jsxs)("div", {
254
+ className: "dsh-ig-gallery-page",
255
+ children: [
256
+ (0, react_jsx_runtime.jsxs)("header", {
257
+ className: "dsh-ig-gallery-page-header",
258
+ children: [(0, react_jsx_runtime.jsx)("div", {
259
+ className: "dsh-ig-gallery-page-title-row",
260
+ children: (0, react_jsx_runtime.jsx)("span", {
261
+ className: "dsh-ig-gallery-page-count",
262
+ children: t("totalCount", { count: String(items.length) })
263
+ })
264
+ }), (0, react_jsx_runtime.jsxs)("div", {
265
+ className: "dsh-ig-gallery-page-tools",
266
+ children: [(0, react_jsx_runtime.jsxs)("div", {
267
+ className: "dsh-ig-gallery-search-wrap",
268
+ children: [(0, react_jsx_runtime.jsxs)("svg", {
269
+ className: "dsh-ig-gallery-search-icon",
270
+ width: "14",
271
+ height: "14",
272
+ viewBox: "0 0 24 24",
273
+ fill: "none",
274
+ stroke: "currentColor",
275
+ strokeWidth: "2",
276
+ strokeLinecap: "round",
277
+ strokeLinejoin: "round",
278
+ children: [(0, react_jsx_runtime.jsx)("circle", {
279
+ cx: "11",
280
+ cy: "11",
281
+ r: "8"
282
+ }), (0, react_jsx_runtime.jsx)("line", {
283
+ x1: "21",
284
+ y1: "21",
285
+ x2: "16.65",
286
+ y2: "16.65"
287
+ })]
288
+ }), (0, react_jsx_runtime.jsx)("input", {
289
+ type: "text",
290
+ className: "dsh-ig-gallery-search-input",
291
+ placeholder: t("searchPlaceholder"),
292
+ value: search,
293
+ onChange: (e) => setSearch(e.target.value)
294
+ })]
295
+ }), (0, react_jsx_runtime.jsxs)("select", {
296
+ className: "dsh-ig-gallery-select",
297
+ value: selectedProvider,
298
+ onChange: (e) => setSelectedProvider(e.target.value),
299
+ children: [
300
+ (0, react_jsx_runtime.jsx)("option", {
301
+ value: "all",
302
+ children: t("filterAll")
303
+ }),
304
+ (0, react_jsx_runtime.jsx)("option", {
305
+ value: "google",
306
+ children: t("filterGoogle")
307
+ }),
308
+ (0, react_jsx_runtime.jsx)("option", {
309
+ value: "openai",
310
+ children: t("filterOpenAI")
311
+ }),
312
+ (0, react_jsx_runtime.jsx)("option", {
313
+ value: "seedream",
314
+ children: t("filterSeedream")
315
+ })
316
+ ]
317
+ })]
318
+ })]
319
+ }),
320
+ (0, react_jsx_runtime.jsx)("div", {
321
+ className: "dsh-ig-gallery-page-body",
322
+ children: items.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
323
+ className: "dsh-ig-gallery-empty",
324
+ children: [
325
+ (0, react_jsx_runtime.jsx)("div", {
326
+ className: "dsh-ig-gallery-empty-icon",
327
+ children: "🖼️"
328
+ }),
329
+ (0, react_jsx_runtime.jsx)("div", {
330
+ className: "dsh-ig-gallery-empty-title",
331
+ children: t("emptyTitle")
332
+ }),
333
+ (0, react_jsx_runtime.jsx)("div", {
334
+ className: "dsh-ig-gallery-empty-desc",
335
+ children: t("emptyDesc")
336
+ })
337
+ ]
338
+ }) : filteredItems.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
339
+ className: "dsh-ig-gallery-empty",
340
+ children: [
341
+ (0, react_jsx_runtime.jsx)("div", {
342
+ className: "dsh-ig-gallery-empty-icon",
343
+ children: "🔍"
344
+ }),
345
+ (0, react_jsx_runtime.jsx)("div", {
346
+ className: "dsh-ig-gallery-empty-title",
347
+ children: t("noMatchTitle")
348
+ }),
349
+ (0, react_jsx_runtime.jsx)("div", {
350
+ className: "dsh-ig-gallery-empty-desc",
351
+ children: t("noMatchDesc")
352
+ })
353
+ ]
354
+ }) : (0, react_jsx_runtime.jsx)("div", {
355
+ className: "dsh-ig-gallery-grid",
356
+ children: filteredItems.map((item) => (0, react_jsx_runtime.jsx)(GalleryCard, {
357
+ item,
358
+ t,
359
+ onPreview: (url, blob) => {
360
+ setPreviewItem(item);
361
+ setPreviewUrl(url);
362
+ setPreviewBlob(blob);
363
+ },
364
+ onToast: showToast
365
+ }, item.id))
366
+ })
367
+ }),
368
+ toast && (0, react_jsx_runtime.jsx)("div", {
369
+ className: "dsh-ig-gallery-page-toast",
370
+ children: toast
371
+ }),
372
+ previewItem && previewUrl && (0, react_jsx_runtime.jsxs)("div", {
373
+ className: "dsh-ig-lightbox-backdrop",
374
+ onClick: () => {
375
+ setPreviewItem(null);
376
+ setPreviewUrl(null);
377
+ setPreviewBlob(null);
378
+ },
379
+ children: [
380
+ (0, react_jsx_runtime.jsxs)("div", {
381
+ className: "dsh-ig-lightbox-topbar",
382
+ onClick: (e) => e.stopPropagation(),
383
+ children: [(0, react_jsx_runtime.jsxs)("div", {
384
+ className: "dsh-ig-lightbox-meta",
385
+ children: [(0, react_jsx_runtime.jsx)("span", {
386
+ className: "dsh-ig-tag",
387
+ children: previewItem.provider
388
+ }), previewItem.model ? (0, react_jsx_runtime.jsx)("span", {
389
+ className: "dsh-ig-tag dsh-ig-tag-model",
390
+ children: previewItem.model
391
+ }) : null]
392
+ }), (0, react_jsx_runtime.jsx)("button", {
393
+ type: "button",
394
+ className: "dsh-ig-lightbox-close-btn",
395
+ title: t("close"),
396
+ onClick: () => {
397
+ setPreviewItem(null);
398
+ setPreviewUrl(null);
399
+ setPreviewBlob(null);
400
+ },
401
+ children: (0, react_jsx_runtime.jsxs)("svg", {
402
+ width: "18",
403
+ height: "18",
404
+ viewBox: "0 0 24 24",
405
+ fill: "none",
406
+ stroke: "currentColor",
407
+ strokeWidth: "2",
408
+ strokeLinecap: "round",
409
+ strokeLinejoin: "round",
410
+ children: [(0, react_jsx_runtime.jsx)("line", {
411
+ x1: "18",
412
+ y1: "6",
413
+ x2: "6",
414
+ y2: "18"
415
+ }), (0, react_jsx_runtime.jsx)("line", {
416
+ x1: "6",
417
+ y1: "6",
418
+ x2: "18",
419
+ y2: "18"
420
+ })]
421
+ })
422
+ })]
423
+ }),
424
+ (0, react_jsx_runtime.jsx)("div", {
425
+ className: "dsh-ig-lightbox-img-wrap",
426
+ onClick: (e) => e.stopPropagation(),
427
+ children: (0, react_jsx_runtime.jsx)("img", {
428
+ className: "dsh-ig-lightbox-img",
429
+ src: previewUrl,
430
+ alt: previewItem.prompt
431
+ })
432
+ }),
433
+ (0, react_jsx_runtime.jsxs)("div", {
434
+ className: "dsh-ig-lightbox-bottombar",
435
+ onClick: (e) => e.stopPropagation(),
436
+ children: [(0, react_jsx_runtime.jsx)("div", {
437
+ className: "dsh-ig-lightbox-prompt-text",
438
+ title: previewItem.prompt,
439
+ children: previewItem.prompt
440
+ }), (0, react_jsx_runtime.jsxs)("div", {
441
+ className: "dsh-ig-lightbox-actions",
442
+ children: [
443
+ (0, react_jsx_runtime.jsxs)("button", {
444
+ type: "button",
445
+ className: "dsh-ig-lightbox-btn",
446
+ title: t("copyPpt"),
447
+ onClick: async () => {
448
+ await navigator.clipboard.writeText(previewItem.prompt);
449
+ showToast(t("copiedPrompt"));
450
+ },
451
+ children: [(0, react_jsx_runtime.jsxs)("svg", {
452
+ width: "14",
453
+ height: "14",
454
+ viewBox: "0 0 24 24",
455
+ fill: "none",
456
+ stroke: "currentColor",
457
+ strokeWidth: "2",
458
+ strokeLinecap: "round",
459
+ strokeLinejoin: "round",
460
+ children: [(0, react_jsx_runtime.jsx)("path", { d: "M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" }), (0, react_jsx_runtime.jsx)("rect", {
461
+ x: "8",
462
+ y: "2",
463
+ width: "8",
464
+ height: "4",
465
+ rx: "1",
466
+ ry: "1"
467
+ })]
468
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("copyPpt") })]
469
+ }),
470
+ (0, react_jsx_runtime.jsxs)("button", {
471
+ type: "button",
472
+ className: "dsh-ig-lightbox-btn",
473
+ title: t("copyImg"),
474
+ onClick: async () => {
475
+ if (!previewBlob) return;
476
+ const ok = await copyImageBlob(previewBlob);
477
+ showToast(ok ? t("copiedImage") : t("copyFailed"));
478
+ },
479
+ children: [(0, react_jsx_runtime.jsxs)("svg", {
480
+ width: "14",
481
+ height: "14",
482
+ viewBox: "0 0 24 24",
483
+ fill: "none",
484
+ stroke: "currentColor",
485
+ strokeWidth: "2",
486
+ strokeLinecap: "round",
487
+ strokeLinejoin: "round",
488
+ children: [(0, react_jsx_runtime.jsx)("rect", {
489
+ x: "9",
490
+ y: "9",
491
+ width: "13",
492
+ height: "13",
493
+ rx: "2",
494
+ ry: "2"
495
+ }), (0, react_jsx_runtime.jsx)("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })]
496
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("copyImg") })]
497
+ }),
498
+ (0, react_jsx_runtime.jsxs)("button", {
499
+ type: "button",
500
+ className: "dsh-ig-lightbox-btn",
501
+ title: t("download"),
502
+ onClick: () => {
503
+ const a = document.createElement("a");
504
+ a.href = previewUrl;
505
+ a.download = `dsh-${previewItem.provider}-${previewItem.id}.png`;
506
+ document.body.appendChild(a);
507
+ a.click();
508
+ document.body.removeChild(a);
509
+ },
510
+ children: [(0, react_jsx_runtime.jsxs)("svg", {
511
+ width: "14",
512
+ height: "14",
513
+ viewBox: "0 0 24 24",
514
+ fill: "none",
515
+ stroke: "currentColor",
516
+ strokeWidth: "2",
517
+ strokeLinecap: "round",
518
+ strokeLinejoin: "round",
519
+ children: [
520
+ (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
521
+ (0, react_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
522
+ (0, react_jsx_runtime.jsx)("line", {
523
+ x1: "12",
524
+ y1: "15",
525
+ x2: "12",
526
+ y2: "3"
527
+ })
528
+ ]
529
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("download") })]
530
+ })
531
+ ]
532
+ })]
533
+ })
534
+ ]
535
+ })
536
+ ]
537
+ });
538
+ };
539
+ const GalleryCard = ({ item, t, onPreview, onToast }) => {
540
+ const [url, setUrl] = (0, react.useState)();
541
+ const [blob, setBlob] = (0, react.useState)();
542
+ const [loading, setLoading] = (0, react.useState)(true);
543
+ const [error, setError] = (0, react.useState)();
544
+ (0, react.useEffect)(() => {
545
+ const controller = new AbortController();
546
+ let objectUrl;
547
+ fetch(IMAGE_ROUTE, {
548
+ method: "POST",
549
+ signal: controller.signal,
550
+ headers: { "content-type": "application/json" },
551
+ body: JSON.stringify({ attachment: item.attachment })
552
+ }).then(async (response) => {
553
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
554
+ const resBlob = await response.blob();
555
+ if (controller.signal.aborted) return;
556
+ setBlob(resBlob);
557
+ objectUrl = URL.createObjectURL(resBlob);
558
+ setUrl(objectUrl);
559
+ setLoading(false);
560
+ }).catch((err) => {
561
+ if (!controller.signal.aborted) {
562
+ setError(err instanceof Error ? err.message : String(err));
563
+ setLoading(false);
564
+ }
565
+ });
566
+ return () => {
567
+ controller.abort();
568
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
569
+ };
570
+ }, [item.attachment]);
571
+ const copyPrompt = async (e) => {
572
+ e.stopPropagation();
573
+ try {
574
+ await navigator.clipboard.writeText(item.prompt);
575
+ onToast(t("copiedPrompt"));
576
+ } catch {
577
+ onToast(t("copyFailed"));
578
+ }
579
+ };
580
+ const copyImage = async (e) => {
581
+ e.stopPropagation();
582
+ if (!blob) return;
583
+ onToast(await copyImageBlob(blob) ? t("copiedImage") : t("copyFailed"));
584
+ };
585
+ const downloadImage = (e) => {
586
+ e.stopPropagation();
587
+ if (!url) return;
588
+ const a = document.createElement("a");
589
+ a.href = url;
590
+ a.download = `dsh-${item.provider}-${item.id}.png`;
591
+ document.body.appendChild(a);
592
+ a.click();
593
+ document.body.removeChild(a);
594
+ };
595
+ return (0, react_jsx_runtime.jsxs)("div", {
596
+ className: "dsh-ig-gallery-card",
597
+ onClick: () => {
598
+ if (url && blob) onPreview(url, blob);
599
+ },
600
+ children: [(0, react_jsx_runtime.jsxs)("div", {
601
+ className: "dsh-ig-gallery-card-media",
602
+ children: [
603
+ loading && (0, react_jsx_runtime.jsx)("div", {
604
+ className: "dsh-ig-gallery-card-loading",
605
+ children: "..."
606
+ }),
607
+ error && (0, react_jsx_runtime.jsxs)("div", {
608
+ className: "dsh-ig-gallery-card-error",
609
+ children: ["⚠️ ", error]
610
+ }),
611
+ url && (0, react_jsx_runtime.jsx)("img", {
612
+ className: "dsh-ig-gallery-card-img",
613
+ src: url,
614
+ alt: item.prompt,
615
+ loading: "lazy"
616
+ }),
617
+ (0, react_jsx_runtime.jsxs)("div", {
618
+ className: "dsh-ig-card-toolbar",
619
+ children: [
620
+ (0, react_jsx_runtime.jsx)("button", {
621
+ type: "button",
622
+ className: "dsh-ig-tool-btn",
623
+ title: t("copyImg"),
624
+ onClick: (e) => {
625
+ copyImage(e);
626
+ },
627
+ children: (0, react_jsx_runtime.jsxs)("svg", {
628
+ width: "14",
629
+ height: "14",
630
+ viewBox: "0 0 24 24",
631
+ fill: "none",
632
+ stroke: "currentColor",
633
+ strokeWidth: "2",
634
+ strokeLinecap: "round",
635
+ strokeLinejoin: "round",
636
+ children: [(0, react_jsx_runtime.jsx)("rect", {
637
+ x: "9",
638
+ y: "9",
639
+ width: "13",
640
+ height: "13",
641
+ rx: "2",
642
+ ry: "2"
643
+ }), (0, react_jsx_runtime.jsx)("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })]
644
+ })
645
+ }),
646
+ (0, react_jsx_runtime.jsx)("button", {
647
+ type: "button",
648
+ className: "dsh-ig-tool-btn",
649
+ title: t("download"),
650
+ onClick: downloadImage,
651
+ children: (0, react_jsx_runtime.jsxs)("svg", {
652
+ width: "14",
653
+ height: "14",
654
+ viewBox: "0 0 24 24",
655
+ fill: "none",
656
+ stroke: "currentColor",
657
+ strokeWidth: "2",
658
+ strokeLinecap: "round",
659
+ strokeLinejoin: "round",
660
+ children: [
661
+ (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
662
+ (0, react_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
663
+ (0, react_jsx_runtime.jsx)("line", {
664
+ x1: "12",
665
+ y1: "15",
666
+ x2: "12",
667
+ y2: "3"
668
+ })
669
+ ]
670
+ })
671
+ }),
672
+ (0, react_jsx_runtime.jsx)("button", {
673
+ type: "button",
674
+ className: "dsh-ig-tool-btn",
675
+ title: t("copyPpt"),
676
+ onClick: (e) => {
677
+ copyPrompt(e);
678
+ },
679
+ children: (0, react_jsx_runtime.jsxs)("svg", {
680
+ width: "14",
681
+ height: "14",
682
+ viewBox: "0 0 24 24",
683
+ fill: "none",
684
+ stroke: "currentColor",
685
+ strokeWidth: "2",
686
+ strokeLinecap: "round",
687
+ strokeLinejoin: "round",
688
+ children: [(0, react_jsx_runtime.jsx)("path", { d: "M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" }), (0, react_jsx_runtime.jsx)("rect", {
689
+ x: "8",
690
+ y: "2",
691
+ width: "8",
692
+ height: "4",
693
+ rx: "1",
694
+ ry: "1"
695
+ })]
696
+ })
697
+ })
698
+ ]
699
+ })
700
+ ]
701
+ }), (0, react_jsx_runtime.jsxs)("div", {
702
+ className: "dsh-ig-gallery-card-meta",
703
+ children: [(0, react_jsx_runtime.jsxs)("div", {
704
+ className: "dsh-ig-gallery-card-header",
705
+ children: [(0, react_jsx_runtime.jsx)("span", {
706
+ className: "dsh-ig-tag",
707
+ children: item.provider
708
+ }), item.model ? (0, react_jsx_runtime.jsx)("span", {
709
+ className: "dsh-ig-tag dsh-ig-tag-model",
710
+ children: item.model
711
+ }) : null]
712
+ }), (0, react_jsx_runtime.jsx)("p", {
713
+ className: "dsh-ig-gallery-card-prompt",
714
+ title: item.prompt,
715
+ children: item.prompt
716
+ })]
717
+ })]
718
+ });
719
+ };
720
+ async function copyImageBlob(blob) {
721
+ try {
722
+ if (blob.type === "image/png") {
723
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
724
+ return true;
725
+ }
726
+ const img = new Image();
727
+ const url = URL.createObjectURL(blob);
728
+ await new Promise((resolve, reject) => {
729
+ img.onload = resolve;
730
+ img.onerror = reject;
731
+ img.src = url;
732
+ });
733
+ const canvas = document.createElement("canvas");
734
+ canvas.width = img.naturalWidth;
735
+ canvas.height = img.naturalHeight;
736
+ const ctx = canvas.getContext("2d");
737
+ if (!ctx) throw new Error("Canvas unavailable");
738
+ ctx.drawImage(img, 0, 0);
739
+ URL.revokeObjectURL(url);
740
+ const pngBlob = await new Promise((res) => {
741
+ canvas.toBlob(res, "image/png");
742
+ });
743
+ if (!pngBlob) throw new Error("Blob conversion failed");
744
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": pngBlob })]);
745
+ return true;
746
+ } catch (_err) {
747
+ return false;
748
+ }
749
+ }
750
+ //#endregion
30
751
  //#region lib/types/client/index.js
31
752
  /** Web settings and generated-image cards contributed by the Bundle. */
32
753
  const KEY_REF = {
33
754
  google: "GEMINI_API_KEY",
34
755
  openai: "OPENAI_API_KEY",
35
- seedream: "ARK_API_KEY"
756
+ seedream: "ARK_API_KEY",
757
+ dashscope: "DASHSCOPE_API_KEY"
36
758
  };
37
759
  const DICT = {
38
760
  zh: {
@@ -42,6 +764,7 @@ window.__ModuleLoader__.load({
42
764
  providerGoogle: "Google Gemini",
43
765
  providerOpenAI: "OpenAI / 中转站",
44
766
  providerSeedream: "字节 Seedream",
767
+ providerDashScope: "阿里 DashScope (通义万相 / Qwen)",
45
768
  apiKeyLabel: "{provider} API Key",
46
769
  apiKeyPlaceholder: "留空即可保留已配置的 Key",
47
770
  apiKeyHint: "安全保存为 {key};页面不会读回明文。",
@@ -51,6 +774,7 @@ window.__ModuleLoader__.load({
51
774
  endpointHintGoogle: "Google 官方地址或反代端点(全路径)。",
52
775
  endpointHintOpenAI: "中转站请填其 OpenAI 兼容的 /v1 地址。",
53
776
  endpointHintSeedream: "火山方舟兼容的 /api/v3 地址。",
777
+ endpointHintDashScope: "阿里云百炼 DashScope 官方接口地址。",
54
778
  model: "模型",
55
779
  saving: "保存中…",
56
780
  save: "保存",
@@ -61,7 +785,12 @@ window.__ModuleLoader__.load({
61
785
  generating: "正在生成图片…",
62
786
  loading: "正在加载图片…",
63
787
  loadFailed: "图片读取失败 ({status})",
64
- generatedTitle: "Generated image"
788
+ generatedTitle: "已生成图片",
789
+ copyImg: "复制图片",
790
+ download: "下载图片",
791
+ openNewTab: "新标签页打开",
792
+ copiedImage: "已复制图片",
793
+ copyFailed: "复制失败"
65
794
  },
66
795
  en: {
67
796
  title: "Image Generation",
@@ -70,6 +799,7 @@ window.__ModuleLoader__.load({
70
799
  providerGoogle: "Google Gemini",
71
800
  providerOpenAI: "OpenAI / Relay",
72
801
  providerSeedream: "ByteDance Seedream",
802
+ providerDashScope: "Aliyun DashScope (Wanx / Qwen)",
73
803
  apiKeyLabel: "{provider} API Key",
74
804
  apiKeyPlaceholder: "Leave empty to keep configured key",
75
805
  apiKeyHint: "Securely saved as {key}; never read back in plaintext.",
@@ -79,6 +809,7 @@ window.__ModuleLoader__.load({
79
809
  endpointHintGoogle: "Official Google endpoint or reverse proxy (full path).",
80
810
  endpointHintOpenAI: "OpenAI-compatible /v1 base URL for relays.",
81
811
  endpointHintSeedream: "Volcengine Ark compatible /api/v3 base URL.",
812
+ endpointHintDashScope: "Official Aliyun DashScope endpoint.",
82
813
  model: "Model",
83
814
  saving: "Saving…",
84
815
  save: "Save",
@@ -89,7 +820,12 @@ window.__ModuleLoader__.load({
89
820
  generating: "Generating image…",
90
821
  loading: "Loading image…",
91
822
  loadFailed: "Failed to load image ({status})",
92
- generatedTitle: "Generated image"
823
+ generatedTitle: "Generated image",
824
+ copyImg: "Copy Image",
825
+ download: "Download Image",
826
+ openNewTab: "Open in new tab",
827
+ copiedImage: "Image copied",
828
+ copyFailed: "Copy failed"
93
829
  }
94
830
  };
95
831
  const STYLE = `
@@ -115,6 +851,7 @@ window.__ModuleLoader__.load({
115
851
  .dsh-ig-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2,#eee)}
116
852
  .dsh-ig-save{appearance:none;border:0;border-radius:8px;padding:6px 16px;background:var(--dsw-alias-label-primary,#111827);color:var(--dsw-alias-bg-layer-3,#fff);font:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .15s}
117
853
  .dsh-ig-save:disabled{opacity:.4;cursor:default}
854
+
118
855
  .dsh-ig-result{display:grid;gap:10px;max-width:520px}
119
856
  .dsh-ig-result-title{font-size:14px;font-weight:600}
120
857
  .dsh-ig-container{position:relative;display:inline-block;width:fit-content;max-width:100%;justify-self:start;border-radius:12px;overflow:hidden;line-height:0}
@@ -124,11 +861,63 @@ window.__ModuleLoader__.load({
124
861
  .dsh-ig-tool-btn:hover{background:rgba(255,255,255,0.25)}
125
862
  .dsh-ig-toast{position:absolute;top:100%;left:0;margin-top:5px;padding:3px 8px;border-radius:6px;background:rgba(0,0,0,0.85);color:#fff;font-size:11px;white-space:nowrap;pointer-events:none;z-index:20}
126
863
  .dsh-ig-image{display:block;max-width:100%;max-height:520px;border-radius:12px;background:#f2f3f5;cursor:pointer}
127
- .dsh-ig-modal{position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.88);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box;cursor:zoom-out;animation:dsh-ig-fade .15s ease-out}
128
864
  @keyframes dsh-ig-fade{from{opacity:0}to{opacity:1}}
129
- .dsh-ig-modal-img{max-width:92vw;max-height:92vh;object-fit:contain;border-radius:8px;box-shadow:0 24px 60px rgba(0,0,0,0.6);user-select:none;cursor:default}
130
865
  .dsh-ig-error{color:var(--dsw-alias-label-error,#d33);font-size:13px}
131
- .dsh-ig-loading{color:var(--dsw-alias-label-tertiary,#7b818b);font-size:13px}`;
866
+ .dsh-ig-loading{color:var(--dsw-alias-label-tertiary,#7b818b);font-size:13px}
867
+
868
+ /* Native Workspace Gallery View (Renders seamlessly inside DSH Session View) */
869
+ .dsh-ig-gallery-page{width:100%;height:100%;background:var(--dsw-alias-bg-layer-1,#ffffff);display:flex;flex-direction:column;overflow:hidden;flex:1}
870
+ .dsh-ig-gallery-page-header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:12px 28px;border-bottom:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-1,#ffffff);flex-shrink:0}
871
+ .dsh-ig-gallery-page-title-row{display:flex;align-items:center;gap:12px}
872
+ .dsh-ig-gallery-page-count{font-size:13px;font-weight:500;color:var(--dsw-alias-label-secondary,#4b5563);background:var(--dsw-alias-bg-layer-3,#f3f4f6);padding:3px 10px;border-radius:20px}
873
+ .dsh-ig-gallery-page-tools{display:flex;align-items:center;gap:12px}
874
+ .dsh-ig-gallery-search-wrap{position:relative;display:flex;align-items:center}
875
+ .dsh-ig-gallery-search-icon{position:absolute;left:10px;color:var(--dsw-alias-label-tertiary,#9ca3af);pointer-events:none}
876
+ .dsh-ig-gallery-search-input{padding:6px 12px 6px 32px;font-size:13px;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;background:var(--dsw-alias-bg-layer-2,#fff);color:inherit;outline:none;width:200px;transition:border-color .15s,width .2s}
877
+ .dsh-ig-gallery-search-input:focus{border-color:var(--dsw-alias-brand-primary,#4c78ff);width:240px}
878
+ .dsh-ig-gallery-select{padding:6px 12px;font-size:13px;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;background:var(--dsw-alias-bg-layer-2,#fff);color:inherit;outline:none;cursor:pointer}
879
+ .dsh-ig-gallery-page-body{flex:1;overflow-y:auto;padding:24px 28px}
880
+ .dsh-ig-gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:20px}
881
+ .dsh-ig-gallery-card{background:var(--dsw-alias-bg-layer-2,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:12px;overflow:hidden;display:flex;flex-direction:column;cursor:pointer;transition:transform .18s ease,box-shadow .18s ease,border-color .18s ease}
882
+ .dsh-ig-gallery-card:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,0.06);border-color:var(--dsw-alias-border-l1,#cfd4dc)}
883
+ .dsh-ig-gallery-card-media{position:relative;width:100%;aspect-ratio:1/1;background:#f3f4f6;overflow:hidden;display:flex;align-items:center;justify-content:center}
884
+ .dsh-ig-gallery-card-img{width:100%;height:100%;object-fit:cover;transition:transform .2s}
885
+ .dsh-ig-gallery-card:hover .dsh-ig-gallery-card-img{transform:scale(1.03)}
886
+ .dsh-ig-gallery-card-loading{font-size:12px;color:#9ca3af}
887
+ .dsh-ig-gallery-card-error{font-size:12px;color:#ef4444;padding:8px;text-align:center}
888
+
889
+ /* Floating Action Toolbar (Matches Chat Image Toolbar) */
890
+ .dsh-ig-gallery-card:hover .dsh-ig-card-toolbar{opacity:1;pointer-events:auto}
891
+ .dsh-ig-card-toolbar{position:absolute;top:8px;left:8px;display:flex;align-items:center;gap:4px;padding:3px 5px;border-radius:8px;background:rgba(0,0,0,0.68);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);opacity:0;pointer-events:none;transition:opacity .18s ease;z-index:10;line-height:1}
892
+
893
+ .dsh-ig-gallery-card-meta{padding:12px 14px;display:flex;flex-direction:column;gap:6px;background:var(--dsw-alias-bg-layer-2,#fff);flex:1}
894
+ .dsh-ig-gallery-card-header{display:flex;align-items:center;justify-content:space-between;font-size:11px}
895
+ .dsh-ig-tag{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-3,#edf0f3);color:var(--dsw-alias-label-secondary,inherit);font-weight:500;text-transform:uppercase;font-size:10px}
896
+ .dsh-ig-tag-model{background:rgba(76,120,255,0.1);color:#4c78ff}
897
+ .dsh-ig-gallery-card-prompt{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-primary,inherit);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}
898
+ .dsh-ig-gallery-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;min-height:360px;text-align:center;color:var(--dsw-alias-label-tertiary,#7b818b)}
899
+ .dsh-ig-gallery-empty-icon{font-size:48px;margin-bottom:12px}
900
+ .dsh-ig-gallery-empty-title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary,inherit);margin-bottom:6px}
901
+ .dsh-ig-gallery-empty-desc{font-size:13px;max-width:360px;line-height:1.5}
902
+
903
+ /* Pure Centered Lightbox */
904
+ .dsh-ig-lightbox-backdrop{position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.88);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;box-sizing:border-box;cursor:zoom-out;animation:dsh-ig-fade .15s ease-out}
905
+ .dsh-ig-lightbox-topbar{position:absolute;top:20px;left:24px;right:24px;display:flex;align-items:center;justify-content:space-between;z-index:10;pointer-events:none}
906
+ .dsh-ig-lightbox-meta{display:flex;align-items:center;gap:8px;pointer-events:auto}
907
+ .dsh-ig-lightbox-close-btn{appearance:none;border:0;background:rgba(255,255,255,0.15);color:#fff;border-radius:50%;width:34px;height:34px;font-size:16px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .15s;pointer-events:auto}
908
+ .dsh-ig-lightbox-close-btn:hover{background:rgba(255,255,255,0.3)}
909
+ .dsh-ig-lightbox-img-wrap{max-width:86vw;max-height:78vh;display:flex;align-items:center;justify-content:center;cursor:default}
910
+ .dsh-ig-lightbox-img{max-width:100%;max-height:78vh;object-fit:contain;border-radius:8px;box-shadow:0 24px 60px rgba(0,0,0,0.7);user-select:none}
911
+ .dsh-ig-lightbox-bottombar{position:absolute;bottom:24px;left:50%;transform:translateX(-50%);max-width:min(90vw,640px);background:rgba(20,22,26,0.85);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,0.15);border-radius:14px;padding:10px 16px;display:flex;flex-direction:column;gap:8px;color:#fff;box-shadow:0 16px 40px rgba(0,0,0,0.5);cursor:default}
912
+ .dsh-ig-lightbox-prompt-text{font-size:13px;line-height:1.4;color:rgba(255,255,255,0.92);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}
913
+ .dsh-ig-lightbox-actions{display:flex;align-items:center;gap:8px;justify-content:flex-end;border-top:1px solid rgba(255,255,255,0.1);padding-top:8px}
914
+ .dsh-ig-lightbox-btn{appearance:none;border:1px solid rgba(255,255,255,0.18);background:rgba(255,255,255,0.08);color:#fff;border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:5px;transition:background .15s}
915
+ .dsh-ig-lightbox-btn:hover{background:rgba(255,255,255,0.22)}
916
+ .dsh-ig-gallery-page-toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#fff;padding:6px 14px;border-radius:8px;font-size:13px;z-index:99999;animation:dsh-ig-fade .15s}
917
+
918
+ /* Hide floating chat composer when gallery page is active */
919
+ [data-conversation-scroll]:has(.dsh-ig-gallery-page) [data-composer-seat]{display:none!important}
920
+ `;
132
921
  /** Required browser services. */
133
922
  const inject = [
134
923
  "slots",
@@ -137,7 +926,7 @@ window.__ModuleLoader__.load({
137
926
  "settingsScope",
138
927
  "locale"
139
928
  ];
140
- /** Mount the settings card and generated-image card. */
929
+ /** Mount the settings card, generated-image card, and native conversation gallery view. */
141
930
  function apply(ctx) {
142
931
  const { api } = ctx.get("connection");
143
932
  const scope = ctx.settingsScope.bind({ namespace: IMAGE_GENERATION_NAMESPACE });
@@ -155,24 +944,32 @@ window.__ModuleLoader__.load({
155
944
  ctx.slots.inject("settings.plugin.item", () => register({
156
945
  name: "settings.plugin.item",
157
946
  key: IMAGE_GENERATION_NAMESPACE,
158
- id: IMAGE_GENERATION_NAMESPACE,
159
- order: 100,
160
947
  inject: () => ({
161
948
  scope,
162
949
  credentials: api.credentials,
163
950
  locale
164
951
  })
165
952
  }, ImageGenerationSettingsCard));
166
- ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
953
+ ctx.slots.inject("tool.call.toolview", () => register({
167
954
  name: "tool.call.toolview",
168
- key: "generate_image"
955
+ key: "generate_image",
956
+ inject: () => ({ locale })
169
957
  }, GeneratedImageCard));
958
+ ctx.slots.inject("conversation.view", () => register({
959
+ name: "conversation.view",
960
+ id: "gallery",
961
+ order: 20,
962
+ label: () => {
963
+ return (locale?.getSnapshot?.()?.active)?.startsWith("en") ? "Gallery" : "画廊";
964
+ },
965
+ inject: () => ({ locale })
966
+ }, GalleryViewTab));
170
967
  }
171
968
  /** Edit provider settings and its write-only API credential. */
172
969
  function ImageGenerationSettingsCard(props) {
173
970
  const [open, setOpen] = (0, react.useState)(false);
174
971
  const [snapshot, setSnapshot] = (0, react.useState)(() => props.scope.getSnapshot());
175
- const [lang, setLang] = (0, react.useState)(() => props.locale?.getSnapshot?.()?.active ?? "zh");
972
+ const [lang, setLang] = (0, react.useState)(() => props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
176
973
  const [provider, setProvider] = (0, react.useState)("google");
177
974
  const [model, setModel] = (0, react.useState)("");
178
975
  const [baseURL, setBaseURL] = (0, react.useState)("");
@@ -185,7 +982,7 @@ window.__ModuleLoader__.load({
185
982
  }), [props.scope]);
186
983
  (0, react.useEffect)(() => {
187
984
  return props.locale?.subscribe?.(() => {
188
- setLang(props.locale?.getSnapshot?.()?.active ?? "zh");
985
+ setLang(props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
189
986
  });
190
987
  }, [props.locale]);
191
988
  const t = (keyName, params) => {
@@ -196,7 +993,8 @@ window.__ModuleLoader__.load({
196
993
  const providerLabels = {
197
994
  google: t("providerGoogle"),
198
995
  openai: t("providerOpenAI"),
199
- seedream: t("providerSeedream")
996
+ seedream: t("providerSeedream"),
997
+ dashscope: t("providerDashScope")
200
998
  };
201
999
  (0, react.useEffect)(() => {
202
1000
  const value = snapshot.value;
@@ -222,8 +1020,8 @@ window.__ModuleLoader__.load({
222
1020
  setMessage("");
223
1021
  try {
224
1022
  await props.scope.set("provider", provider);
225
- await props.scope.set(provider === "google" ? "googleModel" : provider === "openai" ? "openaiModel" : "seedreamModel", model);
226
- await props.scope.set(provider === "google" ? "googleEndpoint" : provider === "openai" ? "openaiBaseURL" : "seedreamBaseURL", baseURL);
1023
+ await props.scope.set(provider === "google" ? "googleModel" : provider === "openai" ? "openaiModel" : provider === "seedream" ? "seedreamModel" : "dashscopeModel", model);
1024
+ await props.scope.set(provider === "google" ? "googleEndpoint" : provider === "openai" ? "openaiBaseURL" : provider === "seedream" ? "seedreamBaseURL" : "dashscopeEndpoint", baseURL);
227
1025
  if (key.trim().length > 0) {
228
1026
  const response = await props.credentials.set({
229
1027
  ref: KEY_REF[provider],
@@ -309,6 +1107,10 @@ window.__ModuleLoader__.load({
309
1107
  (0, react_jsx_runtime.jsx)("option", {
310
1108
  value: "seedream",
311
1109
  children: t("providerSeedream")
1110
+ }),
1111
+ (0, react_jsx_runtime.jsx)("option", {
1112
+ value: "dashscope",
1113
+ children: t("providerDashScope")
312
1114
  })
313
1115
  ]
314
1116
  }),
@@ -370,7 +1172,7 @@ window.__ModuleLoader__.load({
370
1172
  }),
371
1173
  (0, react_jsx_runtime.jsx)("span", {
372
1174
  className: "dsh-ig-hint",
373
- children: provider === "google" ? t("endpointHintGoogle") : provider === "openai" ? t("endpointHintOpenAI") : t("endpointHintSeedream")
1175
+ children: provider === "google" ? t("endpointHintGoogle") : provider === "openai" ? t("endpointHintOpenAI") : provider === "seedream" ? t("endpointHintSeedream") : t("endpointHintDashScope")
374
1176
  })
375
1177
  ]
376
1178
  }),
@@ -413,6 +1215,34 @@ window.__ModuleLoader__.load({
413
1215
  const [error, setError] = (0, react.useState)();
414
1216
  const [previewOpen, setPreviewOpen] = (0, react.useState)(false);
415
1217
  const [toast, setToast] = (0, react.useState)();
1218
+ const [lang, setLang] = (0, react.useState)(() => props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
1219
+ (0, react.useEffect)(() => {
1220
+ return props.locale?.subscribe?.(() => {
1221
+ setLang(props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
1222
+ });
1223
+ }, [props.locale]);
1224
+ const t = (keyName, params) => {
1225
+ let text = (lang === "en" ? DICT.en : DICT.zh)[keyName] || DICT.zh[keyName] || keyName;
1226
+ if (params) for (const [k, v] of Object.entries(params)) text = text.replace(`{${k}}`, v);
1227
+ return text;
1228
+ };
1229
+ (0, react.useEffect)(() => {
1230
+ if (attachment === void 0) return;
1231
+ const blockAny = props.block;
1232
+ const meta = blockAny.meta ?? blockAny.resultView?.meta;
1233
+ const prompt = typeof meta?.prompt === "string" ? meta.prompt : blockAny.call?.args?.prompt ?? "Generated Image";
1234
+ const provider = typeof meta?.provider === "string" ? meta.provider : "google";
1235
+ const model = typeof meta?.model === "string" ? meta.model : "";
1236
+ const output = typeof meta?.output === "string" ? meta.output : "";
1237
+ saveGalleryItem({
1238
+ id: attachment.attachmentId,
1239
+ attachment,
1240
+ prompt,
1241
+ provider,
1242
+ model,
1243
+ output
1244
+ });
1245
+ }, [attachment?.attachmentId]);
416
1246
  (0, react.useEffect)(() => {
417
1247
  if (!previewOpen) return;
418
1248
  const onKeyDown = (e) => {
@@ -433,7 +1263,7 @@ window.__ModuleLoader__.load({
433
1263
  headers: { "content-type": "application/json" },
434
1264
  body: JSON.stringify({ attachment })
435
1265
  }).then(async (response) => {
436
- if (!response.ok) throw new Error(`图片读取失败 (${String(response.status)})`);
1266
+ if (!response.ok) throw new Error(t("loadFailed", { status: String(response.status) }));
437
1267
  const resBlob = await response.blob();
438
1268
  if (controller.signal.aborted) return;
439
1269
  setBlob(resBlob);
@@ -446,12 +1276,12 @@ window.__ModuleLoader__.load({
446
1276
  controller.abort();
447
1277
  if (objectUrl !== void 0) URL.revokeObjectURL(objectUrl);
448
1278
  };
449
- }, [attachment?.attachmentId]);
1279
+ }, [attachment?.attachmentId, lang]);
450
1280
  const copy = async (e) => {
451
1281
  e.stopPropagation();
452
1282
  if (!blob) return;
453
1283
  const ok = await copyImageBlob(blob);
454
- setToast(ok ? "已复制图片" : "复制失败");
1284
+ setToast(ok ? t("copiedImage") : t("copyFailed"));
455
1285
  setTimeout(() => {
456
1286
  setToast(void 0);
457
1287
  }, 2e3);
@@ -473,15 +1303,15 @@ window.__ModuleLoader__.load({
473
1303
  };
474
1304
  if (attachment === void 0) return (0, react_jsx_runtime.jsx)("div", {
475
1305
  className: "dsh-ig-loading",
476
- children: "正在生成图片…"
1306
+ children: t("generating")
477
1307
  });
478
1308
  return (0, react_jsx_runtime.jsxs)("section", {
479
1309
  className: "dsh-ig-result",
480
- "aria-label": "Generated image",
1310
+ "aria-label": t("generatedTitle"),
481
1311
  children: [
482
1312
  (0, react_jsx_runtime.jsx)("div", {
483
1313
  className: "dsh-ig-result-title",
484
- children: "Generated image"
1314
+ children: t("generatedTitle")
485
1315
  }),
486
1316
  error !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
487
1317
  className: "dsh-ig-error",
@@ -489,7 +1319,7 @@ window.__ModuleLoader__.load({
489
1319
  }) : null,
490
1320
  url === void 0 && error === void 0 ? (0, react_jsx_runtime.jsx)("div", {
491
1321
  className: "dsh-ig-loading",
492
- children: "正在加载图片…"
1322
+ children: t("loading")
493
1323
  }) : null,
494
1324
  url !== void 0 ? (0, react_jsx_runtime.jsxs)("div", {
495
1325
  className: "dsh-ig-container",
@@ -506,7 +1336,7 @@ window.__ModuleLoader__.load({
506
1336
  (0, react_jsx_runtime.jsx)("button", {
507
1337
  type: "button",
508
1338
  className: "dsh-ig-tool-btn",
509
- title: "复制图片",
1339
+ title: t("copyImg"),
510
1340
  onClick: (e) => {
511
1341
  copy(e);
512
1342
  },
@@ -532,7 +1362,7 @@ window.__ModuleLoader__.load({
532
1362
  (0, react_jsx_runtime.jsx)("button", {
533
1363
  type: "button",
534
1364
  className: "dsh-ig-tool-btn",
535
- title: "下载图片",
1365
+ title: t("download"),
536
1366
  onClick: download,
537
1367
  children: (0, react_jsx_runtime.jsxs)("svg", {
538
1368
  width: "15",
@@ -558,7 +1388,7 @@ window.__ModuleLoader__.load({
558
1388
  (0, react_jsx_runtime.jsx)("button", {
559
1389
  type: "button",
560
1390
  className: "dsh-ig-tool-btn",
561
- title: "新标签页打开",
1391
+ title: t("openNewTab"),
562
1392
  onClick: openNewTab,
563
1393
  children: (0, react_jsx_runtime.jsxs)("svg", {
564
1394
  width: "15",
@@ -589,58 +1419,31 @@ window.__ModuleLoader__.load({
589
1419
  })]
590
1420
  }) : null,
591
1421
  previewOpen && url !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
592
- className: "dsh-ig-modal",
1422
+ className: "dsh-ig-lightbox-backdrop",
593
1423
  onClick: () => {
594
1424
  setPreviewOpen(false);
595
1425
  },
596
- children: (0, react_jsx_runtime.jsx)("img", {
597
- className: "dsh-ig-modal-img",
598
- src: url,
599
- alt: attachment.name ?? "Generated image preview",
1426
+ children: (0, react_jsx_runtime.jsx)("div", {
1427
+ className: "dsh-ig-lightbox-img-wrap",
600
1428
  onClick: (e) => {
601
1429
  e.stopPropagation();
602
- }
1430
+ },
1431
+ children: (0, react_jsx_runtime.jsx)("img", {
1432
+ className: "dsh-ig-lightbox-img",
1433
+ src: url,
1434
+ alt: attachment.name ?? "Generated image preview"
1435
+ })
603
1436
  })
604
1437
  }) : null
605
1438
  ]
606
1439
  });
607
1440
  }
608
- async function copyImageBlob(blob) {
609
- try {
610
- if (blob.type === "image/png") {
611
- await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
612
- return true;
613
- }
614
- const img = new Image();
615
- const url = URL.createObjectURL(blob);
616
- await new Promise((resolve, reject) => {
617
- img.onload = resolve;
618
- img.onerror = reject;
619
- img.src = url;
620
- });
621
- const canvas = document.createElement("canvas");
622
- canvas.width = img.naturalWidth;
623
- canvas.height = img.naturalHeight;
624
- const ctx = canvas.getContext("2d");
625
- if (!ctx) throw new Error("Canvas unavailable");
626
- ctx.drawImage(img, 0, 0);
627
- URL.revokeObjectURL(url);
628
- const pngBlob = await new Promise((res) => {
629
- canvas.toBlob(res, "image/png");
630
- });
631
- if (!pngBlob) throw new Error("Blob conversion failed");
632
- await navigator.clipboard.write([new ClipboardItem({ "image/png": pngBlob })]);
633
- return true;
634
- } catch (_err) {
635
- return false;
636
- }
637
- }
638
1441
  function modelOf(provider, value) {
639
- const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : value?.seedreamModel;
1442
+ const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : provider === "seedream" ? value?.seedreamModel : value?.dashscopeModel;
640
1443
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
641
1444
  }
642
1445
  function baseURLOf(provider, value) {
643
- const stored = provider === "google" ? value?.googleEndpoint : provider === "openai" ? value?.openaiBaseURL : value?.seedreamBaseURL;
1446
+ const stored = provider === "google" ? value?.googleEndpoint : provider === "openai" ? value?.openaiBaseURL : provider === "seedream" ? value?.seedreamBaseURL : value?.dashscopeEndpoint;
644
1447
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_BASE_URLS[provider];
645
1448
  }
646
1449
  function imageRef(block) {