@dickpy/dsh-imagegen 1.5.4 → 1.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/lib/client.js +1141 -508
- package/lib/client.js.map +1 -1
- package/lib/index.js +217 -170
- package/package.json +1 -1
- package/src/canvas-store.ts +15 -16
- package/src/client/CanvasWorkspace.tsx +333 -31
- package/src/client/ImageGenPanel.tsx +2737 -2679
- package/src/client/SettingsCard.tsx +14 -0
- package/src/client/canvas-workspace.module.css +144 -8
- package/src/client/locales.ts +82 -25
- package/src/client/panel.module.css +20 -26
- package/src/engine.ts +181 -146
- package/src/gallery-store.ts +13 -13
- package/src/generation-runtime.ts +2 -2
- package/src/history-store.ts +12 -12
- package/src/image-storage-path.ts +12 -0
- package/src/index.ts +5 -0
- package/src/protocol.ts +8 -2
- package/src/routes.ts +3 -0
- package/src/task-queue.ts +4 -5
package/lib/index.js
CHANGED
|
@@ -41,7 +41,7 @@ function installSettingsSectionCompat(ctx, ns, schema, entry, hooks) {
|
|
|
41
41
|
/** Settings namespace this plugin owns (host settings seam + bridge). */
|
|
42
42
|
const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
|
|
43
43
|
/** Published package version shared by the host updater and the client UI. */
|
|
44
|
-
const PLUGIN_VERSION = "1.5.
|
|
44
|
+
const PLUGIN_VERSION = "1.5.6";
|
|
45
45
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
46
46
|
const SETTINGS_API = {
|
|
47
47
|
describe: "/api/dsh-imagegen/settings/describe",
|
|
@@ -588,8 +588,13 @@ function requestSignal(source, timeoutMs) {
|
|
|
588
588
|
}
|
|
589
589
|
};
|
|
590
590
|
}
|
|
591
|
-
/**
|
|
592
|
-
|
|
591
|
+
/** Whether an error was produced by a requestSignal budget timeout. These can
|
|
592
|
+
* surface from the fetch call itself or from reading the response body, so the
|
|
593
|
+
* budget must stay armed until the body has been consumed. */
|
|
594
|
+
function isBudgetTimeout(error) {
|
|
595
|
+
return (error instanceof DOMException || error instanceof Error) && error.name === "TimeoutError";
|
|
596
|
+
}
|
|
597
|
+
/** Content-type extension hints for URL-fetched images. */ function mimeOfExtension(path) {
|
|
593
598
|
const match = /\.([a-z0-9]+)$/i.exec(path);
|
|
594
599
|
if (match === null) return void 0;
|
|
595
600
|
switch (match[1].toLowerCase()) {
|
|
@@ -705,26 +710,28 @@ async function normalizeItem(item, upstream, signal) {
|
|
|
705
710
|
};
|
|
706
711
|
}
|
|
707
712
|
const budget = requestSignal(signal, IMAGE_FETCH_TIMEOUT_MS);
|
|
708
|
-
let response;
|
|
709
713
|
try {
|
|
710
|
-
response
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
714
|
+
let response;
|
|
715
|
+
try {
|
|
716
|
+
response = await fetch(url, {
|
|
717
|
+
...isPresignedUrl(url) || upstream.apiKey === "" ? {} : { headers: { authorization: `Bearer ${upstream.apiKey}` } },
|
|
718
|
+
signal: budget.signal
|
|
719
|
+
});
|
|
720
|
+
} catch (error) {
|
|
721
|
+
throw new ImageGenError(`failed to fetch the generated image url: ${error instanceof Error ? error.message : String(error)}`);
|
|
722
|
+
}
|
|
723
|
+
if (!response.ok) throw new ImageGenError(`failed to fetch the generated image url: HTTP ${response.status}`);
|
|
724
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
725
|
+
const contentType = response.headers.get("content-type");
|
|
726
|
+
const mime = detectImageMime(buffer) ?? (contentType !== null && contentType !== "" ? contentType.split(";")[0].trim() : mimeOfExtension(url) ?? "image/png");
|
|
727
|
+
return {
|
|
728
|
+
b64: buffer.toString("base64"),
|
|
729
|
+
mime,
|
|
730
|
+
revisedPrompt
|
|
731
|
+
};
|
|
716
732
|
} finally {
|
|
717
733
|
budget.dispose();
|
|
718
734
|
}
|
|
719
|
-
if (!response.ok) throw new ImageGenError(`failed to fetch the generated image url: HTTP ${response.status}`);
|
|
720
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
721
|
-
const contentType = response.headers.get("content-type");
|
|
722
|
-
const mime = detectImageMime(buffer) ?? (contentType !== null && contentType !== "" ? contentType.split(";")[0].trim() : mimeOfExtension(url) ?? "image/png");
|
|
723
|
-
return {
|
|
724
|
-
b64: buffer.toString("base64"),
|
|
725
|
-
mime,
|
|
726
|
-
revisedPrompt
|
|
727
|
-
};
|
|
728
735
|
}
|
|
729
736
|
/** Expand a provider image item whose URL may be a string or an array. */
|
|
730
737
|
function imageItemsOf(value) {
|
|
@@ -821,43 +828,45 @@ async function pollAsyncTask(baseUrl, upstream, taskId, signal) {
|
|
|
821
828
|
while (Date.now() < deadline) {
|
|
822
829
|
const remaining = deadline - Date.now();
|
|
823
830
|
const budget = requestSignal(signal, Math.min(ASYNC_POLL_REQUEST_TIMEOUT_MS, remaining));
|
|
824
|
-
let response;
|
|
825
831
|
try {
|
|
826
|
-
response
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
832
|
+
let response;
|
|
833
|
+
try {
|
|
834
|
+
response = await fetch(`${baseUrl}/tasks/${encodeURIComponent(taskId)}`, {
|
|
835
|
+
method: "GET",
|
|
836
|
+
headers: { authorization: `Bearer ${upstream.apiKey.trim()}` },
|
|
837
|
+
signal: budget.signal
|
|
838
|
+
});
|
|
839
|
+
} catch (error) {
|
|
840
|
+
if (signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
|
|
841
|
+
if (isBudgetTimeout(error)) throw new ImageGenError("上游异步任务轮询超时", "upstream-timeout");
|
|
842
|
+
throw new ImageGenError(`无法轮询上游异步任务:${error instanceof Error ? error.message : String(error)}`, "upstream-unreachable");
|
|
843
|
+
}
|
|
844
|
+
let payload;
|
|
845
|
+
try {
|
|
846
|
+
payload = await response.json();
|
|
847
|
+
} catch (error) {
|
|
848
|
+
if (isBudgetTimeout(error)) throw new ImageGenError("上游异步任务轮询超时", "upstream-timeout");
|
|
849
|
+
throw new ImageGenError(`上游任务接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
|
|
850
|
+
}
|
|
851
|
+
if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(asyncErrorMessage(payload, `上游任务轮询失败(HTTP ${response.status})`), "upstream-rejected");
|
|
852
|
+
const record = payload;
|
|
853
|
+
const data = record.data;
|
|
854
|
+
const statusRecord = Array.isArray(data) ? data[0] : data !== null && typeof data === "object" ? data : record;
|
|
855
|
+
const statusValue = statusRecord !== null && typeof statusRecord === "object" ? statusRecord.status : void 0;
|
|
856
|
+
const status = typeof statusValue === "string" ? statusValue.toLowerCase() : "";
|
|
857
|
+
if (ASYNC_FAILED_STATUSES.has(status)) throw new ImageGenError(asyncErrorMessage(payload, `上游异步任务失败(${status || "unknown"})`), "upstream-rejected");
|
|
858
|
+
const nested = statusRecord !== null && typeof statusRecord === "object" ? statusRecord : record;
|
|
859
|
+
const result = nested.result ?? (nested.output !== null && typeof nested.output === "object" ? nested.output.result : void 0) ?? record.result;
|
|
860
|
+
const images = (result !== null && typeof result === "object" ? result : void 0)?.images ?? nested.images ?? record.images;
|
|
861
|
+
if (ASYNC_COMPLETED_STATUSES.has(status) || images !== void 0) {
|
|
862
|
+
const items = Array.isArray(images) ? images.flatMap(imageItemsOf) : imageItemsOf(images);
|
|
863
|
+
if (items.length > 0) return items;
|
|
864
|
+
if (ASYNC_COMPLETED_STATUSES.has(status)) throw new ImageGenError("上游异步任务完成但没有图片结果", "upstream-empty");
|
|
865
|
+
}
|
|
866
|
+
if (status !== "" && !ASYNC_PENDING_STATUSES.has(status) && !ASYNC_COMPLETED_STATUSES.has(status)) throw new ImageGenError(`上游返回了未知异步任务状态:${status}`, "upstream-invalid");
|
|
836
867
|
} finally {
|
|
837
868
|
budget.dispose();
|
|
838
869
|
}
|
|
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
870
|
await waitForPoll(Math.min(delay, Math.max(1, deadline - Date.now())), signal);
|
|
862
871
|
delay = Math.min(5e3, delay * 2);
|
|
863
872
|
}
|
|
@@ -872,15 +881,24 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
872
881
|
let body;
|
|
873
882
|
if (request.mode === "edit") {
|
|
874
883
|
if (typeof request.image !== "string" || request.image === "") throw new ImageGenError("图生图需要上传参考图片", "edit-image-missing");
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
+
const decodeReference = (dataUrl) => {
|
|
885
|
+
const parsed = parseDataUrl(dataUrl);
|
|
886
|
+
if (parsed === void 0) throw new ImageGenError("参考图片格式无效", "edit-image-invalid");
|
|
887
|
+
let bytes;
|
|
888
|
+
try {
|
|
889
|
+
bytes = Buffer.from(parsed.base64, "base64");
|
|
890
|
+
} catch {
|
|
891
|
+
throw new ImageGenError("参考图片数据无法解码", "edit-image-invalid");
|
|
892
|
+
}
|
|
893
|
+
if (bytes.byteLength > MAX_EDIT_IMAGE_BYTES) throw new ImageGenError("参考图片超过 10MB 上限", "edit-image-too-large");
|
|
894
|
+
return {
|
|
895
|
+
bytes,
|
|
896
|
+
mime: parsed.mime,
|
|
897
|
+
filename: `reference.${extensionOf$3(parsed.mime)}`
|
|
898
|
+
};
|
|
899
|
+
};
|
|
900
|
+
const primary = decodeReference(request.image);
|
|
901
|
+
const extras = (request.images ?? []).filter((img) => typeof img === "string" && img !== "").slice(0, 4).map(decodeReference);
|
|
884
902
|
if (isGrokImagine(params.model)) {
|
|
885
903
|
headers["content-type"] = "application/json";
|
|
886
904
|
body = JSON.stringify({
|
|
@@ -895,7 +913,7 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
895
913
|
});
|
|
896
914
|
} else if (isNanoBanana(params.model)) {
|
|
897
915
|
const form = new FormData();
|
|
898
|
-
form.append("image", new Blob([bytes], { type:
|
|
916
|
+
form.append("image", new Blob([primary.bytes], { type: primary.mime }), primary.filename);
|
|
899
917
|
form.append("prompt", request.prompt);
|
|
900
918
|
form.append("model", params.model);
|
|
901
919
|
if (params.aspect_ratio !== void 0) form.append("aspect_ratio", params.aspect_ratio);
|
|
@@ -906,14 +924,15 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
906
924
|
body = JSON.stringify({
|
|
907
925
|
model: params.model,
|
|
908
926
|
prompt: request.prompt,
|
|
909
|
-
image: [request.image],
|
|
927
|
+
image: [request.image, ...(request.images ?? []).filter((img) => typeof img === "string" && img !== "").slice(0, 4)],
|
|
910
928
|
...params.size !== void 0 ? { size: params.size } : {},
|
|
911
929
|
...params.resolution !== void 0 ? { resolution: params.resolution } : {},
|
|
912
930
|
response_format: isVolcSeedream(params.model) ? "url" : "b64_json"
|
|
913
931
|
});
|
|
914
932
|
} else {
|
|
915
933
|
const form = new FormData();
|
|
916
|
-
form.append("image", new Blob([bytes], { type:
|
|
934
|
+
if (extras.length > 0) for (const [index, reference] of [primary, ...extras].entries()) form.append("image[]", new Blob([reference.bytes], { type: reference.mime }), `reference-${index}.${extensionOf$3(reference.mime)}`);
|
|
935
|
+
else form.append("image", new Blob([primary.bytes], { type: primary.mime }), primary.filename);
|
|
917
936
|
form.append("prompt", request.prompt);
|
|
918
937
|
form.append("model", params.model);
|
|
919
938
|
if (params.size !== void 0) form.append("size", params.size);
|
|
@@ -929,39 +948,43 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
929
948
|
});
|
|
930
949
|
}
|
|
931
950
|
const budget = requestSignal(signal, UPSTREAM_TIMEOUT_MS);
|
|
932
|
-
let response;
|
|
933
951
|
try {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
952
|
+
let response;
|
|
953
|
+
try {
|
|
954
|
+
const endpoint = request.mode === "edit" && !isSeedream(params.model) ? "/images/edits" : "/images/generations";
|
|
955
|
+
response = await fetch(`${baseUrl}${endpoint}`, {
|
|
956
|
+
method: "POST",
|
|
957
|
+
headers,
|
|
958
|
+
body,
|
|
959
|
+
signal: budget.signal
|
|
960
|
+
});
|
|
961
|
+
} catch (error) {
|
|
962
|
+
if (isBudgetTimeout(error)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
|
|
963
|
+
if (signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
|
|
964
|
+
throw new ImageGenError(`无法连接上游接口:${error instanceof Error ? error.message : String(error)}`, "upstream-unreachable");
|
|
965
|
+
}
|
|
966
|
+
let payload;
|
|
967
|
+
try {
|
|
968
|
+
payload = await response.json();
|
|
969
|
+
} catch (error) {
|
|
970
|
+
if (isBudgetTimeout(error)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
|
|
971
|
+
if (signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
|
|
972
|
+
throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
|
|
973
|
+
}
|
|
974
|
+
if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(upstreamMessage(payload, response.status), "upstream-rejected");
|
|
975
|
+
const data = dataRecordsOf(payload);
|
|
976
|
+
if (data === void 0) throw new ImageGenError("上游响应缺少 data 数组", "upstream-invalid");
|
|
977
|
+
if (data.length === 0) throw new ImageGenError("上游返回了 0 张图片", "upstream-empty");
|
|
978
|
+
const asyncEntries = data.filter((entry) => typeof entry.task_id === "string" && entry.task_id.trim() !== "");
|
|
979
|
+
if (asyncEntries.length > 0) {
|
|
980
|
+
const asyncRecords = (await Promise.all(asyncEntries.map((entry) => pollAsyncTask(baseUrl, upstream, entry.task_id, signal)))).flat();
|
|
981
|
+
if (asyncRecords.length === 0) throw new ImageGenError("上游异步任务完成但没有图片结果", "upstream-empty");
|
|
982
|
+
return Promise.all(asyncRecords.flatMap(imageItemsOf).map((item) => normalizeItem(item, upstream, signal)));
|
|
983
|
+
}
|
|
984
|
+
return Promise.all(data.flatMap(imageItemsOf).map((item) => normalizeItem(item, upstream, signal)));
|
|
945
985
|
} finally {
|
|
946
986
|
budget.dispose();
|
|
947
987
|
}
|
|
948
|
-
let payload;
|
|
949
|
-
try {
|
|
950
|
-
payload = await response.json();
|
|
951
|
-
} catch {
|
|
952
|
-
throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
|
|
953
|
-
}
|
|
954
|
-
if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(upstreamMessage(payload, response.status), "upstream-rejected");
|
|
955
|
-
const data = dataRecordsOf(payload);
|
|
956
|
-
if (data === void 0) throw new ImageGenError("上游响应缺少 data 数组", "upstream-invalid");
|
|
957
|
-
if (data.length === 0) throw new ImageGenError("上游返回了 0 张图片", "upstream-empty");
|
|
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
988
|
}
|
|
966
989
|
/**
|
|
967
990
|
* Qwen-Image (DashScope native multimodal-generation): one chat-style request
|
|
@@ -995,50 +1018,54 @@ async function generateQwenImage(baseUrl, upstream, request, options) {
|
|
|
995
1018
|
}
|
|
996
1019
|
};
|
|
997
1020
|
const budget = requestSignal(options.signal, UPSTREAM_TIMEOUT_MS);
|
|
998
|
-
let response;
|
|
999
1021
|
try {
|
|
1000
|
-
response
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1022
|
+
let response;
|
|
1023
|
+
try {
|
|
1024
|
+
response = await fetch(`${baseUrl}/services/aigc/multimodal-generation/generation`, {
|
|
1025
|
+
method: "POST",
|
|
1026
|
+
headers: {
|
|
1027
|
+
authorization: `Bearer ${upstream.apiKey.trim()}`,
|
|
1028
|
+
"content-type": "application/json"
|
|
1029
|
+
},
|
|
1030
|
+
body: JSON.stringify(body),
|
|
1031
|
+
signal: budget.signal
|
|
1032
|
+
});
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
if (isBudgetTimeout(error)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
|
|
1035
|
+
if (options.signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
|
|
1036
|
+
throw new ImageGenError(`无法连接上游接口:${error instanceof Error ? error.message : String(error)}`, "upstream-unreachable");
|
|
1037
|
+
}
|
|
1038
|
+
let payload;
|
|
1039
|
+
try {
|
|
1040
|
+
payload = await response.json();
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
if (isBudgetTimeout(error)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
|
|
1043
|
+
if (options.signal?.aborted === true) throw new ImageGenError("任务已取消", "cancelled");
|
|
1044
|
+
throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
|
|
1045
|
+
}
|
|
1046
|
+
if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(upstreamMessage(payload, response.status), "upstream-rejected");
|
|
1047
|
+
const output = payload.output;
|
|
1048
|
+
const choices = output !== void 0 && Array.isArray(output.choices) ? output.choices : [];
|
|
1049
|
+
const urls = [];
|
|
1050
|
+
for (const choice of choices) {
|
|
1051
|
+
const message = choice !== null && typeof choice === "object" ? choice.message : void 0;
|
|
1052
|
+
const items = message !== null && typeof message === "object" && Array.isArray(message.content) ? message.content : [];
|
|
1053
|
+
for (const item of items) if (item !== null && typeof item === "object") {
|
|
1054
|
+
const image = item.image;
|
|
1055
|
+
if (typeof image === "string" && image !== "") urls.push(image);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (urls.length === 0) throw new ImageGenError("上游响应缺少图片内容", "upstream-empty");
|
|
1059
|
+
return { images: await Promise.all(urls.map(async (url) => {
|
|
1060
|
+
const normalized = await normalizeItem({ url }, upstream);
|
|
1061
|
+
return {
|
|
1062
|
+
b64: normalized.b64,
|
|
1063
|
+
mime: normalized.mime
|
|
1064
|
+
};
|
|
1065
|
+
})) };
|
|
1013
1066
|
} finally {
|
|
1014
1067
|
budget.dispose();
|
|
1015
1068
|
}
|
|
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
|
-
})) };
|
|
1042
1069
|
}
|
|
1043
1070
|
/**
|
|
1044
1071
|
* Forward one generate request to the configured endpoint. The requested image
|
|
@@ -1161,6 +1188,17 @@ async function testStorage(config) {
|
|
|
1161
1188
|
};
|
|
1162
1189
|
}
|
|
1163
1190
|
//#endregion
|
|
1191
|
+
//#region src/image-storage-path.ts
|
|
1192
|
+
const DEFAULT_ROOT = path.join(process.env.DSH_HOME?.trim() || path.join(homedir(), ".dsh"), "dsh-imagegen");
|
|
1193
|
+
let root = DEFAULT_ROOT;
|
|
1194
|
+
function imageDataRoot() {
|
|
1195
|
+
return root;
|
|
1196
|
+
}
|
|
1197
|
+
function setImageDataRoot(value) {
|
|
1198
|
+
const trimmed = value?.trim();
|
|
1199
|
+
root = trimmed === void 0 || trimmed === "" ? DEFAULT_ROOT : path.resolve(trimmed);
|
|
1200
|
+
}
|
|
1201
|
+
//#endregion
|
|
1164
1202
|
//#region src/history-store.ts
|
|
1165
1203
|
/**
|
|
1166
1204
|
* Host-persisted generation history: images are stored as individual files
|
|
@@ -1171,9 +1209,15 @@ async function testStorage(config) {
|
|
|
1171
1209
|
*
|
|
1172
1210
|
* Framework-free (node:fs only) so the route layer can drive it directly.
|
|
1173
1211
|
*/
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1212
|
+
function historyDir() {
|
|
1213
|
+
return imageDataRoot();
|
|
1214
|
+
}
|
|
1215
|
+
function indexPath$1() {
|
|
1216
|
+
return path.join(historyDir(), "index.json");
|
|
1217
|
+
}
|
|
1218
|
+
function imagesDir$1() {
|
|
1219
|
+
return path.join(historyDir(), "images");
|
|
1220
|
+
}
|
|
1177
1221
|
let pendingMutation$1 = Promise.resolve();
|
|
1178
1222
|
function mutateHistory(operation) {
|
|
1179
1223
|
const next = pendingMutation$1.then(operation, operation);
|
|
@@ -1206,12 +1250,12 @@ function safeId$2(id) {
|
|
|
1206
1250
|
}
|
|
1207
1251
|
/** Ensure the storage directories exist. */
|
|
1208
1252
|
async function ensureDirs$1() {
|
|
1209
|
-
await promises.mkdir(
|
|
1253
|
+
await promises.mkdir(imagesDir$1(), { recursive: true });
|
|
1210
1254
|
}
|
|
1211
1255
|
/** Read the index, tolerating a missing/corrupt file. */
|
|
1212
1256
|
async function readIndex$1() {
|
|
1213
1257
|
try {
|
|
1214
|
-
const raw = await promises.readFile(
|
|
1258
|
+
const raw = await promises.readFile(indexPath$1(), "utf8");
|
|
1215
1259
|
const parsed = JSON.parse(raw);
|
|
1216
1260
|
if (parsed === null || typeof parsed !== "object") return [];
|
|
1217
1261
|
const entries = parsed.entries;
|
|
@@ -1225,9 +1269,9 @@ async function readIndex$1() {
|
|
|
1225
1269
|
async function writeIndex$1(entries) {
|
|
1226
1270
|
await ensureDirs$1();
|
|
1227
1271
|
const payload = { entries };
|
|
1228
|
-
const tmp = `${
|
|
1272
|
+
const tmp = `${indexPath$1()}.tmp-${process.pid}`;
|
|
1229
1273
|
await promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
1230
|
-
await promises.rename(tmp,
|
|
1274
|
+
await promises.rename(tmp, indexPath$1());
|
|
1231
1275
|
}
|
|
1232
1276
|
/** Structural guard for a stored entry. */
|
|
1233
1277
|
function isStoredEntry$1(value) {
|
|
@@ -1242,7 +1286,7 @@ function isStoredEntry$1(value) {
|
|
|
1242
1286
|
/** Remove one entry's image files (best effort). */
|
|
1243
1287
|
async function removeEntryFiles$1(entry) {
|
|
1244
1288
|
for (const image of entry.images) try {
|
|
1245
|
-
await promises.rm(path.join(
|
|
1289
|
+
await promises.rm(path.join(imagesDir$1(), image.file), { force: true });
|
|
1246
1290
|
} catch {}
|
|
1247
1291
|
}
|
|
1248
1292
|
/** Project a stored entry onto the wire shape (image URLs). */
|
|
@@ -1289,8 +1333,8 @@ async function appendHistory(input) {
|
|
|
1289
1333
|
for (let index = 0; index < input.images.length; index++) {
|
|
1290
1334
|
const image = input.images[index];
|
|
1291
1335
|
const file = `${prefix}-${index}.${extensionOf$2(image.mime)}`;
|
|
1292
|
-
await promises.writeFile(path.join(
|
|
1293
|
-
notifyImageSaved("history", path.join(
|
|
1336
|
+
await promises.writeFile(path.join(imagesDir$1(), file), Buffer.from(image.b64, "base64"));
|
|
1337
|
+
notifyImageSaved("history", path.join(imagesDir$1(), file));
|
|
1294
1338
|
storedImages.push({
|
|
1295
1339
|
file,
|
|
1296
1340
|
mime: image.mime,
|
|
@@ -1355,7 +1399,7 @@ async function readHistoryImage(file) {
|
|
|
1355
1399
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*-[0-9]+\.(png|jpg|jpeg|webp|gif)$/.test(file)) return void 0;
|
|
1356
1400
|
try {
|
|
1357
1401
|
return {
|
|
1358
|
-
data: await promises.readFile(path.join(
|
|
1402
|
+
data: await promises.readFile(path.join(imagesDir$1(), file)),
|
|
1359
1403
|
mime: mimeOfFile$2(file)
|
|
1360
1404
|
};
|
|
1361
1405
|
} catch {
|
|
@@ -1372,7 +1416,6 @@ var GenerationTaskQueue = class {
|
|
|
1372
1416
|
controllers = /* @__PURE__ */ new Map();
|
|
1373
1417
|
listeners = /* @__PURE__ */ new Set();
|
|
1374
1418
|
running = 0;
|
|
1375
|
-
serialRunning = false;
|
|
1376
1419
|
constructor(run, concurrency = 1) {
|
|
1377
1420
|
this.run = run;
|
|
1378
1421
|
this.concurrency = concurrency;
|
|
@@ -1413,15 +1456,16 @@ var GenerationTaskQueue = class {
|
|
|
1413
1456
|
const previous = this.tasks.find((item) => item.id === id);
|
|
1414
1457
|
return previous === void 0 ? void 0 : this.submit(previous.request);
|
|
1415
1458
|
}
|
|
1459
|
+
/** Start queued tasks while capacity remains. Plain submissions and
|
|
1460
|
+
* comparison batches alike run in parallel up to the host-wide limit, so one
|
|
1461
|
+
* slow upstream can no longer hold back unrelated generations. */
|
|
1416
1462
|
drain() {
|
|
1417
1463
|
while (this.running < Math.max(1, this.concurrency)) {
|
|
1418
|
-
const task = this.tasks.find((item) => item.status === "queued"
|
|
1464
|
+
const task = this.tasks.find((item) => item.status === "queued");
|
|
1419
1465
|
if (task === void 0) return;
|
|
1420
1466
|
this.running += 1;
|
|
1421
|
-
if (task.request.comparisonId === void 0) this.serialRunning = true;
|
|
1422
1467
|
this.runTask(task).finally(() => {
|
|
1423
1468
|
this.running -= 1;
|
|
1424
|
-
if (task.request.comparisonId === void 0) this.serialRunning = false;
|
|
1425
1469
|
this.drain();
|
|
1426
1470
|
});
|
|
1427
1471
|
}
|
|
@@ -1541,10 +1585,15 @@ var ImageGenerationRuntime = class {
|
|
|
1541
1585
|
* Framework-free (node:fs + node:crypto only) so the route layer can drive it
|
|
1542
1586
|
* directly.
|
|
1543
1587
|
*/
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1588
|
+
function galleryDir() {
|
|
1589
|
+
return path.join(imageDataRoot(), "gallery");
|
|
1590
|
+
}
|
|
1591
|
+
function indexPath() {
|
|
1592
|
+
return path.join(galleryDir(), "index.json");
|
|
1593
|
+
}
|
|
1594
|
+
function imagesDir() {
|
|
1595
|
+
return path.join(galleryDir(), "images");
|
|
1596
|
+
}
|
|
1548
1597
|
let pendingMutation = Promise.resolve();
|
|
1549
1598
|
function mutateGallery(operation) {
|
|
1550
1599
|
const next = pendingMutation.then(operation, operation);
|
|
@@ -1583,12 +1632,12 @@ function fingerprint(input) {
|
|
|
1583
1632
|
}
|
|
1584
1633
|
/** Ensure the storage directories exist. */
|
|
1585
1634
|
async function ensureDirs() {
|
|
1586
|
-
await promises.mkdir(
|
|
1635
|
+
await promises.mkdir(imagesDir(), { recursive: true });
|
|
1587
1636
|
}
|
|
1588
1637
|
/** Read the index, tolerating a missing/corrupt file. */
|
|
1589
1638
|
async function readIndex() {
|
|
1590
1639
|
try {
|
|
1591
|
-
const raw = await promises.readFile(
|
|
1640
|
+
const raw = await promises.readFile(indexPath(), "utf8");
|
|
1592
1641
|
const parsed = JSON.parse(raw);
|
|
1593
1642
|
if (parsed === null || typeof parsed !== "object") return [];
|
|
1594
1643
|
const entries = parsed.entries;
|
|
@@ -1602,9 +1651,9 @@ async function readIndex() {
|
|
|
1602
1651
|
async function writeIndex(entries) {
|
|
1603
1652
|
await ensureDirs();
|
|
1604
1653
|
const payload = { entries };
|
|
1605
|
-
const tmp = `${
|
|
1654
|
+
const tmp = `${indexPath()}.tmp-${process.pid}`;
|
|
1606
1655
|
await promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
1607
|
-
await promises.rename(tmp,
|
|
1656
|
+
await promises.rename(tmp, indexPath());
|
|
1608
1657
|
}
|
|
1609
1658
|
/** Structural guard for a stored entry. */
|
|
1610
1659
|
function isStoredEntry(value) {
|
|
@@ -1619,7 +1668,7 @@ function isStoredEntry(value) {
|
|
|
1619
1668
|
/** Remove one entry's image files (best effort). */
|
|
1620
1669
|
async function removeEntryFiles(entry) {
|
|
1621
1670
|
for (const image of entry.images) try {
|
|
1622
|
-
await promises.rm(path.join(
|
|
1671
|
+
await promises.rm(path.join(imagesDir(), image.file), { force: true });
|
|
1623
1672
|
} catch {}
|
|
1624
1673
|
}
|
|
1625
1674
|
/** Project a stored entry onto the wire shape (image URLs). */
|
|
@@ -1675,8 +1724,8 @@ async function appendGallery(input) {
|
|
|
1675
1724
|
for (let index = 0; index < input.images.length; index++) {
|
|
1676
1725
|
const image = input.images[index];
|
|
1677
1726
|
const file = `${prefix}-${index}.${extensionOf$1(image.mime)}`;
|
|
1678
|
-
await promises.writeFile(path.join(
|
|
1679
|
-
notifyImageSaved("gallery", path.join(
|
|
1727
|
+
await promises.writeFile(path.join(imagesDir(), file), Buffer.from(image.b64, "base64"));
|
|
1728
|
+
notifyImageSaved("gallery", path.join(imagesDir(), file));
|
|
1680
1729
|
storedImages.push({
|
|
1681
1730
|
file,
|
|
1682
1731
|
mime: image.mime,
|
|
@@ -1752,7 +1801,7 @@ async function readGalleryImage(file) {
|
|
|
1752
1801
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*-[0-9]+\.(png|jpg|jpeg|webp|gif)$/.test(file)) return void 0;
|
|
1753
1802
|
try {
|
|
1754
1803
|
return {
|
|
1755
|
-
data: await promises.readFile(path.join(
|
|
1804
|
+
data: await promises.readFile(path.join(imagesDir(), file)),
|
|
1756
1805
|
mime: mimeOfFile$1(file)
|
|
1757
1806
|
};
|
|
1758
1807
|
} catch {
|
|
@@ -1762,11 +1811,6 @@ async function readGalleryImage(file) {
|
|
|
1762
1811
|
//#endregion
|
|
1763
1812
|
//#region src/canvas-store.ts
|
|
1764
1813
|
/** 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
1814
|
var CanvasConflictError = class extends Error {
|
|
1771
1815
|
code = "canvas-conflict";
|
|
1772
1816
|
constructor(message = "画布已在其他窗口更新,请重新加载后再保存。") {
|
|
@@ -1850,7 +1894,7 @@ function isNode(value) {
|
|
|
1850
1894
|
function isDocument(value) {
|
|
1851
1895
|
if (value === null || typeof value !== "object") return false;
|
|
1852
1896
|
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);
|
|
1897
|
+
return document.version === 2 && typeof document.id === "string" && typeof document.title === "string" && typeof document.revision === "number" && document.viewport !== null && typeof document.viewport === "object" && typeof document.viewport.x === "number" && typeof document.viewport.y === "number" && typeof document.viewport.k === "number" && (document.background === "dots" || document.background === "lines" || document.background === "diagonal" || document.background === "checker" || document.background === "blank" || document.background === "image") && (document.backgroundImage === void 0 || typeof document.backgroundImage === "string") && Array.isArray(document.nodes) && document.nodes.every(isNode) && Array.isArray(document.connections);
|
|
1854
1898
|
}
|
|
1855
1899
|
/** Upgrade a v1 (image/text/annotation + edges) document to the v2 node-graph model.
|
|
1856
1900
|
* Images and text notes keep their geometry; annotation prompt cards become plain
|
|
@@ -1970,17 +2014,17 @@ function serialize(operation) {
|
|
|
1970
2014
|
}
|
|
1971
2015
|
var CanvasStore = class {
|
|
1972
2016
|
root;
|
|
1973
|
-
constructor(root
|
|
2017
|
+
constructor(root) {
|
|
1974
2018
|
this.root = root;
|
|
1975
2019
|
}
|
|
1976
2020
|
pagesDir() {
|
|
1977
|
-
return path.join(this.root, "pages");
|
|
2021
|
+
return path.join(this.root ?? path.join(imageDataRoot(), "canvas"), "pages");
|
|
1978
2022
|
}
|
|
1979
2023
|
assetsDir() {
|
|
1980
|
-
return path.join(this.root, "assets");
|
|
2024
|
+
return path.join(this.root ?? path.join(imageDataRoot(), "canvas"), "assets");
|
|
1981
2025
|
}
|
|
1982
2026
|
indexPath() {
|
|
1983
|
-
return path.join(this.root, "index.json");
|
|
2027
|
+
return path.join(this.root ?? path.join(imageDataRoot(), "canvas"), "index.json");
|
|
1984
2028
|
}
|
|
1985
2029
|
async ensure() {
|
|
1986
2030
|
await promises.mkdir(this.pagesDir(), { recursive: true });
|
|
@@ -2844,6 +2888,7 @@ function parseGenerateRequest(body) {
|
|
|
2844
2888
|
n: typeof body.n === "number" ? body.n : 1,
|
|
2845
2889
|
detail: typeof body.detail === "string" ? body.detail : "",
|
|
2846
2890
|
...typeof body.image === "string" && body.image !== "" ? { image: body.image } : {},
|
|
2891
|
+
...Array.isArray(body.images) ? { images: body.images.filter((item) => typeof item === "string" && item !== "").slice(0, 4) } : {},
|
|
2847
2892
|
...typeof body.refName === "string" && body.refName !== "" ? { refName: body.refName } : {},
|
|
2848
2893
|
...typeof body.channelId === "string" && body.channelId !== "" ? { channelId: body.channelId } : {},
|
|
2849
2894
|
...typeof body.comparisonId === "string" && body.comparisonId !== "" ? { comparisonId: body.comparisonId } : {},
|
|
@@ -4959,6 +5004,7 @@ const Config = z.object({
|
|
|
4959
5004
|
promptApiUrl: z.string().default(""),
|
|
4960
5005
|
promptApiKey: z.string().role("secret").default(""),
|
|
4961
5006
|
promptModel: z.string().default(""),
|
|
5007
|
+
localStoragePath: z.string().default(""),
|
|
4962
5008
|
storageEnabled: z.boolean().default(false),
|
|
4963
5009
|
storageEndpoint: z.string().default(""),
|
|
4964
5010
|
storageRegion: z.string().default(""),
|
|
@@ -5031,6 +5077,7 @@ function apply(ctx, config) {
|
|
|
5031
5077
|
let current = () => config ?? {};
|
|
5032
5078
|
const resolve = () => {
|
|
5033
5079
|
const value = current() ?? {};
|
|
5080
|
+
setImageDataRoot(value.localStoragePath);
|
|
5034
5081
|
let channels = normalizeChannels(value.channels);
|
|
5035
5082
|
const secrets = { ...value.channelSecrets ?? {} };
|
|
5036
5083
|
if (channels.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dickpy/dsh-imagegen",
|
|
3
3
|
"description": "AI image generation plugin for the dsh web GUI: text-to-image and image-to-image through configurable provider channels (gpt-image-2 / grok-imagine-image / nanobanana series / seedream-5.0-pro / dall-e-3, with native xAI Grok Imagine, Google Nano Banana and ByteDance Seedream request shaping), with per-channel model catalogs and a New Session / Image Generation tab entry opening a three-column studio beside the native conversation.",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|