@dickpy/dsh-imagegen 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +11 -8
  2. package/docs/images/ecommerce-mode.png +0 -0
  3. package/docs/images/gallery-workspace.png +0 -0
  4. package/docs/images/image-generation-studio-three-column.png +0 -0
  5. package/docs/images/imagegen-overview.png +0 -0
  6. package/docs/images/plugin-settings.png +0 -0
  7. package/docs/images/prompt-template-library.png +0 -0
  8. package/lib/client.js +824 -449
  9. package/lib/client.js.map +1 -1
  10. package/lib/index.js +496 -80
  11. package/package.json +23 -17
  12. package/src/client/ImageGenPanel.tsx +10 -8
  13. package/src/client/InspirationGallery.tsx +106 -0
  14. package/src/client/SettingsCard.tsx +1 -1
  15. package/src/client/TemplateLibrary.tsx +159 -48
  16. package/src/client/api.ts +41 -8
  17. package/src/client/channels-form.ts +1 -1
  18. package/src/client/conversation-sync.ts +1 -1
  19. package/src/client/image-toolview.tsx +16 -10
  20. package/src/client/index.ts +5 -4
  21. package/src/client/inspiration.module.css +148 -0
  22. package/src/client/locales.ts +34 -8
  23. package/src/client/mount.tsx +1 -1
  24. package/src/client/settings-form.ts +2 -1
  25. package/src/client/settings-scope.ts +9 -5
  26. package/src/client/templates.module.css +95 -0
  27. package/src/index.ts +26 -6
  28. package/src/protocol.ts +81 -7
  29. package/src/routes.ts +128 -12
  30. package/src/settings-compat.ts +60 -0
  31. package/src/template-favorites.ts +108 -0
  32. package/src/templates/canghe-cases.json +11126 -0
  33. package/src/templates-store.ts +179 -68
  34. package/docs/images/image-generation-studio-single.png +0 -0
  35. package/docs/images/image-generation-studio.png +0 -0
  36. package/docs/images/poster-features-16x9.png +0 -0
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import { SettingsConflictError, installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
1
+ import * as settingsModule from "@deepseek-ai/dsh-settings";
2
+ import { SettingsConflictError } from "@deepseek-ai/dsh-settings";
2
3
  import z from "schemastery";
3
4
  import { createHash, randomUUID } from "node:crypto";
4
5
  import { promises } from "node:fs";
@@ -7,6 +8,29 @@ import path from "node:path";
7
8
  import { fileURLToPath } from "node:url";
8
9
  import { spawn } from "node:child_process";
9
10
  import { defineTool } from "@deepseek-ai/dsh-tools";
11
+ //#region src/settings-compat.ts
12
+ const compatModule = settingsModule;
13
+ /** Brand namespaces where the installed settings package still exposes it. */
14
+ function settingsNamespaceCompat(value) {
15
+ return compatModule.settingsNamespace?.(value) ?? value;
16
+ }
17
+ /**
18
+ * Register an optional settings section across the rc.7 and alpha.2 APIs.
19
+ * rc.7 exposes a module helper; alpha.2 moves the helper onto the provider.
20
+ */
21
+ function installSettingsSectionCompat(ctx, ns, schema, entry, hooks) {
22
+ const legacyInstaller = compatModule.installSettingsSection;
23
+ if (legacyInstaller !== void 0) {
24
+ legacyInstaller(ctx, ns, schema, entry, hooks);
25
+ return;
26
+ }
27
+ ctx.inject(["settings"], (sctx) => {
28
+ const provider = sctx.get("settings");
29
+ if (provider.installSection === void 0) throw new TypeError("dsh-settings does not expose installSection");
30
+ provider.installSection(ctx, ns, schema, entry, hooks);
31
+ });
32
+ }
33
+ //#endregion
10
34
  //#region src/protocol.ts
11
35
  /**
12
36
  * Wire contract shared by the host and client halves of dsh-imagegen: the
@@ -16,7 +40,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
16
40
  /** Settings namespace this plugin owns (host settings seam + bridge). */
17
41
  const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
18
42
  /** Published package version shared by the host updater and the client UI. */
19
- const PLUGIN_VERSION = "1.5.0";
43
+ const PLUGIN_VERSION = "1.5.2";
20
44
  /** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
21
45
  const SETTINGS_API = {
22
46
  describe: "/api/dsh-imagegen/settings/describe",
@@ -82,16 +106,48 @@ const GALLERY_API = {
82
106
  image: "/api/dsh-imagegen/gallery/image"
83
107
  };
84
108
  /**
85
- * Same-origin route family for the bundled prompt-template library
86
- * (awesome-gpt-image-2 mirror). The case list ships inside the package and is
87
- * served by the host; reference images are proxied through the `image` prefix
88
- * route and cached on disk so repeated views never hit the network again.
109
+ * Same-origin route family for the prompt-template libraries. The library is
110
+ * multi-source: every request names a source id from {@link TEMPLATE_SOURCES},
111
+ * each source keeps an independent snapshot/image cache host-side, and
112
+ * reference images are proxied through the source-scoped `image` prefix route
113
+ * (`…/image/<sourceId>/<file>`) and cached on disk so repeated views never hit
114
+ * the network again.
89
115
  */
90
116
  const TEMPLATES_API = {
91
117
  list: "/api/dsh-imagegen/templates/list",
92
118
  refresh: "/api/dsh-imagegen/templates/refresh",
119
+ sample: "/api/dsh-imagegen/templates/sample",
93
120
  image: "/api/dsh-imagegen/templates/image"
94
121
  };
122
+ /** Same-origin route family for the user's saved (favorited) templates. */
123
+ const TEMPLATE_FAVORITES_API = {
124
+ list: "/api/dsh-imagegen/templates/favorites/list",
125
+ add: "/api/dsh-imagegen/templates/favorites/add",
126
+ remove: "/api/dsh-imagegen/templates/favorites/remove"
127
+ };
128
+ /**
129
+ * The template-library source registry. Each entry is fully independent (own
130
+ * upstream JSON, own image pool, own refresh state) and renders as its own
131
+ * tab; adding a source later means appending an entry here plus a host-side
132
+ * fetch definition in templates-store.ts and an optional bundled snapshot.
133
+ */
134
+ const TEMPLATE_SOURCES = [{
135
+ id: "vibeui",
136
+ label: "精选案例库",
137
+ homepage: "https://vibeui.top/",
138
+ description: "awesome-gpt-image-2 精选提示词案例(vibeui.top 镜像)"
139
+ }, {
140
+ id: "canghe",
141
+ label: "沧河案例库",
142
+ homepage: "https://gpt-image2.canghe.ai/",
143
+ description: "GPT-Image2 Prompt Gallery(gpt-image2.canghe.ai,定期更新)"
144
+ }];
145
+ /** Default source id when a request does not name one (legacy clients). */
146
+ const DEFAULT_TEMPLATE_SOURCE_ID = TEMPLATE_SOURCES[0].id;
147
+ /** True when the id names a registered template source. */
148
+ function isTemplateSourceId(id) {
149
+ return TEMPLATE_SOURCES.some((source) => source.id === id);
150
+ }
95
151
  //#endregion
96
152
  //#region src/model-catalog.ts
97
153
  const ENTRIES = {
@@ -1335,24 +1391,42 @@ async function readGalleryImage(file) {
1335
1391
  //#endregion
1336
1392
  //#region src/templates-store.ts
1337
1393
  /**
1338
- * Prompt-template library store (awesome-gpt-image-2 mirror).
1394
+ * Prompt-template library store (multi-source).
1395
+ *
1396
+ * The library is a registry of independent sources (see TEMPLATE_SOURCES in
1397
+ * protocol.ts): each source has its own upstream JSON list, its own bundled
1398
+ * snapshot, its own refreshed runtime copy, and its own on-disk image pool.
1399
+ * Sources never mix — the overlay shows one tab per source and every request
1400
+ * names the source explicitly.
1339
1401
  *
1340
- * The case list ships as a bundled snapshot (src/templates/cases.json, inside
1341
- * the npm package) so the library works offline out of the box; a successful
1342
- * manual refresh writes a runtime copy under ~/.dsh/dsh-imagegen/templates/
1343
- * which then takes precedence. Reference images are not bundled (441 files,
1344
- * ≈100 MB) they are fetched from the vibeui.top mirror on demand, cached on
1345
- * disk under ~/.dsh/dsh-imagegen/template-images/, and served from there on
1402
+ * Each case list ships as a bundled snapshot (src/templates/<file>, inside the
1403
+ * npm package) so every library works offline out of the box; a successful
1404
+ * refresh (manual, or the periodic background sync) writes a runtime copy
1405
+ * under ~/.dsh/dsh-imagegen/templates/<sourceId>/ which then takes precedence.
1406
+ * Reference images are not bundled (hundreds of files, ≈100 MB per source)
1407
+ * they are fetched from the source's mirror on demand, cached on disk under
1408
+ * ~/.dsh/dsh-imagegen/template-images/<sourceId>/, and served from there on
1346
1409
  * every later view.
1347
1410
  *
1348
1411
  * Framework-free (node:fs only) so the route layer and tests can drive it
1349
1412
  * directly.
1350
1413
  */
1351
- /** Upstream mirror the library refreshes from (vibeui.top static mirror). */
1352
- const SOURCE_URL = "https://vibeui.top/extra/awesome-gpt-image-2/data/cases.json";
1353
- /** Remote image directory (file names come from the case list). */
1354
- const IMAGE_BASE_URL = "https://vibeui.top/extra/awesome-gpt-image-2/data/images/";
1355
- /** Category label map mirrored from vibeui.top's site.js (zh display names). */
1414
+ /** Source registry (host half): where each TEMPLATE_SOURCES entry loads from. */
1415
+ const SOURCE_DEFS = {
1416
+ vibeui: {
1417
+ listUrl: "https://vibeui.top/extra/awesome-gpt-image-2/data/cases.json",
1418
+ imageBaseUrl: "https://vibeui.top/extra/awesome-gpt-image-2/data/images/",
1419
+ bundledPath: fileURLToPath(new URL("../src/templates/cases.json", import.meta.url)),
1420
+ legacySnapshotPath: "legacy",
1421
+ legacyImageDir: "legacy"
1422
+ },
1423
+ canghe: {
1424
+ listUrl: "https://gpt-image2.canghe.ai/cases.json",
1425
+ imageBaseUrl: "https://gpt-image2.canghe.ai/images/",
1426
+ bundledPath: fileURLToPath(new URL("../src/templates/canghe-cases.json", import.meta.url))
1427
+ }
1428
+ };
1429
+ /** Category label map mirrored from the upstream sites' site.js (zh names). */
1356
1430
  const CATEGORY_ZH = {
1357
1431
  "Architecture & Spaces": "建筑与空间",
1358
1432
  "Brand & Logos": "品牌与标志",
@@ -1382,26 +1456,26 @@ const CATEGORY_ZH = {
1382
1456
  "Animals & Nature": "动物与自然",
1383
1457
  "Other Creative Uses": "其他创意用途"
1384
1458
  };
1385
- const DATA_DIR = path.join(homedir(), ".dsh", "dsh-imagegen");
1386
- const REFRESHED_CASES_PATH = path.join(DATA_DIR, "templates", "cases.json");
1387
- const IMAGE_CACHE_DIR = path.join(DATA_DIR, "template-images");
1388
- /**
1389
- * Bundled snapshot path. The host bundle emits to lib/index.js while this
1390
- * source file lives at src/templates-store.ts — both exactly one level below
1391
- * the package root — so `../src/templates/cases.json` resolves to the shipped
1392
- * snapshot in development and in the installed package alike.
1393
- */
1394
- const BUNDLED_CASES_PATH = fileURLToPath(new URL("../src/templates/cases.json", import.meta.url));
1459
+ const DATA_DIR$1 = path.join(homedir(), ".dsh", "dsh-imagegen");
1460
+ const REFRESHED_DIR = path.join(DATA_DIR$1, "templates");
1461
+ const IMAGE_CACHE_ROOT = path.join(DATA_DIR$1, "template-images");
1462
+ /** Pre-1.6 single-source locations (vibeui fallbacks). */
1463
+ const LEGACY_SNAPSHOT_PATH = path.join(REFRESHED_DIR, "cases.json");
1464
+ const LEGACY_IMAGE_DIR = IMAGE_CACHE_ROOT;
1395
1465
  /** Budget for one upstream fetch (list refresh or one image). */
1396
1466
  const FETCH_TIMEOUT_MS = 6e4;
1397
1467
  /** Refuse to cache implausibly large "images". */
1398
1468
  const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
1399
1469
  /** Strict reference-image file names this store writes and serves. */
1400
1470
  const IMAGE_FILE_PATTERN = /^case\d+\.(jpg|jpeg|png|webp|gif)$/i;
1401
- /** In-memory memo of the active list (avoid re-parsing on every request). */
1402
- let memo;
1471
+ /** Per-source in-memory memo of the active list (avoid re-parsing per request). */
1472
+ const memos = /* @__PURE__ */ new Map();
1403
1473
  /** Per-file in-flight downloads, so a gallery scroll never double-fetches. */
1404
1474
  const inflightImages = /* @__PURE__ */ new Map();
1475
+ /** Resolve a registered source id to its fetch definition. */
1476
+ function sourceDefOf(sourceId) {
1477
+ return SOURCE_DEFS[sourceId];
1478
+ }
1405
1479
  /** Validate + normalize one raw upstream case; undefined when unusable. */
1406
1480
  function normalizeCase(raw) {
1407
1481
  if (raw === null || typeof raw !== "object") return void 0;
@@ -1459,50 +1533,61 @@ async function readSnapshotFile(file) {
1459
1533
  }
1460
1534
  }
1461
1535
  /**
1462
- * The active template list: the refreshed runtime copy wins, the bundled
1463
- * snapshot is the always-available fallback. Memoized; a successful refresh
1464
- * replaces the memo.
1536
+ * The active template list of one source: the refreshed runtime copy wins, the
1537
+ * bundled snapshot is the always-available fallback. Memoized per source; a
1538
+ * successful refresh replaces that source's memo.
1465
1539
  */
1466
- async function listTemplates() {
1540
+ async function listTemplates(sourceId = DEFAULT_TEMPLATE_SOURCE_ID) {
1541
+ const def = sourceDefOf(sourceId);
1542
+ if (def === void 0) throw new Error(`未知的模板库来源:${sourceId}`);
1543
+ const memo = memos.get(sourceId);
1467
1544
  if (memo !== void 0) return memo;
1468
- const refreshed = await readSnapshotFile(REFRESHED_CASES_PATH);
1545
+ const refreshed = await readSnapshotFile(path.join(REFRESHED_DIR, sourceId, "cases.json")) ?? (def.legacySnapshotPath !== void 0 ? await readSnapshotFile(LEGACY_SNAPSHOT_PATH) : void 0);
1469
1546
  if (refreshed !== void 0) {
1470
- memo = {
1547
+ const result = {
1548
+ sourceId,
1471
1549
  ...refreshed,
1472
1550
  total: refreshed.cases.length,
1473
1551
  origin: "refreshed"
1474
1552
  };
1475
- return memo;
1553
+ memos.set(sourceId, result);
1554
+ return result;
1476
1555
  }
1477
- const bundled = await readSnapshotFile(BUNDLED_CASES_PATH);
1556
+ const bundled = await readSnapshotFile(def.bundledPath);
1478
1557
  if (bundled !== void 0) {
1479
- memo = {
1558
+ const result = {
1559
+ sourceId,
1480
1560
  ...bundled,
1481
1561
  total: bundled.cases.length,
1482
1562
  origin: "bundled"
1483
1563
  };
1484
- return memo;
1564
+ memos.set(sourceId, result);
1565
+ return result;
1485
1566
  }
1486
- memo = {
1567
+ const result = {
1568
+ sourceId,
1487
1569
  cases: [],
1488
1570
  total: 0,
1489
1571
  origin: "bundled",
1490
1572
  repository: "freestylefly/awesome-gpt-image-2",
1491
1573
  fetchedAt: ""
1492
1574
  };
1493
- return memo;
1575
+ memos.set(sourceId, result);
1576
+ return result;
1494
1577
  }
1495
1578
  /**
1496
- * Re-download the case list from the upstream mirror and persist it as the
1497
- * runtime copy. Throws with a user-presentable message on failure; the
1579
+ * Re-download one source's case list from its upstream mirror and persist it
1580
+ * as the runtime copy. Throws with a user-presentable message on failure; the
1498
1581
  * previous list (refreshed or bundled) stays active.
1499
1582
  */
1500
- async function refreshTemplates() {
1583
+ async function refreshTemplates(sourceId = DEFAULT_TEMPLATE_SOURCE_ID) {
1584
+ const def = sourceDefOf(sourceId);
1585
+ if (def === void 0) throw new Error(`未知的模板库来源:${sourceId}`);
1501
1586
  let response;
1502
1587
  try {
1503
- response = await fetch(SOURCE_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
1588
+ response = await fetch(def.listUrl, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
1504
1589
  } catch (error) {
1505
- throw new Error(`无法连接模板库源站:${error instanceof Error ? error.message : String(error)}`);
1590
+ throw new Error(`无法连接模板库源站(${sourceId}):${error instanceof Error ? error.message : String(error)}`);
1506
1591
  }
1507
1592
  if (!response.ok) throw new Error(`模板库源站拒绝请求(HTTP ${response.status})`);
1508
1593
  let payload;
@@ -1516,27 +1601,89 @@ async function refreshTemplates() {
1516
1601
  const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
1517
1602
  const snapshot = {
1518
1603
  repository: parsed.repository,
1519
- sourceUrl: SOURCE_URL,
1604
+ sourceUrl: def.listUrl,
1520
1605
  fetchedAt,
1521
1606
  totalCases: parsed.cases.length,
1522
1607
  cases: parsed.cases
1523
1608
  };
1524
- await promises.mkdir(path.dirname(REFRESHED_CASES_PATH), { recursive: true });
1525
- const tmp = `${REFRESHED_CASES_PATH}.tmp-${process.pid}`;
1609
+ const target = path.join(REFRESHED_DIR, sourceId, "cases.json");
1610
+ await promises.mkdir(path.dirname(target), { recursive: true });
1611
+ const tmp = `${target}.tmp-${process.pid}`;
1526
1612
  await promises.writeFile(tmp, JSON.stringify(snapshot), "utf8");
1527
- await promises.rename(tmp, REFRESHED_CASES_PATH);
1528
- memo = {
1613
+ await promises.rename(tmp, target);
1614
+ const result = {
1615
+ sourceId,
1529
1616
  cases: parsed.cases,
1530
1617
  total: parsed.cases.length,
1531
1618
  origin: "refreshed",
1532
1619
  repository: parsed.repository,
1533
1620
  fetchedAt
1534
1621
  };
1622
+ memos.set(sourceId, result);
1535
1623
  return {
1624
+ sourceId,
1536
1625
  total: parsed.cases.length,
1537
1626
  fetchedAt
1538
1627
  };
1539
1628
  }
1629
+ /** Fisher–Yates shuffle (returns a copy; never mutates the pool). */
1630
+ function shuffled(items) {
1631
+ const out = [...items];
1632
+ for (let i = out.length - 1; i > 0; i -= 1) {
1633
+ const j = Math.floor(Math.random() * (i + 1));
1634
+ const a = out[i];
1635
+ out[i] = out[j];
1636
+ out[j] = a;
1637
+ }
1638
+ return out;
1639
+ }
1640
+ /**
1641
+ * Draw up to `count` random cases across every source that has data,
1642
+ * round-robin between sources so one huge library cannot crowd out the
1643
+ * others, then shuffle the final pick. Reads memoized lists, so this never
1644
+ * touches the network — cheap enough for every shuffle click.
1645
+ */
1646
+ async function sampleTemplates(count = 9) {
1647
+ const size = Math.min(12, Math.max(1, Math.floor(count) || 9));
1648
+ const pools = [];
1649
+ for (const source of TEMPLATE_SOURCES) try {
1650
+ const list = await listTemplates(source.id);
1651
+ if (list.cases.length > 0) pools.push({
1652
+ sourceId: source.id,
1653
+ cases: shuffled(list.cases)
1654
+ });
1655
+ } catch {}
1656
+ const picks = [];
1657
+ for (let round = 0; picks.length < size && pools.some((pool) => pool.cases.length > 0); round += 1) {
1658
+ const pool = pools[round % pools.length];
1659
+ const picked = pool.cases.pop();
1660
+ if (picked !== void 0) picks.push({
1661
+ sourceId: pool.sourceId,
1662
+ case: picked
1663
+ });
1664
+ }
1665
+ return shuffled(picks);
1666
+ }
1667
+ /** Serially refresh every registered source; one failure never stops the rest. */
1668
+ async function syncAllTemplates() {
1669
+ const reports = [];
1670
+ for (const source of TEMPLATE_SOURCES) try {
1671
+ const result = await refreshTemplates(source.id);
1672
+ reports.push({
1673
+ sourceId: source.id,
1674
+ ok: true,
1675
+ total: result.total
1676
+ });
1677
+ } catch (error) {
1678
+ reports.push({
1679
+ sourceId: source.id,
1680
+ ok: false,
1681
+ total: 0,
1682
+ error: error instanceof Error ? error.message : String(error)
1683
+ });
1684
+ }
1685
+ return reports;
1686
+ }
1540
1687
  /** MIME type for a cached reference-image file name. */
1541
1688
  function mimeOfFile(file) {
1542
1689
  switch (path.extname(file).toLowerCase()) {
@@ -1547,11 +1694,11 @@ function mimeOfFile(file) {
1547
1694
  default: return "image/png";
1548
1695
  }
1549
1696
  }
1550
- /** Download one reference image into the disk cache; undefined on failure. */
1551
- async function fetchTemplateImage(file) {
1697
+ /** Download one reference image into the source's disk cache; undefined on failure. */
1698
+ async function fetchTemplateImage(def, file, cacheDir) {
1552
1699
  let response;
1553
1700
  try {
1554
- response = await fetch(`${IMAGE_BASE_URL}${encodeURIComponent(file)}`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
1701
+ response = await fetch(`${def.imageBaseUrl}${encodeURIComponent(file)}`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
1555
1702
  } catch {
1556
1703
  return;
1557
1704
  }
@@ -1561,10 +1708,10 @@ async function fetchTemplateImage(file) {
1561
1708
  if (data.byteLength === 0 || data.byteLength > MAX_IMAGE_BYTES) return void 0;
1562
1709
  const mime = mimeOfFile(file);
1563
1710
  try {
1564
- await promises.mkdir(IMAGE_CACHE_DIR, { recursive: true });
1565
- const tmp = path.join(IMAGE_CACHE_DIR, `${file}.tmp-${process.pid}`);
1711
+ await promises.mkdir(cacheDir, { recursive: true });
1712
+ const tmp = path.join(cacheDir, `${file}.tmp-${process.pid}`);
1566
1713
  await promises.writeFile(tmp, data);
1567
- await promises.rename(tmp, path.join(IMAGE_CACHE_DIR, file));
1714
+ await promises.rename(tmp, path.join(cacheDir, file));
1568
1715
  } catch {}
1569
1716
  return {
1570
1717
  data,
@@ -1572,33 +1719,143 @@ async function fetchTemplateImage(file) {
1572
1719
  };
1573
1720
  }
1574
1721
  /**
1575
- * Read one reference image for the template library. Cache hit → disk; miss →
1576
- * fetch from the upstream mirror, cache, and serve. Only file names present in
1577
- * the active case list are served, so the route can never act as an open
1722
+ * Read one reference image for a source's library. Cache hit → disk; miss →
1723
+ * fetch from that source's mirror, cache, and serve. Only file names present
1724
+ * in the active case list are served, so the route can never act as an open
1578
1725
  * proxy. Undefined when the name is unknown or the fetch failed.
1579
1726
  */
1580
- async function readTemplateImage(file) {
1727
+ async function readTemplateImage(sourceId, file) {
1728
+ const def = sourceDefOf(sourceId);
1729
+ if (def === void 0) return void 0;
1581
1730
  if (!IMAGE_FILE_PATTERN.test(file) || file.includes("..")) return void 0;
1582
- if (!(await listTemplates()).cases.some((entry) => entry.image === file)) return void 0;
1583
- const cached = path.join(IMAGE_CACHE_DIR, file);
1731
+ if (!(await listTemplates(sourceId)).cases.some((entry) => entry.image === file)) return void 0;
1732
+ const cacheDir = path.join(IMAGE_CACHE_ROOT, sourceId);
1584
1733
  try {
1585
1734
  return {
1586
- data: await promises.readFile(cached),
1735
+ data: await promises.readFile(path.join(cacheDir, file)),
1587
1736
  mime: mimeOfFile(file)
1588
1737
  };
1589
1738
  } catch {}
1590
- const inflight = inflightImages.get(file);
1739
+ if (def.legacyImageDir !== void 0) try {
1740
+ return {
1741
+ data: await promises.readFile(path.join(LEGACY_IMAGE_DIR, file)),
1742
+ mime: mimeOfFile(file)
1743
+ };
1744
+ } catch {}
1745
+ const cacheKey = `${sourceId}/${file}`;
1746
+ const inflight = inflightImages.get(cacheKey);
1591
1747
  if (inflight !== void 0) return inflight;
1592
- const pending = fetchTemplateImage(file);
1593
- inflightImages.set(file, pending);
1748
+ const pending = fetchTemplateImage(def, file, cacheDir);
1749
+ inflightImages.set(cacheKey, pending);
1594
1750
  try {
1595
1751
  return await pending;
1596
1752
  } finally {
1597
- inflightImages.delete(file);
1753
+ inflightImages.delete(cacheKey);
1598
1754
  }
1599
1755
  }
1600
- /** Drop the in-memory list memo (tests). */
1756
+ /** Drop the in-memory list memos (tests). */
1601
1757
  function clearTemplateMemo() {
1758
+ memos.clear();
1759
+ }
1760
+ //#endregion
1761
+ //#region src/template-favorites.ts
1762
+ /**
1763
+ * Favorites store for the prompt-template library.
1764
+ *
1765
+ * The user's starred templates persist host-side as full case snapshots under
1766
+ * ~/.dsh/dsh-imagegen/templates/favorites.json, keyed by
1767
+ * `${sourceId}:${caseId}` — the snapshot means a favorite stays usable even
1768
+ * after the upstream list drops or renumbers the case. Framework-free
1769
+ * (node:fs only) so the route layer and tests can drive it directly.
1770
+ */
1771
+ const DATA_DIR = path.join(homedir(), ".dsh", "dsh-imagegen");
1772
+ const FAVORITES_PATH = path.join(DATA_DIR, "templates", "favorites.json");
1773
+ /** Refuse to grow the file without bound; the user curates this list. */
1774
+ const MAX_FAVORITES = 1e3;
1775
+ /** In-memory memo of the persisted list. */
1776
+ let memo;
1777
+ /** Build the stable key of one case within a source. */
1778
+ function templateFavoriteKey(sourceId, caseId) {
1779
+ return `${sourceId}:${caseId}`;
1780
+ }
1781
+ /** Validate + normalize one raw stored favorite; undefined when unusable. */
1782
+ function normalizeFavorite(raw) {
1783
+ if (raw === null || typeof raw !== "object") return void 0;
1784
+ const record = raw;
1785
+ if (typeof record.key !== "string" || typeof record.savedAt !== "string") return void 0;
1786
+ const sourceId = typeof record.sourceId === "string" ? record.sourceId : "";
1787
+ if (!isTemplateSourceId(sourceId)) return void 0;
1788
+ if (record.key !== templateFavoriteKey(sourceId, Number(record.case && record.case.id))) return void 0;
1789
+ const rawCase = record.case;
1790
+ if (rawCase === null || typeof rawCase !== "object") return void 0;
1791
+ const item = rawCase;
1792
+ const id = Number(item.id);
1793
+ const title = typeof item.title === "string" ? item.title : "";
1794
+ const prompt = typeof item.prompt === "string" ? item.prompt : "";
1795
+ if (!Number.isInteger(id) || title === "" || prompt === "") return void 0;
1796
+ const snapshot = {
1797
+ id,
1798
+ title,
1799
+ prompt,
1800
+ category: typeof item.category === "string" ? item.category : "",
1801
+ categoryZh: typeof item.categoryZh === "string" ? item.categoryZh : "",
1802
+ styles: Array.isArray(item.styles) ? item.styles.map(String) : [],
1803
+ scenes: Array.isArray(item.scenes) ? item.scenes.map(String) : [],
1804
+ sourceLabel: typeof item.sourceLabel === "string" ? item.sourceLabel : "",
1805
+ sourceUrl: typeof item.sourceUrl === "string" ? item.sourceUrl : "",
1806
+ githubUrl: typeof item.githubUrl === "string" ? item.githubUrl : "",
1807
+ image: typeof item.image === "string" ? item.image : "",
1808
+ featured: item.featured === true
1809
+ };
1810
+ return {
1811
+ key: record.key,
1812
+ sourceId,
1813
+ savedAt: record.savedAt,
1814
+ case: snapshot
1815
+ };
1816
+ }
1817
+ /** Read + parse the favorites file (memoized). */
1818
+ async function listTemplateFavorites() {
1819
+ if (memo !== void 0) return memo;
1820
+ try {
1821
+ const parsed = JSON.parse(await promises.readFile(FAVORITES_PATH, "utf8"));
1822
+ memo = Array.isArray(parsed) ? parsed.map(normalizeFavorite).filter((entry) => entry !== void 0) : [];
1823
+ } catch {
1824
+ memo = [];
1825
+ }
1826
+ return memo;
1827
+ }
1828
+ /** Persist the list atomically and update the memo. */
1829
+ async function writeFavorites(entries) {
1830
+ memo = entries;
1831
+ await promises.mkdir(path.dirname(FAVORITES_PATH), { recursive: true });
1832
+ const tmp = `${FAVORITES_PATH}.tmp-${process.pid}`;
1833
+ await promises.writeFile(tmp, JSON.stringify(entries, null, 2), "utf8");
1834
+ await promises.rename(tmp, FAVORITES_PATH);
1835
+ }
1836
+ /** Star one template. Re-starring refreshes the snapshot and is idempotent. */
1837
+ async function addTemplateFavorite(sourceId, item) {
1838
+ if (!isTemplateSourceId(sourceId)) throw new Error(`未知的模板库来源:${sourceId}`);
1839
+ const key = templateFavoriteKey(sourceId, item.id);
1840
+ const rest = (await listTemplateFavorites()).filter((entry) => entry.key !== key);
1841
+ const next = [{
1842
+ key,
1843
+ sourceId,
1844
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
1845
+ case: item
1846
+ }, ...rest].slice(0, MAX_FAVORITES);
1847
+ await writeFavorites(next);
1848
+ return next;
1849
+ }
1850
+ /** Unstar one template by key; unknown keys are a no-op. */
1851
+ async function removeTemplateFavorite(key) {
1852
+ const next = (await listTemplateFavorites()).filter((entry) => entry.key !== key);
1853
+ if (next.length === memo?.length) return next;
1854
+ await writeFavorites(next);
1855
+ return next;
1856
+ }
1857
+ /** Drop the in-memory memo (tests). */
1858
+ function clearTemplateFavoritesMemo() {
1602
1859
  memo = void 0;
1603
1860
  }
1604
1861
  //#endregion
@@ -1817,6 +2074,12 @@ async function readJsonBody(req, maxBytes = MAX_JSON_BODY_BYTES) {
1817
2074
  function messageOf(error) {
1818
2075
  return error instanceof Error ? error.message : String(error);
1819
2076
  }
2077
+ /** Validate the { source } body of a template-library request. */
2078
+ function templateSourceOf(body) {
2079
+ const raw = body?.source;
2080
+ if (raw === void 0 || raw === "") return DEFAULT_TEMPLATE_SOURCE_ID;
2081
+ return typeof raw === "string" && isTemplateSourceId(raw) ? raw : void 0;
2082
+ }
1820
2083
  function parseGenerateRequest(body) {
1821
2084
  const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
1822
2085
  if (prompt === "") return void 0;
@@ -1987,8 +2250,14 @@ function makeRoutes(deps) {
1987
2250
  const templates = deps.templates ?? {
1988
2251
  list: listTemplates,
1989
2252
  refresh: refreshTemplates,
2253
+ sample: sampleTemplates,
1990
2254
  readImage: readTemplateImage
1991
2255
  };
2256
+ const favorites = deps.favorites ?? {
2257
+ list: listTemplateFavorites,
2258
+ add: addTemplateFavorite,
2259
+ remove: removeTemplateFavorite
2260
+ };
1992
2261
  const resolvePrompt = deps.resolvePrompt ?? (() => ({
1993
2262
  apiUrl: "",
1994
2263
  apiKey: "",
@@ -2312,7 +2581,7 @@ function makeRoutes(deps) {
2312
2581
  }
2313
2582
  const expectedRevision = typeof body.expectedRevision === "number" ? body.expectedRevision : void 0;
2314
2583
  try {
2315
- await deps.settings.mutate(settingsNamespace(ns), body.ops, expectedRevision);
2584
+ await deps.settings.mutate(ns, body.ops, expectedRevision);
2316
2585
  } catch (error) {
2317
2586
  writeJson(res, 200, failureOf(error));
2318
2587
  return;
@@ -2822,10 +3091,20 @@ function makeRoutes(deps) {
2822
3091
  path: TEMPLATES_API.list,
2823
3092
  handler: async (req, res) => {
2824
3093
  if (!guard(req, res, "POST")) return;
3094
+ const body = await readJsonBody(req);
3095
+ const sourceId = templateSourceOf(body);
3096
+ if (sourceId === void 0) {
3097
+ writeJson(res, 200, {
3098
+ ok: false,
3099
+ code: "templates-source-unknown",
3100
+ message: `未知的模板库来源:${String(body?.source ?? "")}`
3101
+ });
3102
+ return;
3103
+ }
2825
3104
  try {
2826
3105
  writeJson(res, 200, {
2827
3106
  ok: true,
2828
- ...await templates.list()
3107
+ ...await templates.list(sourceId)
2829
3108
  });
2830
3109
  } catch (error) {
2831
3110
  writeJson(res, 200, {
@@ -2841,10 +3120,20 @@ function makeRoutes(deps) {
2841
3120
  path: TEMPLATES_API.refresh,
2842
3121
  handler: async (req, res) => {
2843
3122
  if (!guard(req, res, "POST")) return;
3123
+ const body = await readJsonBody(req);
3124
+ const sourceId = templateSourceOf(body);
3125
+ if (sourceId === void 0) {
3126
+ writeJson(res, 200, {
3127
+ ok: false,
3128
+ code: "templates-source-unknown",
3129
+ message: `未知的模板库来源:${String(body?.source ?? "")}`
3130
+ });
3131
+ return;
3132
+ }
2844
3133
  try {
2845
3134
  writeJson(res, 200, {
2846
3135
  ok: true,
2847
- ...await templates.refresh()
3136
+ ...await templates.refresh(sourceId)
2848
3137
  });
2849
3138
  } catch (error) {
2850
3139
  writeJson(res, 200, {
@@ -2855,6 +3144,28 @@ function makeRoutes(deps) {
2855
3144
  }
2856
3145
  }
2857
3146
  },
3147
+ {
3148
+ kind: "exact",
3149
+ path: TEMPLATES_API.sample,
3150
+ handler: async (req, res) => {
3151
+ if (!guard(req, res, "POST")) return;
3152
+ const body = await readJsonBody(req);
3153
+ const requested = Number(body?.count);
3154
+ const count = Number.isFinite(requested) ? requested : 9;
3155
+ try {
3156
+ writeJson(res, 200, {
3157
+ ok: true,
3158
+ samples: await templates.sample(count)
3159
+ });
3160
+ } catch (error) {
3161
+ writeJson(res, 200, {
3162
+ ok: false,
3163
+ code: "templates-sample-failed",
3164
+ message: messageOf(error)
3165
+ });
3166
+ }
3167
+ }
3168
+ },
2858
3169
  {
2859
3170
  kind: "prefix",
2860
3171
  path: TEMPLATES_API.image,
@@ -2867,12 +3178,15 @@ function makeRoutes(deps) {
2867
3178
  writeJson(res, 405, { error: `method not allowed: ${req.method}` });
2868
3179
  return;
2869
3180
  }
2870
- const file = imageFileFrom(req.url, TEMPLATES_API.image);
2871
- if (file === void 0) {
3181
+ const raw = imageFileFrom(req.url, TEMPLATES_API.image);
3182
+ const slash = raw?.indexOf("/") ?? -1;
3183
+ const sourceId = slash > 0 ? raw.slice(0, slash) : "";
3184
+ const file = slash > 0 ? raw.slice(slash + 1) : "";
3185
+ if (sourceId === "" || !isTemplateSourceId(sourceId) || file === "") {
2872
3186
  writeJson(res, 404, { error: "not found" });
2873
3187
  return;
2874
3188
  }
2875
- const found = await templates.readImage(file);
3189
+ const found = await templates.readImage(sourceId, file);
2876
3190
  if (found === void 0) {
2877
3191
  writeJson(res, 404, { error: "not found" });
2878
3192
  return;
@@ -2884,6 +3198,96 @@ function makeRoutes(deps) {
2884
3198
  });
2885
3199
  res.end(found.data);
2886
3200
  }
3201
+ },
3202
+ {
3203
+ kind: "exact",
3204
+ path: TEMPLATE_FAVORITES_API.list,
3205
+ handler: async (req, res) => {
3206
+ if (!guard(req, res, "POST")) return;
3207
+ try {
3208
+ writeJson(res, 200, {
3209
+ ok: true,
3210
+ favorites: await favorites.list()
3211
+ });
3212
+ } catch (error) {
3213
+ writeJson(res, 200, {
3214
+ ok: false,
3215
+ code: "template-favorites-failed",
3216
+ message: messageOf(error)
3217
+ });
3218
+ }
3219
+ }
3220
+ },
3221
+ {
3222
+ kind: "exact",
3223
+ path: TEMPLATE_FAVORITES_API.add,
3224
+ handler: async (req, res) => {
3225
+ if (!guard(req, res, "POST")) return;
3226
+ const body = await readJsonBody(req);
3227
+ const sourceId = templateSourceOf(body);
3228
+ const rawCase = body?.case;
3229
+ if (sourceId === void 0 || rawCase === null || typeof rawCase !== "object") {
3230
+ writeJson(res, 200, {
3231
+ ok: false,
3232
+ code: "template-favorite-invalid",
3233
+ message: "收藏请求缺少有效的来源或模板数据"
3234
+ });
3235
+ return;
3236
+ }
3237
+ const record = rawCase;
3238
+ const id = Number(record.id);
3239
+ const title = typeof record.title === "string" ? record.title.trim() : "";
3240
+ const prompt = typeof record.prompt === "string" ? record.prompt.trim() : "";
3241
+ if (!Number.isInteger(id) || title === "" || prompt === "") {
3242
+ writeJson(res, 200, {
3243
+ ok: false,
3244
+ code: "template-favorite-invalid",
3245
+ message: "收藏请求缺少有效的模板数据"
3246
+ });
3247
+ return;
3248
+ }
3249
+ try {
3250
+ writeJson(res, 200, {
3251
+ ok: true,
3252
+ favorites: await favorites.add(sourceId, rawCase)
3253
+ });
3254
+ } catch (error) {
3255
+ writeJson(res, 200, {
3256
+ ok: false,
3257
+ code: "template-favorites-failed",
3258
+ message: messageOf(error)
3259
+ });
3260
+ }
3261
+ }
3262
+ },
3263
+ {
3264
+ kind: "exact",
3265
+ path: TEMPLATE_FAVORITES_API.remove,
3266
+ handler: async (req, res) => {
3267
+ if (!guard(req, res, "POST")) return;
3268
+ const body = await readJsonBody(req);
3269
+ const key = typeof body?.key === "string" ? body.key : "";
3270
+ if (key === "") {
3271
+ writeJson(res, 200, {
3272
+ ok: false,
3273
+ code: "template-favorite-invalid",
3274
+ message: "取消收藏请求缺少模板标识"
3275
+ });
3276
+ return;
3277
+ }
3278
+ try {
3279
+ writeJson(res, 200, {
3280
+ ok: true,
3281
+ favorites: await favorites.remove(key)
3282
+ });
3283
+ } catch (error) {
3284
+ writeJson(res, 200, {
3285
+ ok: false,
3286
+ code: "template-favorites-failed",
3287
+ message: messageOf(error)
3288
+ });
3289
+ }
3290
+ }
2887
3291
  }
2888
3292
  ];
2889
3293
  }
@@ -3446,7 +3850,7 @@ const inject = [
3446
3850
  "commands"
3447
3851
  ];
3448
3852
  /** The branded settings namespace of this plugin (the card edits it). */
3449
- const ImageGenSettingsNamespace = settingsNamespace(IMAGEGEN_SETTINGS_NAMESPACE);
3853
+ const ImageGenSettingsNamespace = settingsNamespaceCompat(IMAGEGEN_SETTINGS_NAMESPACE);
3450
3854
  const Config = z.object({
3451
3855
  enabled: z.boolean().default(true),
3452
3856
  announceToAgent: z.boolean().default(true),
@@ -3477,7 +3881,7 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
3477
3881
  /** Order of the announcement section within the tool-guidance band. */
3478
3882
  const SECTION_ORDER = 150;
3479
3883
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
3480
- const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:通过「渠道」对接 OpenAI 兼容图像生成 API(每个渠道 = 一个 API 端点 + 各自的模型目录),支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送,nanobanana 系列按 aspect_ratio / image_size 参数协议发送;seedream 系列统一走 /images/generations,参考图以 JSON image 数组发送;智谱 `glm-image` 使用官方 `/api/paas/v4/images/generations`,当前仅支持文生图)。API 地址与密钥在 GUI 设置中按渠道配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用用户在各渠道配置目录中的模型;检测模型时会过滤聊天、Embedding 等非图片模型,但模型出现在 /models 中仍不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选与复用。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片显示在工具调用对应的左侧结果区域,模型收到状态和附件引用,不会额外伪造用户消息。用户也可以使用 `/edit_image <修改描述>`,命令会直接读取当前对话最近图片并调用插件图片模型,不经过对话模型的图片能力检查。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问 vibeui.top。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
3884
+ const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:通过「渠道」对接 OpenAI 兼容图像生成 API(每个渠道 = 一个 API 端点 + 各自的模型目录),支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送,nanobanana 系列按 aspect_ratio / image_size 参数协议发送;seedream 系列统一走 /images/generations,参考图以 JSON image 数组发送;智谱 `glm-image` 使用官方 `/api/paas/v4/images/generations`,当前仅支持文生图)。API 地址与密钥在 GUI 设置中按渠道配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用用户在各渠道配置目录中的模型;检测模型时会过滤聊天、Embedding 等非图片模型,但模型出现在 /models 中仍不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):多来源标签页(精选案例库 / 沧河案例库,后续可扩展),打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选、收藏(星标,宿主持久化)与复用;各来源列表独立刷新,宿主每 12 小时后台自动同步一次。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片显示在工具调用对应的左侧结果区域,模型收到状态和附件引用,不会额外伪造用户消息。用户也可以使用 `/edit_image <修改描述>`,命令会直接读取当前对话最近图片并调用插件图片模型,不经过对话模型的图片能力检查。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问对应来源站点(vibeui.top / gpt-image2.canghe.ai)。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
3481
3885
  /** Append the live channel × model table so an Agent can honor user choices. */
3482
3886
  function guidanceFor(channels, defaultChannelId) {
3483
3887
  if (channels.length === 0) return `${IMAGEGEN_GUIDANCE} 尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥。`;
@@ -3609,7 +4013,19 @@ function apply(ctx, config) {
3609
4013
  pendingConversationImages,
3610
4014
  runtime
3611
4015
  }).map((route) => ctx.webServer.register(route));
4016
+ const TEMPLATE_SYNC_INITIAL_DELAY_MS = 3e4;
4017
+ const TEMPLATE_SYNC_INTERVAL_MS = 720 * 60 * 1e3;
4018
+ let syncTimer;
4019
+ const runSync = () => {
4020
+ if (!resolve().enabled) return;
4021
+ syncAllTemplates().catch(() => {});
4022
+ };
4023
+ const startTimer = setTimeout(runSync, TEMPLATE_SYNC_INITIAL_DELAY_MS);
4024
+ syncTimer = setInterval(runSync, TEMPLATE_SYNC_INTERVAL_MS);
4025
+ syncTimer.unref?.();
3612
4026
  return () => {
4027
+ clearTimeout(startTimer);
4028
+ clearInterval(syncTimer);
3613
4029
  for (const dispose of disposers) dispose();
3614
4030
  };
3615
4031
  }, "dsh-imagegen: routes");
@@ -3656,7 +4072,7 @@ function apply(ctx, config) {
3656
4072
  text: guidanceFor(value.channels, value.defaultChannelId)
3657
4073
  });
3658
4074
  };
3659
- installSettingsSection(ctx, ImageGenSettingsNamespace, Config, config ?? {}, {
4075
+ installSettingsSectionCompat(ctx, ImageGenSettingsNamespace, Config, config ?? {}, {
3660
4076
  setSource: (source) => {
3661
4077
  current = source;
3662
4078
  sync();
@@ -3666,4 +4082,4 @@ function apply(ctx, config) {
3666
4082
  sync();
3667
4083
  }
3668
4084
  //#endregion
3669
- export { CURRENT_VERSION, Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, ImageGenerationRuntime, appendGallery, apply, checkForUpdate, clearGallery, clearTemplateMemo, clearUpdateCache, compareVersions, generateImage, inject, installUpdate, latestSessionImage, listGallery, listTemplates, makeRoutes, name, profileFromProcess, readGalleryImage, readTemplateImage, refreshTemplates, registerAgentImageTools, registerEditImageCommand, removeGallery, updateGalleryTags };
4085
+ export { CURRENT_VERSION, Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, ImageGenerationRuntime, addTemplateFavorite, appendGallery, apply, checkForUpdate, clearGallery, clearTemplateFavoritesMemo, clearTemplateMemo, clearUpdateCache, compareVersions, generateImage, inject, installUpdate, latestSessionImage, listGallery, listTemplateFavorites, listTemplates, makeRoutes, name, profileFromProcess, readGalleryImage, readTemplateImage, refreshTemplates, registerAgentImageTools, registerEditImageCommand, removeGallery, removeTemplateFavorite, sampleTemplates, syncAllTemplates, updateGalleryTags };