@dickpy/dsh-imagegen 1.5.1 → 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.
- package/README.md +11 -8
- package/docs/images/ecommerce-mode.png +0 -0
- package/docs/images/gallery-workspace.png +0 -0
- package/docs/images/image-generation-studio-three-column.png +0 -0
- package/docs/images/imagegen-overview.png +0 -0
- package/docs/images/plugin-settings.png +0 -0
- package/docs/images/prompt-template-library.png +0 -0
- package/lib/client.js +807 -435
- package/lib/client.js.map +1 -1
- package/lib/index.js +468 -76
- package/package.json +1 -1
- package/src/client/ImageGenPanel.tsx +10 -9
- package/src/client/InspirationGallery.tsx +106 -0
- package/src/client/TemplateLibrary.tsx +159 -48
- package/src/client/api.ts +41 -8
- package/src/client/inspiration.module.css +148 -0
- package/src/client/locales.ts +34 -8
- package/src/client/templates.module.css +95 -0
- package/src/index.ts +23 -3
- package/src/protocol.ts +81 -7
- package/src/routes.ts +124 -10
- package/src/template-favorites.ts +108 -0
- package/src/templates/canghe-cases.json +11126 -0
- package/src/templates-store.ts +179 -68
- package/docs/images/image-generation-studio-single.png +0 -0
- package/docs/images/image-generation-studio.png +0 -0
- package/docs/images/poster-features-16x9.png +0 -0
package/lib/index.js
CHANGED
|
@@ -40,7 +40,7 @@ function installSettingsSectionCompat(ctx, ns, schema, entry, hooks) {
|
|
|
40
40
|
/** Settings namespace this plugin owns (host settings seam + bridge). */
|
|
41
41
|
const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
|
|
42
42
|
/** Published package version shared by the host updater and the client UI. */
|
|
43
|
-
const PLUGIN_VERSION = "1.5.
|
|
43
|
+
const PLUGIN_VERSION = "1.5.2";
|
|
44
44
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
45
45
|
const SETTINGS_API = {
|
|
46
46
|
describe: "/api/dsh-imagegen/settings/describe",
|
|
@@ -106,16 +106,48 @@ const GALLERY_API = {
|
|
|
106
106
|
image: "/api/dsh-imagegen/gallery/image"
|
|
107
107
|
};
|
|
108
108
|
/**
|
|
109
|
-
* Same-origin route family for the
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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.
|
|
113
115
|
*/
|
|
114
116
|
const TEMPLATES_API = {
|
|
115
117
|
list: "/api/dsh-imagegen/templates/list",
|
|
116
118
|
refresh: "/api/dsh-imagegen/templates/refresh",
|
|
119
|
+
sample: "/api/dsh-imagegen/templates/sample",
|
|
117
120
|
image: "/api/dsh-imagegen/templates/image"
|
|
118
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
|
+
}
|
|
119
151
|
//#endregion
|
|
120
152
|
//#region src/model-catalog.ts
|
|
121
153
|
const ENTRIES = {
|
|
@@ -1359,24 +1391,42 @@ async function readGalleryImage(file) {
|
|
|
1359
1391
|
//#endregion
|
|
1360
1392
|
//#region src/templates-store.ts
|
|
1361
1393
|
/**
|
|
1362
|
-
* Prompt-template library store (
|
|
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.
|
|
1363
1401
|
*
|
|
1364
|
-
*
|
|
1365
|
-
*
|
|
1366
|
-
* manual
|
|
1367
|
-
* which then takes precedence.
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
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
|
|
1370
1409
|
* every later view.
|
|
1371
1410
|
*
|
|
1372
1411
|
* Framework-free (node:fs only) so the route layer and tests can drive it
|
|
1373
1412
|
* directly.
|
|
1374
1413
|
*/
|
|
1375
|
-
/**
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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). */
|
|
1380
1430
|
const CATEGORY_ZH = {
|
|
1381
1431
|
"Architecture & Spaces": "建筑与空间",
|
|
1382
1432
|
"Brand & Logos": "品牌与标志",
|
|
@@ -1406,26 +1456,26 @@ const CATEGORY_ZH = {
|
|
|
1406
1456
|
"Animals & Nature": "动物与自然",
|
|
1407
1457
|
"Other Creative Uses": "其他创意用途"
|
|
1408
1458
|
};
|
|
1409
|
-
const DATA_DIR = path.join(homedir(), ".dsh", "dsh-imagegen");
|
|
1410
|
-
const
|
|
1411
|
-
const
|
|
1412
|
-
/**
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
* the package root — so `../src/templates/cases.json` resolves to the shipped
|
|
1416
|
-
* snapshot in development and in the installed package alike.
|
|
1417
|
-
*/
|
|
1418
|
-
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;
|
|
1419
1465
|
/** Budget for one upstream fetch (list refresh or one image). */
|
|
1420
1466
|
const FETCH_TIMEOUT_MS = 6e4;
|
|
1421
1467
|
/** Refuse to cache implausibly large "images". */
|
|
1422
1468
|
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
1423
1469
|
/** Strict reference-image file names this store writes and serves. */
|
|
1424
1470
|
const IMAGE_FILE_PATTERN = /^case\d+\.(jpg|jpeg|png|webp|gif)$/i;
|
|
1425
|
-
/**
|
|
1426
|
-
|
|
1471
|
+
/** Per-source in-memory memo of the active list (avoid re-parsing per request). */
|
|
1472
|
+
const memos = /* @__PURE__ */ new Map();
|
|
1427
1473
|
/** Per-file in-flight downloads, so a gallery scroll never double-fetches. */
|
|
1428
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
|
+
}
|
|
1429
1479
|
/** Validate + normalize one raw upstream case; undefined when unusable. */
|
|
1430
1480
|
function normalizeCase(raw) {
|
|
1431
1481
|
if (raw === null || typeof raw !== "object") return void 0;
|
|
@@ -1483,50 +1533,61 @@ async function readSnapshotFile(file) {
|
|
|
1483
1533
|
}
|
|
1484
1534
|
}
|
|
1485
1535
|
/**
|
|
1486
|
-
* The active template list: the refreshed runtime copy wins, the
|
|
1487
|
-
* snapshot is the always-available fallback. Memoized; a
|
|
1488
|
-
* replaces
|
|
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.
|
|
1489
1539
|
*/
|
|
1490
|
-
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);
|
|
1491
1544
|
if (memo !== void 0) return memo;
|
|
1492
|
-
const refreshed = await readSnapshotFile(
|
|
1545
|
+
const refreshed = await readSnapshotFile(path.join(REFRESHED_DIR, sourceId, "cases.json")) ?? (def.legacySnapshotPath !== void 0 ? await readSnapshotFile(LEGACY_SNAPSHOT_PATH) : void 0);
|
|
1493
1546
|
if (refreshed !== void 0) {
|
|
1494
|
-
|
|
1547
|
+
const result = {
|
|
1548
|
+
sourceId,
|
|
1495
1549
|
...refreshed,
|
|
1496
1550
|
total: refreshed.cases.length,
|
|
1497
1551
|
origin: "refreshed"
|
|
1498
1552
|
};
|
|
1499
|
-
|
|
1553
|
+
memos.set(sourceId, result);
|
|
1554
|
+
return result;
|
|
1500
1555
|
}
|
|
1501
|
-
const bundled = await readSnapshotFile(
|
|
1556
|
+
const bundled = await readSnapshotFile(def.bundledPath);
|
|
1502
1557
|
if (bundled !== void 0) {
|
|
1503
|
-
|
|
1558
|
+
const result = {
|
|
1559
|
+
sourceId,
|
|
1504
1560
|
...bundled,
|
|
1505
1561
|
total: bundled.cases.length,
|
|
1506
1562
|
origin: "bundled"
|
|
1507
1563
|
};
|
|
1508
|
-
|
|
1564
|
+
memos.set(sourceId, result);
|
|
1565
|
+
return result;
|
|
1509
1566
|
}
|
|
1510
|
-
|
|
1567
|
+
const result = {
|
|
1568
|
+
sourceId,
|
|
1511
1569
|
cases: [],
|
|
1512
1570
|
total: 0,
|
|
1513
1571
|
origin: "bundled",
|
|
1514
1572
|
repository: "freestylefly/awesome-gpt-image-2",
|
|
1515
1573
|
fetchedAt: ""
|
|
1516
1574
|
};
|
|
1517
|
-
|
|
1575
|
+
memos.set(sourceId, result);
|
|
1576
|
+
return result;
|
|
1518
1577
|
}
|
|
1519
1578
|
/**
|
|
1520
|
-
* Re-download
|
|
1521
|
-
* 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
|
|
1522
1581
|
* previous list (refreshed or bundled) stays active.
|
|
1523
1582
|
*/
|
|
1524
|
-
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}`);
|
|
1525
1586
|
let response;
|
|
1526
1587
|
try {
|
|
1527
|
-
response = await fetch(
|
|
1588
|
+
response = await fetch(def.listUrl, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
1528
1589
|
} catch (error) {
|
|
1529
|
-
throw new Error(
|
|
1590
|
+
throw new Error(`无法连接模板库源站(${sourceId}):${error instanceof Error ? error.message : String(error)}`);
|
|
1530
1591
|
}
|
|
1531
1592
|
if (!response.ok) throw new Error(`模板库源站拒绝请求(HTTP ${response.status})`);
|
|
1532
1593
|
let payload;
|
|
@@ -1540,27 +1601,89 @@ async function refreshTemplates() {
|
|
|
1540
1601
|
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1541
1602
|
const snapshot = {
|
|
1542
1603
|
repository: parsed.repository,
|
|
1543
|
-
sourceUrl:
|
|
1604
|
+
sourceUrl: def.listUrl,
|
|
1544
1605
|
fetchedAt,
|
|
1545
1606
|
totalCases: parsed.cases.length,
|
|
1546
1607
|
cases: parsed.cases
|
|
1547
1608
|
};
|
|
1548
|
-
|
|
1549
|
-
|
|
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}`;
|
|
1550
1612
|
await promises.writeFile(tmp, JSON.stringify(snapshot), "utf8");
|
|
1551
|
-
await promises.rename(tmp,
|
|
1552
|
-
|
|
1613
|
+
await promises.rename(tmp, target);
|
|
1614
|
+
const result = {
|
|
1615
|
+
sourceId,
|
|
1553
1616
|
cases: parsed.cases,
|
|
1554
1617
|
total: parsed.cases.length,
|
|
1555
1618
|
origin: "refreshed",
|
|
1556
1619
|
repository: parsed.repository,
|
|
1557
1620
|
fetchedAt
|
|
1558
1621
|
};
|
|
1622
|
+
memos.set(sourceId, result);
|
|
1559
1623
|
return {
|
|
1624
|
+
sourceId,
|
|
1560
1625
|
total: parsed.cases.length,
|
|
1561
1626
|
fetchedAt
|
|
1562
1627
|
};
|
|
1563
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
|
+
}
|
|
1564
1687
|
/** MIME type for a cached reference-image file name. */
|
|
1565
1688
|
function mimeOfFile(file) {
|
|
1566
1689
|
switch (path.extname(file).toLowerCase()) {
|
|
@@ -1571,11 +1694,11 @@ function mimeOfFile(file) {
|
|
|
1571
1694
|
default: return "image/png";
|
|
1572
1695
|
}
|
|
1573
1696
|
}
|
|
1574
|
-
/** Download one reference image into the disk cache; undefined on failure. */
|
|
1575
|
-
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) {
|
|
1576
1699
|
let response;
|
|
1577
1700
|
try {
|
|
1578
|
-
response = await fetch(`${
|
|
1701
|
+
response = await fetch(`${def.imageBaseUrl}${encodeURIComponent(file)}`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
1579
1702
|
} catch {
|
|
1580
1703
|
return;
|
|
1581
1704
|
}
|
|
@@ -1585,10 +1708,10 @@ async function fetchTemplateImage(file) {
|
|
|
1585
1708
|
if (data.byteLength === 0 || data.byteLength > MAX_IMAGE_BYTES) return void 0;
|
|
1586
1709
|
const mime = mimeOfFile(file);
|
|
1587
1710
|
try {
|
|
1588
|
-
await promises.mkdir(
|
|
1589
|
-
const tmp = path.join(
|
|
1711
|
+
await promises.mkdir(cacheDir, { recursive: true });
|
|
1712
|
+
const tmp = path.join(cacheDir, `${file}.tmp-${process.pid}`);
|
|
1590
1713
|
await promises.writeFile(tmp, data);
|
|
1591
|
-
await promises.rename(tmp, path.join(
|
|
1714
|
+
await promises.rename(tmp, path.join(cacheDir, file));
|
|
1592
1715
|
} catch {}
|
|
1593
1716
|
return {
|
|
1594
1717
|
data,
|
|
@@ -1596,33 +1719,143 @@ async function fetchTemplateImage(file) {
|
|
|
1596
1719
|
};
|
|
1597
1720
|
}
|
|
1598
1721
|
/**
|
|
1599
|
-
* Read one reference image for
|
|
1600
|
-
* fetch from
|
|
1601
|
-
* 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
|
|
1602
1725
|
* proxy. Undefined when the name is unknown or the fetch failed.
|
|
1603
1726
|
*/
|
|
1604
|
-
async function readTemplateImage(file) {
|
|
1727
|
+
async function readTemplateImage(sourceId, file) {
|
|
1728
|
+
const def = sourceDefOf(sourceId);
|
|
1729
|
+
if (def === void 0) return void 0;
|
|
1605
1730
|
if (!IMAGE_FILE_PATTERN.test(file) || file.includes("..")) return void 0;
|
|
1606
|
-
if (!(await listTemplates()).cases.some((entry) => entry.image === file)) return void 0;
|
|
1607
|
-
const
|
|
1731
|
+
if (!(await listTemplates(sourceId)).cases.some((entry) => entry.image === file)) return void 0;
|
|
1732
|
+
const cacheDir = path.join(IMAGE_CACHE_ROOT, sourceId);
|
|
1608
1733
|
try {
|
|
1609
1734
|
return {
|
|
1610
|
-
data: await promises.readFile(
|
|
1735
|
+
data: await promises.readFile(path.join(cacheDir, file)),
|
|
1736
|
+
mime: mimeOfFile(file)
|
|
1737
|
+
};
|
|
1738
|
+
} catch {}
|
|
1739
|
+
if (def.legacyImageDir !== void 0) try {
|
|
1740
|
+
return {
|
|
1741
|
+
data: await promises.readFile(path.join(LEGACY_IMAGE_DIR, file)),
|
|
1611
1742
|
mime: mimeOfFile(file)
|
|
1612
1743
|
};
|
|
1613
1744
|
} catch {}
|
|
1614
|
-
const
|
|
1745
|
+
const cacheKey = `${sourceId}/${file}`;
|
|
1746
|
+
const inflight = inflightImages.get(cacheKey);
|
|
1615
1747
|
if (inflight !== void 0) return inflight;
|
|
1616
|
-
const pending = fetchTemplateImage(file);
|
|
1617
|
-
inflightImages.set(
|
|
1748
|
+
const pending = fetchTemplateImage(def, file, cacheDir);
|
|
1749
|
+
inflightImages.set(cacheKey, pending);
|
|
1618
1750
|
try {
|
|
1619
1751
|
return await pending;
|
|
1620
1752
|
} finally {
|
|
1621
|
-
inflightImages.delete(
|
|
1753
|
+
inflightImages.delete(cacheKey);
|
|
1622
1754
|
}
|
|
1623
1755
|
}
|
|
1624
|
-
/** Drop the in-memory list
|
|
1756
|
+
/** Drop the in-memory list memos (tests). */
|
|
1625
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() {
|
|
1626
1859
|
memo = void 0;
|
|
1627
1860
|
}
|
|
1628
1861
|
//#endregion
|
|
@@ -1841,6 +2074,12 @@ async function readJsonBody(req, maxBytes = MAX_JSON_BODY_BYTES) {
|
|
|
1841
2074
|
function messageOf(error) {
|
|
1842
2075
|
return error instanceof Error ? error.message : String(error);
|
|
1843
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
|
+
}
|
|
1844
2083
|
function parseGenerateRequest(body) {
|
|
1845
2084
|
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
1846
2085
|
if (prompt === "") return void 0;
|
|
@@ -2011,8 +2250,14 @@ function makeRoutes(deps) {
|
|
|
2011
2250
|
const templates = deps.templates ?? {
|
|
2012
2251
|
list: listTemplates,
|
|
2013
2252
|
refresh: refreshTemplates,
|
|
2253
|
+
sample: sampleTemplates,
|
|
2014
2254
|
readImage: readTemplateImage
|
|
2015
2255
|
};
|
|
2256
|
+
const favorites = deps.favorites ?? {
|
|
2257
|
+
list: listTemplateFavorites,
|
|
2258
|
+
add: addTemplateFavorite,
|
|
2259
|
+
remove: removeTemplateFavorite
|
|
2260
|
+
};
|
|
2016
2261
|
const resolvePrompt = deps.resolvePrompt ?? (() => ({
|
|
2017
2262
|
apiUrl: "",
|
|
2018
2263
|
apiKey: "",
|
|
@@ -2846,10 +3091,20 @@ function makeRoutes(deps) {
|
|
|
2846
3091
|
path: TEMPLATES_API.list,
|
|
2847
3092
|
handler: async (req, res) => {
|
|
2848
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
|
+
}
|
|
2849
3104
|
try {
|
|
2850
3105
|
writeJson(res, 200, {
|
|
2851
3106
|
ok: true,
|
|
2852
|
-
...await templates.list()
|
|
3107
|
+
...await templates.list(sourceId)
|
|
2853
3108
|
});
|
|
2854
3109
|
} catch (error) {
|
|
2855
3110
|
writeJson(res, 200, {
|
|
@@ -2865,10 +3120,20 @@ function makeRoutes(deps) {
|
|
|
2865
3120
|
path: TEMPLATES_API.refresh,
|
|
2866
3121
|
handler: async (req, res) => {
|
|
2867
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
|
+
}
|
|
2868
3133
|
try {
|
|
2869
3134
|
writeJson(res, 200, {
|
|
2870
3135
|
ok: true,
|
|
2871
|
-
...await templates.refresh()
|
|
3136
|
+
...await templates.refresh(sourceId)
|
|
2872
3137
|
});
|
|
2873
3138
|
} catch (error) {
|
|
2874
3139
|
writeJson(res, 200, {
|
|
@@ -2879,6 +3144,28 @@ function makeRoutes(deps) {
|
|
|
2879
3144
|
}
|
|
2880
3145
|
}
|
|
2881
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
|
+
},
|
|
2882
3169
|
{
|
|
2883
3170
|
kind: "prefix",
|
|
2884
3171
|
path: TEMPLATES_API.image,
|
|
@@ -2891,12 +3178,15 @@ function makeRoutes(deps) {
|
|
|
2891
3178
|
writeJson(res, 405, { error: `method not allowed: ${req.method}` });
|
|
2892
3179
|
return;
|
|
2893
3180
|
}
|
|
2894
|
-
const
|
|
2895
|
-
|
|
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 === "") {
|
|
2896
3186
|
writeJson(res, 404, { error: "not found" });
|
|
2897
3187
|
return;
|
|
2898
3188
|
}
|
|
2899
|
-
const found = await templates.readImage(file);
|
|
3189
|
+
const found = await templates.readImage(sourceId, file);
|
|
2900
3190
|
if (found === void 0) {
|
|
2901
3191
|
writeJson(res, 404, { error: "not found" });
|
|
2902
3192
|
return;
|
|
@@ -2908,6 +3198,96 @@ function makeRoutes(deps) {
|
|
|
2908
3198
|
});
|
|
2909
3199
|
res.end(found.data);
|
|
2910
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
|
+
}
|
|
2911
3291
|
}
|
|
2912
3292
|
];
|
|
2913
3293
|
}
|
|
@@ -3501,7 +3881,7 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
|
|
|
3501
3881
|
/** Order of the announcement section within the tool-guidance band. */
|
|
3502
3882
|
const SECTION_ORDER = 150;
|
|
3503
3883
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
3504
|
-
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
|
|
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)。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
|
|
3505
3885
|
/** Append the live channel × model table so an Agent can honor user choices. */
|
|
3506
3886
|
function guidanceFor(channels, defaultChannelId) {
|
|
3507
3887
|
if (channels.length === 0) return `${IMAGEGEN_GUIDANCE} 尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥。`;
|
|
@@ -3633,7 +4013,19 @@ function apply(ctx, config) {
|
|
|
3633
4013
|
pendingConversationImages,
|
|
3634
4014
|
runtime
|
|
3635
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?.();
|
|
3636
4026
|
return () => {
|
|
4027
|
+
clearTimeout(startTimer);
|
|
4028
|
+
clearInterval(syncTimer);
|
|
3637
4029
|
for (const dispose of disposers) dispose();
|
|
3638
4030
|
};
|
|
3639
4031
|
}, "dsh-imagegen: routes");
|
|
@@ -3690,4 +4082,4 @@ function apply(ctx, config) {
|
|
|
3690
4082
|
sync();
|
|
3691
4083
|
}
|
|
3692
4084
|
//#endregion
|
|
3693
|
-
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 };
|