dsh-image-gen 0.2.5 → 0.2.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/lib/index.js CHANGED
@@ -2,13 +2,15 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
2
  import * as dshSettings from "@deepseek-ai/dsh-settings";
3
3
  import { defineTool } from "@deepseek-ai/dsh-tools";
4
4
  import z from "@deepseek-ai/schemastery";
5
- import { mkdir, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
5
+ import { lstat, mkdir, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
6
6
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { randomUUID } from "node:crypto";
8
8
  //#region lib/types/shared.js
9
9
  /** Values shared by the Host and browser Bundle faces. */
10
10
  /** Browser route used by the generated-image card. */
11
11
  const IMAGE_ROUTE = "/plugins/dsh-image-gen/image";
12
+ /** Browser route used for deleting generated images and workspace files. */
13
+ const DELETE_ROUTE = "/plugins/dsh-image-gen/delete";
12
14
  /** Namespace persisted through DSH Settings. */
13
15
  const IMAGE_GENERATION_NAMESPACE = "image-generation";
14
16
  /** Supported providers. */
@@ -929,7 +931,7 @@ function record$1(value) {
929
931
  }
930
932
  //#endregion
931
933
  //#region lib/types/image-route.js
932
- const MAX_BODY_BYTES = 4096;
934
+ const MAX_BODY_BYTES = 65536;
933
935
  /** Serve one verified durable image reference to a same-origin browser request. */
934
936
  async function serveImage(req, res, deps) {
935
937
  if (req.method !== "POST") return jsonError(res, 405, "method-not-allowed");
@@ -958,6 +960,54 @@ async function serveImage(req, res, deps) {
958
960
  jsonError(res, 404, "image-unavailable");
959
961
  }
960
962
  }
963
+ /** Safely delete one or more generated image files from the workspace disk. */
964
+ async function serveDelete(req, res, deps) {
965
+ if (req.method !== "POST") return jsonError(res, 405, "method-not-allowed");
966
+ if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError(res, 415, "json-required");
967
+ const origin = req.headers.origin;
968
+ const host = req.headers.host;
969
+ if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError(res, 403, "origin-rejected");
970
+ let body;
971
+ try {
972
+ body = JSON.parse(await readBody(req));
973
+ } catch {
974
+ return jsonError(res, 400, "invalid-request");
975
+ }
976
+ const rawPaths = typeof body === "object" && body !== null && "paths" in body && Array.isArray(body.paths) ? body.paths : [];
977
+ const rawAttachmentIds = typeof body === "object" && body !== null && "attachmentIds" in body && Array.isArray(body.attachmentIds) ? body.attachmentIds : [];
978
+ const deletedFiles = [];
979
+ const failedFiles = [];
980
+ for (const p of rawPaths) if (typeof p === "string" && p.trim()) try {
981
+ if (await deps.deleteWorkspaceImage(p)) deletedFiles.push(p);
982
+ else failedFiles.push({
983
+ path: p,
984
+ error: "File not found or rejected by safety checks"
985
+ });
986
+ } catch (err) {
987
+ failedFiles.push({
988
+ path: p,
989
+ error: err instanceof Error ? err.message : String(err)
990
+ });
991
+ }
992
+ for (const id of rawAttachmentIds) if (typeof id === "string" && id.trim()) try {
993
+ if (await deps.deleteWorkspaceImageByAttachmentId(id)) deletedFiles.push(id);
994
+ } catch (err) {
995
+ failedFiles.push({
996
+ path: id,
997
+ error: err instanceof Error ? err.message : String(err)
998
+ });
999
+ }
1000
+ res.writeHead(200, {
1001
+ "content-type": "application/json",
1002
+ "cache-control": "no-store"
1003
+ });
1004
+ res.end(JSON.stringify({
1005
+ ok: failedFiles.length === 0,
1006
+ deletedCount: deletedFiles.length,
1007
+ deletedFiles,
1008
+ failedFiles
1009
+ }));
1010
+ }
961
1011
  /** Validate the persisted reference carried by a tool presentation. */
962
1012
  function imageAttachmentFromMeta(meta) {
963
1013
  const value = record(meta);
@@ -1375,6 +1425,88 @@ async function saveImageToWorkspace(options) {
1375
1425
  }
1376
1426
  return target;
1377
1427
  }
1428
+ /** Regex pattern strictly matching generated image file names (e.g. image-01234567.png). */
1429
+ const GENERATED_IMAGE_NAME_REGEX = /^image-[0-9a-f]{8,}\.(png|jpg|jpeg|webp|gif)$/i;
1430
+ /**
1431
+ * Safely delete a generated image file from workspace disk by its path.
1432
+ * Enforces strict safety gates:
1433
+ * 1. Base name must match generated image pattern (`image-<hex>.<ext>`) to avoid touching user files.
1434
+ * 2. Path must be a regular file and NOT a symbolic link.
1435
+ * 3. Real canonical path's basename must also match the pattern.
1436
+ * 4. Real canonical path must reside within an allowed workspace root (or process.cwd()).
1437
+ */
1438
+ async function deleteImageFromWorkspace(filePath, allowedWorkspaceRoots) {
1439
+ const resolved = resolve(filePath);
1440
+ const base = resolved.split(/[/\\]/).pop() ?? "";
1441
+ if (!GENERATED_IMAGE_NAME_REGEX.test(base)) return false;
1442
+ try {
1443
+ const fileLstat = await lstat(resolved);
1444
+ if (fileLstat.isSymbolicLink() || !fileLstat.isFile()) return false;
1445
+ } catch (err) {
1446
+ if (err?.code === "ENOENT") return true;
1447
+ throw err;
1448
+ }
1449
+ try {
1450
+ const realFile = await realpath(resolved);
1451
+ const realBase = realFile.split(/[/\\]/).pop() ?? "";
1452
+ if (!GENERATED_IMAGE_NAME_REGEX.test(realBase)) return false;
1453
+ const rootsToCheck = /* @__PURE__ */ new Set();
1454
+ if (allowedWorkspaceRoots) {
1455
+ for (const r of allowedWorkspaceRoots) if (typeof r === "string" && r.trim()) rootsToCheck.add(resolve(r));
1456
+ }
1457
+ rootsToCheck.add(resolve(process.cwd()));
1458
+ let withinAllowed = false;
1459
+ for (const root of rootsToCheck) try {
1460
+ if (containsPath(await realpath(root), realFile)) {
1461
+ withinAllowed = true;
1462
+ break;
1463
+ }
1464
+ } catch {}
1465
+ if (!withinAllowed) return false;
1466
+ await unlink(realFile);
1467
+ return true;
1468
+ } catch (err) {
1469
+ if (err?.code === "ENOENT") return true;
1470
+ throw err;
1471
+ }
1472
+ }
1473
+ /**
1474
+ * Safely find and delete a generated image file by its attachment ID within a single workspace root.
1475
+ * Strictly checks sha256:64hex format and avoids scanning unrelated workspaces.
1476
+ * Only used as fallback for historical images where savedTo was not persisted.
1477
+ */
1478
+ async function deleteImageByAttachmentIdFromWorkspace(attachmentId, options = {}) {
1479
+ const match = attachmentId.match(/^sha256:([0-9a-f]{64})$/i);
1480
+ if (!match || !match[1]) return false;
1481
+ const prefix = match[1].slice(0, 8);
1482
+ const rootsToCheck = /* @__PURE__ */ new Set();
1483
+ if (options.workspaceRoot && options.workspaceRoot.trim()) rootsToCheck.add(resolve(options.workspaceRoot.trim()));
1484
+ else if (options.allowedWorkspaceRoots) {
1485
+ for (const r of options.allowedWorkspaceRoots) if (typeof r === "string" && r.trim()) rootsToCheck.add(resolve(r));
1486
+ }
1487
+ rootsToCheck.add(resolve(process.cwd()));
1488
+ const candidateDirs = /* @__PURE__ */ new Set();
1489
+ if (options.folder && options.folder.trim()) candidateDirs.add(options.folder.trim());
1490
+ candidateDirs.add("dsh-image-gen");
1491
+ candidateDirs.add("image");
1492
+ candidateDirs.add("images");
1493
+ candidateDirs.add("");
1494
+ const candidateExtensions = [
1495
+ "png",
1496
+ "jpg",
1497
+ "jpeg",
1498
+ "webp",
1499
+ "gif"
1500
+ ];
1501
+ let deleted = false;
1502
+ for (const root of rootsToCheck) for (const dir of candidateDirs) for (const ext of candidateExtensions) {
1503
+ const targetPath = join(root, dir, `image-${prefix}.${ext}`);
1504
+ try {
1505
+ if (await deleteImageFromWorkspace(targetPath, rootsToCheck)) deleted = true;
1506
+ } catch {}
1507
+ }
1508
+ return deleted;
1509
+ }
1378
1510
  //#endregion
1379
1511
  //#region lib/types/index.js
1380
1512
  const name = "dsh-image-gen";
@@ -1386,6 +1518,7 @@ const inject = [
1386
1518
  ];
1387
1519
  function apply(ctx, config = {}) {
1388
1520
  let current = () => config;
1521
+ const knownWorkspaceRoots = /* @__PURE__ */ new Set();
1389
1522
  installImageSettings(ctx, config, {
1390
1523
  setSource: (source) => {
1391
1524
  current = source;
@@ -1397,6 +1530,17 @@ function apply(ctx, config = {}) {
1397
1530
  path: IMAGE_ROUTE,
1398
1531
  handler: (req, res) => serveImage(req, res, { readImage: (ref) => ctx.attachments.readImage(ref) })
1399
1532
  }), "dsh-image-gen: image route");
1533
+ ctx.effect(() => ctx.webServer.register({
1534
+ kind: "exact",
1535
+ path: DELETE_ROUTE,
1536
+ handler: (req, res) => serveDelete(req, res, {
1537
+ deleteWorkspaceImage: (filePath) => deleteImageFromWorkspace(filePath, knownWorkspaceRoots),
1538
+ deleteWorkspaceImageByAttachmentId: (attachmentId) => deleteImageByAttachmentIdFromWorkspace(attachmentId, {
1539
+ folder: current().workspaceFolder,
1540
+ allowedWorkspaceRoots: knownWorkspaceRoots
1541
+ })
1542
+ })
1543
+ }), "dsh-image-gen: delete route");
1400
1544
  ctx.tools.register(defineTool({
1401
1545
  name: "generate_image",
1402
1546
  description: "Generate a new image with the configured provider. Use when the user asks to create or draw a new image; use edit_image instead when they want to change an existing image. Give a complete visual prompt including subject, composition, style, lighting, and any exact text that should appear. A successful image is attached directly to the conversation and may also be saved under the session workspace. Do not call read, glob, or other tools to locate or verify the image.",
@@ -1449,7 +1593,7 @@ function apply(ctx, config = {}) {
1449
1593
  timeoutMs: active.timeoutMs,
1450
1594
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1451
1595
  signal: exec.signal
1452
- }), active.provider, workflow.name, "API workflow", current(), exec);
1596
+ }), active.provider, workflow.name, "API workflow", current(), exec, knownWorkspaceRoots);
1453
1597
  }
1454
1598
  const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
1455
1599
  if (credential === void 0 || credential.value.length === 0) throw new Error(`generate_image requires the ${active.apiKeyEnv} credential; configure it in Settings > Plugins > Image generation.`);
@@ -1465,7 +1609,7 @@ function apply(ctx, config = {}) {
1465
1609
  imageSize,
1466
1610
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1467
1611
  signal: exec.signal
1468
- }), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec);
1612
+ }), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec, knownWorkspaceRoots);
1469
1613
  }
1470
1614
  if (active.provider === "dashscope") {
1471
1615
  const size = args.size ?? active.imageSize;
@@ -1477,7 +1621,7 @@ function apply(ctx, config = {}) {
1477
1621
  size,
1478
1622
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1479
1623
  signal: exec.signal
1480
- }), active.provider, active.model, size, current(), exec);
1624
+ }), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
1481
1625
  }
1482
1626
  const size = args.size ?? active.imageSize;
1483
1627
  return saveGenerated(ctx, await generateOpenAICompatibleImage({
@@ -1489,7 +1633,7 @@ function apply(ctx, config = {}) {
1489
1633
  size,
1490
1634
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1491
1635
  signal: exec.signal
1492
- }), active.provider, active.model, size, current(), exec);
1636
+ }), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
1493
1637
  },
1494
1638
  presentResult: (_args, result) => imagePresentation(result)
1495
1639
  }));
@@ -1580,7 +1724,7 @@ function apply(ctx, config = {}) {
1580
1724
  timeoutMs: active.timeoutMs,
1581
1725
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1582
1726
  signal: exec.signal
1583
- }), active.provider, workflow.name, "API workflow", current(), exec);
1727
+ }), active.provider, workflow.name, "API workflow", current(), exec, knownWorkspaceRoots);
1584
1728
  }
1585
1729
  const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
1586
1730
  if (credential === void 0 || credential.value.length === 0) throw new Error(`edit_image requires the ${active.apiKeyEnv} credential; configure it in Settings > Plugins > Image generation.`);
@@ -1597,7 +1741,7 @@ function apply(ctx, config = {}) {
1597
1741
  imageSize,
1598
1742
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1599
1743
  signal: exec.signal
1600
- }), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec);
1744
+ }), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec, knownWorkspaceRoots);
1601
1745
  }
1602
1746
  const size = args.size ?? active.imageSize;
1603
1747
  if (active.provider === "openai") return saveGenerated(ctx, await editOpenAICompatibleImage({
@@ -1609,7 +1753,7 @@ function apply(ctx, config = {}) {
1609
1753
  size,
1610
1754
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1611
1755
  signal: exec.signal
1612
- }), active.provider, active.model, size, current(), exec);
1756
+ }), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
1613
1757
  if (active.provider === "seedream") return saveGenerated(ctx, await editSeedreamImage({
1614
1758
  apiKey: credential.value,
1615
1759
  baseURL: active.baseURL,
@@ -1619,7 +1763,7 @@ function apply(ctx, config = {}) {
1619
1763
  size,
1620
1764
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1621
1765
  signal: exec.signal
1622
- }), active.provider, active.model, size, current(), exec);
1766
+ }), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
1623
1767
  return saveGenerated(ctx, await editDashScopeImage({
1624
1768
  apiKey: credential.value,
1625
1769
  endpoint: active.endpoint,
@@ -1629,7 +1773,7 @@ function apply(ctx, config = {}) {
1629
1773
  size,
1630
1774
  maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1631
1775
  signal: exec.signal
1632
- }), active.provider, active.model, size, current(), exec);
1776
+ }), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
1633
1777
  },
1634
1778
  presentResult: (_args, result) => imagePresentation(result)
1635
1779
  }));
@@ -1736,7 +1880,7 @@ function attachmentMeta(ref) {
1736
1880
  } }
1737
1881
  };
1738
1882
  }
1739
- async function saveGenerated(ctx, generated, provider, model, output, config, exec) {
1883
+ async function saveGenerated(ctx, generated, provider, model, output, config, exec, knownRoots) {
1740
1884
  if (!ctx.attachments.imageLimits.mediaTypes.includes(generated.mediaType)) throw new Error(`This DSH deployment does not accept ${generated.mediaType} generated images`);
1741
1885
  const attachment = await ctx.attachments.saveImage({
1742
1886
  data: generated.data,
@@ -1753,6 +1897,7 @@ async function saveGenerated(ctx, generated, provider, model, output, config, ex
1753
1897
  if (config.saveToWorkspace === false) return value;
1754
1898
  const workspaceRoot = exec.agent?.session.header.cwd;
1755
1899
  if (workspaceRoot === void 0) return value;
1900
+ knownRoots?.add(workspaceRoot);
1756
1901
  try {
1757
1902
  value.savedTo = await saveImageToWorkspace({
1758
1903
  workspaceRoot,
@@ -1804,4 +1949,4 @@ function installImageSettings(ctx, config, hooks) {
1804
1949
  ctx.logger.warn("dsh-image-gen: this DSH exposes neither settings API generation; settings UI stays on the composition entry");
1805
1950
  }
1806
1951
  //#endregion
1807
- export { Config, IMAGE_ROUTE, apply, imageAttachmentFromMeta, inject, name };
1952
+ export { Config, DELETE_ROUTE, IMAGE_ROUTE, apply, imageAttachmentFromMeta, inject, name };
@@ -12,11 +12,17 @@ export interface GalleryItem {
12
12
  provider: ImageProvider;
13
13
  model: string;
14
14
  createdAt: number;
15
- aspectRatio?: string;
16
- imageSize?: string;
17
- output?: string;
15
+ aspectRatio?: string | undefined;
16
+ imageSize?: string | undefined;
17
+ output?: string | undefined;
18
18
  /** Workflow seed reported by the ComfyUI provider. Optional so records written by older versions stay readable; the object store is schemaless, so no IndexedDB version bump is needed. */
19
- seed?: number;
19
+ seed?: number | undefined;
20
+ /** Saved file path on workspace disk if savedTo was returned */
21
+ savedTo?: string | undefined;
22
+ /** Whether this item is marked as favorite */
23
+ isFavorite?: boolean | undefined;
24
+ /** Custom tags or collection markers */
25
+ tags?: string[] | undefined;
20
26
  }
21
27
  type GalleryListener = () => void;
22
28
  /**
@@ -26,6 +32,7 @@ export declare function subscribeGallery(listener: GalleryListener): () => void;
26
32
  /**
27
33
  * Save or update a gallery record by attachmentId.
28
34
  * Skipped if the item was previously deleted (tombstoned).
35
+ * Preserves existing isFavorite, tags, and original createdAt on re-renders.
29
36
  */
30
37
  export declare function saveGalleryItem(item: Omit<GalleryItem, 'createdAt'> & {
31
38
  createdAt?: number;
@@ -34,10 +41,19 @@ export declare function saveGalleryItem(item: Omit<GalleryItem, 'createdAt'> & {
34
41
  * Retrieve all gallery records sorted by createdAt descending.
35
42
  */
36
43
  export declare function getGalleryItems(): Promise<GalleryItem[]>;
44
+ /**
45
+ * Toggle favorite status of a gallery item.
46
+ * Returns the new favorite status (true if favorited, false if unfavorited).
47
+ */
48
+ export declare function toggleFavoriteGalleryItem(id: string): Promise<boolean>;
37
49
  /**
38
50
  * Delete a single gallery record by ID and record a tombstone.
39
51
  */
40
52
  export declare function deleteGalleryItem(id: string): Promise<void>;
53
+ /**
54
+ * Bulk delete multiple gallery records by IDs and record tombstones in a single transaction.
55
+ */
56
+ export declare function bulkDeleteGalleryItems(ids: string[]): Promise<void>;
41
57
  /**
42
58
  * Clear all gallery records and reset tombstones.
43
59
  */
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Native Workspace Gallery View Component for DSH `conversation.view` slot.
3
- * Fully i18n-reactive (Chinese & English).
2
+ * Native Workspace Gallery & Studio View Component for DSH `conversation.view` slot.
3
+ * Fully i18n-reactive (Chinese & English) with modular tabs, multi-dimensional filters,
4
+ * responsive image grid, and placeholder routes.
4
5
  */
5
6
  import { type FC } from 'react';
6
7
  export interface LocaleService {
@@ -9,23 +10,38 @@ export interface LocaleService {
9
10
  };
10
11
  subscribe(fn: () => void): () => void;
11
12
  }
13
+ export type TabKey = 'gallery' | 'studio' | 'favorites' | 'compare' | 'tasks';
14
+ export type SortKey = 'newest' | 'oldest';
12
15
  declare const DICT: {
13
16
  readonly zh: {
14
- readonly totalCount: "共 {count} 张生成图片";
15
- readonly searchPlaceholder: "搜索 Prompt 关键词…";
16
- readonly filterAll: "全部厂商";
17
+ readonly tabGallery: "图库";
18
+ readonly tabStudio: "工作台";
19
+ readonly tabFavorites: "收藏";
20
+ readonly tabCompare: "对比";
21
+ readonly tabTasks: "任务";
22
+ readonly filterAllProviders: "全部提供商";
17
23
  readonly filterGoogle: "Google Gemini";
18
24
  readonly filterOpenAI: "OpenAI / 中转站";
19
25
  readonly filterSeedream: "字节 Seedream";
20
26
  readonly filterDashScope: "阿里 DashScope";
21
27
  readonly filterComfyUI: "本地 ComfyUI";
28
+ readonly filterAllModels: "全部模型";
29
+ readonly filterAllRatios: "全部比例";
30
+ readonly searchPlaceholder: "搜索 Prompt、标签…";
31
+ readonly sortNewest: "最新生成";
32
+ readonly sortOldest: "最早生成";
33
+ readonly totalCount: "共 {count} 张生成图片";
22
34
  readonly emptyTitle: "暂无生图记录";
23
35
  readonly emptyDesc: "在对话中让 Agent 生图后,生成的图片会自动收录到这里。";
36
+ readonly favEmptyTitle: "暂无收藏图片";
37
+ readonly favEmptyDesc: "在图库中点击卡片右下角的 ♡ 按钮,即可将喜爱的图片收录到这里。";
24
38
  readonly noMatchTitle: "未找到匹配结果";
25
- readonly noMatchDesc: "尝试更换搜索关键词或选择其他厂商。";
39
+ readonly noMatchDesc: "尝试更换搜索关键词或调整筛选条件。";
26
40
  readonly copiedPrompt: "已复制 Prompt";
27
41
  readonly copiedImage: "已复制图片";
28
42
  readonly copyFailed: "复制失败";
43
+ readonly favoriteAdded: "已添加到收藏";
44
+ readonly favoriteRemoved: "已取消收藏";
29
45
  readonly preview: "查看大图";
30
46
  readonly download: "下载图片";
31
47
  readonly copyImg: "复制图片";
@@ -36,23 +52,68 @@ declare const DICT: {
36
52
  readonly model: "模型";
37
53
  readonly prompt: "Prompt";
38
54
  readonly close: "关闭 (Esc)";
55
+ readonly prevImage: "上一张 (←)";
56
+ readonly nextImage: "下一张 (→)";
57
+ readonly manage: "批量删除";
58
+ readonly exitManage: "退出选择";
59
+ readonly selectedCount: "已选 {n} 项";
60
+ readonly selectAll: "全选";
61
+ readonly invertSelect: "反选";
62
+ readonly clearSelect: "清空";
63
+ readonly batchDelete: "批量删除";
64
+ readonly batchDeleteTitle: "确认批量删除选中的 {count} 张图片?";
65
+ readonly batchDeleteTitleSingle: "确认从画廊中删除这张图片?";
66
+ readonly batchDeleteDesc: "将从画廊历史记录中移除所选图片。";
67
+ readonly deleteWorkspaceFilesOpt: "同时清理工作区磁盘物理文件(不可恢复)";
68
+ readonly cancel: "取消";
69
+ readonly confirmBatchDelete: "确认删除 ({count})";
70
+ readonly confirmDeleteSingle: "确认删除";
71
+ readonly batchDeletedToast: "已删除 {count} 张图片";
72
+ readonly batchDeletedWithFilesToast: "已删除 {count} 张图片,并清理了 {files} 个工作区文件";
73
+ readonly deleteFailedFileLocked: "已删除 {count} 张图片,但有 {files} 个文件因被系统占用未能删除";
74
+ readonly deleteFailedDatabase: "删除失败:本地数据库操作异常";
75
+ readonly justNow: "刚刚";
76
+ readonly minutesAgo: "{n} 分钟前";
77
+ readonly hoursAgo: "{n} 小时前";
78
+ readonly daysAgo: "{n} 天前";
79
+ readonly studioTitle: "AI 图像工作台 (Studio)";
80
+ readonly studioDesc: "工作台模块正在紧锣密鼓开发中。在此你将体验大图精修、变体生成 (Variations)、参数重调并一键将生成结果无缝插回 DSH 正在进行的对话。";
81
+ readonly studioTip: "💡 提示:目前你可以在“图库”中点击任意图片,在弹窗中进行查看、复制与下载。";
82
+ readonly compareTitle: "多模型横向对比 (Compare)";
83
+ readonly compareDesc: "支持单个 Prompt 一键同时调度 Gemini、Seedream、DashScope 及本地 ComfyUI 模型并排生成,直观横评画质与细节。";
84
+ readonly tasksTitle: "异步任务队列 (Tasks)";
85
+ readonly tasksDesc: "集中管理后台批量生图、多模型并发生成与本地 ComfyUI 耗时任务。支持状态追踪、失败重试与执行耗时分析。";
86
+ readonly comingSoonBadge: "即将推出";
39
87
  };
40
88
  readonly en: {
41
- readonly totalCount: "{count} images total";
42
- readonly searchPlaceholder: "Search prompt keywords…";
43
- readonly filterAll: "All Providers";
89
+ readonly tabGallery: "Gallery";
90
+ readonly tabStudio: "Studio";
91
+ readonly tabFavorites: "Favorites";
92
+ readonly tabCompare: "Compare";
93
+ readonly tabTasks: "Tasks";
94
+ readonly filterAllProviders: "All Providers";
44
95
  readonly filterGoogle: "Google Gemini";
45
96
  readonly filterOpenAI: "OpenAI / Relay";
46
97
  readonly filterSeedream: "ByteDance Seedream";
47
98
  readonly filterDashScope: "Aliyun DashScope";
48
99
  readonly filterComfyUI: "Local ComfyUI";
100
+ readonly filterAllModels: "All Models";
101
+ readonly filterAllRatios: "All Ratios";
102
+ readonly searchPlaceholder: "Search prompt, tags…";
103
+ readonly sortNewest: "Newest first";
104
+ readonly sortOldest: "Oldest first";
105
+ readonly totalCount: "{count} images total";
49
106
  readonly emptyTitle: "No images generated yet";
50
107
  readonly emptyDesc: "Images generated during conversations will automatically appear here.";
108
+ readonly favEmptyTitle: "No favorite images yet";
109
+ readonly favEmptyDesc: "Click the ♡ button on any card in the gallery to collect your favorite images here.";
51
110
  readonly noMatchTitle: "No matching images";
52
- readonly noMatchDesc: "Try a different search keyword or provider filter.";
111
+ readonly noMatchDesc: "Try a different search keyword or adjust filter criteria.";
53
112
  readonly copiedPrompt: "Prompt copied";
54
113
  readonly copiedImage: "Image copied";
55
114
  readonly copyFailed: "Copy failed";
115
+ readonly favoriteAdded: "Added to favorites";
116
+ readonly favoriteRemoved: "Removed from favorites";
56
117
  readonly preview: "Full Preview";
57
118
  readonly download: "Download";
58
119
  readonly copyImg: "Copy Image";
@@ -63,6 +124,38 @@ declare const DICT: {
63
124
  readonly model: "Model";
64
125
  readonly prompt: "Prompt";
65
126
  readonly close: "Close (Esc)";
127
+ readonly prevImage: "Previous (←)";
128
+ readonly nextImage: "Next (→)";
129
+ readonly manage: "Batch Delete";
130
+ readonly exitManage: "Done";
131
+ readonly selectedCount: "{n} selected";
132
+ readonly selectAll: "Select All";
133
+ readonly invertSelect: "Invert";
134
+ readonly clearSelect: "Clear";
135
+ readonly batchDelete: "Batch Delete";
136
+ readonly batchDeleteTitle: "Delete {count} selected images?";
137
+ readonly batchDeleteTitleSingle: "Delete this image from gallery?";
138
+ readonly batchDeleteDesc: "These images will be removed from your gallery history.";
139
+ readonly deleteWorkspaceFilesOpt: "Also delete files from workspace disk (cannot be undone)";
140
+ readonly cancel: "Cancel";
141
+ readonly confirmBatchDelete: "Delete ({count})";
142
+ readonly confirmDeleteSingle: "Delete";
143
+ readonly batchDeletedToast: "Deleted {count} images";
144
+ readonly batchDeletedWithFilesToast: "Deleted {count} images and cleaned {files} workspace files";
145
+ readonly deleteFailedFileLocked: "Deleted {count} images, but {files} files could not be deleted (busy/locked)";
146
+ readonly deleteFailedDatabase: "Failed to delete: local database error";
147
+ readonly justNow: "Just now";
148
+ readonly minutesAgo: "{n}m ago";
149
+ readonly hoursAgo: "{n}h ago";
150
+ readonly daysAgo: "{n}d ago";
151
+ readonly studioTitle: "AI Image Studio";
152
+ readonly studioDesc: "Studio workbench is under active development. Fine-tune prompts, generate variations (2x/4x), and inject images directly into DSH chat.";
153
+ readonly studioTip: "💡 Tip: You can currently click any image in the Gallery to preview, copy, or download it.";
154
+ readonly compareTitle: "Model Comparison (Compare)";
155
+ readonly compareDesc: "Side-by-side multi-model benchmarking coming soon. Test Gemini, Seedream, DashScope, and ComfyUI with a single prompt.";
156
+ readonly tasksTitle: "Task Queue (Tasks)";
157
+ readonly tasksDesc: "Centralized view for batch generation, asynchronous ComfyUI runs, live progress tracking, and retry controls.";
158
+ readonly comingSoonBadge: "Coming Soon";
66
159
  };
67
160
  };
68
161
  export type DictKey = keyof typeof DICT.zh;
@@ -1,12 +1,19 @@
1
1
  /** Same-origin HTTP bridge from the Web result card to the Attachment service. */
2
2
  import type { IncomingMessage, ServerResponse } from 'node:http';
3
3
  import type { ImageAttachmentRef, StoredImageAttachment } from '@deepseek-ai/dsh-attachment';
4
- export { IMAGE_ROUTE } from './shared.js';
4
+ export { IMAGE_ROUTE, DELETE_ROUTE } from './shared.js';
5
5
  /** Dependencies required by the image route. */
6
6
  export interface ImageRouteDeps {
7
7
  readImage(ref: ImageAttachmentRef): Promise<StoredImageAttachment>;
8
8
  }
9
+ /** Dependencies required by the delete route. */
10
+ export interface DeleteRouteDeps {
11
+ deleteWorkspaceImage(filePath: string): Promise<boolean>;
12
+ deleteWorkspaceImageByAttachmentId(attachmentId: string): Promise<boolean>;
13
+ }
9
14
  /** Serve one verified durable image reference to a same-origin browser request. */
10
15
  export declare function serveImage(req: IncomingMessage, res: ServerResponse, deps: ImageRouteDeps): Promise<void>;
16
+ /** Safely delete one or more generated image files from the workspace disk. */
17
+ export declare function serveDelete(req: IncomingMessage, res: ServerResponse, deps: DeleteRouteDeps): Promise<void>;
11
18
  /** Validate the persisted reference carried by a tool presentation. */
12
19
  export declare function imageAttachmentFromMeta(meta: unknown): ImageAttachmentRef | undefined;
@@ -2,7 +2,7 @@
2
2
  import type { Context } from '@deepseek-ai/cordis';
3
3
  import { Config } from './config.js';
4
4
  export { Config } from './config.js';
5
- export { IMAGE_ROUTE, imageAttachmentFromMeta } from './image-route.js';
5
+ export { IMAGE_ROUTE, DELETE_ROUTE, imageAttachmentFromMeta } from './image-route.js';
6
6
  export declare const name = "dsh-image-gen";
7
7
  export declare const inject: string[];
8
8
  export declare function apply(ctx: Context, config?: Config): void;
@@ -1,6 +1,8 @@
1
1
  /** Values shared by the Host and browser Bundle faces. */
2
2
  /** Browser route used by the generated-image card. */
3
3
  export declare const IMAGE_ROUTE = "/plugins/dsh-image-gen/image";
4
+ /** Browser route used for deleting generated images and workspace files. */
5
+ export declare const DELETE_ROUTE = "/plugins/dsh-image-gen/delete";
4
6
  /** Namespace persisted through DSH Settings. */
5
7
  export declare const IMAGE_GENERATION_NAMESPACE = "image-generation";
6
8
  /** Supported providers. */
@@ -50,3 +50,24 @@ export declare function saveImageToWorkspace(options: {
50
50
  data: Uint8Array;
51
51
  signal?: AbortSignal;
52
52
  }): Promise<string>;
53
+ /** Discover all DSH workspace paths recorded in local application storage. */
54
+ export declare function getDshWorkspaceRoots(): Promise<string[]>;
55
+ /**
56
+ * Safely delete a generated image file from workspace disk by its path.
57
+ * Enforces strict safety gates:
58
+ * 1. Base name must match generated image pattern (`image-<hex>.<ext>`) to avoid touching user files.
59
+ * 2. Path must be a regular file and NOT a symbolic link.
60
+ * 3. Real canonical path's basename must also match the pattern.
61
+ * 4. Real canonical path must reside within an allowed workspace root (or process.cwd()).
62
+ */
63
+ export declare function deleteImageFromWorkspace(filePath: string, allowedWorkspaceRoots?: Iterable<string>): Promise<boolean>;
64
+ /**
65
+ * Safely find and delete a generated image file by its attachment ID within a single workspace root.
66
+ * Strictly checks sha256:64hex format and avoids scanning unrelated workspaces.
67
+ * Only used as fallback for historical images where savedTo was not persisted.
68
+ */
69
+ export declare function deleteImageByAttachmentIdFromWorkspace(attachmentId: string, options?: {
70
+ workspaceRoot?: string | undefined;
71
+ folder?: string | undefined;
72
+ allowedWorkspaceRoots?: Iterable<string> | undefined;
73
+ }): Promise<boolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-image-gen",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Bring ChatGPT-like image generation to DeepSeek Harness — Gemini, OpenAI, Seedream, DashScope, local ComfyUI & more.",
5
5
  "type": "module",
6
6
  "author": "shanliuling",
@@ -130,5 +130,8 @@
130
130
  "unrun": "^0.3.1",
131
131
  "vitest": "^4.1.8"
132
132
  },
133
- "packageManager": "pnpm@11.7.0"
133
+ "packageManager": "pnpm@11.7.0",
134
+ "dependencies": {
135
+ "lucide-react": "^1.38.0"
136
+ }
134
137
  }