@dickpy/dsh-imagegen 1.5.3 → 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 +21 -1
- package/lib/client.js +5310 -1934
- package/lib/client.js.map +1 -1
- package/lib/index.js +986 -32
- package/package.json +1 -1
- package/src/canvas-store.ts +376 -0
- package/src/client/CanvasWorkspace.tsx +1569 -0
- package/src/client/ImageGenPanel.tsx +159 -96
- package/src/client/SettingsCard.tsx +173 -1
- package/src/client/api.ts +48 -1
- package/src/client/canvas-workspace.module.css +929 -0
- package/src/client/locales.ts +1443 -1146
- package/src/client/panel.module.css +83 -33
- package/src/engine.ts +156 -14
- 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 +70 -3
- 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},
|
|
@@ -667,7 +683,7 @@ function effectiveCount(request) {
|
|
|
667
683
|
return clampCount(request.n);
|
|
668
684
|
}
|
|
669
685
|
/** Normalize one upstream data item into a base64 image. */
|
|
670
|
-
async function normalizeItem(item, upstream) {
|
|
686
|
+
async function normalizeItem(item, upstream, signal) {
|
|
671
687
|
const revisedPrompt = typeof item.revised_prompt === "string" ? item.revised_prompt : void 0;
|
|
672
688
|
if (typeof item.b64_json === "string" && item.b64_json.trim() !== "") {
|
|
673
689
|
const b64 = bareBase64(item.b64_json);
|
|
@@ -688,7 +704,7 @@ async function normalizeItem(item, upstream) {
|
|
|
688
704
|
revisedPrompt
|
|
689
705
|
};
|
|
690
706
|
}
|
|
691
|
-
const budget = requestSignal(
|
|
707
|
+
const budget = requestSignal(signal, IMAGE_FETCH_TIMEOUT_MS);
|
|
692
708
|
let response;
|
|
693
709
|
try {
|
|
694
710
|
response = await fetch(url, {
|
|
@@ -710,6 +726,143 @@ async function normalizeItem(item, upstream) {
|
|
|
710
726
|
revisedPrompt
|
|
711
727
|
};
|
|
712
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
|
+
}
|
|
713
866
|
/**
|
|
714
867
|
* Issue one single-image request (never sends `n`). The response is kept as a
|
|
715
868
|
* list so a gateway that happens to return several images per call still works.
|
|
@@ -742,7 +895,7 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
742
895
|
});
|
|
743
896
|
} else if (isNanoBanana(params.model)) {
|
|
744
897
|
const form = new FormData();
|
|
745
|
-
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)}`);
|
|
746
899
|
form.append("prompt", request.prompt);
|
|
747
900
|
form.append("model", params.model);
|
|
748
901
|
if (params.aspect_ratio !== void 0) form.append("aspect_ratio", params.aspect_ratio);
|
|
@@ -760,7 +913,7 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
760
913
|
});
|
|
761
914
|
} else {
|
|
762
915
|
const form = new FormData();
|
|
763
|
-
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)}`);
|
|
764
917
|
form.append("prompt", request.prompt);
|
|
765
918
|
form.append("model", params.model);
|
|
766
919
|
if (params.size !== void 0) form.append("size", params.size);
|
|
@@ -799,14 +952,16 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
799
952
|
throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
|
|
800
953
|
}
|
|
801
954
|
if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(upstreamMessage(payload, response.status), "upstream-rejected");
|
|
802
|
-
const
|
|
803
|
-
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);
|
|
804
956
|
if (data === void 0) throw new ImageGenError("上游响应缺少 data 数组", "upstream-invalid");
|
|
805
957
|
if (data.length === 0) throw new ImageGenError("上游返回了 0 张图片", "upstream-empty");
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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)));
|
|
810
965
|
}
|
|
811
966
|
/**
|
|
812
967
|
* Qwen-Image (DashScope native multimodal-generation): one chat-style request
|
|
@@ -916,7 +1071,7 @@ function upstreamMessage(payload, status) {
|
|
|
916
1071
|
return `上游接口拒绝请求(HTTP ${status})`;
|
|
917
1072
|
}
|
|
918
1073
|
/** File extension for a MIME type (multipart reference image). */
|
|
919
|
-
function extensionOf$
|
|
1074
|
+
function extensionOf$3(mime) {
|
|
920
1075
|
switch (mime.split(";")[0].trim()) {
|
|
921
1076
|
case "image/jpeg": return "jpg";
|
|
922
1077
|
case "image/webp": return "webp";
|
|
@@ -925,6 +1080,87 @@ function extensionOf$2(mime) {
|
|
|
925
1080
|
}
|
|
926
1081
|
}
|
|
927
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
|
|
928
1164
|
//#region src/history-store.ts
|
|
929
1165
|
/**
|
|
930
1166
|
* Host-persisted generation history: images are stored as individual files
|
|
@@ -945,7 +1181,7 @@ function mutateHistory(operation) {
|
|
|
945
1181
|
return next;
|
|
946
1182
|
}
|
|
947
1183
|
/** File extension for a MIME type (image file names). */
|
|
948
|
-
function extensionOf$
|
|
1184
|
+
function extensionOf$2(mime) {
|
|
949
1185
|
switch (mime.split(";")[0].trim()) {
|
|
950
1186
|
case "image/jpeg": return "jpg";
|
|
951
1187
|
case "image/webp": return "webp";
|
|
@@ -964,7 +1200,7 @@ function mimeOfFile$2(file) {
|
|
|
964
1200
|
}
|
|
965
1201
|
}
|
|
966
1202
|
/** Sanitize an entry id for use as a file-name prefix. */
|
|
967
|
-
function safeId$
|
|
1203
|
+
function safeId$2(id) {
|
|
968
1204
|
const cleaned = id.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
969
1205
|
return cleaned === "" ? "entry" : cleaned;
|
|
970
1206
|
}
|
|
@@ -1035,7 +1271,8 @@ function toWire$1(entry) {
|
|
|
1035
1271
|
...entry.projectId === void 0 ? {} : { projectId: entry.projectId },
|
|
1036
1272
|
...entry.projectName === void 0 ? {} : { projectName: entry.projectName },
|
|
1037
1273
|
...entry.slotKey === void 0 ? {} : { slotKey: entry.slotKey },
|
|
1038
|
-
...entry.slotLabel === void 0 ? {} : { slotLabel: entry.slotLabel }
|
|
1274
|
+
...entry.slotLabel === void 0 ? {} : { slotLabel: entry.slotLabel },
|
|
1275
|
+
...entry.canvas === void 0 ? {} : { canvas: entry.canvas }
|
|
1039
1276
|
};
|
|
1040
1277
|
}
|
|
1041
1278
|
/** List the persisted history, newest first, as wire entries. */
|
|
@@ -1046,13 +1283,14 @@ async function listHistory() {
|
|
|
1046
1283
|
async function appendHistory(input) {
|
|
1047
1284
|
return mutateHistory(async () => {
|
|
1048
1285
|
await ensureDirs$1();
|
|
1049
|
-
const prefix = safeId$
|
|
1286
|
+
const prefix = safeId$2(input.id);
|
|
1050
1287
|
const storedImages = [];
|
|
1051
1288
|
try {
|
|
1052
1289
|
for (let index = 0; index < input.images.length; index++) {
|
|
1053
1290
|
const image = input.images[index];
|
|
1054
|
-
const file = `${prefix}-${index}.${extensionOf$
|
|
1291
|
+
const file = `${prefix}-${index}.${extensionOf$2(image.mime)}`;
|
|
1055
1292
|
await promises.writeFile(path.join(IMAGES_DIR$1, file), Buffer.from(image.b64, "base64"));
|
|
1293
|
+
notifyImageSaved("history", path.join(IMAGES_DIR$1, file));
|
|
1056
1294
|
storedImages.push({
|
|
1057
1295
|
file,
|
|
1058
1296
|
mime: image.mime,
|
|
@@ -1083,7 +1321,8 @@ async function appendHistory(input) {
|
|
|
1083
1321
|
...input.projectId === void 0 ? {} : { projectId: input.projectId },
|
|
1084
1322
|
...input.projectName === void 0 ? {} : { projectName: input.projectName },
|
|
1085
1323
|
...input.slotKey === void 0 ? {} : { slotKey: input.slotKey },
|
|
1086
|
-
...input.slotLabel === void 0 ? {} : { slotLabel: input.slotLabel }
|
|
1324
|
+
...input.slotLabel === void 0 ? {} : { slotLabel: input.slotLabel },
|
|
1325
|
+
...input.canvas === void 0 ? {} : { canvas: input.canvas }
|
|
1087
1326
|
}, ...await readIndex$1()];
|
|
1088
1327
|
const kept = merged.slice(0, 50);
|
|
1089
1328
|
for (const dropped of merged.slice(50)) await removeEntryFiles$1(dropped);
|
|
@@ -1276,7 +1515,8 @@ var ImageGenerationRuntime = class {
|
|
|
1276
1515
|
...request.projectId === void 0 ? {} : { projectId: request.projectId },
|
|
1277
1516
|
...request.projectName === void 0 ? {} : { projectName: request.projectName },
|
|
1278
1517
|
...request.slotKey === void 0 ? {} : { slotKey: request.slotKey },
|
|
1279
|
-
...request.slotLabel === void 0 ? {} : { slotLabel: request.slotLabel }
|
|
1518
|
+
...request.slotLabel === void 0 ? {} : { slotLabel: request.slotLabel },
|
|
1519
|
+
...request.canvas === void 0 ? {} : { canvas: request.canvas }
|
|
1280
1520
|
});
|
|
1281
1521
|
return {
|
|
1282
1522
|
...result,
|
|
@@ -1312,7 +1552,7 @@ function mutateGallery(operation) {
|
|
|
1312
1552
|
return next;
|
|
1313
1553
|
}
|
|
1314
1554
|
/** File extension for a MIME type (image file names). */
|
|
1315
|
-
function extensionOf(mime) {
|
|
1555
|
+
function extensionOf$1(mime) {
|
|
1316
1556
|
switch (mime.split(";")[0].trim()) {
|
|
1317
1557
|
case "image/jpeg": return "jpg";
|
|
1318
1558
|
case "image/webp": return "webp";
|
|
@@ -1331,7 +1571,7 @@ function mimeOfFile$1(file) {
|
|
|
1331
1571
|
}
|
|
1332
1572
|
}
|
|
1333
1573
|
/** Sanitize an entry id for use as a file-name prefix. */
|
|
1334
|
-
function safeId(id) {
|
|
1574
|
+
function safeId$1(id) {
|
|
1335
1575
|
const cleaned = id.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
1336
1576
|
return cleaned === "" ? "entry" : cleaned;
|
|
1337
1577
|
}
|
|
@@ -1407,7 +1647,8 @@ function toWire(entry) {
|
|
|
1407
1647
|
...entry.projectId === void 0 ? {} : { projectId: entry.projectId },
|
|
1408
1648
|
...entry.projectName === void 0 ? {} : { projectName: entry.projectName },
|
|
1409
1649
|
...entry.slotKey === void 0 ? {} : { slotKey: entry.slotKey },
|
|
1410
|
-
...entry.slotLabel === void 0 ? {} : { slotLabel: entry.slotLabel }
|
|
1650
|
+
...entry.slotLabel === void 0 ? {} : { slotLabel: entry.slotLabel },
|
|
1651
|
+
...entry.canvas === void 0 ? {} : { canvas: entry.canvas }
|
|
1411
1652
|
};
|
|
1412
1653
|
}
|
|
1413
1654
|
/** List the persisted gallery, newest first, as wire entries. */
|
|
@@ -1428,13 +1669,14 @@ async function appendGallery(input) {
|
|
|
1428
1669
|
added: false
|
|
1429
1670
|
};
|
|
1430
1671
|
}
|
|
1431
|
-
const prefix = safeId(input.id);
|
|
1672
|
+
const prefix = safeId$1(input.id);
|
|
1432
1673
|
const storedImages = [];
|
|
1433
1674
|
try {
|
|
1434
1675
|
for (let index = 0; index < input.images.length; index++) {
|
|
1435
1676
|
const image = input.images[index];
|
|
1436
|
-
const file = `${prefix}-${index}.${extensionOf(image.mime)}`;
|
|
1677
|
+
const file = `${prefix}-${index}.${extensionOf$1(image.mime)}`;
|
|
1437
1678
|
await promises.writeFile(path.join(IMAGES_DIR, file), Buffer.from(image.b64, "base64"));
|
|
1679
|
+
notifyImageSaved("gallery", path.join(IMAGES_DIR, file));
|
|
1438
1680
|
storedImages.push({
|
|
1439
1681
|
file,
|
|
1440
1682
|
mime: image.mime,
|
|
@@ -1464,7 +1706,8 @@ async function appendGallery(input) {
|
|
|
1464
1706
|
...input.projectId === void 0 ? {} : { projectId: input.projectId },
|
|
1465
1707
|
...input.projectName === void 0 ? {} : { projectName: input.projectName },
|
|
1466
1708
|
...input.slotKey === void 0 ? {} : { slotKey: input.slotKey },
|
|
1467
|
-
...input.slotLabel === void 0 ? {} : { slotLabel: input.slotLabel }
|
|
1709
|
+
...input.slotLabel === void 0 ? {} : { slotLabel: input.slotLabel },
|
|
1710
|
+
...input.canvas === void 0 ? {} : { canvas: input.canvas }
|
|
1468
1711
|
}, ...await readIndex()];
|
|
1469
1712
|
await writeIndex(merged);
|
|
1470
1713
|
return {
|
|
@@ -1517,6 +1760,349 @@ async function readGalleryImage(file) {
|
|
|
1517
1760
|
}
|
|
1518
1761
|
}
|
|
1519
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
|
|
1520
2106
|
//#region src/templates-store.ts
|
|
1521
2107
|
/**
|
|
1522
2108
|
* Prompt-template library store (multi-source).
|
|
@@ -2248,6 +2834,7 @@ function parseGenerateRequest(body) {
|
|
|
2248
2834
|
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
2249
2835
|
if (prompt === "") return void 0;
|
|
2250
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);
|
|
2251
2838
|
return {
|
|
2252
2839
|
mode: body.mode === "edit" ? "edit" : "text",
|
|
2253
2840
|
model: typeof body.model === "string" ? body.model : "",
|
|
@@ -2261,6 +2848,7 @@ function parseGenerateRequest(body) {
|
|
|
2261
2848
|
...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {},
|
|
2262
2849
|
...typeof body.comparisonId === "string" && body.comparisonId !== "" ? { comparisonId: body.comparisonId } : {},
|
|
2263
2850
|
...comparisonModels.length > 1 ? { comparisonModels } : {},
|
|
2851
|
+
...canvas === void 0 ? {} : { canvas },
|
|
2264
2852
|
...body.workflow === "ecommerce" ? { workflow: "ecommerce" } : {},
|
|
2265
2853
|
...typeof body.projectId === "string" && body.projectId !== "" ? { projectId: body.projectId } : {},
|
|
2266
2854
|
...typeof body.projectName === "string" && body.projectName !== "" ? { projectName: body.projectName } : {},
|
|
@@ -2268,6 +2856,18 @@ function parseGenerateRequest(body) {
|
|
|
2268
2856
|
...typeof body.slotLabel === "string" && body.slotLabel !== "" ? { slotLabel: body.slotLabel } : {}
|
|
2269
2857
|
};
|
|
2270
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
|
+
}
|
|
2271
2871
|
/** Validate a submitted history entry (images carry base64). */
|
|
2272
2872
|
function parseHistoryEntryInput(body) {
|
|
2273
2873
|
const raw = body.entry;
|
|
@@ -2291,6 +2891,7 @@ function parseHistoryEntryInput(body) {
|
|
|
2291
2891
|
});
|
|
2292
2892
|
}
|
|
2293
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);
|
|
2294
2895
|
return {
|
|
2295
2896
|
id: entry.id,
|
|
2296
2897
|
createdAt: entry.createdAt,
|
|
@@ -2307,6 +2908,7 @@ function parseHistoryEntryInput(body) {
|
|
|
2307
2908
|
...typeof entry.channel === "string" ? { channel: entry.channel } : {},
|
|
2308
2909
|
...typeof entry.comparisonId === "string" ? { comparisonId: entry.comparisonId } : {},
|
|
2309
2910
|
...comparisonModels.length > 1 ? { comparisonModels } : {},
|
|
2911
|
+
...canvas === void 0 ? {} : { canvas },
|
|
2310
2912
|
...entry.workflow === "ecommerce" ? { workflow: "ecommerce" } : {},
|
|
2311
2913
|
...typeof entry.projectId === "string" ? { projectId: entry.projectId } : {},
|
|
2312
2914
|
...typeof entry.projectName === "string" ? { projectName: entry.projectName } : {},
|
|
@@ -2411,6 +3013,7 @@ function makeRoutes(deps) {
|
|
|
2411
3013
|
updateTags: updateGalleryTags,
|
|
2412
3014
|
readImage: readGalleryImage
|
|
2413
3015
|
};
|
|
3016
|
+
const canvas = deps.canvas ?? canvasStore;
|
|
2414
3017
|
const templates = deps.templates ?? {
|
|
2415
3018
|
list: listTemplates,
|
|
2416
3019
|
refresh: refreshTemplates,
|
|
@@ -3250,6 +3853,253 @@ function makeRoutes(deps) {
|
|
|
3250
3853
|
res.end(found.data);
|
|
3251
3854
|
}
|
|
3252
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
|
+
},
|
|
3253
4103
|
{
|
|
3254
4104
|
kind: "exact",
|
|
3255
4105
|
path: TEMPLATES_API.list,
|
|
@@ -3452,6 +4302,71 @@ function makeRoutes(deps) {
|
|
|
3452
4302
|
});
|
|
3453
4303
|
}
|
|
3454
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
|
+
}
|
|
3455
4370
|
}
|
|
3456
4371
|
];
|
|
3457
4372
|
}
|
|
@@ -4005,6 +4920,16 @@ function registerEditImageCommand(ctx, runtime, resolve, pendingImages) {
|
|
|
4005
4920
|
}
|
|
4006
4921
|
//#endregion
|
|
4007
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
|
+
}
|
|
4008
4933
|
/** Stable cordis plugin name. */
|
|
4009
4934
|
const name = "imagegen";
|
|
4010
4935
|
/** Services required before the surfaces can mount. */
|
|
@@ -4034,6 +4959,14 @@ const Config = z.object({
|
|
|
4034
4959
|
promptApiUrl: z.string().default(""),
|
|
4035
4960
|
promptApiKey: z.string().role("secret").default(""),
|
|
4036
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),
|
|
4037
4970
|
apiUrl: z.string().default(""),
|
|
4038
4971
|
apiKey: z.string().role("secret").default(""),
|
|
4039
4972
|
imageModels: z.array(z.string()).default([])
|
|
@@ -4134,7 +5067,17 @@ function apply(ctx, config) {
|
|
|
4134
5067
|
defaultChannelId,
|
|
4135
5068
|
promptApiUrl: typeof value.promptApiUrl === "string" ? value.promptApiUrl.trim() : "",
|
|
4136
5069
|
promptApiKey: typeof value.promptApiKey === "string" ? value.promptApiKey.trim() : "",
|
|
4137
|
-
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
|
+
}
|
|
4138
5081
|
};
|
|
4139
5082
|
};
|
|
4140
5083
|
const channelsView = () => {
|
|
@@ -4144,6 +5087,13 @@ function apply(ctx, config) {
|
|
|
4144
5087
|
defaultChannelId: value.defaultChannelId
|
|
4145
5088
|
};
|
|
4146
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
|
+
});
|
|
4147
5097
|
const runtime = new ImageGenerationRuntime(channelsView);
|
|
4148
5098
|
const pendingConversationImages = /* @__PURE__ */ new Map();
|
|
4149
5099
|
ctx.inject(["settings", "attachments"], (sctx) => {
|
|
@@ -4175,7 +5125,8 @@ function apply(ctx, config) {
|
|
|
4175
5125
|
},
|
|
4176
5126
|
attachments: sctx.attachments,
|
|
4177
5127
|
pendingConversationImages,
|
|
4178
|
-
runtime
|
|
5128
|
+
runtime,
|
|
5129
|
+
resolveStorage: () => resolve().storage
|
|
4179
5130
|
}).map((route) => ctx.webServer.register(route));
|
|
4180
5131
|
const TEMPLATE_SYNC_INITIAL_DELAY_MS = 3e4;
|
|
4181
5132
|
const TEMPLATE_SYNC_INTERVAL_MS = 720 * 60 * 1e3;
|
|
@@ -4244,6 +5195,9 @@ function apply(ctx, config) {
|
|
|
4244
5195
|
onChange: sync
|
|
4245
5196
|
});
|
|
4246
5197
|
sync();
|
|
5198
|
+
return () => {
|
|
5199
|
+
setStorageSyncHandler(void 0);
|
|
5200
|
+
};
|
|
4247
5201
|
}
|
|
4248
5202
|
//#endregion
|
|
4249
|
-
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 };
|