@dickpy/dsh-imagegen 1.5.5 → 1.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/lib/client.js +994 -462
- package/lib/client.js.map +1 -1
- package/lib/index.js +80 -50
- package/package.json +1 -1
- package/src/canvas-store.ts +15 -16
- package/src/client/CanvasWorkspace.tsx +273 -30
- package/src/client/ImageGenPanel.tsx +2737 -2679
- package/src/client/SettingsCard.tsx +14 -0
- package/src/client/canvas-workspace.module.css +144 -8
- package/src/client/locales.ts +79 -25
- package/src/client/panel.module.css +20 -26
- package/src/engine.ts +31 -14
- package/src/gallery-store.ts +13 -13
- package/src/generation-runtime.ts +2 -2
- package/src/history-store.ts +12 -12
- package/src/image-storage-path.ts +12 -0
- package/src/index.ts +5 -0
- package/src/protocol.ts +8 -2
- package/src/routes.ts +3 -0
- package/src/task-queue.ts +4 -5
package/lib/index.js
CHANGED
|
@@ -41,7 +41,7 @@ function installSettingsSectionCompat(ctx, ns, schema, entry, hooks) {
|
|
|
41
41
|
/** Settings namespace this plugin owns (host settings seam + bridge). */
|
|
42
42
|
const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
|
|
43
43
|
/** Published package version shared by the host updater and the client UI. */
|
|
44
|
-
const PLUGIN_VERSION = "1.5.
|
|
44
|
+
const PLUGIN_VERSION = "1.5.6";
|
|
45
45
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
46
46
|
const SETTINGS_API = {
|
|
47
47
|
describe: "/api/dsh-imagegen/settings/describe",
|
|
@@ -881,15 +881,24 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
881
881
|
let body;
|
|
882
882
|
if (request.mode === "edit") {
|
|
883
883
|
if (typeof request.image !== "string" || request.image === "") throw new ImageGenError("图生图需要上传参考图片", "edit-image-missing");
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
884
|
+
const decodeReference = (dataUrl) => {
|
|
885
|
+
const parsed = parseDataUrl(dataUrl);
|
|
886
|
+
if (parsed === void 0) throw new ImageGenError("参考图片格式无效", "edit-image-invalid");
|
|
887
|
+
let bytes;
|
|
888
|
+
try {
|
|
889
|
+
bytes = Buffer.from(parsed.base64, "base64");
|
|
890
|
+
} catch {
|
|
891
|
+
throw new ImageGenError("参考图片数据无法解码", "edit-image-invalid");
|
|
892
|
+
}
|
|
893
|
+
if (bytes.byteLength > MAX_EDIT_IMAGE_BYTES) throw new ImageGenError("参考图片超过 10MB 上限", "edit-image-too-large");
|
|
894
|
+
return {
|
|
895
|
+
bytes,
|
|
896
|
+
mime: parsed.mime,
|
|
897
|
+
filename: `reference.${extensionOf$3(parsed.mime)}`
|
|
898
|
+
};
|
|
899
|
+
};
|
|
900
|
+
const primary = decodeReference(request.image);
|
|
901
|
+
const extras = (request.images ?? []).filter((img) => typeof img === "string" && img !== "").slice(0, 4).map(decodeReference);
|
|
893
902
|
if (isGrokImagine(params.model)) {
|
|
894
903
|
headers["content-type"] = "application/json";
|
|
895
904
|
body = JSON.stringify({
|
|
@@ -904,7 +913,7 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
904
913
|
});
|
|
905
914
|
} else if (isNanoBanana(params.model)) {
|
|
906
915
|
const form = new FormData();
|
|
907
|
-
form.append("image", new Blob([bytes], { type:
|
|
916
|
+
form.append("image", new Blob([primary.bytes], { type: primary.mime }), primary.filename);
|
|
908
917
|
form.append("prompt", request.prompt);
|
|
909
918
|
form.append("model", params.model);
|
|
910
919
|
if (params.aspect_ratio !== void 0) form.append("aspect_ratio", params.aspect_ratio);
|
|
@@ -915,14 +924,15 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
915
924
|
body = JSON.stringify({
|
|
916
925
|
model: params.model,
|
|
917
926
|
prompt: request.prompt,
|
|
918
|
-
image: [request.image],
|
|
927
|
+
image: [request.image, ...(request.images ?? []).filter((img) => typeof img === "string" && img !== "").slice(0, 4)],
|
|
919
928
|
...params.size !== void 0 ? { size: params.size } : {},
|
|
920
929
|
...params.resolution !== void 0 ? { resolution: params.resolution } : {},
|
|
921
930
|
response_format: isVolcSeedream(params.model) ? "url" : "b64_json"
|
|
922
931
|
});
|
|
923
932
|
} else {
|
|
924
933
|
const form = new FormData();
|
|
925
|
-
form.append("image", new Blob([bytes], { type:
|
|
934
|
+
if (extras.length > 0) for (const [index, reference] of [primary, ...extras].entries()) form.append("image[]", new Blob([reference.bytes], { type: reference.mime }), `reference-${index}.${extensionOf$3(reference.mime)}`);
|
|
935
|
+
else form.append("image", new Blob([primary.bytes], { type: primary.mime }), primary.filename);
|
|
926
936
|
form.append("prompt", request.prompt);
|
|
927
937
|
form.append("model", params.model);
|
|
928
938
|
if (params.size !== void 0) form.append("size", params.size);
|
|
@@ -1178,6 +1188,17 @@ async function testStorage(config) {
|
|
|
1178
1188
|
};
|
|
1179
1189
|
}
|
|
1180
1190
|
//#endregion
|
|
1191
|
+
//#region src/image-storage-path.ts
|
|
1192
|
+
const DEFAULT_ROOT = path.join(process.env.DSH_HOME?.trim() || path.join(homedir(), ".dsh"), "dsh-imagegen");
|
|
1193
|
+
let root = DEFAULT_ROOT;
|
|
1194
|
+
function imageDataRoot() {
|
|
1195
|
+
return root;
|
|
1196
|
+
}
|
|
1197
|
+
function setImageDataRoot(value) {
|
|
1198
|
+
const trimmed = value?.trim();
|
|
1199
|
+
root = trimmed === void 0 || trimmed === "" ? DEFAULT_ROOT : path.resolve(trimmed);
|
|
1200
|
+
}
|
|
1201
|
+
//#endregion
|
|
1181
1202
|
//#region src/history-store.ts
|
|
1182
1203
|
/**
|
|
1183
1204
|
* Host-persisted generation history: images are stored as individual files
|
|
@@ -1188,9 +1209,15 @@ async function testStorage(config) {
|
|
|
1188
1209
|
*
|
|
1189
1210
|
* Framework-free (node:fs only) so the route layer can drive it directly.
|
|
1190
1211
|
*/
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1212
|
+
function historyDir() {
|
|
1213
|
+
return imageDataRoot();
|
|
1214
|
+
}
|
|
1215
|
+
function indexPath$1() {
|
|
1216
|
+
return path.join(historyDir(), "index.json");
|
|
1217
|
+
}
|
|
1218
|
+
function imagesDir$1() {
|
|
1219
|
+
return path.join(historyDir(), "images");
|
|
1220
|
+
}
|
|
1194
1221
|
let pendingMutation$1 = Promise.resolve();
|
|
1195
1222
|
function mutateHistory(operation) {
|
|
1196
1223
|
const next = pendingMutation$1.then(operation, operation);
|
|
@@ -1223,12 +1250,12 @@ function safeId$2(id) {
|
|
|
1223
1250
|
}
|
|
1224
1251
|
/** Ensure the storage directories exist. */
|
|
1225
1252
|
async function ensureDirs$1() {
|
|
1226
|
-
await promises.mkdir(
|
|
1253
|
+
await promises.mkdir(imagesDir$1(), { recursive: true });
|
|
1227
1254
|
}
|
|
1228
1255
|
/** Read the index, tolerating a missing/corrupt file. */
|
|
1229
1256
|
async function readIndex$1() {
|
|
1230
1257
|
try {
|
|
1231
|
-
const raw = await promises.readFile(
|
|
1258
|
+
const raw = await promises.readFile(indexPath$1(), "utf8");
|
|
1232
1259
|
const parsed = JSON.parse(raw);
|
|
1233
1260
|
if (parsed === null || typeof parsed !== "object") return [];
|
|
1234
1261
|
const entries = parsed.entries;
|
|
@@ -1242,9 +1269,9 @@ async function readIndex$1() {
|
|
|
1242
1269
|
async function writeIndex$1(entries) {
|
|
1243
1270
|
await ensureDirs$1();
|
|
1244
1271
|
const payload = { entries };
|
|
1245
|
-
const tmp = `${
|
|
1272
|
+
const tmp = `${indexPath$1()}.tmp-${process.pid}`;
|
|
1246
1273
|
await promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
1247
|
-
await promises.rename(tmp,
|
|
1274
|
+
await promises.rename(tmp, indexPath$1());
|
|
1248
1275
|
}
|
|
1249
1276
|
/** Structural guard for a stored entry. */
|
|
1250
1277
|
function isStoredEntry$1(value) {
|
|
@@ -1259,7 +1286,7 @@ function isStoredEntry$1(value) {
|
|
|
1259
1286
|
/** Remove one entry's image files (best effort). */
|
|
1260
1287
|
async function removeEntryFiles$1(entry) {
|
|
1261
1288
|
for (const image of entry.images) try {
|
|
1262
|
-
await promises.rm(path.join(
|
|
1289
|
+
await promises.rm(path.join(imagesDir$1(), image.file), { force: true });
|
|
1263
1290
|
} catch {}
|
|
1264
1291
|
}
|
|
1265
1292
|
/** Project a stored entry onto the wire shape (image URLs). */
|
|
@@ -1306,8 +1333,8 @@ async function appendHistory(input) {
|
|
|
1306
1333
|
for (let index = 0; index < input.images.length; index++) {
|
|
1307
1334
|
const image = input.images[index];
|
|
1308
1335
|
const file = `${prefix}-${index}.${extensionOf$2(image.mime)}`;
|
|
1309
|
-
await promises.writeFile(path.join(
|
|
1310
|
-
notifyImageSaved("history", path.join(
|
|
1336
|
+
await promises.writeFile(path.join(imagesDir$1(), file), Buffer.from(image.b64, "base64"));
|
|
1337
|
+
notifyImageSaved("history", path.join(imagesDir$1(), file));
|
|
1311
1338
|
storedImages.push({
|
|
1312
1339
|
file,
|
|
1313
1340
|
mime: image.mime,
|
|
@@ -1372,7 +1399,7 @@ async function readHistoryImage(file) {
|
|
|
1372
1399
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*-[0-9]+\.(png|jpg|jpeg|webp|gif)$/.test(file)) return void 0;
|
|
1373
1400
|
try {
|
|
1374
1401
|
return {
|
|
1375
|
-
data: await promises.readFile(path.join(
|
|
1402
|
+
data: await promises.readFile(path.join(imagesDir$1(), file)),
|
|
1376
1403
|
mime: mimeOfFile$2(file)
|
|
1377
1404
|
};
|
|
1378
1405
|
} catch {
|
|
@@ -1389,7 +1416,6 @@ var GenerationTaskQueue = class {
|
|
|
1389
1416
|
controllers = /* @__PURE__ */ new Map();
|
|
1390
1417
|
listeners = /* @__PURE__ */ new Set();
|
|
1391
1418
|
running = 0;
|
|
1392
|
-
serialRunning = false;
|
|
1393
1419
|
constructor(run, concurrency = 1) {
|
|
1394
1420
|
this.run = run;
|
|
1395
1421
|
this.concurrency = concurrency;
|
|
@@ -1430,15 +1456,16 @@ var GenerationTaskQueue = class {
|
|
|
1430
1456
|
const previous = this.tasks.find((item) => item.id === id);
|
|
1431
1457
|
return previous === void 0 ? void 0 : this.submit(previous.request);
|
|
1432
1458
|
}
|
|
1459
|
+
/** Start queued tasks while capacity remains. Plain submissions and
|
|
1460
|
+
* comparison batches alike run in parallel up to the host-wide limit, so one
|
|
1461
|
+
* slow upstream can no longer hold back unrelated generations. */
|
|
1433
1462
|
drain() {
|
|
1434
1463
|
while (this.running < Math.max(1, this.concurrency)) {
|
|
1435
|
-
const task = this.tasks.find((item) => item.status === "queued"
|
|
1464
|
+
const task = this.tasks.find((item) => item.status === "queued");
|
|
1436
1465
|
if (task === void 0) return;
|
|
1437
1466
|
this.running += 1;
|
|
1438
|
-
if (task.request.comparisonId === void 0) this.serialRunning = true;
|
|
1439
1467
|
this.runTask(task).finally(() => {
|
|
1440
1468
|
this.running -= 1;
|
|
1441
|
-
if (task.request.comparisonId === void 0) this.serialRunning = false;
|
|
1442
1469
|
this.drain();
|
|
1443
1470
|
});
|
|
1444
1471
|
}
|
|
@@ -1558,10 +1585,15 @@ var ImageGenerationRuntime = class {
|
|
|
1558
1585
|
* Framework-free (node:fs + node:crypto only) so the route layer can drive it
|
|
1559
1586
|
* directly.
|
|
1560
1587
|
*/
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1588
|
+
function galleryDir() {
|
|
1589
|
+
return path.join(imageDataRoot(), "gallery");
|
|
1590
|
+
}
|
|
1591
|
+
function indexPath() {
|
|
1592
|
+
return path.join(galleryDir(), "index.json");
|
|
1593
|
+
}
|
|
1594
|
+
function imagesDir() {
|
|
1595
|
+
return path.join(galleryDir(), "images");
|
|
1596
|
+
}
|
|
1565
1597
|
let pendingMutation = Promise.resolve();
|
|
1566
1598
|
function mutateGallery(operation) {
|
|
1567
1599
|
const next = pendingMutation.then(operation, operation);
|
|
@@ -1600,12 +1632,12 @@ function fingerprint(input) {
|
|
|
1600
1632
|
}
|
|
1601
1633
|
/** Ensure the storage directories exist. */
|
|
1602
1634
|
async function ensureDirs() {
|
|
1603
|
-
await promises.mkdir(
|
|
1635
|
+
await promises.mkdir(imagesDir(), { recursive: true });
|
|
1604
1636
|
}
|
|
1605
1637
|
/** Read the index, tolerating a missing/corrupt file. */
|
|
1606
1638
|
async function readIndex() {
|
|
1607
1639
|
try {
|
|
1608
|
-
const raw = await promises.readFile(
|
|
1640
|
+
const raw = await promises.readFile(indexPath(), "utf8");
|
|
1609
1641
|
const parsed = JSON.parse(raw);
|
|
1610
1642
|
if (parsed === null || typeof parsed !== "object") return [];
|
|
1611
1643
|
const entries = parsed.entries;
|
|
@@ -1619,9 +1651,9 @@ async function readIndex() {
|
|
|
1619
1651
|
async function writeIndex(entries) {
|
|
1620
1652
|
await ensureDirs();
|
|
1621
1653
|
const payload = { entries };
|
|
1622
|
-
const tmp = `${
|
|
1654
|
+
const tmp = `${indexPath()}.tmp-${process.pid}`;
|
|
1623
1655
|
await promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
1624
|
-
await promises.rename(tmp,
|
|
1656
|
+
await promises.rename(tmp, indexPath());
|
|
1625
1657
|
}
|
|
1626
1658
|
/** Structural guard for a stored entry. */
|
|
1627
1659
|
function isStoredEntry(value) {
|
|
@@ -1636,7 +1668,7 @@ function isStoredEntry(value) {
|
|
|
1636
1668
|
/** Remove one entry's image files (best effort). */
|
|
1637
1669
|
async function removeEntryFiles(entry) {
|
|
1638
1670
|
for (const image of entry.images) try {
|
|
1639
|
-
await promises.rm(path.join(
|
|
1671
|
+
await promises.rm(path.join(imagesDir(), image.file), { force: true });
|
|
1640
1672
|
} catch {}
|
|
1641
1673
|
}
|
|
1642
1674
|
/** Project a stored entry onto the wire shape (image URLs). */
|
|
@@ -1692,8 +1724,8 @@ async function appendGallery(input) {
|
|
|
1692
1724
|
for (let index = 0; index < input.images.length; index++) {
|
|
1693
1725
|
const image = input.images[index];
|
|
1694
1726
|
const file = `${prefix}-${index}.${extensionOf$1(image.mime)}`;
|
|
1695
|
-
await promises.writeFile(path.join(
|
|
1696
|
-
notifyImageSaved("gallery", path.join(
|
|
1727
|
+
await promises.writeFile(path.join(imagesDir(), file), Buffer.from(image.b64, "base64"));
|
|
1728
|
+
notifyImageSaved("gallery", path.join(imagesDir(), file));
|
|
1697
1729
|
storedImages.push({
|
|
1698
1730
|
file,
|
|
1699
1731
|
mime: image.mime,
|
|
@@ -1769,7 +1801,7 @@ async function readGalleryImage(file) {
|
|
|
1769
1801
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*-[0-9]+\.(png|jpg|jpeg|webp|gif)$/.test(file)) return void 0;
|
|
1770
1802
|
try {
|
|
1771
1803
|
return {
|
|
1772
|
-
data: await promises.readFile(path.join(
|
|
1804
|
+
data: await promises.readFile(path.join(imagesDir(), file)),
|
|
1773
1805
|
mime: mimeOfFile$1(file)
|
|
1774
1806
|
};
|
|
1775
1807
|
} catch {
|
|
@@ -1779,11 +1811,6 @@ async function readGalleryImage(file) {
|
|
|
1779
1811
|
//#endregion
|
|
1780
1812
|
//#region src/canvas-store.ts
|
|
1781
1813
|
/** Host-persisted infinite canvas documents and content-addressed assets. */
|
|
1782
|
-
const DATA_ROOT = process.env.DSH_HOME?.trim() || path.join(homedir(), ".dsh");
|
|
1783
|
-
const CANVAS_ROOT = path.join(DATA_ROOT, "dsh-imagegen", "canvas");
|
|
1784
|
-
path.join(CANVAS_ROOT, "pages");
|
|
1785
|
-
path.join(CANVAS_ROOT, "assets");
|
|
1786
|
-
path.join(CANVAS_ROOT, "index.json");
|
|
1787
1814
|
var CanvasConflictError = class extends Error {
|
|
1788
1815
|
code = "canvas-conflict";
|
|
1789
1816
|
constructor(message = "画布已在其他窗口更新,请重新加载后再保存。") {
|
|
@@ -1867,7 +1894,7 @@ function isNode(value) {
|
|
|
1867
1894
|
function isDocument(value) {
|
|
1868
1895
|
if (value === null || typeof value !== "object") return false;
|
|
1869
1896
|
const document = value;
|
|
1870
|
-
return document.version === 2 && typeof document.id === "string" && typeof document.title === "string" && typeof document.revision === "number" && document.viewport !== null && typeof document.viewport === "object" && typeof document.viewport.x === "number" && typeof document.viewport.y === "number" && typeof document.viewport.k === "number" && (document.background === "dots" || document.background === "lines" || document.background === "blank") && Array.isArray(document.nodes) && document.nodes.every(isNode) && Array.isArray(document.connections);
|
|
1897
|
+
return document.version === 2 && typeof document.id === "string" && typeof document.title === "string" && typeof document.revision === "number" && document.viewport !== null && typeof document.viewport === "object" && typeof document.viewport.x === "number" && typeof document.viewport.y === "number" && typeof document.viewport.k === "number" && (document.background === "dots" || document.background === "lines" || document.background === "diagonal" || document.background === "checker" || document.background === "blank" || document.background === "image") && (document.backgroundImage === void 0 || typeof document.backgroundImage === "string") && Array.isArray(document.nodes) && document.nodes.every(isNode) && Array.isArray(document.connections);
|
|
1871
1898
|
}
|
|
1872
1899
|
/** Upgrade a v1 (image/text/annotation + edges) document to the v2 node-graph model.
|
|
1873
1900
|
* Images and text notes keep their geometry; annotation prompt cards become plain
|
|
@@ -1987,17 +2014,17 @@ function serialize(operation) {
|
|
|
1987
2014
|
}
|
|
1988
2015
|
var CanvasStore = class {
|
|
1989
2016
|
root;
|
|
1990
|
-
constructor(root
|
|
2017
|
+
constructor(root) {
|
|
1991
2018
|
this.root = root;
|
|
1992
2019
|
}
|
|
1993
2020
|
pagesDir() {
|
|
1994
|
-
return path.join(this.root, "pages");
|
|
2021
|
+
return path.join(this.root ?? path.join(imageDataRoot(), "canvas"), "pages");
|
|
1995
2022
|
}
|
|
1996
2023
|
assetsDir() {
|
|
1997
|
-
return path.join(this.root, "assets");
|
|
2024
|
+
return path.join(this.root ?? path.join(imageDataRoot(), "canvas"), "assets");
|
|
1998
2025
|
}
|
|
1999
2026
|
indexPath() {
|
|
2000
|
-
return path.join(this.root, "index.json");
|
|
2027
|
+
return path.join(this.root ?? path.join(imageDataRoot(), "canvas"), "index.json");
|
|
2001
2028
|
}
|
|
2002
2029
|
async ensure() {
|
|
2003
2030
|
await promises.mkdir(this.pagesDir(), { recursive: true });
|
|
@@ -2861,6 +2888,7 @@ function parseGenerateRequest(body) {
|
|
|
2861
2888
|
n: typeof body.n === "number" ? body.n : 1,
|
|
2862
2889
|
detail: typeof body.detail === "string" ? body.detail : "",
|
|
2863
2890
|
...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {},
|
|
2891
|
+
...Array.isArray(body.images) ? { images: body.images.filter((item) => typeof item === "string" && item !== "").slice(0, 4) } : {},
|
|
2864
2892
|
...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {},
|
|
2865
2893
|
...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {},
|
|
2866
2894
|
...typeof body.comparisonId === "string" && body.comparisonId !== "" ? { comparisonId: body.comparisonId } : {},
|
|
@@ -4976,6 +5004,7 @@ const Config = z.object({
|
|
|
4976
5004
|
promptApiUrl: z.string().default(""),
|
|
4977
5005
|
promptApiKey: z.string().role("secret").default(""),
|
|
4978
5006
|
promptModel: z.string().default(""),
|
|
5007
|
+
localStoragePath: z.string().default(""),
|
|
4979
5008
|
storageEnabled: z.boolean().default(false),
|
|
4980
5009
|
storageEndpoint: z.string().default(""),
|
|
4981
5010
|
storageRegion: z.string().default(""),
|
|
@@ -5048,6 +5077,7 @@ function apply(ctx, config) {
|
|
|
5048
5077
|
let current = () => config ?? {};
|
|
5049
5078
|
const resolve = () => {
|
|
5050
5079
|
const value = current() ?? {};
|
|
5080
|
+
setImageDataRoot(value.localStoragePath);
|
|
5051
5081
|
let channels = normalizeChannels(value.channels);
|
|
5052
5082
|
const secrets = { ...value.channelSecrets ?? {} };
|
|
5053
5083
|
if (channels.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dickpy/dsh-imagegen",
|
|
3
3
|
"description": "AI image generation plugin for the dsh web GUI: text-to-image and image-to-image through configurable provider channels (gpt-image-2 / grok-imagine-image / nanobanana series / seedream-5.0-pro / dall-e-3, with native xAI Grok Imagine, Google Nano Banana and ByteDance Seedream request shaping), with per-channel model catalogs and a New Session / Image Generation tab entry opening a three-column studio beside the native conversation.",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
package/src/canvas-store.ts
CHANGED
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { promises as fs } from 'node:fs'
|
|
4
4
|
import { createHash, randomUUID } from 'node:crypto'
|
|
5
|
-
import { homedir } from 'node:os'
|
|
6
5
|
import path from 'node:path'
|
|
7
6
|
import type { CanvasAssetRef, CanvasDocument, CanvasNode, CanvasSummary } from './protocol.ts'
|
|
7
|
+
import { imageDataRoot } from './image-storage-path.ts'
|
|
8
|
+
|
|
8
9
|
|
|
9
|
-
const DATA_ROOT = (process.env.DSH_HOME?.trim() || path.join(homedir(), '.dsh'))
|
|
10
|
-
const CANVAS_ROOT = path.join(DATA_ROOT, 'dsh-imagegen', 'canvas')
|
|
11
|
-
const PAGES_DIR = path.join(CANVAS_ROOT, 'pages')
|
|
12
|
-
const ASSETS_DIR = path.join(CANVAS_ROOT, 'assets')
|
|
13
|
-
const INDEX_PATH = path.join(CANVAS_ROOT, 'index.json')
|
|
14
10
|
|
|
15
11
|
export interface CanvasImageInput {
|
|
16
12
|
data: Uint8Array
|
|
@@ -61,20 +57,21 @@ function safeId(value: string): string {
|
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
function pagePath(id: string): string {
|
|
64
|
-
return path.join(
|
|
60
|
+
return path.join(path.join(imageDataRoot(), 'canvas', 'pages'), `${safeId(id)}.json`)
|
|
65
61
|
}
|
|
66
62
|
|
|
67
63
|
function assetFilePath(id: string): string | undefined {
|
|
68
64
|
if (!/^[a-f0-9]{64}\.(png|jpg|jpeg|webp|gif)$/.test(id)) return undefined
|
|
69
|
-
const
|
|
70
|
-
const
|
|
65
|
+
const root = path.join(imageDataRoot(), 'canvas', 'assets')
|
|
66
|
+
const file = path.join(root, id)
|
|
67
|
+
const relative = path.relative(root, file)
|
|
71
68
|
if (relative.startsWith('..') || path.isAbsolute(relative)) return undefined
|
|
72
69
|
return file
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
async function ensureDirs(): Promise<void> {
|
|
76
|
-
await fs.mkdir(
|
|
77
|
-
await fs.mkdir(
|
|
73
|
+
await fs.mkdir(path.join(imageDataRoot(), 'canvas', 'pages'), { recursive: true })
|
|
74
|
+
await fs.mkdir(path.join(imageDataRoot(), 'canvas', 'assets'), { recursive: true })
|
|
78
75
|
}
|
|
79
76
|
|
|
80
77
|
async function writeJsonAtomic(file: string, value: unknown): Promise<void> {
|
|
@@ -138,7 +135,9 @@ function isDocument(value: unknown): value is CanvasDocument {
|
|
|
138
135
|
&& typeof (document.viewport as { x?: unknown }).x === 'number'
|
|
139
136
|
&& typeof (document.viewport as { y?: unknown }).y === 'number'
|
|
140
137
|
&& typeof (document.viewport as { k?: unknown }).k === 'number'
|
|
141
|
-
&& (document.background === 'dots' || document.background === 'lines' || document.background === '
|
|
138
|
+
&& (document.background === 'dots' || document.background === 'lines' || document.background === 'diagonal'
|
|
139
|
+
|| document.background === 'checker' || document.background === 'blank' || document.background === 'image')
|
|
140
|
+
&& (document.backgroundImage === undefined || typeof document.backgroundImage === 'string')
|
|
142
141
|
&& Array.isArray(document.nodes) && document.nodes.every(isNode)
|
|
143
142
|
&& Array.isArray(document.connections)
|
|
144
143
|
}
|
|
@@ -245,11 +244,11 @@ function serialize<T>(operation: () => Promise<T>): Promise<T> {
|
|
|
245
244
|
}
|
|
246
245
|
|
|
247
246
|
export class CanvasStore {
|
|
248
|
-
constructor(private readonly root
|
|
247
|
+
constructor(private readonly root?: string) {}
|
|
249
248
|
|
|
250
|
-
private pagesDir(): string { return path.join(this.root, 'pages') }
|
|
251
|
-
private assetsDir(): string { return path.join(this.root, 'assets') }
|
|
252
|
-
private indexPath(): string { return path.join(this.root, 'index.json') }
|
|
249
|
+
private pagesDir(): string { return path.join(this.root ?? path.join(imageDataRoot(), 'canvas'), 'pages') }
|
|
250
|
+
private assetsDir(): string { return path.join(this.root ?? path.join(imageDataRoot(), 'canvas'), 'assets') }
|
|
251
|
+
private indexPath(): string { return path.join(this.root ?? path.join(imageDataRoot(), 'canvas'), 'index.json') }
|
|
253
252
|
|
|
254
253
|
private async ensure(): Promise<void> {
|
|
255
254
|
await fs.mkdir(this.pagesDir(), { recursive: true })
|