@dickpy/dsh-imagegen 1.5.2 → 1.5.4
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 +23 -2
- package/lib/client.js +5803 -1942
- package/lib/client.js.map +1 -1
- package/lib/index.js +1151 -33
- package/package.json +1 -1
- package/src/canvas-store.ts +376 -0
- package/src/client/CanvasWorkspace.tsx +1569 -0
- package/src/client/ImageGenPanel.tsx +164 -96
- package/src/client/SettingsCard.tsx +182 -4
- package/src/client/api.ts +48 -1
- package/src/client/canvas-workspace.module.css +929 -0
- package/src/client/helpers.ts +71 -33
- package/src/client/index.ts +59 -7
- package/src/client/locales.ts +1452 -772
- package/src/client/panel.module.css +83 -33
- package/src/client/use-language.ts +14 -0
- package/src/engine.ts +302 -12
- package/src/gallery-store.ts +5 -0
- package/src/generation-runtime.ts +1 -0
- package/src/history-store.ts +5 -0
- package/src/index.ts +71 -4
- package/src/model-catalog.ts +10 -1
- package/src/presets.ts +15 -0
- package/src/protocol.ts +121 -1
- package/src/routes.ts +231 -1
- package/src/storage-sync.ts +105 -0
package/lib/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { promises, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import * as settingsModule from "@deepseek-ai/dsh-settings";
|
|
2
4
|
import { SettingsConflictError } from "@deepseek-ai/dsh-settings";
|
|
3
5
|
import z from "schemastery";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { mkdir } from "node:fs/promises";
|
|
8
|
+
import { createHash, createHmac, randomUUID } from "node:crypto";
|
|
6
9
|
import { homedir } from "node:os";
|
|
7
|
-
import path from "node:path";
|
|
8
10
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { spawn } from "node:child_process";
|
|
10
11
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
11
12
|
//#region src/settings-compat.ts
|
|
12
13
|
const compatModule = settingsModule;
|
|
@@ -40,7 +41,7 @@ function installSettingsSectionCompat(ctx, ns, schema, entry, hooks) {
|
|
|
40
41
|
/** Settings namespace this plugin owns (host settings seam + bridge). */
|
|
41
42
|
const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
|
|
42
43
|
/** Published package version shared by the host updater and the client UI. */
|
|
43
|
-
const PLUGIN_VERSION = "1.5.
|
|
44
|
+
const PLUGIN_VERSION = "1.5.4";
|
|
44
45
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
45
46
|
const SETTINGS_API = {
|
|
46
47
|
describe: "/api/dsh-imagegen/settings/describe",
|
|
@@ -74,6 +75,10 @@ const TASK_API = {
|
|
|
74
75
|
cancel: "/api/dsh-imagegen/tasks/cancel",
|
|
75
76
|
retry: "/api/dsh-imagegen/tasks/retry"
|
|
76
77
|
};
|
|
78
|
+
/** Reveal the host data directory (saved images) in the OS file manager. */
|
|
79
|
+
const DATA_FOLDER_API = "/api/dsh-imagegen/data-folder/open";
|
|
80
|
+
/** Probe the configured S3-compatible object storage. */
|
|
81
|
+
const STORAGE_API = { test: "/api/dsh-imagegen/storage/test" };
|
|
77
82
|
/** Host-mediated GitHub Release update routes. */
|
|
78
83
|
const UPDATE_API = {
|
|
79
84
|
check: "/api/dsh-imagegen/update/check",
|
|
@@ -105,6 +110,17 @@ const GALLERY_API = {
|
|
|
105
110
|
tags: "/api/dsh-imagegen/gallery/tags",
|
|
106
111
|
image: "/api/dsh-imagegen/gallery/image"
|
|
107
112
|
};
|
|
113
|
+
/** Host-persisted infinite canvas projects and their content-addressed assets. */
|
|
114
|
+
const CANVAS_API = {
|
|
115
|
+
list: "/api/dsh-imagegen/canvas/list",
|
|
116
|
+
create: "/api/dsh-imagegen/canvas/create",
|
|
117
|
+
read: "/api/dsh-imagegen/canvas/read",
|
|
118
|
+
save: "/api/dsh-imagegen/canvas/save",
|
|
119
|
+
remove: "/api/dsh-imagegen/canvas/remove",
|
|
120
|
+
assetUpload: "/api/dsh-imagegen/canvas/asset/upload",
|
|
121
|
+
assetImport: "/api/dsh-imagegen/canvas/asset/import",
|
|
122
|
+
asset: "/api/dsh-imagegen/canvas/asset"
|
|
123
|
+
};
|
|
108
124
|
/**
|
|
109
125
|
* Same-origin route family for the prompt-template libraries. The library is
|
|
110
126
|
* multi-source: every request names a source id from {@link TEMPLATE_SOURCES},
|
|
@@ -206,6 +222,14 @@ const ENTRIES = {
|
|
|
206
222
|
supportsEdit: false,
|
|
207
223
|
supportsAspectRatio: false,
|
|
208
224
|
qualityTiers: ["HD"]
|
|
225
|
+
},
|
|
226
|
+
qwen: {
|
|
227
|
+
label: "qwen-image",
|
|
228
|
+
labelZh: "千问图像",
|
|
229
|
+
known: true,
|
|
230
|
+
supportsEdit: true,
|
|
231
|
+
supportsAspectRatio: true,
|
|
232
|
+
qualityTiers: ["auto"]
|
|
209
233
|
}
|
|
210
234
|
};
|
|
211
235
|
/** Official Gemini image ids served by Nano Banana gateways. */
|
|
@@ -244,6 +268,10 @@ function describeModel(model) {
|
|
|
244
268
|
family: "zhipu",
|
|
245
269
|
...ENTRIES.zhipu
|
|
246
270
|
};
|
|
271
|
+
if (/^qwen-image(?:[-_.]|$)/i.test(id)) return {
|
|
272
|
+
family: "qwen",
|
|
273
|
+
...ENTRIES.qwen
|
|
274
|
+
};
|
|
247
275
|
return {
|
|
248
276
|
family: "unknown",
|
|
249
277
|
label: "unknown",
|
|
@@ -471,6 +499,12 @@ function isSeedream(model) {
|
|
|
471
499
|
function isZhipuImage(model) {
|
|
472
500
|
return modelFamily(model) === "zhipu";
|
|
473
501
|
}
|
|
502
|
+
/** Whether the model is Alibaba Qwen-Image, which speaks the DashScope native
|
|
503
|
+
* multimodal-generation contract (NOT OpenAI-compatible): a chat-style
|
|
504
|
+
* messages body, `宽*高` pixel sizes, and image URLs in the reply content. */
|
|
505
|
+
function isQwenImage(model) {
|
|
506
|
+
return modelFamily(model) === "qwen";
|
|
507
|
+
}
|
|
474
508
|
function isGlmImage(model) {
|
|
475
509
|
return /^glm-image(?:-|$)/i.test(model.trim());
|
|
476
510
|
}
|
|
@@ -483,6 +517,38 @@ function seedreamSize(quality) {
|
|
|
483
517
|
if (quality === "1k") return "1K";
|
|
484
518
|
return "2K";
|
|
485
519
|
}
|
|
520
|
+
/** The panel's aspect ratios mapped to Qwen-Image's `宽*高` pixel sizes.
|
|
521
|
+
* The classic series (qwen-image / -plus / -max) documents this fixed list;
|
|
522
|
+
* 2.0 / 3.0-series models accept any size within their pixel budget and
|
|
523
|
+
* recommend the larger set. */
|
|
524
|
+
const QWEN_SIZE_CLASSIC = {
|
|
525
|
+
"16:9": "1664*928",
|
|
526
|
+
"21:9": "1664*928",
|
|
527
|
+
"4:3": "1472*1104",
|
|
528
|
+
"3:2": "1472*1104",
|
|
529
|
+
"1:1": "1328*1328",
|
|
530
|
+
"3:4": "1104*1472",
|
|
531
|
+
"2:3": "1104*1472",
|
|
532
|
+
"9:16": "928*1664"
|
|
533
|
+
};
|
|
534
|
+
const QWEN_SIZE_HD = {
|
|
535
|
+
"16:9": "2688*1536",
|
|
536
|
+
"21:9": "2688*1536",
|
|
537
|
+
"4:3": "2368*1728",
|
|
538
|
+
"3:2": "2368*1728",
|
|
539
|
+
"1:1": "2048*2048",
|
|
540
|
+
"3:4": "1728*2368",
|
|
541
|
+
"2:3": "1728*2368",
|
|
542
|
+
"9:16": "1536*2688"
|
|
543
|
+
};
|
|
544
|
+
/** Versioned ids (qwen-image-2.0 / -3.0-pro / …) take the large size set. */
|
|
545
|
+
function isVersionedQwenImage(model) {
|
|
546
|
+
return /^qwen-image-\d+\.\d/i.test(model.trim());
|
|
547
|
+
}
|
|
548
|
+
function qwenSize(model, ratio) {
|
|
549
|
+
if (ratio === "" || ratio === "auto") return void 0;
|
|
550
|
+
return (isVersionedQwenImage(model) ? QWEN_SIZE_HD : QWEN_SIZE_CLASSIC)[ratio];
|
|
551
|
+
}
|
|
486
552
|
/** The panel's aspect ratios mapped to the closest OpenAI pixel size
|
|
487
553
|
* (gpt-image-2 / generic OpenAI-compatible endpoints). */
|
|
488
554
|
const OPENAI_SIZE_BY_RATIO = {
|
|
@@ -617,7 +683,7 @@ function effectiveCount(request) {
|
|
|
617
683
|
return clampCount(request.n);
|
|
618
684
|
}
|
|
619
685
|
/** Normalize one upstream data item into a base64 image. */
|
|
620
|
-
async function normalizeItem(item, upstream) {
|
|
686
|
+
async function normalizeItem(item, upstream, signal) {
|
|
621
687
|
const revisedPrompt = typeof item.revised_prompt === "string" ? item.revised_prompt : void 0;
|
|
622
688
|
if (typeof item.b64_json === "string" && item.b64_json.trim() !== "") {
|
|
623
689
|
const b64 = bareBase64(item.b64_json);
|
|
@@ -638,7 +704,7 @@ async function normalizeItem(item, upstream) {
|
|
|
638
704
|
revisedPrompt
|
|
639
705
|
};
|
|
640
706
|
}
|
|
641
|
-
const budget = requestSignal(
|
|
707
|
+
const budget = requestSignal(signal, IMAGE_FETCH_TIMEOUT_MS);
|
|
642
708
|
let response;
|
|
643
709
|
try {
|
|
644
710
|
response = await fetch(url, {
|
|
@@ -660,6 +726,143 @@ async function normalizeItem(item, upstream) {
|
|
|
660
726
|
revisedPrompt
|
|
661
727
|
};
|
|
662
728
|
}
|
|
729
|
+
/** Expand a provider image item whose URL may be a string or an array. */
|
|
730
|
+
function imageItemsOf(value) {
|
|
731
|
+
if (value === null || typeof value !== "object") return [];
|
|
732
|
+
const item = value;
|
|
733
|
+
if (Array.isArray(item.url)) return item.url.filter((url) => typeof url === "string" && url !== "").map((url) => ({
|
|
734
|
+
...item,
|
|
735
|
+
url
|
|
736
|
+
}));
|
|
737
|
+
return [item];
|
|
738
|
+
}
|
|
739
|
+
/** Return the data records from the response shapes shared by sync gateways. */
|
|
740
|
+
function dataRecordsOf(payload) {
|
|
741
|
+
const data = Array.isArray(payload.data) ? payload.data : payload.data !== null && typeof payload.data === "object" ? [payload.data] : Array.isArray(payload.images) ? payload.images : Array.isArray(payload.output) ? payload.output : void 0;
|
|
742
|
+
if (data === void 0) return void 0;
|
|
743
|
+
return data.filter((entry) => entry !== null && typeof entry === "object");
|
|
744
|
+
}
|
|
745
|
+
const ASYNC_PENDING_STATUSES = /* @__PURE__ */ new Set([
|
|
746
|
+
"submitted",
|
|
747
|
+
"pending",
|
|
748
|
+
"processing",
|
|
749
|
+
"running",
|
|
750
|
+
"in_progress",
|
|
751
|
+
"queued"
|
|
752
|
+
]);
|
|
753
|
+
const ASYNC_COMPLETED_STATUSES = /* @__PURE__ */ new Set([
|
|
754
|
+
"completed",
|
|
755
|
+
"succeeded",
|
|
756
|
+
"success",
|
|
757
|
+
"done"
|
|
758
|
+
]);
|
|
759
|
+
const ASYNC_FAILED_STATUSES = /* @__PURE__ */ new Set([
|
|
760
|
+
"failed",
|
|
761
|
+
"failure",
|
|
762
|
+
"cancelled",
|
|
763
|
+
"canceled",
|
|
764
|
+
"error"
|
|
765
|
+
]);
|
|
766
|
+
const ASYNC_POLL_MAX_MS = 24e4;
|
|
767
|
+
const ASYNC_POLL_REQUEST_TIMEOUT_MS = 3e4;
|
|
768
|
+
/** Read a provider error message from the common nested locations. */
|
|
769
|
+
function asyncErrorMessage(payload, fallback) {
|
|
770
|
+
if (payload !== null && typeof payload === "object") {
|
|
771
|
+
const record = payload;
|
|
772
|
+
const candidates = [record.message, record.error];
|
|
773
|
+
const data = record.data;
|
|
774
|
+
const entries = Array.isArray(data) ? data : [data];
|
|
775
|
+
for (const entry of entries) {
|
|
776
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
777
|
+
const item = entry;
|
|
778
|
+
candidates.push(item.message, item.error);
|
|
779
|
+
const nested = item.error;
|
|
780
|
+
if (nested !== null && typeof nested === "object") candidates.push(nested.message);
|
|
781
|
+
}
|
|
782
|
+
for (const candidate of candidates) {
|
|
783
|
+
if (typeof candidate === "string" && candidate.trim() !== "") return candidate;
|
|
784
|
+
if (candidate !== null && typeof candidate === "object") {
|
|
785
|
+
const message = candidate.message;
|
|
786
|
+
if (typeof message === "string" && message.trim() !== "") return message;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return fallback;
|
|
791
|
+
}
|
|
792
|
+
/** Wait between async-provider polls, but wake immediately when cancelled. */
|
|
793
|
+
function waitForPoll(ms, signal) {
|
|
794
|
+
return new Promise((resolve, reject) => {
|
|
795
|
+
if (signal?.aborted === true) {
|
|
796
|
+
reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const onAbort = () => {
|
|
800
|
+
clearTimeout(timer);
|
|
801
|
+
signal?.removeEventListener("abort", onAbort);
|
|
802
|
+
reject(signal?.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
803
|
+
};
|
|
804
|
+
const done = () => {
|
|
805
|
+
signal?.removeEventListener("abort", onAbort);
|
|
806
|
+
resolve();
|
|
807
|
+
};
|
|
808
|
+
const timer = setTimeout(done, ms);
|
|
809
|
+
timer.unref();
|
|
810
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Poll one apib/apimart-style provider task until it yields image records.
|
|
815
|
+
* The total deadline is shared by every poll and the final image downloads;
|
|
816
|
+
* local task cancellation propagates through every request and sleep.
|
|
817
|
+
*/
|
|
818
|
+
async function pollAsyncTask(baseUrl, upstream, taskId, signal) {
|
|
819
|
+
const deadline = Date.now() + ASYNC_POLL_MAX_MS;
|
|
820
|
+
let delay = 1e3;
|
|
821
|
+
while (Date.now() < deadline) {
|
|
822
|
+
const remaining = deadline - Date.now();
|
|
823
|
+
const budget = requestSignal(signal, Math.min(ASYNC_POLL_REQUEST_TIMEOUT_MS, remaining));
|
|
824
|
+
let response;
|
|
825
|
+
try {
|
|
826
|
+
response = await fetch(`${baseUrl}/tasks/${encodeURIComponent(taskId)}`, {
|
|
827
|
+
method: "GET",
|
|
828
|
+
headers: { authorization: `Bearer ${upstream.apiKey.trim()}` },
|
|
829
|
+
signal: budget.signal
|
|
830
|
+
});
|
|
831
|
+
} catch (error) {
|
|
832
|
+
if (signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
|
|
833
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
834
|
+
if (/timeout|abort/i.test(message)) throw new ImageGenError("上游异步任务轮询超时", "upstream-timeout");
|
|
835
|
+
throw new ImageGenError(`无法轮询上游异步任务:${message}`, "upstream-unreachable");
|
|
836
|
+
} finally {
|
|
837
|
+
budget.dispose();
|
|
838
|
+
}
|
|
839
|
+
let payload;
|
|
840
|
+
try {
|
|
841
|
+
payload = await response.json();
|
|
842
|
+
} catch {
|
|
843
|
+
throw new ImageGenError(`上游任务接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
|
|
844
|
+
}
|
|
845
|
+
if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(asyncErrorMessage(payload, `上游任务轮询失败(HTTP ${response.status})`), "upstream-rejected");
|
|
846
|
+
const record = payload;
|
|
847
|
+
const data = record.data;
|
|
848
|
+
const statusRecord = Array.isArray(data) ? data[0] : data !== null && typeof data === "object" ? data : record;
|
|
849
|
+
const statusValue = statusRecord !== null && typeof statusRecord === "object" ? statusRecord.status : void 0;
|
|
850
|
+
const status = typeof statusValue === "string" ? statusValue.toLowerCase() : "";
|
|
851
|
+
if (ASYNC_FAILED_STATUSES.has(status)) throw new ImageGenError(asyncErrorMessage(payload, `上游异步任务失败(${status || "unknown"})`), "upstream-rejected");
|
|
852
|
+
const nested = statusRecord !== null && typeof statusRecord === "object" ? statusRecord : record;
|
|
853
|
+
const result = nested.result ?? (nested.output !== null && typeof nested.output === "object" ? nested.output.result : void 0) ?? record.result;
|
|
854
|
+
const images = (result !== null && typeof result === "object" ? result : void 0)?.images ?? nested.images ?? record.images;
|
|
855
|
+
if (ASYNC_COMPLETED_STATUSES.has(status) || images !== void 0) {
|
|
856
|
+
const items = Array.isArray(images) ? images.flatMap(imageItemsOf) : imageItemsOf(images);
|
|
857
|
+
if (items.length > 0) return items;
|
|
858
|
+
if (ASYNC_COMPLETED_STATUSES.has(status)) throw new ImageGenError("上游异步任务完成但没有图片结果", "upstream-empty");
|
|
859
|
+
}
|
|
860
|
+
if (status !== "" && !ASYNC_PENDING_STATUSES.has(status) && !ASYNC_COMPLETED_STATUSES.has(status)) throw new ImageGenError(`上游返回了未知异步任务状态:${status}`, "upstream-invalid");
|
|
861
|
+
await waitForPoll(Math.min(delay, Math.max(1, deadline - Date.now())), signal);
|
|
862
|
+
delay = Math.min(5e3, delay * 2);
|
|
863
|
+
}
|
|
864
|
+
throw new ImageGenError("上游异步任务轮询超时(240 秒)", "upstream-timeout");
|
|
865
|
+
}
|
|
663
866
|
/**
|
|
664
867
|
* Issue one single-image request (never sends `n`). The response is kept as a
|
|
665
868
|
* list so a gateway that happens to return several images per call still works.
|
|
@@ -692,7 +895,7 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
692
895
|
});
|
|
693
896
|
} else if (isNanoBanana(params.model)) {
|
|
694
897
|
const form = new FormData();
|
|
695
|
-
form.append("image", new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf$
|
|
898
|
+
form.append("image", new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf$3(parsed.mime)}`);
|
|
696
899
|
form.append("prompt", request.prompt);
|
|
697
900
|
form.append("model", params.model);
|
|
698
901
|
if (params.aspect_ratio !== void 0) form.append("aspect_ratio", params.aspect_ratio);
|
|
@@ -710,7 +913,7 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
710
913
|
});
|
|
711
914
|
} else {
|
|
712
915
|
const form = new FormData();
|
|
713
|
-
form.append("image", new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf$
|
|
916
|
+
form.append("image", new Blob([bytes], { type: parsed.mime }), `reference.${extensionOf$3(parsed.mime)}`);
|
|
714
917
|
form.append("prompt", request.prompt);
|
|
715
918
|
form.append("model", params.model);
|
|
716
919
|
if (params.size !== void 0) form.append("size", params.size);
|
|
@@ -749,14 +952,93 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
749
952
|
throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
|
|
750
953
|
}
|
|
751
954
|
if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(upstreamMessage(payload, response.status), "upstream-rejected");
|
|
752
|
-
const
|
|
753
|
-
const data = Array.isArray(record.data) ? record.data : Array.isArray(record.images) ? record.images : Array.isArray(record.output) ? record.output : void 0;
|
|
955
|
+
const data = dataRecordsOf(payload);
|
|
754
956
|
if (data === void 0) throw new ImageGenError("上游响应缺少 data 数组", "upstream-invalid");
|
|
755
957
|
if (data.length === 0) throw new ImageGenError("上游返回了 0 张图片", "upstream-empty");
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
958
|
+
const asyncEntries = data.filter((entry) => typeof entry.task_id === "string" && entry.task_id.trim() !== "");
|
|
959
|
+
if (asyncEntries.length > 0) {
|
|
960
|
+
const asyncRecords = (await Promise.all(asyncEntries.map((entry) => pollAsyncTask(baseUrl, upstream, entry.task_id, signal)))).flat();
|
|
961
|
+
if (asyncRecords.length === 0) throw new ImageGenError("上游异步任务完成但没有图片结果", "upstream-empty");
|
|
962
|
+
return Promise.all(asyncRecords.flatMap(imageItemsOf).map((item) => normalizeItem(item, upstream, signal)));
|
|
963
|
+
}
|
|
964
|
+
return Promise.all(data.flatMap(imageItemsOf).map((item) => normalizeItem(item, upstream, signal)));
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Qwen-Image (DashScope native multimodal-generation): one chat-style request
|
|
968
|
+
* carries the prompt (plus the reference image for edit mode) and answers
|
|
969
|
+
* synchronously with image URLs in the reply content. The versioned series
|
|
970
|
+
* batches natively (n ≤ 6; the panel caps at 4), the classic series is
|
|
971
|
+
* single-image per call.
|
|
972
|
+
*/
|
|
973
|
+
async function generateQwenImage(baseUrl, upstream, request, options) {
|
|
974
|
+
const model = wireModel(request);
|
|
975
|
+
const content = [];
|
|
976
|
+
if (request.mode === "edit") {
|
|
977
|
+
if (typeof request.image !== "string" || request.image === "") throw new ImageGenError("图生图需要上传参考图片", "edit-image-missing");
|
|
978
|
+
const parsed = parseDataUrl(request.image);
|
|
979
|
+
if (parsed === void 0) throw new ImageGenError("参考图片格式无效", "edit-image-invalid");
|
|
980
|
+
if (Buffer.from(parsed.base64, "base64").byteLength > MAX_EDIT_IMAGE_BYTES) throw new ImageGenError("参考图片超过 10MB 上限", "edit-image-too-large");
|
|
981
|
+
content.push({ image: request.image });
|
|
982
|
+
}
|
|
983
|
+
content.push({ text: request.prompt });
|
|
984
|
+
const count = isVersionedQwenImage(model) ? clampCount(request.n) : 1;
|
|
985
|
+
const size = qwenSize(model, request.size);
|
|
986
|
+
const body = {
|
|
987
|
+
model,
|
|
988
|
+
input: { messages: [{
|
|
989
|
+
role: "user",
|
|
990
|
+
content
|
|
991
|
+
}] },
|
|
992
|
+
parameters: {
|
|
993
|
+
...size !== void 0 ? { size } : {},
|
|
994
|
+
...count > 1 ? { n: count } : {}
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
const budget = requestSignal(options.signal, UPSTREAM_TIMEOUT_MS);
|
|
998
|
+
let response;
|
|
999
|
+
try {
|
|
1000
|
+
response = await fetch(`${baseUrl}/services/aigc/multimodal-generation/generation`, {
|
|
1001
|
+
method: "POST",
|
|
1002
|
+
headers: {
|
|
1003
|
+
authorization: `Bearer ${upstream.apiKey.trim()}`,
|
|
1004
|
+
"content-type": "application/json"
|
|
1005
|
+
},
|
|
1006
|
+
body: JSON.stringify(body),
|
|
1007
|
+
signal: budget.signal
|
|
1008
|
+
});
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1011
|
+
if (/aborter/i.test(message) || /timeout/i.test(message)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
|
|
1012
|
+
throw new ImageGenError(`无法连接上游接口:${message}`, "upstream-unreachable");
|
|
1013
|
+
} finally {
|
|
1014
|
+
budget.dispose();
|
|
1015
|
+
}
|
|
1016
|
+
let payload;
|
|
1017
|
+
try {
|
|
1018
|
+
payload = await response.json();
|
|
1019
|
+
} catch {
|
|
1020
|
+
throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
|
|
1021
|
+
}
|
|
1022
|
+
if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(upstreamMessage(payload, response.status), "upstream-rejected");
|
|
1023
|
+
const output = payload.output;
|
|
1024
|
+
const choices = output !== void 0 && Array.isArray(output.choices) ? output.choices : [];
|
|
1025
|
+
const urls = [];
|
|
1026
|
+
for (const choice of choices) {
|
|
1027
|
+
const message = choice !== null && typeof choice === "object" ? choice.message : void 0;
|
|
1028
|
+
const items = message !== null && typeof message === "object" && Array.isArray(message.content) ? message.content : [];
|
|
1029
|
+
for (const item of items) if (item !== null && typeof item === "object") {
|
|
1030
|
+
const image = item.image;
|
|
1031
|
+
if (typeof image === "string" && image !== "") urls.push(image);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (urls.length === 0) throw new ImageGenError("上游响应缺少图片内容", "upstream-empty");
|
|
1035
|
+
return { images: await Promise.all(urls.map(async (url) => {
|
|
1036
|
+
const normalized = await normalizeItem({ url }, upstream);
|
|
1037
|
+
return {
|
|
1038
|
+
b64: normalized.b64,
|
|
1039
|
+
mime: normalized.mime
|
|
1040
|
+
};
|
|
1041
|
+
})) };
|
|
760
1042
|
}
|
|
761
1043
|
/**
|
|
762
1044
|
* Forward one generate request to the configured endpoint. The requested image
|
|
@@ -768,6 +1050,7 @@ async function generateImage(upstream, request, options = {}) {
|
|
|
768
1050
|
const baseUrl = upstream.apiUrl.trim().replace(/\/+$/, "");
|
|
769
1051
|
if (baseUrl === "") throw new ImageGenError("api_url 未配置:请先在「设置 → 插件 → 可配置」中填写", "config-missing");
|
|
770
1052
|
if (upstream.apiKey.trim() === "") throw new ImageGenError("api_key 未配置:请先在「设置 → 插件 → 可配置」中填写", "config-missing");
|
|
1053
|
+
if (isQwenImage(wireModel(request))) return generateQwenImage(baseUrl, upstream, request, options);
|
|
771
1054
|
if (request.mode === "edit" && isZhipuImage(wireModel(request))) throw new ImageGenError("智谱 GLM-Image 当前仅支持文生图,请切换到文生图模式或选择支持图生图的模型", "edit-unsupported");
|
|
772
1055
|
const params = effectiveParams(request);
|
|
773
1056
|
const count = effectiveCount(request);
|
|
@@ -788,7 +1071,7 @@ function upstreamMessage(payload, status) {
|
|
|
788
1071
|
return `上游接口拒绝请求(HTTP ${status})`;
|
|
789
1072
|
}
|
|
790
1073
|
/** File extension for a MIME type (multipart reference image). */
|
|
791
|
-
function extensionOf$
|
|
1074
|
+
function extensionOf$3(mime) {
|
|
792
1075
|
switch (mime.split(";")[0].trim()) {
|
|
793
1076
|
case "image/jpeg": return "jpg";
|
|
794
1077
|
case "image/webp": return "webp";
|
|
@@ -797,6 +1080,87 @@ function extensionOf$2(mime) {
|
|
|
797
1080
|
}
|
|
798
1081
|
}
|
|
799
1082
|
//#endregion
|
|
1083
|
+
//#region src/storage-sync.ts
|
|
1084
|
+
/**
|
|
1085
|
+
* Object-storage sync for saved images: one S3-compatible uploader (SigV4,
|
|
1086
|
+
* zero dependencies) that covers Tencent COS / Alibaba OSS / Qiniu S3 /
|
|
1087
|
+
* MinIO / R2 style endpoints, plus a fire-and-forget hook the image stores
|
|
1088
|
+
* call after a file lands on disk. The handler is registered by the plugin
|
|
1089
|
+
* root (it owns the live settings), so framework-free stores stay decoupled
|
|
1090
|
+
* from the settings seam.
|
|
1091
|
+
*
|
|
1092
|
+
* Object keys: `${prefix}/gallery/<file>` and `${prefix}/images/<file>` —
|
|
1093
|
+
* content-addressed file names dedupe re-uploads naturally.
|
|
1094
|
+
*/
|
|
1095
|
+
let uploadHandler;
|
|
1096
|
+
/** Register the live uploader (index.ts apply). Pass undefined to clear. */
|
|
1097
|
+
function setStorageSyncHandler(handler) {
|
|
1098
|
+
uploadHandler = handler;
|
|
1099
|
+
}
|
|
1100
|
+
/** Fire-and-forget notification from the image stores after a file write. */
|
|
1101
|
+
function notifyImageSaved(kind, filePath) {
|
|
1102
|
+
try {
|
|
1103
|
+
uploadHandler?.(kind, filePath);
|
|
1104
|
+
} catch {}
|
|
1105
|
+
}
|
|
1106
|
+
/** URL-encode per RFC 3986 (AWS SigV4 canonical forms). */
|
|
1107
|
+
function uriEncode(value, encodeSlash = true) {
|
|
1108
|
+
return value.replace(/[^A-Za-z0-9-_.~]/g, (char) => {
|
|
1109
|
+
return `%${char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`;
|
|
1110
|
+
}).replace(/%2F/g, encodeSlash ? "%2F" : "/");
|
|
1111
|
+
}
|
|
1112
|
+
/** HMAC-SHA256 helper. */
|
|
1113
|
+
function hmac(key, data) {
|
|
1114
|
+
return createHmac("sha256", key).update(data, "utf8").digest();
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* PUT one object to an S3-compatible endpoint (SigV4, virtual-hosted or
|
|
1118
|
+
* path-style — the endpoint URL already includes the bucket). Returns the
|
|
1119
|
+
* elapsed milliseconds so the settings card can show a latency reading.
|
|
1120
|
+
*/
|
|
1121
|
+
async function putObject(config, key, data, contentType = "application/octet-stream") {
|
|
1122
|
+
const endpoint = config.endpoint.trim().replace(/\/+$/, "");
|
|
1123
|
+
if (endpoint === "" || config.accessKey.trim() === "" || config.secretKey.trim() === "") throw new Error("对象存储配置不完整:请填写接口地址与密钥");
|
|
1124
|
+
const url = new URL(`${endpoint}/${key.split("/").map((part) => uriEncode(part)).join("/")}`);
|
|
1125
|
+
const payloadHash = createHash("sha256").update(data).digest("hex");
|
|
1126
|
+
const amzDate = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "")}`;
|
|
1127
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
1128
|
+
const host = url.host;
|
|
1129
|
+
const canonicalUri = url.pathname;
|
|
1130
|
+
const canonicalHeaders = `content-type:${contentType}\nhost:${host}\nx-amz-content-sha256:${payloadHash}\nx-amz-date:${amzDate}\n`;
|
|
1131
|
+
const signedHeaders = "content-type;host;x-amz-content-sha256;x-amz-date";
|
|
1132
|
+
const canonicalRequest = `PUT\n${canonicalUri}\n\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
|
|
1133
|
+
const scope = `${dateStamp}/${config.region.trim() || "us-east-1"}/s3/aws4_request`;
|
|
1134
|
+
const stringToSign = `AWS4-HMAC-SHA256\n${amzDate}\n${scope}\n${createHash("sha256").update(canonicalRequest, "utf8").digest("hex")}`;
|
|
1135
|
+
const signature = createHmac("sha256", hmac(hmac(hmac(hmac(`AWS4${config.secretKey.trim()}`, dateStamp), config.region.trim() || "us-east-1"), "s3"), "aws4_request")).update(stringToSign, "utf8").digest("hex");
|
|
1136
|
+
const authorization = `AWS4-HMAC-SHA256 Credential=${config.accessKey.trim()}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
1137
|
+
const started = Date.now();
|
|
1138
|
+
const response = await fetch(url, {
|
|
1139
|
+
method: "PUT",
|
|
1140
|
+
headers: {
|
|
1141
|
+
"content-type": contentType,
|
|
1142
|
+
"x-amz-content-sha256": payloadHash,
|
|
1143
|
+
"x-amz-date": amzDate,
|
|
1144
|
+
authorization
|
|
1145
|
+
},
|
|
1146
|
+
body: new Uint8Array(data)
|
|
1147
|
+
});
|
|
1148
|
+
if (!response.ok) {
|
|
1149
|
+
const text = await response.text().catch(() => "");
|
|
1150
|
+
throw new Error(`对象存储拒绝上传(HTTP ${response.status})${text !== "" ? `:${text.slice(0, 200)}` : ""}`);
|
|
1151
|
+
}
|
|
1152
|
+
return { ms: Date.now() - started };
|
|
1153
|
+
}
|
|
1154
|
+
/** Upload a small probe object; used by the settings card's test button. */
|
|
1155
|
+
async function testStorage(config) {
|
|
1156
|
+
const key = `${config.prefix.trim() || "dsh-imagegen"}/ping.txt`;
|
|
1157
|
+
const { ms } = await putObject(config, key, Buffer.from("dsh-imagegen storage ok", "utf8"), "text/plain");
|
|
1158
|
+
return {
|
|
1159
|
+
ms,
|
|
1160
|
+
key
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
//#endregion
|
|
800
1164
|
//#region src/history-store.ts
|
|
801
1165
|
/**
|
|
802
1166
|
* Host-persisted generation history: images are stored as individual files
|
|
@@ -817,7 +1181,7 @@ function mutateHistory(operation) {
|
|
|
817
1181
|
return next;
|
|
818
1182
|
}
|
|
819
1183
|
/** File extension for a MIME type (image file names). */
|
|
820
|
-
function extensionOf$
|
|
1184
|
+
function extensionOf$2(mime) {
|
|
821
1185
|
switch (mime.split(";")[0].trim()) {
|
|
822
1186
|
case "image/jpeg": return "jpg";
|
|
823
1187
|
case "image/webp": return "webp";
|
|
@@ -836,7 +1200,7 @@ function mimeOfFile$2(file) {
|
|
|
836
1200
|
}
|
|
837
1201
|
}
|
|
838
1202
|
/** Sanitize an entry id for use as a file-name prefix. */
|
|
839
|
-
function safeId$
|
|
1203
|
+
function safeId$2(id) {
|
|
840
1204
|
const cleaned = id.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
841
1205
|
return cleaned === "" ? "entry" : cleaned;
|
|
842
1206
|
}
|
|
@@ -907,7 +1271,8 @@ function toWire$1(entry) {
|
|
|
907
1271
|
...entry.projectId === void 0 ? {} : { projectId: entry.projectId },
|
|
908
1272
|
...entry.projectName === void 0 ? {} : { projectName: entry.projectName },
|
|
909
1273
|
...entry.slotKey === void 0 ? {} : { slotKey: entry.slotKey },
|
|
910
|
-
...entry.slotLabel === void 0 ? {} : { slotLabel: entry.slotLabel }
|
|
1274
|
+
...entry.slotLabel === void 0 ? {} : { slotLabel: entry.slotLabel },
|
|
1275
|
+
...entry.canvas === void 0 ? {} : { canvas: entry.canvas }
|
|
911
1276
|
};
|
|
912
1277
|
}
|
|
913
1278
|
/** List the persisted history, newest first, as wire entries. */
|
|
@@ -918,13 +1283,14 @@ async function listHistory() {
|
|
|
918
1283
|
async function appendHistory(input) {
|
|
919
1284
|
return mutateHistory(async () => {
|
|
920
1285
|
await ensureDirs$1();
|
|
921
|
-
const prefix = safeId$
|
|
1286
|
+
const prefix = safeId$2(input.id);
|
|
922
1287
|
const storedImages = [];
|
|
923
1288
|
try {
|
|
924
1289
|
for (let index = 0; index < input.images.length; index++) {
|
|
925
1290
|
const image = input.images[index];
|
|
926
|
-
const file = `${prefix}-${index}.${extensionOf$
|
|
1291
|
+
const file = `${prefix}-${index}.${extensionOf$2(image.mime)}`;
|
|
927
1292
|
await promises.writeFile(path.join(IMAGES_DIR$1, file), Buffer.from(image.b64, "base64"));
|
|
1293
|
+
notifyImageSaved("history", path.join(IMAGES_DIR$1, file));
|
|
928
1294
|
storedImages.push({
|
|
929
1295
|
file,
|
|
930
1296
|
mime: image.mime,
|
|
@@ -955,7 +1321,8 @@ async function appendHistory(input) {
|
|
|
955
1321
|
...input.projectId === void 0 ? {} : { projectId: input.projectId },
|
|
956
1322
|
...input.projectName === void 0 ? {} : { projectName: input.projectName },
|
|
957
1323
|
...input.slotKey === void 0 ? {} : { slotKey: input.slotKey },
|
|
958
|
-
...input.slotLabel === void 0 ? {} : { slotLabel: input.slotLabel }
|
|
1324
|
+
...input.slotLabel === void 0 ? {} : { slotLabel: input.slotLabel },
|
|
1325
|
+
...input.canvas === void 0 ? {} : { canvas: input.canvas }
|
|
959
1326
|
}, ...await readIndex$1()];
|
|
960
1327
|
const kept = merged.slice(0, 50);
|
|
961
1328
|
for (const dropped of merged.slice(50)) await removeEntryFiles$1(dropped);
|
|
@@ -1148,7 +1515,8 @@ var ImageGenerationRuntime = class {
|
|
|
1148
1515
|
...request.projectId === void 0 ? {} : { projectId: request.projectId },
|
|
1149
1516
|
...request.projectName === void 0 ? {} : { projectName: request.projectName },
|
|
1150
1517
|
...request.slotKey === void 0 ? {} : { slotKey: request.slotKey },
|
|
1151
|
-
...request.slotLabel === void 0 ? {} : { slotLabel: request.slotLabel }
|
|
1518
|
+
...request.slotLabel === void 0 ? {} : { slotLabel: request.slotLabel },
|
|
1519
|
+
...request.canvas === void 0 ? {} : { canvas: request.canvas }
|
|
1152
1520
|
});
|
|
1153
1521
|
return {
|
|
1154
1522
|
...result,
|
|
@@ -1184,7 +1552,7 @@ function mutateGallery(operation) {
|
|
|
1184
1552
|
return next;
|
|
1185
1553
|
}
|
|
1186
1554
|
/** File extension for a MIME type (image file names). */
|
|
1187
|
-
function extensionOf(mime) {
|
|
1555
|
+
function extensionOf$1(mime) {
|
|
1188
1556
|
switch (mime.split(";")[0].trim()) {
|
|
1189
1557
|
case "image/jpeg": return "jpg";
|
|
1190
1558
|
case "image/webp": return "webp";
|
|
@@ -1203,7 +1571,7 @@ function mimeOfFile$1(file) {
|
|
|
1203
1571
|
}
|
|
1204
1572
|
}
|
|
1205
1573
|
/** Sanitize an entry id for use as a file-name prefix. */
|
|
1206
|
-
function safeId(id) {
|
|
1574
|
+
function safeId$1(id) {
|
|
1207
1575
|
const cleaned = id.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
1208
1576
|
return cleaned === "" ? "entry" : cleaned;
|
|
1209
1577
|
}
|
|
@@ -1279,7 +1647,8 @@ function toWire(entry) {
|
|
|
1279
1647
|
...entry.projectId === void 0 ? {} : { projectId: entry.projectId },
|
|
1280
1648
|
...entry.projectName === void 0 ? {} : { projectName: entry.projectName },
|
|
1281
1649
|
...entry.slotKey === void 0 ? {} : { slotKey: entry.slotKey },
|
|
1282
|
-
...entry.slotLabel === void 0 ? {} : { slotLabel: entry.slotLabel }
|
|
1650
|
+
...entry.slotLabel === void 0 ? {} : { slotLabel: entry.slotLabel },
|
|
1651
|
+
...entry.canvas === void 0 ? {} : { canvas: entry.canvas }
|
|
1283
1652
|
};
|
|
1284
1653
|
}
|
|
1285
1654
|
/** List the persisted gallery, newest first, as wire entries. */
|
|
@@ -1300,13 +1669,14 @@ async function appendGallery(input) {
|
|
|
1300
1669
|
added: false
|
|
1301
1670
|
};
|
|
1302
1671
|
}
|
|
1303
|
-
const prefix = safeId(input.id);
|
|
1672
|
+
const prefix = safeId$1(input.id);
|
|
1304
1673
|
const storedImages = [];
|
|
1305
1674
|
try {
|
|
1306
1675
|
for (let index = 0; index < input.images.length; index++) {
|
|
1307
1676
|
const image = input.images[index];
|
|
1308
|
-
const file = `${prefix}-${index}.${extensionOf(image.mime)}`;
|
|
1677
|
+
const file = `${prefix}-${index}.${extensionOf$1(image.mime)}`;
|
|
1309
1678
|
await promises.writeFile(path.join(IMAGES_DIR, file), Buffer.from(image.b64, "base64"));
|
|
1679
|
+
notifyImageSaved("gallery", path.join(IMAGES_DIR, file));
|
|
1310
1680
|
storedImages.push({
|
|
1311
1681
|
file,
|
|
1312
1682
|
mime: image.mime,
|
|
@@ -1336,7 +1706,8 @@ async function appendGallery(input) {
|
|
|
1336
1706
|
...input.projectId === void 0 ? {} : { projectId: input.projectId },
|
|
1337
1707
|
...input.projectName === void 0 ? {} : { projectName: input.projectName },
|
|
1338
1708
|
...input.slotKey === void 0 ? {} : { slotKey: input.slotKey },
|
|
1339
|
-
...input.slotLabel === void 0 ? {} : { slotLabel: input.slotLabel }
|
|
1709
|
+
...input.slotLabel === void 0 ? {} : { slotLabel: input.slotLabel },
|
|
1710
|
+
...input.canvas === void 0 ? {} : { canvas: input.canvas }
|
|
1340
1711
|
}, ...await readIndex()];
|
|
1341
1712
|
await writeIndex(merged);
|
|
1342
1713
|
return {
|
|
@@ -1389,6 +1760,349 @@ async function readGalleryImage(file) {
|
|
|
1389
1760
|
}
|
|
1390
1761
|
}
|
|
1391
1762
|
//#endregion
|
|
1763
|
+
//#region src/canvas-store.ts
|
|
1764
|
+
/** Host-persisted infinite canvas documents and content-addressed assets. */
|
|
1765
|
+
const DATA_ROOT = process.env.DSH_HOME?.trim() || path.join(homedir(), ".dsh");
|
|
1766
|
+
const CANVAS_ROOT = path.join(DATA_ROOT, "dsh-imagegen", "canvas");
|
|
1767
|
+
path.join(CANVAS_ROOT, "pages");
|
|
1768
|
+
path.join(CANVAS_ROOT, "assets");
|
|
1769
|
+
path.join(CANVAS_ROOT, "index.json");
|
|
1770
|
+
var CanvasConflictError = class extends Error {
|
|
1771
|
+
code = "canvas-conflict";
|
|
1772
|
+
constructor(message = "画布已在其他窗口更新,请重新加载后再保存。") {
|
|
1773
|
+
super(message);
|
|
1774
|
+
this.name = "CanvasConflictError";
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
function extensionOf(mime) {
|
|
1778
|
+
switch (mime.split(";")[0].trim().toLowerCase()) {
|
|
1779
|
+
case "image/jpeg": return "jpg";
|
|
1780
|
+
case "image/webp": return "webp";
|
|
1781
|
+
case "image/gif": return "gif";
|
|
1782
|
+
default: return "png";
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function mimeOf(file) {
|
|
1786
|
+
switch (path.extname(file).toLowerCase()) {
|
|
1787
|
+
case ".jpg":
|
|
1788
|
+
case ".jpeg": return "image/jpeg";
|
|
1789
|
+
case ".webp": return "image/webp";
|
|
1790
|
+
case ".gif": return "image/gif";
|
|
1791
|
+
default: return "image/png";
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
function safeId(value) {
|
|
1795
|
+
const id = value.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
1796
|
+
return id === "" ? randomUUID() : id;
|
|
1797
|
+
}
|
|
1798
|
+
async function writeJsonAtomic(file, value) {
|
|
1799
|
+
await promises.mkdir(path.dirname(file), { recursive: true });
|
|
1800
|
+
const temp = `${file}.tmp-${process.pid}-${randomUUID()}`;
|
|
1801
|
+
await promises.writeFile(temp, `${JSON.stringify(value)}\n`, "utf8");
|
|
1802
|
+
await promises.rename(temp, file);
|
|
1803
|
+
}
|
|
1804
|
+
async function readJson(file) {
|
|
1805
|
+
try {
|
|
1806
|
+
return JSON.parse(await promises.readFile(file, "utf8"));
|
|
1807
|
+
} catch {
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
function defaultDocument(id, title) {
|
|
1812
|
+
const now = Date.now();
|
|
1813
|
+
return {
|
|
1814
|
+
version: 2,
|
|
1815
|
+
id,
|
|
1816
|
+
title,
|
|
1817
|
+
revision: 1,
|
|
1818
|
+
viewport: {
|
|
1819
|
+
x: 0,
|
|
1820
|
+
y: 0,
|
|
1821
|
+
k: 1
|
|
1822
|
+
},
|
|
1823
|
+
background: "dots",
|
|
1824
|
+
nodes: [],
|
|
1825
|
+
connections: [],
|
|
1826
|
+
createdAt: now,
|
|
1827
|
+
updatedAt: now
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
function isAssetRef(value) {
|
|
1831
|
+
if (value === null || typeof value !== "object") return false;
|
|
1832
|
+
const asset = value;
|
|
1833
|
+
return typeof asset.assetId === "string" && typeof asset.url === "string" && typeof asset.mime === "string" && typeof asset.width === "number" && typeof asset.height === "number";
|
|
1834
|
+
}
|
|
1835
|
+
function isNode(value) {
|
|
1836
|
+
if (value === null || typeof value !== "object") return false;
|
|
1837
|
+
const node = value;
|
|
1838
|
+
if (typeof node.id !== "string" || typeof node.title !== "string" || typeof node.x !== "number" || typeof node.y !== "number" || typeof node.width !== "number" || typeof node.height !== "number") return false;
|
|
1839
|
+
if (node.type !== "image" && node.type !== "text" && node.type !== "config") return false;
|
|
1840
|
+
const metadata = node.metadata;
|
|
1841
|
+
if (metadata !== void 0 && (metadata === null || typeof metadata !== "object")) return false;
|
|
1842
|
+
const state = metadata ?? {};
|
|
1843
|
+
if (node.type === "image") {
|
|
1844
|
+
if (state.asset !== void 0 && !isAssetRef(state.asset)) return false;
|
|
1845
|
+
return state.status === void 0 || state.status === "idle" || state.status === "generating" || state.status === "success" || state.status === "error";
|
|
1846
|
+
}
|
|
1847
|
+
if (node.type === "config") return state.prompt === void 0 || typeof state.prompt === "string";
|
|
1848
|
+
return state.text === void 0 || typeof state.text === "string";
|
|
1849
|
+
}
|
|
1850
|
+
function isDocument(value) {
|
|
1851
|
+
if (value === null || typeof value !== "object") return false;
|
|
1852
|
+
const document = value;
|
|
1853
|
+
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);
|
|
1854
|
+
}
|
|
1855
|
+
/** Upgrade a v1 (image/text/annotation + edges) document to the v2 node-graph model.
|
|
1856
|
+
* Images and text notes keep their geometry; annotation prompt cards become plain
|
|
1857
|
+
* text notes carrying the prompt, and old edges survive only between surviving nodes. */
|
|
1858
|
+
function migrateLegacyDocument(input) {
|
|
1859
|
+
const now = Date.now();
|
|
1860
|
+
const legacyNodes = Array.isArray(input.nodes) ? input.nodes : [];
|
|
1861
|
+
const nodes = [];
|
|
1862
|
+
const annotationIds = /* @__PURE__ */ new Set();
|
|
1863
|
+
for (const raw of legacyNodes) {
|
|
1864
|
+
if (raw === null || typeof raw !== "object") continue;
|
|
1865
|
+
const node = raw;
|
|
1866
|
+
if (typeof node.id !== "string" || typeof node.x !== "number" || typeof node.y !== "number") continue;
|
|
1867
|
+
const base = {
|
|
1868
|
+
id: node.id,
|
|
1869
|
+
title: typeof node.title === "string" ? node.title : "未命名节点",
|
|
1870
|
+
x: node.x,
|
|
1871
|
+
y: node.y,
|
|
1872
|
+
width: typeof node.width === "number" ? node.width : 300,
|
|
1873
|
+
height: typeof node.height === "number" ? node.height : 220
|
|
1874
|
+
};
|
|
1875
|
+
if (node.type === "image" && isAssetRef(node.asset)) {
|
|
1876
|
+
const generation = node.generation ?? {};
|
|
1877
|
+
nodes.push({
|
|
1878
|
+
...base,
|
|
1879
|
+
type: "image",
|
|
1880
|
+
metadata: {
|
|
1881
|
+
asset: node.asset,
|
|
1882
|
+
status: typeof node.status === "string" && [
|
|
1883
|
+
"idle",
|
|
1884
|
+
"generating",
|
|
1885
|
+
"success",
|
|
1886
|
+
"error"
|
|
1887
|
+
].includes(node.status) ? node.status : "success",
|
|
1888
|
+
...typeof node.error === "string" ? { error: node.error } : {},
|
|
1889
|
+
...typeof generation.prompt === "string" ? { prompt: generation.prompt } : {},
|
|
1890
|
+
...typeof generation.model === "string" ? { model: generation.model } : {},
|
|
1891
|
+
...typeof generation.taskId === "string" ? { taskId: generation.taskId } : {},
|
|
1892
|
+
...typeof generation.sourceNodeId === "string" ? { sourceNodeId: generation.sourceNodeId } : {}
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
} else if (node.type === "text") nodes.push({
|
|
1896
|
+
...base,
|
|
1897
|
+
type: "text",
|
|
1898
|
+
metadata: {
|
|
1899
|
+
text: typeof node.text === "string" ? node.text : "",
|
|
1900
|
+
...typeof node.fontSize === "number" ? { fontSize: node.fontSize } : {}
|
|
1901
|
+
}
|
|
1902
|
+
});
|
|
1903
|
+
else if (node.type === "annotation") {
|
|
1904
|
+
annotationIds.add(node.id);
|
|
1905
|
+
const prompt = typeof node.prompt === "string" && node.prompt.trim() !== "" ? node.prompt : "(旧版标注,提示词见此)";
|
|
1906
|
+
nodes.push({
|
|
1907
|
+
...base,
|
|
1908
|
+
type: "text",
|
|
1909
|
+
title: "旧版标注",
|
|
1910
|
+
metadata: { text: prompt }
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
nodes.sort((a, b) => {
|
|
1915
|
+
const za = legacyNodes.find((item) => item?.id === a.id)?.zIndex;
|
|
1916
|
+
const zb = legacyNodes.find((item) => item?.id === b.id)?.zIndex;
|
|
1917
|
+
return (typeof za === "number" ? za : 0) - (typeof zb === "number" ? zb : 0);
|
|
1918
|
+
});
|
|
1919
|
+
const validIds = new Set(nodes.map((node) => node.id));
|
|
1920
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1921
|
+
const connections = (Array.isArray(input.edges) ? input.edges : []).flatMap((raw) => {
|
|
1922
|
+
if (raw === null || typeof raw !== "object") return [];
|
|
1923
|
+
const edge = raw;
|
|
1924
|
+
if (typeof edge.fromNodeId !== "string" || typeof edge.toNodeId !== "string") return [];
|
|
1925
|
+
if (annotationIds.has(edge.fromNodeId) || annotationIds.has(edge.toNodeId)) return [];
|
|
1926
|
+
if (!validIds.has(edge.fromNodeId) || !validIds.has(edge.toNodeId) || edge.fromNodeId === edge.toNodeId) return [];
|
|
1927
|
+
const key = `${edge.fromNodeId}->${edge.toNodeId}`;
|
|
1928
|
+
if (seen.has(key)) return [];
|
|
1929
|
+
seen.add(key);
|
|
1930
|
+
return [{
|
|
1931
|
+
id: typeof edge.id === "string" ? edge.id : `edge-${randomUUID()}`,
|
|
1932
|
+
fromNodeId: edge.fromNodeId,
|
|
1933
|
+
toNodeId: edge.toNodeId
|
|
1934
|
+
}];
|
|
1935
|
+
});
|
|
1936
|
+
const legacyViewport = input.viewport ?? {};
|
|
1937
|
+
const background = input.background === "grid" ? "lines" : input.background === "blank" ? "blank" : "dots";
|
|
1938
|
+
return {
|
|
1939
|
+
version: 2,
|
|
1940
|
+
id: typeof input.id === "string" ? input.id : randomUUID(),
|
|
1941
|
+
title: typeof input.title === "string" ? input.title : "未命名画布",
|
|
1942
|
+
revision: typeof input.revision === "number" ? input.revision : 1,
|
|
1943
|
+
viewport: {
|
|
1944
|
+
x: typeof legacyViewport.x === "number" ? legacyViewport.x : 0,
|
|
1945
|
+
y: typeof legacyViewport.y === "number" ? legacyViewport.y : 0,
|
|
1946
|
+
k: typeof legacyViewport.scale === "number" && legacyViewport.scale > 0 ? legacyViewport.scale : 1
|
|
1947
|
+
},
|
|
1948
|
+
background,
|
|
1949
|
+
nodes,
|
|
1950
|
+
connections,
|
|
1951
|
+
createdAt: typeof input.createdAt === "number" ? input.createdAt : now,
|
|
1952
|
+
updatedAt: typeof input.updatedAt === "number" ? input.updatedAt : now
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
/** Accept either the v2 document or a legacy v1 payload and return v2. */
|
|
1956
|
+
function coerceDocument(value) {
|
|
1957
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
1958
|
+
const document = value;
|
|
1959
|
+
if (document.version === 1) {
|
|
1960
|
+
const migrated = migrateLegacyDocument(document);
|
|
1961
|
+
return isDocument(migrated) ? migrated : void 0;
|
|
1962
|
+
}
|
|
1963
|
+
return isDocument(value) ? value : void 0;
|
|
1964
|
+
}
|
|
1965
|
+
let mutation = Promise.resolve();
|
|
1966
|
+
function serialize(operation) {
|
|
1967
|
+
const next = mutation.then(operation, operation);
|
|
1968
|
+
mutation = next.then(() => void 0, () => void 0);
|
|
1969
|
+
return next;
|
|
1970
|
+
}
|
|
1971
|
+
var CanvasStore = class {
|
|
1972
|
+
root;
|
|
1973
|
+
constructor(root = CANVAS_ROOT) {
|
|
1974
|
+
this.root = root;
|
|
1975
|
+
}
|
|
1976
|
+
pagesDir() {
|
|
1977
|
+
return path.join(this.root, "pages");
|
|
1978
|
+
}
|
|
1979
|
+
assetsDir() {
|
|
1980
|
+
return path.join(this.root, "assets");
|
|
1981
|
+
}
|
|
1982
|
+
indexPath() {
|
|
1983
|
+
return path.join(this.root, "index.json");
|
|
1984
|
+
}
|
|
1985
|
+
async ensure() {
|
|
1986
|
+
await promises.mkdir(this.pagesDir(), { recursive: true });
|
|
1987
|
+
await promises.mkdir(this.assetsDir(), { recursive: true });
|
|
1988
|
+
}
|
|
1989
|
+
pagePath(id) {
|
|
1990
|
+
return path.join(this.pagesDir(), `${safeId(id)}.json`);
|
|
1991
|
+
}
|
|
1992
|
+
assetPath(id) {
|
|
1993
|
+
if (!/^[a-f0-9]{64}\.(png|jpg|jpeg|webp|gif)$/.test(id)) return void 0;
|
|
1994
|
+
const target = path.join(this.assetsDir(), id);
|
|
1995
|
+
const relative = path.relative(this.assetsDir(), target);
|
|
1996
|
+
return relative.startsWith("..") || path.isAbsolute(relative) ? void 0 : target;
|
|
1997
|
+
}
|
|
1998
|
+
async readIndex() {
|
|
1999
|
+
const value = await readJson(this.indexPath());
|
|
2000
|
+
if (value === void 0 || typeof value !== "object" || !Array.isArray(value.projects)) return [];
|
|
2001
|
+
return value.projects.filter((item) => {
|
|
2002
|
+
if (item === null || typeof item !== "object") return false;
|
|
2003
|
+
const project = item;
|
|
2004
|
+
return typeof project.id === "string" && typeof project.title === "string" && typeof project.revision === "number" && typeof project.nodeCount === "number" && typeof project.createdAt === "number" && typeof project.updatedAt === "number";
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
async writeIndex(projects) {
|
|
2008
|
+
await this.ensure();
|
|
2009
|
+
await writeJsonAtomic(this.indexPath(), { projects });
|
|
2010
|
+
}
|
|
2011
|
+
async list() {
|
|
2012
|
+
return this.readIndex();
|
|
2013
|
+
}
|
|
2014
|
+
async create(title = "未命名画布") {
|
|
2015
|
+
return serialize(async () => {
|
|
2016
|
+
await this.ensure();
|
|
2017
|
+
const id = randomUUID();
|
|
2018
|
+
const document = defaultDocument(id, title.trim() || "未命名画布");
|
|
2019
|
+
await writeJsonAtomic(this.pagePath(id), document);
|
|
2020
|
+
const projects = await this.readIndex();
|
|
2021
|
+
await this.writeIndex([this.summaryOf(document), ...projects]);
|
|
2022
|
+
return document;
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
async read(id) {
|
|
2026
|
+
return coerceDocument(await readJson(this.pagePath(id)));
|
|
2027
|
+
}
|
|
2028
|
+
async save(document, expectedRevision) {
|
|
2029
|
+
return serialize(async () => {
|
|
2030
|
+
const incoming = coerceDocument(document);
|
|
2031
|
+
if (incoming === void 0) throw new Error("malformed canvas document");
|
|
2032
|
+
const current = await this.read(incoming.id);
|
|
2033
|
+
if (current !== void 0 && expectedRevision !== void 0 && current.revision !== expectedRevision) throw new CanvasConflictError();
|
|
2034
|
+
const next = {
|
|
2035
|
+
...incoming,
|
|
2036
|
+
revision: Math.max(current?.revision ?? 0, incoming.revision) + 1,
|
|
2037
|
+
updatedAt: Date.now()
|
|
2038
|
+
};
|
|
2039
|
+
await this.ensure();
|
|
2040
|
+
await writeJsonAtomic(this.pagePath(next.id), next);
|
|
2041
|
+
const projects = (await this.readIndex()).filter((item) => item.id !== next.id);
|
|
2042
|
+
await this.writeIndex([this.summaryOf(next), ...projects]);
|
|
2043
|
+
return next;
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
async remove(id) {
|
|
2047
|
+
return serialize(async () => {
|
|
2048
|
+
try {
|
|
2049
|
+
await promises.rm(this.pagePath(id), { force: true });
|
|
2050
|
+
} catch {}
|
|
2051
|
+
const projects = (await this.readIndex()).filter((item) => item.id !== id);
|
|
2052
|
+
await this.writeIndex(projects);
|
|
2053
|
+
return projects;
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
async putImage(input) {
|
|
2057
|
+
if (!input.data.byteLength) throw new Error("image data is empty");
|
|
2058
|
+
if (!/^image\/(png|jpeg|webp|gif)$/.test(input.mime)) throw new Error("unsupported image type");
|
|
2059
|
+
if (!Number.isSafeInteger(input.width) || input.width < 1 || !Number.isSafeInteger(input.height) || input.height < 1) throw new Error("image dimensions are invalid");
|
|
2060
|
+
await this.ensure();
|
|
2061
|
+
const file = `${createHash("sha256").update(input.data).digest("hex")}.${extensionOf(input.mime)}`;
|
|
2062
|
+
const target = path.join(this.assetsDir(), file);
|
|
2063
|
+
try {
|
|
2064
|
+
await promises.access(target);
|
|
2065
|
+
} catch {
|
|
2066
|
+
await promises.writeFile(target, input.data);
|
|
2067
|
+
}
|
|
2068
|
+
return {
|
|
2069
|
+
assetId: file,
|
|
2070
|
+
url: `/api/dsh-imagegen/canvas/asset/${file}`,
|
|
2071
|
+
mime: input.mime,
|
|
2072
|
+
bytes: input.data.byteLength,
|
|
2073
|
+
width: input.width,
|
|
2074
|
+
height: input.height,
|
|
2075
|
+
origin: input.origin,
|
|
2076
|
+
...input.originId === void 0 ? {} : { originId: input.originId },
|
|
2077
|
+
...input.entryId === void 0 ? {} : { entryId: input.entryId },
|
|
2078
|
+
...input.imageIndex === void 0 ? {} : { imageIndex: input.imageIndex }
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
async readAsset(file) {
|
|
2082
|
+
const target = this.assetPath(file);
|
|
2083
|
+
if (target === void 0) return void 0;
|
|
2084
|
+
try {
|
|
2085
|
+
return {
|
|
2086
|
+
data: await promises.readFile(target),
|
|
2087
|
+
mime: mimeOf(file)
|
|
2088
|
+
};
|
|
2089
|
+
} catch {
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
summaryOf(document) {
|
|
2094
|
+
return {
|
|
2095
|
+
id: document.id,
|
|
2096
|
+
title: document.title,
|
|
2097
|
+
revision: document.revision,
|
|
2098
|
+
nodeCount: document.nodes.length,
|
|
2099
|
+
createdAt: document.createdAt,
|
|
2100
|
+
updatedAt: document.updatedAt
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
const canvasStore = new CanvasStore();
|
|
2105
|
+
//#endregion
|
|
1392
2106
|
//#region src/templates-store.ts
|
|
1393
2107
|
/**
|
|
1394
2108
|
* Prompt-template library store (multi-source).
|
|
@@ -2001,6 +2715,42 @@ const IMAGE_PRESETS = [
|
|
|
2001
2715
|
id: "glm-image"
|
|
2002
2716
|
}]
|
|
2003
2717
|
},
|
|
2718
|
+
{
|
|
2719
|
+
id: "aliyun-dashscope-qwen",
|
|
2720
|
+
name: "阿里云百炼(Qwen-Image)",
|
|
2721
|
+
apiUrl: "https://dashscope.aliyuncs.com/api/v1",
|
|
2722
|
+
hint: "阿里云百炼 DashScope 原生接口:通义千问 Qwen-Image 系列(该渠道不可复用于提示词增强)",
|
|
2723
|
+
models: [
|
|
2724
|
+
{
|
|
2725
|
+
alias: "qwen-image-3.0-pro",
|
|
2726
|
+
id: "qwen-image-3.0-pro"
|
|
2727
|
+
},
|
|
2728
|
+
{
|
|
2729
|
+
alias: "qwen-image-3.0",
|
|
2730
|
+
id: "qwen-image-3.0"
|
|
2731
|
+
},
|
|
2732
|
+
{
|
|
2733
|
+
alias: "qwen-image-2.0-pro",
|
|
2734
|
+
id: "qwen-image-2.0-pro"
|
|
2735
|
+
},
|
|
2736
|
+
{
|
|
2737
|
+
alias: "qwen-image-2.0",
|
|
2738
|
+
id: "qwen-image-2.0"
|
|
2739
|
+
},
|
|
2740
|
+
{
|
|
2741
|
+
alias: "qwen-image-max",
|
|
2742
|
+
id: "qwen-image-max"
|
|
2743
|
+
},
|
|
2744
|
+
{
|
|
2745
|
+
alias: "qwen-image-plus",
|
|
2746
|
+
id: "qwen-image-plus"
|
|
2747
|
+
},
|
|
2748
|
+
{
|
|
2749
|
+
alias: "qwen-image",
|
|
2750
|
+
id: "qwen-image"
|
|
2751
|
+
}
|
|
2752
|
+
]
|
|
2753
|
+
},
|
|
2004
2754
|
{
|
|
2005
2755
|
id: "xai-grok",
|
|
2006
2756
|
name: "xAI(Grok)",
|
|
@@ -2084,6 +2834,7 @@ function parseGenerateRequest(body) {
|
|
|
2084
2834
|
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
2085
2835
|
if (prompt === "") return void 0;
|
|
2086
2836
|
const comparisonModels = Array.isArray(body.comparisonModels) ? [...new Set(body.comparisonModels.filter((model) => typeof model === "string").map((model) => model.trim()).filter(Boolean))] : [];
|
|
2837
|
+
const canvas = parseCanvasMeta(body.canvas);
|
|
2087
2838
|
return {
|
|
2088
2839
|
mode: body.mode === "edit" ? "edit" : "text",
|
|
2089
2840
|
model: typeof body.model === "string" ? body.model : "",
|
|
@@ -2097,6 +2848,7 @@ function parseGenerateRequest(body) {
|
|
|
2097
2848
|
...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {},
|
|
2098
2849
|
...typeof body.comparisonId === "string" && body.comparisonId !== "" ? { comparisonId: body.comparisonId } : {},
|
|
2099
2850
|
...comparisonModels.length > 1 ? { comparisonModels } : {},
|
|
2851
|
+
...canvas === void 0 ? {} : { canvas },
|
|
2100
2852
|
...body.workflow === "ecommerce" ? { workflow: "ecommerce" } : {},
|
|
2101
2853
|
...typeof body.projectId === "string" && body.projectId !== "" ? { projectId: body.projectId } : {},
|
|
2102
2854
|
...typeof body.projectName === "string" && body.projectName !== "" ? { projectName: body.projectName } : {},
|
|
@@ -2104,6 +2856,18 @@ function parseGenerateRequest(body) {
|
|
|
2104
2856
|
...typeof body.slotLabel === "string" && body.slotLabel !== "" ? { slotLabel: body.slotLabel } : {}
|
|
2105
2857
|
};
|
|
2106
2858
|
}
|
|
2859
|
+
function parseCanvasMeta(value) {
|
|
2860
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
2861
|
+
const raw = value;
|
|
2862
|
+
if (typeof raw.canvasId !== "string" || raw.canvasId.trim() === "") return void 0;
|
|
2863
|
+
return {
|
|
2864
|
+
canvasId: raw.canvasId.trim(),
|
|
2865
|
+
...typeof raw.sourceNodeId === "string" && raw.sourceNodeId.trim() !== "" ? { sourceNodeId: raw.sourceNodeId.trim() } : {},
|
|
2866
|
+
...typeof raw.annotationNodeId === "string" && raw.annotationNodeId.trim() !== "" ? { annotationNodeId: raw.annotationNodeId.trim() } : {},
|
|
2867
|
+
...typeof raw.parentNodeId === "string" && raw.parentNodeId.trim() !== "" ? { parentNodeId: raw.parentNodeId.trim() } : {},
|
|
2868
|
+
...raw.placement === "right" || raw.placement === "below" ? { placement: raw.placement } : {}
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2107
2871
|
/** Validate a submitted history entry (images carry base64). */
|
|
2108
2872
|
function parseHistoryEntryInput(body) {
|
|
2109
2873
|
const raw = body.entry;
|
|
@@ -2127,6 +2891,7 @@ function parseHistoryEntryInput(body) {
|
|
|
2127
2891
|
});
|
|
2128
2892
|
}
|
|
2129
2893
|
const comparisonModels = Array.isArray(entry.comparisonModels) ? [...new Set(entry.comparisonModels.filter((model) => typeof model === "string").map((model) => model.trim()).filter(Boolean))] : [];
|
|
2894
|
+
const canvas = parseCanvasMeta(entry.canvas);
|
|
2130
2895
|
return {
|
|
2131
2896
|
id: entry.id,
|
|
2132
2897
|
createdAt: entry.createdAt,
|
|
@@ -2143,6 +2908,7 @@ function parseHistoryEntryInput(body) {
|
|
|
2143
2908
|
...typeof entry.channel === "string" ? { channel: entry.channel } : {},
|
|
2144
2909
|
...typeof entry.comparisonId === "string" ? { comparisonId: entry.comparisonId } : {},
|
|
2145
2910
|
...comparisonModels.length > 1 ? { comparisonModels } : {},
|
|
2911
|
+
...canvas === void 0 ? {} : { canvas },
|
|
2146
2912
|
...entry.workflow === "ecommerce" ? { workflow: "ecommerce" } : {},
|
|
2147
2913
|
...typeof entry.projectId === "string" ? { projectId: entry.projectId } : {},
|
|
2148
2914
|
...typeof entry.projectName === "string" ? { projectName: entry.projectName } : {},
|
|
@@ -2247,6 +3013,7 @@ function makeRoutes(deps) {
|
|
|
2247
3013
|
updateTags: updateGalleryTags,
|
|
2248
3014
|
readImage: readGalleryImage
|
|
2249
3015
|
};
|
|
3016
|
+
const canvas = deps.canvas ?? canvasStore;
|
|
2250
3017
|
const templates = deps.templates ?? {
|
|
2251
3018
|
list: listTemplates,
|
|
2252
3019
|
refresh: refreshTemplates,
|
|
@@ -3086,6 +3853,253 @@ function makeRoutes(deps) {
|
|
|
3086
3853
|
res.end(found.data);
|
|
3087
3854
|
}
|
|
3088
3855
|
},
|
|
3856
|
+
{
|
|
3857
|
+
kind: "exact",
|
|
3858
|
+
path: CANVAS_API.list,
|
|
3859
|
+
handler: async (req, res) => {
|
|
3860
|
+
if (!guard(req, res, "POST")) return;
|
|
3861
|
+
try {
|
|
3862
|
+
writeJson(res, 200, {
|
|
3863
|
+
ok: true,
|
|
3864
|
+
projects: await canvas.list()
|
|
3865
|
+
});
|
|
3866
|
+
} catch (error) {
|
|
3867
|
+
writeJson(res, 200, {
|
|
3868
|
+
ok: false,
|
|
3869
|
+
code: "canvas-failed",
|
|
3870
|
+
message: messageOf(error)
|
|
3871
|
+
});
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
},
|
|
3875
|
+
{
|
|
3876
|
+
kind: "exact",
|
|
3877
|
+
path: CANVAS_API.create,
|
|
3878
|
+
handler: async (req, res) => {
|
|
3879
|
+
if (!guard(req, res, "POST")) return;
|
|
3880
|
+
const body = await readJsonBody(req);
|
|
3881
|
+
const title = typeof body?.title === "string" ? body.title : "未命名画布";
|
|
3882
|
+
try {
|
|
3883
|
+
writeJson(res, 200, {
|
|
3884
|
+
ok: true,
|
|
3885
|
+
document: await canvas.create(title)
|
|
3886
|
+
});
|
|
3887
|
+
} catch (error) {
|
|
3888
|
+
writeJson(res, 200, {
|
|
3889
|
+
ok: false,
|
|
3890
|
+
code: "canvas-failed",
|
|
3891
|
+
message: messageOf(error)
|
|
3892
|
+
});
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
},
|
|
3896
|
+
{
|
|
3897
|
+
kind: "exact",
|
|
3898
|
+
path: CANVAS_API.read,
|
|
3899
|
+
handler: async (req, res) => {
|
|
3900
|
+
if (!guard(req, res, "POST")) return;
|
|
3901
|
+
const body = await readJsonBody(req);
|
|
3902
|
+
const id = typeof body?.id === "string" ? body.id : "";
|
|
3903
|
+
const document = id === "" ? void 0 : await canvas.read(id);
|
|
3904
|
+
if (document === void 0) writeJson(res, 200, {
|
|
3905
|
+
ok: false,
|
|
3906
|
+
code: "not-found",
|
|
3907
|
+
message: "画布不存在"
|
|
3908
|
+
});
|
|
3909
|
+
else writeJson(res, 200, {
|
|
3910
|
+
ok: true,
|
|
3911
|
+
document
|
|
3912
|
+
});
|
|
3913
|
+
}
|
|
3914
|
+
},
|
|
3915
|
+
{
|
|
3916
|
+
kind: "exact",
|
|
3917
|
+
path: CANVAS_API.save,
|
|
3918
|
+
handler: async (req, res) => {
|
|
3919
|
+
if (!guard(req, res, "POST")) return;
|
|
3920
|
+
const body = await readJsonBody(req);
|
|
3921
|
+
const document = body?.document;
|
|
3922
|
+
const expectedRevision = typeof body?.expectedRevision === "number" ? body.expectedRevision : void 0;
|
|
3923
|
+
if (document === void 0 || typeof document !== "object") {
|
|
3924
|
+
writeJson(res, 200, {
|
|
3925
|
+
ok: false,
|
|
3926
|
+
code: "bad-request",
|
|
3927
|
+
message: "canvas document is required"
|
|
3928
|
+
});
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3931
|
+
try {
|
|
3932
|
+
writeJson(res, 200, {
|
|
3933
|
+
ok: true,
|
|
3934
|
+
document: await canvas.save(document, expectedRevision)
|
|
3935
|
+
});
|
|
3936
|
+
} catch (error) {
|
|
3937
|
+
writeJson(res, 200, {
|
|
3938
|
+
ok: false,
|
|
3939
|
+
code: error instanceof CanvasConflictError ? error.code : "canvas-failed",
|
|
3940
|
+
message: messageOf(error)
|
|
3941
|
+
});
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
},
|
|
3945
|
+
{
|
|
3946
|
+
kind: "exact",
|
|
3947
|
+
path: CANVAS_API.remove,
|
|
3948
|
+
handler: async (req, res) => {
|
|
3949
|
+
if (!guard(req, res, "POST")) return;
|
|
3950
|
+
const body = await readJsonBody(req);
|
|
3951
|
+
const id = typeof body?.id === "string" ? body.id : "";
|
|
3952
|
+
if (id === "") {
|
|
3953
|
+
writeJson(res, 200, {
|
|
3954
|
+
ok: false,
|
|
3955
|
+
code: "bad-request",
|
|
3956
|
+
message: "canvas id is required"
|
|
3957
|
+
});
|
|
3958
|
+
return;
|
|
3959
|
+
}
|
|
3960
|
+
try {
|
|
3961
|
+
writeJson(res, 200, {
|
|
3962
|
+
ok: true,
|
|
3963
|
+
projects: await canvas.remove(id)
|
|
3964
|
+
});
|
|
3965
|
+
} catch (error) {
|
|
3966
|
+
writeJson(res, 200, {
|
|
3967
|
+
ok: false,
|
|
3968
|
+
code: "canvas-failed",
|
|
3969
|
+
message: messageOf(error)
|
|
3970
|
+
});
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
},
|
|
3974
|
+
{
|
|
3975
|
+
kind: "exact",
|
|
3976
|
+
path: CANVAS_API.assetUpload,
|
|
3977
|
+
handler: async (req, res) => {
|
|
3978
|
+
if (!guard(req, res, "POST")) return;
|
|
3979
|
+
const body = await readJsonBody(req, MAX_HISTORY_BODY_BYTES);
|
|
3980
|
+
const parsed = typeof body?.dataUrl === "string" ? imageDataUrl$1(body.dataUrl) : void 0;
|
|
3981
|
+
const width = Number(body?.width);
|
|
3982
|
+
const height = Number(body?.height);
|
|
3983
|
+
if (parsed === void 0 || !Number.isSafeInteger(width) || width < 1 || !Number.isSafeInteger(height) || height < 1) {
|
|
3984
|
+
writeJson(res, 200, {
|
|
3985
|
+
ok: false,
|
|
3986
|
+
code: "bad-request",
|
|
3987
|
+
message: "image data and dimensions are required"
|
|
3988
|
+
});
|
|
3989
|
+
return;
|
|
3990
|
+
}
|
|
3991
|
+
try {
|
|
3992
|
+
writeJson(res, 200, {
|
|
3993
|
+
ok: true,
|
|
3994
|
+
asset: await canvas.putImage({
|
|
3995
|
+
data: parsed.data,
|
|
3996
|
+
mime: parsed.mediaType,
|
|
3997
|
+
width,
|
|
3998
|
+
height,
|
|
3999
|
+
origin: body?.origin === "history" || body?.origin === "gallery" || body?.origin === "generated" ? body.origin : "upload",
|
|
4000
|
+
...typeof body?.originId === "string" ? { originId: body.originId } : {},
|
|
4001
|
+
...typeof body?.entryId === "string" ? { entryId: body.entryId } : {},
|
|
4002
|
+
...Number.isSafeInteger(Number(body?.imageIndex)) ? { imageIndex: Number(body?.imageIndex) } : {}
|
|
4003
|
+
})
|
|
4004
|
+
});
|
|
4005
|
+
} catch (error) {
|
|
4006
|
+
writeJson(res, 200, {
|
|
4007
|
+
ok: false,
|
|
4008
|
+
code: "canvas-asset-failed",
|
|
4009
|
+
message: messageOf(error)
|
|
4010
|
+
});
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
},
|
|
4014
|
+
{
|
|
4015
|
+
kind: "exact",
|
|
4016
|
+
path: CANVAS_API.assetImport,
|
|
4017
|
+
handler: async (req, res) => {
|
|
4018
|
+
if (!guard(req, res, "POST")) return;
|
|
4019
|
+
const body = await readJsonBody(req);
|
|
4020
|
+
const source = body?.source === "history" || body?.source === "gallery" ? body.source : void 0;
|
|
4021
|
+
const entryId = typeof body?.entryId === "string" ? body.entryId : "";
|
|
4022
|
+
const imageIndex = Number(body?.imageIndex);
|
|
4023
|
+
const width = Number(body?.width);
|
|
4024
|
+
const height = Number(body?.height);
|
|
4025
|
+
if (source === void 0 || entryId === "" || !Number.isSafeInteger(imageIndex) || imageIndex < 0 || !Number.isSafeInteger(width) || width < 1 || !Number.isSafeInteger(height) || height < 1) {
|
|
4026
|
+
writeJson(res, 200, {
|
|
4027
|
+
ok: false,
|
|
4028
|
+
code: "bad-request",
|
|
4029
|
+
message: "source, entryId, imageIndex and dimensions are required"
|
|
4030
|
+
});
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
4033
|
+
try {
|
|
4034
|
+
const backend = source === "history" ? history : gallery;
|
|
4035
|
+
const image = (await backend.list()).find((item) => item.id === entryId)?.images[imageIndex];
|
|
4036
|
+
if (image === void 0) {
|
|
4037
|
+
writeJson(res, 200, {
|
|
4038
|
+
ok: false,
|
|
4039
|
+
code: "not-found",
|
|
4040
|
+
message: "image not found"
|
|
4041
|
+
});
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
4044
|
+
const base = source === "history" ? HISTORY_API.image : GALLERY_API.image;
|
|
4045
|
+
const file = imageFileFrom(image.url, base);
|
|
4046
|
+
const found = file === void 0 ? void 0 : await backend.readImage(file);
|
|
4047
|
+
if (found === void 0) {
|
|
4048
|
+
writeJson(res, 200, {
|
|
4049
|
+
ok: false,
|
|
4050
|
+
code: "not-found",
|
|
4051
|
+
message: "image not found"
|
|
4052
|
+
});
|
|
4053
|
+
return;
|
|
4054
|
+
}
|
|
4055
|
+
writeJson(res, 200, {
|
|
4056
|
+
ok: true,
|
|
4057
|
+
asset: await canvas.putImage({
|
|
4058
|
+
data: found.data,
|
|
4059
|
+
mime: found.mime,
|
|
4060
|
+
width,
|
|
4061
|
+
height,
|
|
4062
|
+
origin: source,
|
|
4063
|
+
originId: entryId,
|
|
4064
|
+
entryId,
|
|
4065
|
+
imageIndex
|
|
4066
|
+
})
|
|
4067
|
+
});
|
|
4068
|
+
} catch (error) {
|
|
4069
|
+
writeJson(res, 200, {
|
|
4070
|
+
ok: false,
|
|
4071
|
+
code: "canvas-asset-failed",
|
|
4072
|
+
message: messageOf(error)
|
|
4073
|
+
});
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
},
|
|
4077
|
+
{
|
|
4078
|
+
kind: "prefix",
|
|
4079
|
+
path: CANVAS_API.asset,
|
|
4080
|
+
handler: async (req, res) => {
|
|
4081
|
+
if (!isLoopbackRequest(req)) {
|
|
4082
|
+
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
4083
|
+
return;
|
|
4084
|
+
}
|
|
4085
|
+
if (req.method !== "GET") {
|
|
4086
|
+
writeJson(res, 405, { error: `method not allowed: ${req.method}` });
|
|
4087
|
+
return;
|
|
4088
|
+
}
|
|
4089
|
+
const file = imageFileFrom(req.url, CANVAS_API.asset);
|
|
4090
|
+
const found = file === void 0 ? void 0 : await canvas.readAsset(file);
|
|
4091
|
+
if (found === void 0) {
|
|
4092
|
+
writeJson(res, 404, { error: "not found" });
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
res.writeHead(200, {
|
|
4096
|
+
"content-type": found.mime,
|
|
4097
|
+
"content-length": found.data.length,
|
|
4098
|
+
"cache-control": "private, max-age=3600"
|
|
4099
|
+
});
|
|
4100
|
+
res.end(found.data);
|
|
4101
|
+
}
|
|
4102
|
+
},
|
|
3089
4103
|
{
|
|
3090
4104
|
kind: "exact",
|
|
3091
4105
|
path: TEMPLATES_API.list,
|
|
@@ -3288,6 +4302,71 @@ function makeRoutes(deps) {
|
|
|
3288
4302
|
});
|
|
3289
4303
|
}
|
|
3290
4304
|
}
|
|
4305
|
+
},
|
|
4306
|
+
{
|
|
4307
|
+
kind: "exact",
|
|
4308
|
+
path: DATA_FOLDER_API,
|
|
4309
|
+
handler: async (req, res) => {
|
|
4310
|
+
if (!guard(req, res, "POST")) return;
|
|
4311
|
+
const body = await readJsonBody(req);
|
|
4312
|
+
const dir = path.join(homedir(), ".dsh", "dsh-imagegen");
|
|
4313
|
+
try {
|
|
4314
|
+
await mkdir(dir, { recursive: true });
|
|
4315
|
+
} catch {}
|
|
4316
|
+
if (body?.open === false) {
|
|
4317
|
+
writeJson(res, 200, {
|
|
4318
|
+
ok: true,
|
|
4319
|
+
path: dir
|
|
4320
|
+
});
|
|
4321
|
+
return;
|
|
4322
|
+
}
|
|
4323
|
+
try {
|
|
4324
|
+
spawn(process.platform === "win32" ? "explorer.exe" : process.platform === "darwin" ? "open" : "xdg-open", [dir], {
|
|
4325
|
+
detached: true,
|
|
4326
|
+
stdio: "ignore"
|
|
4327
|
+
}).unref();
|
|
4328
|
+
writeJson(res, 200, {
|
|
4329
|
+
ok: true,
|
|
4330
|
+
path: dir
|
|
4331
|
+
});
|
|
4332
|
+
} catch (error) {
|
|
4333
|
+
writeJson(res, 200, {
|
|
4334
|
+
ok: false,
|
|
4335
|
+
code: "data-folder-failed",
|
|
4336
|
+
message: messageOf(error)
|
|
4337
|
+
});
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
},
|
|
4341
|
+
{
|
|
4342
|
+
kind: "exact",
|
|
4343
|
+
path: STORAGE_API.test,
|
|
4344
|
+
handler: async (req, res) => {
|
|
4345
|
+
if (!guard(req, res, "POST")) return;
|
|
4346
|
+
const storage = deps.resolveStorage?.();
|
|
4347
|
+
if (storage === void 0) {
|
|
4348
|
+
writeJson(res, 200, {
|
|
4349
|
+
ok: false,
|
|
4350
|
+
code: "storage-unavailable",
|
|
4351
|
+
message: "存储配置不可用"
|
|
4352
|
+
});
|
|
4353
|
+
return;
|
|
4354
|
+
}
|
|
4355
|
+
try {
|
|
4356
|
+
const result = await testStorage(storage);
|
|
4357
|
+
writeJson(res, 200, {
|
|
4358
|
+
ok: true,
|
|
4359
|
+
ms: result.ms,
|
|
4360
|
+
key: result.key
|
|
4361
|
+
});
|
|
4362
|
+
} catch (error) {
|
|
4363
|
+
writeJson(res, 200, {
|
|
4364
|
+
ok: false,
|
|
4365
|
+
code: "storage-test-failed",
|
|
4366
|
+
message: messageOf(error)
|
|
4367
|
+
});
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
3291
4370
|
}
|
|
3292
4371
|
];
|
|
3293
4372
|
}
|
|
@@ -3841,6 +4920,16 @@ function registerEditImageCommand(ctx, runtime, resolve, pendingImages) {
|
|
|
3841
4920
|
}
|
|
3842
4921
|
//#endregion
|
|
3843
4922
|
//#region src/index.ts
|
|
4923
|
+
/** Content type for a saved image file name (object uploads). */
|
|
4924
|
+
function mimeOfPath(filePath) {
|
|
4925
|
+
switch (path.extname(filePath).toLowerCase()) {
|
|
4926
|
+
case ".jpg":
|
|
4927
|
+
case ".jpeg": return "image/jpeg";
|
|
4928
|
+
case ".webp": return "image/webp";
|
|
4929
|
+
case ".gif": return "image/gif";
|
|
4930
|
+
default: return "image/png";
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
3844
4933
|
/** Stable cordis plugin name. */
|
|
3845
4934
|
const name = "imagegen";
|
|
3846
4935
|
/** Services required before the surfaces can mount. */
|
|
@@ -3870,6 +4959,14 @@ const Config = z.object({
|
|
|
3870
4959
|
promptApiUrl: z.string().default(""),
|
|
3871
4960
|
promptApiKey: z.string().role("secret").default(""),
|
|
3872
4961
|
promptModel: z.string().default(""),
|
|
4962
|
+
storageEnabled: z.boolean().default(false),
|
|
4963
|
+
storageEndpoint: z.string().default(""),
|
|
4964
|
+
storageRegion: z.string().default(""),
|
|
4965
|
+
storagePrefix: z.string().default("dsh-imagegen"),
|
|
4966
|
+
storageAccessKey: z.string().default(""),
|
|
4967
|
+
storageSecretKey: z.string().role("secret").default(""),
|
|
4968
|
+
storageSyncGallery: z.boolean().default(true),
|
|
4969
|
+
storageSyncHistory: z.boolean().default(false),
|
|
3873
4970
|
apiUrl: z.string().default(""),
|
|
3874
4971
|
apiKey: z.string().role("secret").default(""),
|
|
3875
4972
|
imageModels: z.array(z.string()).default([])
|
|
@@ -3881,7 +4978,7 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
|
|
|
3881
4978
|
/** Order of the announcement section within the tool-guidance band. */
|
|
3882
4979
|
const SECTION_ORDER = 150;
|
|
3883
4980
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
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
|
|
4981
|
+
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`,当前仅支持文生图;qwen-image 系列使用阿里云 DashScope 原生接口(api_url 填 https://dashscope.aliyuncs.com/api/v1,不支持 OpenAI 兼容模式,该渠道不可复用于提示词增强,尺寸自动映射为宽*高)。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)。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
|
|
3885
4982
|
/** Append the live channel × model table so an Agent can honor user choices. */
|
|
3886
4983
|
function guidanceFor(channels, defaultChannelId) {
|
|
3887
4984
|
if (channels.length === 0) return `${IMAGEGEN_GUIDANCE} 尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥。`;
|
|
@@ -3970,7 +5067,17 @@ function apply(ctx, config) {
|
|
|
3970
5067
|
defaultChannelId,
|
|
3971
5068
|
promptApiUrl: typeof value.promptApiUrl === "string" ? value.promptApiUrl.trim() : "",
|
|
3972
5069
|
promptApiKey: typeof value.promptApiKey === "string" ? value.promptApiKey.trim() : "",
|
|
3973
|
-
promptModel: typeof value.promptModel === "string" ? value.promptModel.trim() : ""
|
|
5070
|
+
promptModel: typeof value.promptModel === "string" ? value.promptModel.trim() : "",
|
|
5071
|
+
storage: {
|
|
5072
|
+
enabled: value.storageEnabled ?? false,
|
|
5073
|
+
endpoint: typeof value.storageEndpoint === "string" ? value.storageEndpoint.trim() : "",
|
|
5074
|
+
region: typeof value.storageRegion === "string" ? value.storageRegion.trim() : "",
|
|
5075
|
+
accessKey: typeof value.storageAccessKey === "string" ? value.storageAccessKey.trim() : "",
|
|
5076
|
+
secretKey: typeof value.storageSecretKey === "string" ? value.storageSecretKey.trim() : "",
|
|
5077
|
+
prefix: typeof value.storagePrefix === "string" && value.storagePrefix.trim() !== "" ? value.storagePrefix.trim() : "dsh-imagegen",
|
|
5078
|
+
syncGallery: value.storageSyncGallery ?? true,
|
|
5079
|
+
syncHistory: value.storageSyncHistory ?? false
|
|
5080
|
+
}
|
|
3974
5081
|
};
|
|
3975
5082
|
};
|
|
3976
5083
|
const channelsView = () => {
|
|
@@ -3980,6 +5087,13 @@ function apply(ctx, config) {
|
|
|
3980
5087
|
defaultChannelId: value.defaultChannelId
|
|
3981
5088
|
};
|
|
3982
5089
|
};
|
|
5090
|
+
setStorageSyncHandler((kind, filePath) => {
|
|
5091
|
+
const storage = resolve().storage;
|
|
5092
|
+
if (!storage.enabled || !storage.endpoint.trim() || storage.secretKey.trim() === "") return;
|
|
5093
|
+
if (kind === "gallery" && !storage.syncGallery) return;
|
|
5094
|
+
if (kind === "history" && !storage.syncHistory) return;
|
|
5095
|
+
putObject(storage, `${storage.prefix}/${kind === "gallery" ? "gallery" : "images"}/${path.basename(filePath)}`, readFileSync(filePath), mimeOfPath(filePath)).catch(() => {});
|
|
5096
|
+
});
|
|
3983
5097
|
const runtime = new ImageGenerationRuntime(channelsView);
|
|
3984
5098
|
const pendingConversationImages = /* @__PURE__ */ new Map();
|
|
3985
5099
|
ctx.inject(["settings", "attachments"], (sctx) => {
|
|
@@ -4011,7 +5125,8 @@ function apply(ctx, config) {
|
|
|
4011
5125
|
},
|
|
4012
5126
|
attachments: sctx.attachments,
|
|
4013
5127
|
pendingConversationImages,
|
|
4014
|
-
runtime
|
|
5128
|
+
runtime,
|
|
5129
|
+
resolveStorage: () => resolve().storage
|
|
4015
5130
|
}).map((route) => ctx.webServer.register(route));
|
|
4016
5131
|
const TEMPLATE_SYNC_INITIAL_DELAY_MS = 3e4;
|
|
4017
5132
|
const TEMPLATE_SYNC_INTERVAL_MS = 720 * 60 * 1e3;
|
|
@@ -4080,6 +5195,9 @@ function apply(ctx, config) {
|
|
|
4080
5195
|
onChange: sync
|
|
4081
5196
|
});
|
|
4082
5197
|
sync();
|
|
5198
|
+
return () => {
|
|
5199
|
+
setStorageSyncHandler(void 0);
|
|
5200
|
+
};
|
|
4083
5201
|
}
|
|
4084
5202
|
//#endregion
|
|
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 };
|
|
5203
|
+
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, putObject, readGalleryImage, readTemplateImage, refreshTemplates, registerAgentImageTools, registerEditImageCommand, removeGallery, removeTemplateFavorite, sampleTemplates, setStorageSyncHandler, syncAllTemplates, testStorage, updateGalleryTags };
|