@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/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.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 parsed = parseDataUrl(request.image);
885
- if (parsed === void 0) throw new ImageGenError("参考图片格式无效", "edit-image-invalid");
886
- let bytes;
887
- try {
888
- bytes = Buffer.from(parsed.base64, "base64");
889
- } catch {
890
- throw new ImageGenError("参考图片数据无法解码", "edit-image-invalid");
891
- }
892
- if (bytes.byteLength > MAX_EDIT_IMAGE_BYTES) throw new ImageGenError("参考图片超过 10MB 上限", "edit-image-too-large");
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: parsed.mime }), `reference.${extensionOf$3(parsed.mime)}`);
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: parsed.mime }), `reference.${extensionOf$3(parsed.mime)}`);
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
- const HISTORY_DIR$1 = path.join(homedir(), ".dsh", "dsh-imagegen");
1192
- const INDEX_PATH$1 = path.join(HISTORY_DIR$1, "index.json");
1193
- const IMAGES_DIR$1 = path.join(HISTORY_DIR$1, "images");
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(IMAGES_DIR$1, { recursive: true });
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(INDEX_PATH$1, "utf8");
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 = `${INDEX_PATH$1}.tmp-${process.pid}`;
1272
+ const tmp = `${indexPath$1()}.tmp-${process.pid}`;
1246
1273
  await promises.writeFile(tmp, JSON.stringify(payload), "utf8");
1247
- await promises.rename(tmp, INDEX_PATH$1);
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(IMAGES_DIR$1, image.file), { force: true });
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(IMAGES_DIR$1, file), Buffer.from(image.b64, "base64"));
1310
- notifyImageSaved("history", path.join(IMAGES_DIR$1, file));
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(IMAGES_DIR$1, file)),
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" && (this.running === 0 || item.request.comparisonId !== void 0 && !this.serialRunning));
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
- const HISTORY_DIR = path.join(homedir(), ".dsh", "dsh-imagegen");
1562
- const GALLERY_DIR = path.join(HISTORY_DIR, "gallery");
1563
- const INDEX_PATH = path.join(GALLERY_DIR, "index.json");
1564
- const IMAGES_DIR = path.join(GALLERY_DIR, "images");
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(IMAGES_DIR, { recursive: true });
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(INDEX_PATH, "utf8");
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 = `${INDEX_PATH}.tmp-${process.pid}`;
1654
+ const tmp = `${indexPath()}.tmp-${process.pid}`;
1623
1655
  await promises.writeFile(tmp, JSON.stringify(payload), "utf8");
1624
- await promises.rename(tmp, INDEX_PATH);
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(IMAGES_DIR, image.file), { force: true });
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(IMAGES_DIR, file), Buffer.from(image.b64, "base64"));
1696
- notifyImageSaved("gallery", path.join(IMAGES_DIR, file));
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(IMAGES_DIR, file)),
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 = CANVAS_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.5",
4
+ "version": "1.5.6",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -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(PAGES_DIR, `${safeId(id)}.json`)
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 file = path.join(ASSETS_DIR, id)
70
- const relative = path.relative(ASSETS_DIR, file)
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(PAGES_DIR, { recursive: true })
77
- await fs.mkdir(ASSETS_DIR, { recursive: true })
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 === 'blank')
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 = CANVAS_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 })