dsh-image-gen 0.2.4 → 0.2.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.en.md +2 -2
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/lib/client.js +1812 -492
- package/lib/client.js.map +1 -1
- package/lib/index.js +394 -52
- package/lib/types/client/gallery-store.d.ts +21 -3
- package/lib/types/client/gallery-view.d.ts +105 -10
- package/lib/types/client/image-result-node.d.ts +18 -1
- package/lib/types/client/index.d.ts +3 -1
- package/lib/types/comfyui-workflow.d.ts +13 -2
- package/lib/types/comfyui.d.ts +22 -5
- package/lib/types/config.d.ts +15 -6
- package/lib/types/image-route.d.ts +8 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/shared.d.ts +33 -0
- package/lib/types/workspace-save.d.ts +21 -0
- package/package.json +7 -3
package/lib/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
2
|
-
import
|
|
2
|
+
import * as dshSettings from "@deepseek-ai/dsh-settings";
|
|
3
3
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
|
-
import { mkdir, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { lstat, mkdir, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
6
6
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
//#region lib/types/shared.js
|
|
9
9
|
/** Values shared by the Host and browser Bundle faces. */
|
|
10
10
|
/** Browser route used by the generated-image card. */
|
|
11
11
|
const IMAGE_ROUTE = "/plugins/dsh-image-gen/image";
|
|
12
|
+
/** Browser route used for deleting generated images and workspace files. */
|
|
13
|
+
const DELETE_ROUTE = "/plugins/dsh-image-gen/delete";
|
|
12
14
|
/** Namespace persisted through DSH Settings. */
|
|
13
15
|
const IMAGE_GENERATION_NAMESPACE = "image-generation";
|
|
14
16
|
/** Supported providers. */
|
|
@@ -26,11 +28,58 @@ const DEFAULT_SEEDREAM_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
|
|
|
26
28
|
const DEFAULT_DASHSCOPE_ENDPOINT = "https://dashscope.aliyuncs.com/api/v1";
|
|
27
29
|
const DEFAULT_COMFYUI_BASE_URL = "http://127.0.0.1:8188";
|
|
28
30
|
const DEFAULT_COMFYUI_TIMEOUT_MS = 3e5;
|
|
31
|
+
const DEFAULT_COMFYUI_WORKFLOW_LABEL = "API workflow";
|
|
29
32
|
/** Default model names. */
|
|
30
33
|
const DEFAULT_GOOGLE_MODEL = "gemini-3.1-flash-image";
|
|
31
34
|
const DEFAULT_OPENAI_MODEL = "gpt-image-2";
|
|
32
35
|
const DEFAULT_SEEDREAM_MODEL = "doubao-seedream-5-0-260128";
|
|
33
36
|
const DEFAULT_DASHSCOPE_MODEL = "qwen-image-3.0";
|
|
37
|
+
/** Named workflows, falling back to the legacy single-workflow fields when the list is empty. */
|
|
38
|
+
function resolveComfyUIWorkflows(source) {
|
|
39
|
+
const named = [];
|
|
40
|
+
for (const entry of source.comfyuiWorkflows ?? []) {
|
|
41
|
+
const name = typeof entry?.name === "string" ? entry.name.trim() : "";
|
|
42
|
+
const json = typeof entry?.json === "string" ? entry.json : "";
|
|
43
|
+
if (name.length > 0 && json.trim().length > 0) {
|
|
44
|
+
const presetPrompt = typeof entry.presetPrompt === "string" ? entry.presetPrompt.trim() : "";
|
|
45
|
+
named.push(presetPrompt.length > 0 ? {
|
|
46
|
+
name,
|
|
47
|
+
json,
|
|
48
|
+
presetPrompt
|
|
49
|
+
} : {
|
|
50
|
+
name,
|
|
51
|
+
json
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (named.length > 0) return named;
|
|
56
|
+
const legacyJson = typeof source.comfyuiWorkflowJson === "string" ? source.comfyuiWorkflowJson : "";
|
|
57
|
+
if (legacyJson.trim().length === 0) return [];
|
|
58
|
+
const legacyName = typeof source.comfyuiWorkflowName === "string" ? source.comfyuiWorkflowName.trim() : "";
|
|
59
|
+
return [{
|
|
60
|
+
name: legacyName.length > 0 ? legacyName : DEFAULT_COMFYUI_WORKFLOW_LABEL,
|
|
61
|
+
json: legacyJson
|
|
62
|
+
}];
|
|
63
|
+
}
|
|
64
|
+
/** The workflow ComfyUI calls use by default: the configured active name, else the first entry. */
|
|
65
|
+
function activeComfyUIWorkflow(source) {
|
|
66
|
+
const workflows = resolveComfyUIWorkflows(source);
|
|
67
|
+
if (workflows.length === 0) return void 0;
|
|
68
|
+
const activeName = typeof source.comfyuiActiveWorkflow === "string" ? source.comfyuiActiveWorkflow.trim() : "";
|
|
69
|
+
return workflows.find((workflow) => workflow.name === activeName) ?? workflows[0];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Combine a workflow's preset with the user prompt: preset first, user second,
|
|
73
|
+
* joined by one comma — never doubled when the preset already ends in a
|
|
74
|
+
* separator, and reduced to the non-empty side when the other is blank.
|
|
75
|
+
*/
|
|
76
|
+
function mergeComfyUIPrompt(preset, user) {
|
|
77
|
+
const presetText = typeof preset === "string" ? preset.trim().replace(/[,;\s]+$/, "") : "";
|
|
78
|
+
const userText = user.trim();
|
|
79
|
+
if (presetText.length === 0) return userText;
|
|
80
|
+
if (userText.length === 0) return presetText;
|
|
81
|
+
return `${presetText}, ${userText}`;
|
|
82
|
+
}
|
|
34
83
|
//#endregion
|
|
35
84
|
//#region lib/types/config.js
|
|
36
85
|
/** User-facing configuration for supported image providers. */
|
|
@@ -56,6 +105,12 @@ const Config = z.object({
|
|
|
56
105
|
dashscopeEndpoint: z.string().default(DEFAULT_DASHSCOPE_ENDPOINT),
|
|
57
106
|
dashscopeModel: z.string().default(DEFAULT_DASHSCOPE_MODEL),
|
|
58
107
|
comfyuiBaseURL: z.string().default(DEFAULT_COMFYUI_BASE_URL),
|
|
108
|
+
comfyuiWorkflows: z.array(z.object({
|
|
109
|
+
name: z.string(),
|
|
110
|
+
json: z.string(),
|
|
111
|
+
presetPrompt: z.string().default("")
|
|
112
|
+
})).default([]),
|
|
113
|
+
comfyuiActiveWorkflow: z.string().default(""),
|
|
59
114
|
comfyuiWorkflowJson: z.string().default(""),
|
|
60
115
|
comfyuiWorkflowName: z.string().default(""),
|
|
61
116
|
comfyuiTimeoutMs: z.number().min(1e3).max(36e5).default(DEFAULT_COMFYUI_TIMEOUT_MS),
|
|
@@ -86,13 +141,17 @@ function resolveProvider(config) {
|
|
|
86
141
|
endpoint: config.dashscopeEndpoint ?? "https://dashscope.aliyuncs.com/api/v1",
|
|
87
142
|
imageSize: "1024*1024"
|
|
88
143
|
};
|
|
89
|
-
case "comfyui":
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
144
|
+
case "comfyui": {
|
|
145
|
+
const workflows = resolveComfyUIWorkflows(config);
|
|
146
|
+
const workflow = activeComfyUIWorkflow(config);
|
|
147
|
+
return {
|
|
148
|
+
provider: "comfyui",
|
|
149
|
+
baseURL: config.comfyuiBaseURL ?? "http://127.0.0.1:8188",
|
|
150
|
+
workflows,
|
|
151
|
+
...workflow === void 0 ? {} : { workflow },
|
|
152
|
+
timeoutMs: config.comfyuiTimeoutMs ?? 3e5
|
|
153
|
+
};
|
|
154
|
+
}
|
|
96
155
|
case "google": return {
|
|
97
156
|
provider: "google",
|
|
98
157
|
apiKeyEnv: GOOGLE_API_KEY_ENV,
|
|
@@ -103,43 +162,82 @@ function resolveProvider(config) {
|
|
|
103
162
|
};
|
|
104
163
|
}
|
|
105
164
|
}
|
|
165
|
+
/** The workflow a ComfyUI call runs: the requested name when given, else the active one. */
|
|
166
|
+
function selectComfyUIWorkflow(active, requested) {
|
|
167
|
+
if (active.workflow === void 0) throw new Error("ComfyUI image generation requires an imported workflow; import one in Settings > Plugins > Image generation.");
|
|
168
|
+
if (typeof requested !== "string" || requested.trim().length === 0) return active.workflow;
|
|
169
|
+
const name = requested.trim();
|
|
170
|
+
const workflow = active.workflows.find((candidate) => candidate.name === name);
|
|
171
|
+
if (workflow === void 0) throw new Error(`No ComfyUI workflow named "${name}" is configured. Available workflows: ${active.workflows.map((entry) => entry.name).join(", ")}.`);
|
|
172
|
+
return workflow;
|
|
173
|
+
}
|
|
106
174
|
//#endregion
|
|
107
175
|
//#region lib/types/comfyui-workflow.js
|
|
108
176
|
/** Pure ComfyUI API-workflow validation and placeholder injection. */
|
|
109
177
|
const COMFYUI_PROMPT_PLACEHOLDER = "{{prompt}}";
|
|
110
178
|
const COMFYUI_SEED_PLACEHOLDER = "{{seed}}";
|
|
111
|
-
|
|
112
|
-
|
|
179
|
+
const COMFYUI_IMAGE_PLACEHOLDER = "{{image}}";
|
|
180
|
+
/** Legacy single-percent placeholders from early releases, still accepted. */
|
|
181
|
+
const LEGACY_PROMPT_PLACEHOLDER = "%prompt%";
|
|
182
|
+
const LEGACY_SEED_PLACEHOLDER = "%seed%";
|
|
183
|
+
const LEGACY_IMAGE_PLACEHOLDER = "%image%";
|
|
184
|
+
/** LoadImage-style input key that receives the uploaded source image name. */
|
|
185
|
+
const IMAGE_INPUT_KEY = "image";
|
|
186
|
+
/**
|
|
187
|
+
* Parse, clone, and inject one prompt plus an optional randomized seed.
|
|
188
|
+
*
|
|
189
|
+
* Prompt and seed placeholders are replaced inside any string so existing
|
|
190
|
+
* workflows that embed them in longer text keep working. The image
|
|
191
|
+
* placeholder is stricter: it only matches a dedicated `inputs.image` field
|
|
192
|
+
* whose value is exactly the placeholder, and at most one may exist, because
|
|
193
|
+
* the field must carry a single uploaded file name.
|
|
194
|
+
*/
|
|
195
|
+
function prepareComfyUIWorkflow(workflowJson, prompt, seed = randomSeed(), image) {
|
|
113
196
|
const workflow = parseWorkflow(workflowJson);
|
|
114
197
|
let promptReplacements = 0;
|
|
115
198
|
const inject = (value) => {
|
|
116
199
|
if (typeof value === "string") {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return prompt;
|
|
120
|
-
}
|
|
121
|
-
if (value === "{{seed}}") return seed;
|
|
122
|
-
if (value.includes("{{prompt}}")) {
|
|
200
|
+
let replaced = value;
|
|
201
|
+
if (replaced.includes("{{prompt}}") || replaced.includes(LEGACY_PROMPT_PLACEHOLDER)) {
|
|
123
202
|
promptReplacements += 1;
|
|
124
|
-
|
|
203
|
+
replaced = replaced.replaceAll(COMFYUI_PROMPT_PLACEHOLDER, prompt).replaceAll(LEGACY_PROMPT_PLACEHOLDER, prompt);
|
|
125
204
|
}
|
|
126
|
-
|
|
205
|
+
if (replaced === "{{seed}}" || replaced === LEGACY_SEED_PLACEHOLDER) return seed;
|
|
206
|
+
if (replaced.includes("{{seed}}") || replaced.includes(LEGACY_SEED_PLACEHOLDER)) replaced = replaced.replaceAll(COMFYUI_SEED_PLACEHOLDER, String(seed)).replaceAll(LEGACY_SEED_PLACEHOLDER, String(seed));
|
|
207
|
+
return replaced;
|
|
127
208
|
}
|
|
128
209
|
if (Array.isArray(value)) return value.map(inject);
|
|
129
210
|
if (!isRecord(value)) return value;
|
|
130
211
|
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, inject(child)]));
|
|
131
212
|
};
|
|
213
|
+
let imageInputs = 0;
|
|
132
214
|
const prepared = Object.fromEntries(Object.entries(workflow).map(([nodeId, value]) => {
|
|
133
215
|
const node = record$4(value);
|
|
134
216
|
const inputs = node === void 0 ? void 0 : record$4(node.inputs);
|
|
135
|
-
|
|
217
|
+
if (inputs === void 0) return [nodeId, value];
|
|
218
|
+
const nextInputs = inject(inputs);
|
|
219
|
+
if (isImagePlaceholder(nextInputs[IMAGE_INPUT_KEY])) {
|
|
220
|
+
imageInputs += 1;
|
|
221
|
+
if (image !== void 0) nextInputs[IMAGE_INPUT_KEY] = image;
|
|
222
|
+
}
|
|
223
|
+
return [nodeId, {
|
|
136
224
|
...node,
|
|
137
|
-
inputs:
|
|
225
|
+
inputs: nextInputs
|
|
138
226
|
}];
|
|
139
227
|
}));
|
|
140
228
|
if (promptReplacements === 0) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} in a text input`);
|
|
229
|
+
if (image === void 0) {
|
|
230
|
+
if (imageInputs > 0) throw new Error(`ComfyUI workflow contains an ${COMFYUI_IMAGE_PLACEHOLDER} image input, which requires edit_image with a source image`);
|
|
231
|
+
return prepared;
|
|
232
|
+
}
|
|
233
|
+
if (imageInputs === 0) throw new Error(`ComfyUI workflow must contain exactly one ${COMFYUI_IMAGE_PLACEHOLDER} image input to edit images`);
|
|
234
|
+
if (imageInputs > 1) throw new Error(`ComfyUI workflow must contain exactly one ${COMFYUI_IMAGE_PLACEHOLDER} image input; found ${String(imageInputs)}`);
|
|
141
235
|
return prepared;
|
|
142
236
|
}
|
|
237
|
+
/** Random seed in the 32-bit range ComfyUI samplers accept. */
|
|
238
|
+
function randomSeed() {
|
|
239
|
+
return Math.floor(Math.random() * 4294967296);
|
|
240
|
+
}
|
|
143
241
|
function parseWorkflow(workflowJson) {
|
|
144
242
|
if (workflowJson.trim().length === 0) throw new Error("Import a ComfyUI API workflow JSON file in Settings before generating");
|
|
145
243
|
if (new TextEncoder().encode(workflowJson).byteLength > 5242880) throw new Error("ComfyUI workflow file must be no larger than 5 MB");
|
|
@@ -153,11 +251,14 @@ function parseWorkflow(workflowJson) {
|
|
|
153
251
|
if (!Object.values(value).some((node) => {
|
|
154
252
|
const inputs = record$4(node)?.inputs;
|
|
155
253
|
return inputs !== void 0 && containsPromptPlaceholder(inputs);
|
|
156
|
-
})) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} in a text input`);
|
|
254
|
+
})) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} (or ${LEGACY_PROMPT_PLACEHOLDER}) in a text input`);
|
|
157
255
|
return value;
|
|
158
256
|
}
|
|
257
|
+
function isImagePlaceholder(value) {
|
|
258
|
+
return value === "{{image}}" || value === LEGACY_IMAGE_PLACEHOLDER;
|
|
259
|
+
}
|
|
159
260
|
function containsPromptPlaceholder(value) {
|
|
160
|
-
if (typeof value === "string") return value.includes(
|
|
261
|
+
if (typeof value === "string") return value.includes("{{prompt}}") || value.includes(LEGACY_PROMPT_PLACEHOLDER);
|
|
161
262
|
if (Array.isArray(value)) return value.some(containsPromptPlaceholder);
|
|
162
263
|
return isRecord(value) && Object.values(value).some(containsPromptPlaceholder);
|
|
163
264
|
}
|
|
@@ -167,19 +268,37 @@ function isRecord(value) {
|
|
|
167
268
|
function record$4(value) {
|
|
168
269
|
return isRecord(value) ? value : void 0;
|
|
169
270
|
}
|
|
170
|
-
function randomSeed() {
|
|
171
|
-
return Math.floor(Math.random() * 4294967296);
|
|
172
|
-
}
|
|
173
271
|
//#endregion
|
|
174
272
|
//#region lib/types/comfyui.js
|
|
175
273
|
const ERROR_LIMIT$3 = 4096;
|
|
176
274
|
const POLL_INTERVAL_MS = 500;
|
|
177
275
|
const MAX_HISTORY_BYTES = 16777216;
|
|
178
|
-
/** Run one ComfyUI workflow and return its first final image. */
|
|
276
|
+
/** Run one ComfyUI text-to-image workflow and return its first final image. */
|
|
179
277
|
async function generateComfyUIImage(input) {
|
|
278
|
+
const seed = randomSeed();
|
|
279
|
+
const workflow = prepareComfyUIWorkflow(input.workflowJson, input.prompt, seed);
|
|
280
|
+
return {
|
|
281
|
+
...await runJob(input, async (baseURL, signal) => {
|
|
282
|
+
return await downloadOutput(baseURL, await waitForOutput(baseURL, await submitWorkflow(baseURL, workflow, signal), signal), input.maxBytes, signal);
|
|
283
|
+
}),
|
|
284
|
+
seed
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** Upload one source image, run the workflow, and return its first final image. */
|
|
288
|
+
async function editComfyUIImage(input) {
|
|
289
|
+
const seed = randomSeed();
|
|
290
|
+
return {
|
|
291
|
+
...await runJob(input, async (baseURL, signal) => {
|
|
292
|
+
const imageName = await uploadSourceImage(baseURL, input.sourceImage, signal);
|
|
293
|
+
return await downloadOutput(baseURL, await waitForOutput(baseURL, await submitWorkflow(baseURL, prepareComfyUIWorkflow(input.workflowJson, input.prompt, seed, imageName), signal), signal), input.maxBytes, signal);
|
|
294
|
+
}),
|
|
295
|
+
seed
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/** Share abort forwarding, the single timeout, and error normalization. */
|
|
299
|
+
async function runJob(input, run) {
|
|
180
300
|
input.signal.throwIfAborted();
|
|
181
301
|
const baseURL = comfyUIBaseURL(input.baseURL);
|
|
182
|
-
const workflow = prepareComfyUIWorkflow(input.workflowJson, input.prompt);
|
|
183
302
|
const controller = new AbortController();
|
|
184
303
|
const forwardAbort = () => {
|
|
185
304
|
controller.abort(input.signal.reason);
|
|
@@ -189,7 +308,7 @@ async function generateComfyUIImage(input) {
|
|
|
189
308
|
controller.abort(/* @__PURE__ */ new Error("ComfyUI generation timed out"));
|
|
190
309
|
}, input.timeoutMs);
|
|
191
310
|
try {
|
|
192
|
-
return await
|
|
311
|
+
return await run(baseURL, controller.signal);
|
|
193
312
|
} catch (error) {
|
|
194
313
|
input.signal.throwIfAborted();
|
|
195
314
|
if (controller.signal.aborted) throw new Error(`ComfyUI generation timed out after ${String(input.timeoutMs)} ms`);
|
|
@@ -200,6 +319,33 @@ async function generateComfyUIImage(input) {
|
|
|
200
319
|
input.signal.removeEventListener("abort", forwardAbort);
|
|
201
320
|
}
|
|
202
321
|
}
|
|
322
|
+
/** Upload the source image and return the LoadImage-compatible name ComfyUI stored it under. */
|
|
323
|
+
async function uploadSourceImage(baseURL, image, signal) {
|
|
324
|
+
const filename = uploadFilename(image.mediaType);
|
|
325
|
+
const bytes = new Uint8Array(image.data.byteLength);
|
|
326
|
+
bytes.set(image.data);
|
|
327
|
+
const form = new FormData();
|
|
328
|
+
form.append("image", new Blob([bytes], { type: image.mediaType }), filename);
|
|
329
|
+
const response = await fetch(endpoint(baseURL, "upload/image"), {
|
|
330
|
+
method: "POST",
|
|
331
|
+
redirect: "error",
|
|
332
|
+
signal,
|
|
333
|
+
headers: { accept: "application/json" },
|
|
334
|
+
body: form
|
|
335
|
+
});
|
|
336
|
+
const text = await readBoundedText$3(response, ERROR_LIMIT$3);
|
|
337
|
+
if (!response.ok) throw new Error(`ComfyUI image upload failed (${response.status}): ${text}`);
|
|
338
|
+
const payload = parseJsonRecord(text, "ComfyUI /upload/image returned invalid JSON");
|
|
339
|
+
if (typeof payload.name !== "string" || payload.name.length === 0) throw new Error(`ComfyUI /upload/image returned no name: ${text}`);
|
|
340
|
+
const subfolder = typeof payload.subfolder === "string" ? payload.subfolder : "";
|
|
341
|
+
return subfolder.length > 0 ? `${subfolder}/${payload.name}` : payload.name;
|
|
342
|
+
}
|
|
343
|
+
/** LoadImage-style inputs need a file name with a decodable extension. */
|
|
344
|
+
function uploadFilename(mediaType) {
|
|
345
|
+
const extension = mediaType === "image/png" ? "png" : mediaType === "image/jpeg" ? "jpg" : mediaType === "image/webp" ? "webp" : void 0;
|
|
346
|
+
if (extension === void 0) throw new Error(`ComfyUI edit_image accepts PNG, JPEG, or WebP source images; got ${mediaType}`);
|
|
347
|
+
return `dsh-image-gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.${extension}`;
|
|
348
|
+
}
|
|
203
349
|
async function submitWorkflow(baseURL, workflow, signal) {
|
|
204
350
|
const response = await fetch(endpoint(baseURL, "prompt"), {
|
|
205
351
|
method: "POST",
|
|
@@ -785,7 +931,7 @@ function record$1(value) {
|
|
|
785
931
|
}
|
|
786
932
|
//#endregion
|
|
787
933
|
//#region lib/types/image-route.js
|
|
788
|
-
const MAX_BODY_BYTES =
|
|
934
|
+
const MAX_BODY_BYTES = 65536;
|
|
789
935
|
/** Serve one verified durable image reference to a same-origin browser request. */
|
|
790
936
|
async function serveImage(req, res, deps) {
|
|
791
937
|
if (req.method !== "POST") return jsonError(res, 405, "method-not-allowed");
|
|
@@ -814,6 +960,54 @@ async function serveImage(req, res, deps) {
|
|
|
814
960
|
jsonError(res, 404, "image-unavailable");
|
|
815
961
|
}
|
|
816
962
|
}
|
|
963
|
+
/** Safely delete one or more generated image files from the workspace disk. */
|
|
964
|
+
async function serveDelete(req, res, deps) {
|
|
965
|
+
if (req.method !== "POST") return jsonError(res, 405, "method-not-allowed");
|
|
966
|
+
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError(res, 415, "json-required");
|
|
967
|
+
const origin = req.headers.origin;
|
|
968
|
+
const host = req.headers.host;
|
|
969
|
+
if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError(res, 403, "origin-rejected");
|
|
970
|
+
let body;
|
|
971
|
+
try {
|
|
972
|
+
body = JSON.parse(await readBody(req));
|
|
973
|
+
} catch {
|
|
974
|
+
return jsonError(res, 400, "invalid-request");
|
|
975
|
+
}
|
|
976
|
+
const rawPaths = typeof body === "object" && body !== null && "paths" in body && Array.isArray(body.paths) ? body.paths : [];
|
|
977
|
+
const rawAttachmentIds = typeof body === "object" && body !== null && "attachmentIds" in body && Array.isArray(body.attachmentIds) ? body.attachmentIds : [];
|
|
978
|
+
const deletedFiles = [];
|
|
979
|
+
const failedFiles = [];
|
|
980
|
+
for (const p of rawPaths) if (typeof p === "string" && p.trim()) try {
|
|
981
|
+
if (await deps.deleteWorkspaceImage(p)) deletedFiles.push(p);
|
|
982
|
+
else failedFiles.push({
|
|
983
|
+
path: p,
|
|
984
|
+
error: "File not found or rejected by safety checks"
|
|
985
|
+
});
|
|
986
|
+
} catch (err) {
|
|
987
|
+
failedFiles.push({
|
|
988
|
+
path: p,
|
|
989
|
+
error: err instanceof Error ? err.message : String(err)
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
for (const id of rawAttachmentIds) if (typeof id === "string" && id.trim()) try {
|
|
993
|
+
if (await deps.deleteWorkspaceImageByAttachmentId(id)) deletedFiles.push(id);
|
|
994
|
+
} catch (err) {
|
|
995
|
+
failedFiles.push({
|
|
996
|
+
path: id,
|
|
997
|
+
error: err instanceof Error ? err.message : String(err)
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
res.writeHead(200, {
|
|
1001
|
+
"content-type": "application/json",
|
|
1002
|
+
"cache-control": "no-store"
|
|
1003
|
+
});
|
|
1004
|
+
res.end(JSON.stringify({
|
|
1005
|
+
ok: failedFiles.length === 0,
|
|
1006
|
+
deletedCount: deletedFiles.length,
|
|
1007
|
+
deletedFiles,
|
|
1008
|
+
failedFiles
|
|
1009
|
+
}));
|
|
1010
|
+
}
|
|
817
1011
|
/** Validate the persisted reference carried by a tool presentation. */
|
|
818
1012
|
function imageAttachmentFromMeta(meta) {
|
|
819
1013
|
const value = record(meta);
|
|
@@ -1231,6 +1425,88 @@ async function saveImageToWorkspace(options) {
|
|
|
1231
1425
|
}
|
|
1232
1426
|
return target;
|
|
1233
1427
|
}
|
|
1428
|
+
/** Regex pattern strictly matching generated image file names (e.g. image-01234567.png). */
|
|
1429
|
+
const GENERATED_IMAGE_NAME_REGEX = /^image-[0-9a-f]{8,}\.(png|jpg|jpeg|webp|gif)$/i;
|
|
1430
|
+
/**
|
|
1431
|
+
* Safely delete a generated image file from workspace disk by its path.
|
|
1432
|
+
* Enforces strict safety gates:
|
|
1433
|
+
* 1. Base name must match generated image pattern (`image-<hex>.<ext>`) to avoid touching user files.
|
|
1434
|
+
* 2. Path must be a regular file and NOT a symbolic link.
|
|
1435
|
+
* 3. Real canonical path's basename must also match the pattern.
|
|
1436
|
+
* 4. Real canonical path must reside within an allowed workspace root (or process.cwd()).
|
|
1437
|
+
*/
|
|
1438
|
+
async function deleteImageFromWorkspace(filePath, allowedWorkspaceRoots) {
|
|
1439
|
+
const resolved = resolve(filePath);
|
|
1440
|
+
const base = resolved.split(/[/\\]/).pop() ?? "";
|
|
1441
|
+
if (!GENERATED_IMAGE_NAME_REGEX.test(base)) return false;
|
|
1442
|
+
try {
|
|
1443
|
+
const fileLstat = await lstat(resolved);
|
|
1444
|
+
if (fileLstat.isSymbolicLink() || !fileLstat.isFile()) return false;
|
|
1445
|
+
} catch (err) {
|
|
1446
|
+
if (err?.code === "ENOENT") return true;
|
|
1447
|
+
throw err;
|
|
1448
|
+
}
|
|
1449
|
+
try {
|
|
1450
|
+
const realFile = await realpath(resolved);
|
|
1451
|
+
const realBase = realFile.split(/[/\\]/).pop() ?? "";
|
|
1452
|
+
if (!GENERATED_IMAGE_NAME_REGEX.test(realBase)) return false;
|
|
1453
|
+
const rootsToCheck = /* @__PURE__ */ new Set();
|
|
1454
|
+
if (allowedWorkspaceRoots) {
|
|
1455
|
+
for (const r of allowedWorkspaceRoots) if (typeof r === "string" && r.trim()) rootsToCheck.add(resolve(r));
|
|
1456
|
+
}
|
|
1457
|
+
rootsToCheck.add(resolve(process.cwd()));
|
|
1458
|
+
let withinAllowed = false;
|
|
1459
|
+
for (const root of rootsToCheck) try {
|
|
1460
|
+
if (containsPath(await realpath(root), realFile)) {
|
|
1461
|
+
withinAllowed = true;
|
|
1462
|
+
break;
|
|
1463
|
+
}
|
|
1464
|
+
} catch {}
|
|
1465
|
+
if (!withinAllowed) return false;
|
|
1466
|
+
await unlink(realFile);
|
|
1467
|
+
return true;
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
if (err?.code === "ENOENT") return true;
|
|
1470
|
+
throw err;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Safely find and delete a generated image file by its attachment ID within a single workspace root.
|
|
1475
|
+
* Strictly checks sha256:64hex format and avoids scanning unrelated workspaces.
|
|
1476
|
+
* Only used as fallback for historical images where savedTo was not persisted.
|
|
1477
|
+
*/
|
|
1478
|
+
async function deleteImageByAttachmentIdFromWorkspace(attachmentId, options = {}) {
|
|
1479
|
+
const match = attachmentId.match(/^sha256:([0-9a-f]{64})$/i);
|
|
1480
|
+
if (!match || !match[1]) return false;
|
|
1481
|
+
const prefix = match[1].slice(0, 8);
|
|
1482
|
+
const rootsToCheck = /* @__PURE__ */ new Set();
|
|
1483
|
+
if (options.workspaceRoot && options.workspaceRoot.trim()) rootsToCheck.add(resolve(options.workspaceRoot.trim()));
|
|
1484
|
+
else if (options.allowedWorkspaceRoots) {
|
|
1485
|
+
for (const r of options.allowedWorkspaceRoots) if (typeof r === "string" && r.trim()) rootsToCheck.add(resolve(r));
|
|
1486
|
+
}
|
|
1487
|
+
rootsToCheck.add(resolve(process.cwd()));
|
|
1488
|
+
const candidateDirs = /* @__PURE__ */ new Set();
|
|
1489
|
+
if (options.folder && options.folder.trim()) candidateDirs.add(options.folder.trim());
|
|
1490
|
+
candidateDirs.add("dsh-image-gen");
|
|
1491
|
+
candidateDirs.add("image");
|
|
1492
|
+
candidateDirs.add("images");
|
|
1493
|
+
candidateDirs.add("");
|
|
1494
|
+
const candidateExtensions = [
|
|
1495
|
+
"png",
|
|
1496
|
+
"jpg",
|
|
1497
|
+
"jpeg",
|
|
1498
|
+
"webp",
|
|
1499
|
+
"gif"
|
|
1500
|
+
];
|
|
1501
|
+
let deleted = false;
|
|
1502
|
+
for (const root of rootsToCheck) for (const dir of candidateDirs) for (const ext of candidateExtensions) {
|
|
1503
|
+
const targetPath = join(root, dir, `image-${prefix}.${ext}`);
|
|
1504
|
+
try {
|
|
1505
|
+
if (await deleteImageFromWorkspace(targetPath, rootsToCheck)) deleted = true;
|
|
1506
|
+
} catch {}
|
|
1507
|
+
}
|
|
1508
|
+
return deleted;
|
|
1509
|
+
}
|
|
1234
1510
|
//#endregion
|
|
1235
1511
|
//#region lib/types/index.js
|
|
1236
1512
|
const name = "dsh-image-gen";
|
|
@@ -1242,7 +1518,8 @@ const inject = [
|
|
|
1242
1518
|
];
|
|
1243
1519
|
function apply(ctx, config = {}) {
|
|
1244
1520
|
let current = () => config;
|
|
1245
|
-
|
|
1521
|
+
const knownWorkspaceRoots = /* @__PURE__ */ new Set();
|
|
1522
|
+
installImageSettings(ctx, config, {
|
|
1246
1523
|
setSource: (source) => {
|
|
1247
1524
|
current = source;
|
|
1248
1525
|
},
|
|
@@ -1253,6 +1530,17 @@ function apply(ctx, config = {}) {
|
|
|
1253
1530
|
path: IMAGE_ROUTE,
|
|
1254
1531
|
handler: (req, res) => serveImage(req, res, { readImage: (ref) => ctx.attachments.readImage(ref) })
|
|
1255
1532
|
}), "dsh-image-gen: image route");
|
|
1533
|
+
ctx.effect(() => ctx.webServer.register({
|
|
1534
|
+
kind: "exact",
|
|
1535
|
+
path: DELETE_ROUTE,
|
|
1536
|
+
handler: (req, res) => serveDelete(req, res, {
|
|
1537
|
+
deleteWorkspaceImage: (filePath) => deleteImageFromWorkspace(filePath, knownWorkspaceRoots),
|
|
1538
|
+
deleteWorkspaceImageByAttachmentId: (attachmentId) => deleteImageByAttachmentIdFromWorkspace(attachmentId, {
|
|
1539
|
+
folder: current().workspaceFolder,
|
|
1540
|
+
allowedWorkspaceRoots: knownWorkspaceRoots
|
|
1541
|
+
})
|
|
1542
|
+
})
|
|
1543
|
+
}), "dsh-image-gen: delete route");
|
|
1256
1544
|
ctx.tools.register(defineTool({
|
|
1257
1545
|
name: "generate_image",
|
|
1258
1546
|
description: "Generate a new image with the configured provider. Use when the user asks to create or draw a new image; use edit_image instead when they want to change an existing image. Give a complete visual prompt including subject, composition, style, lighting, and any exact text that should appear. A successful image is attached directly to the conversation and may also be saved under the session workspace. Do not call read, glob, or other tools to locate or verify the image.",
|
|
@@ -1287,19 +1575,26 @@ function apply(ctx, config = {}) {
|
|
|
1287
1575
|
size: {
|
|
1288
1576
|
type: "string",
|
|
1289
1577
|
description: "Optional dimensions or size tier for OpenAI, Seedream, or DashScope."
|
|
1578
|
+
},
|
|
1579
|
+
workflow: {
|
|
1580
|
+
type: "string",
|
|
1581
|
+
description: "Optional name of the ComfyUI workflow to run; omit to use the active workflow from settings. Only meaningful when the ComfyUI provider is selected."
|
|
1290
1582
|
}
|
|
1291
1583
|
},
|
|
1292
1584
|
output: imageOutput("Generated"),
|
|
1293
1585
|
async execute(args, exec) {
|
|
1294
1586
|
const active = resolveProvider(current());
|
|
1295
|
-
if (active.provider === "comfyui")
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1587
|
+
if (active.provider === "comfyui") {
|
|
1588
|
+
const workflow = selectComfyUIWorkflow(active, args.workflow);
|
|
1589
|
+
return saveGenerated(ctx, await generateComfyUIImage({
|
|
1590
|
+
baseURL: active.baseURL,
|
|
1591
|
+
workflowJson: workflow.json,
|
|
1592
|
+
prompt: mergeComfyUIPrompt(workflow.presetPrompt, args.prompt),
|
|
1593
|
+
timeoutMs: active.timeoutMs,
|
|
1594
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1595
|
+
signal: exec.signal
|
|
1596
|
+
}), active.provider, workflow.name, "API workflow", current(), exec, knownWorkspaceRoots);
|
|
1597
|
+
}
|
|
1303
1598
|
const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
|
|
1304
1599
|
if (credential === void 0 || credential.value.length === 0) throw new Error(`generate_image requires the ${active.apiKeyEnv} credential; configure it in Settings > Plugins > Image generation.`);
|
|
1305
1600
|
if (active.provider === "google") {
|
|
@@ -1314,7 +1609,7 @@ function apply(ctx, config = {}) {
|
|
|
1314
1609
|
imageSize,
|
|
1315
1610
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1316
1611
|
signal: exec.signal
|
|
1317
|
-
}), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec);
|
|
1612
|
+
}), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec, knownWorkspaceRoots);
|
|
1318
1613
|
}
|
|
1319
1614
|
if (active.provider === "dashscope") {
|
|
1320
1615
|
const size = args.size ?? active.imageSize;
|
|
@@ -1326,7 +1621,7 @@ function apply(ctx, config = {}) {
|
|
|
1326
1621
|
size,
|
|
1327
1622
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1328
1623
|
signal: exec.signal
|
|
1329
|
-
}), active.provider, active.model, size, current(), exec);
|
|
1624
|
+
}), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
|
|
1330
1625
|
}
|
|
1331
1626
|
const size = args.size ?? active.imageSize;
|
|
1332
1627
|
return saveGenerated(ctx, await generateOpenAICompatibleImage({
|
|
@@ -1338,7 +1633,7 @@ function apply(ctx, config = {}) {
|
|
|
1338
1633
|
size,
|
|
1339
1634
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1340
1635
|
signal: exec.signal
|
|
1341
|
-
}), active.provider, active.model, size, current(), exec);
|
|
1636
|
+
}), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
|
|
1342
1637
|
},
|
|
1343
1638
|
presentResult: (_args, result) => imagePresentation(result)
|
|
1344
1639
|
}));
|
|
@@ -1394,14 +1689,15 @@ function apply(ctx, config = {}) {
|
|
|
1394
1689
|
size: {
|
|
1395
1690
|
type: "string",
|
|
1396
1691
|
description: "Optional output size for OpenAI, Seedream, or DashScope."
|
|
1692
|
+
},
|
|
1693
|
+
workflow: {
|
|
1694
|
+
type: "string",
|
|
1695
|
+
description: "Optional name of the ComfyUI workflow to run; omit to use the active workflow from settings. Only meaningful when the ComfyUI provider is selected."
|
|
1397
1696
|
}
|
|
1398
1697
|
},
|
|
1399
1698
|
output: imageOutput("Edited"),
|
|
1400
1699
|
async execute(args, exec) {
|
|
1401
1700
|
const active = resolveProvider(current());
|
|
1402
|
-
if (active.provider === "comfyui") throw new Error("edit_image is not yet supported by the ComfyUI provider; switch providers or use generate_image");
|
|
1403
|
-
const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
|
|
1404
|
-
if (credential === void 0 || credential.value.length === 0) throw new Error(`edit_image requires the ${active.apiKeyEnv} credential; configure it in Settings > Plugins > Image generation.`);
|
|
1405
1701
|
const sourceImages = await resolveReferenceImages({
|
|
1406
1702
|
...exec.agent === void 0 ? {} : { agent: exec.agent },
|
|
1407
1703
|
attachments: ctx.attachments,
|
|
@@ -1412,6 +1708,26 @@ function apply(ctx, config = {}) {
|
|
|
1412
1708
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1413
1709
|
signal: exec.signal
|
|
1414
1710
|
});
|
|
1711
|
+
if (active.provider === "comfyui") {
|
|
1712
|
+
if (sourceImages.length > 1) throw new Error(`ComfyUI edit_image supports exactly one source image per call; this call resolved ${String(sourceImages.length)} images. Call edit_image again with source_attachment_id set to the single attachment ID of the image to edit.`);
|
|
1713
|
+
const sourceImage = sourceImages[0];
|
|
1714
|
+
if (sourceImage === void 0) throw new Error("edit_image requires a reference image");
|
|
1715
|
+
const workflow = selectComfyUIWorkflow(active, args.workflow);
|
|
1716
|
+
return saveGenerated(ctx, await editComfyUIImage({
|
|
1717
|
+
baseURL: active.baseURL,
|
|
1718
|
+
workflowJson: workflow.json,
|
|
1719
|
+
prompt: mergeComfyUIPrompt(workflow.presetPrompt, args.prompt),
|
|
1720
|
+
sourceImage: {
|
|
1721
|
+
data: sourceImage.data,
|
|
1722
|
+
mediaType: sourceImage.mediaType
|
|
1723
|
+
},
|
|
1724
|
+
timeoutMs: active.timeoutMs,
|
|
1725
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1726
|
+
signal: exec.signal
|
|
1727
|
+
}), active.provider, workflow.name, "API workflow", current(), exec, knownWorkspaceRoots);
|
|
1728
|
+
}
|
|
1729
|
+
const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
|
|
1730
|
+
if (credential === void 0 || credential.value.length === 0) throw new Error(`edit_image requires the ${active.apiKeyEnv} credential; configure it in Settings > Plugins > Image generation.`);
|
|
1415
1731
|
if (active.provider === "google") {
|
|
1416
1732
|
const aspectRatio = args.aspect_ratio ?? active.aspectRatio;
|
|
1417
1733
|
const imageSize = args.image_size ?? active.imageSize;
|
|
@@ -1425,7 +1741,7 @@ function apply(ctx, config = {}) {
|
|
|
1425
1741
|
imageSize,
|
|
1426
1742
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1427
1743
|
signal: exec.signal
|
|
1428
|
-
}), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec);
|
|
1744
|
+
}), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec, knownWorkspaceRoots);
|
|
1429
1745
|
}
|
|
1430
1746
|
const size = args.size ?? active.imageSize;
|
|
1431
1747
|
if (active.provider === "openai") return saveGenerated(ctx, await editOpenAICompatibleImage({
|
|
@@ -1437,7 +1753,7 @@ function apply(ctx, config = {}) {
|
|
|
1437
1753
|
size,
|
|
1438
1754
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1439
1755
|
signal: exec.signal
|
|
1440
|
-
}), active.provider, active.model, size, current(), exec);
|
|
1756
|
+
}), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
|
|
1441
1757
|
if (active.provider === "seedream") return saveGenerated(ctx, await editSeedreamImage({
|
|
1442
1758
|
apiKey: credential.value,
|
|
1443
1759
|
baseURL: active.baseURL,
|
|
@@ -1447,7 +1763,7 @@ function apply(ctx, config = {}) {
|
|
|
1447
1763
|
size,
|
|
1448
1764
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1449
1765
|
signal: exec.signal
|
|
1450
|
-
}), active.provider, active.model, size, current(), exec);
|
|
1766
|
+
}), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
|
|
1451
1767
|
return saveGenerated(ctx, await editDashScopeImage({
|
|
1452
1768
|
apiKey: credential.value,
|
|
1453
1769
|
endpoint: active.endpoint,
|
|
@@ -1457,7 +1773,7 @@ function apply(ctx, config = {}) {
|
|
|
1457
1773
|
size,
|
|
1458
1774
|
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1459
1775
|
signal: exec.signal
|
|
1460
|
-
}), active.provider, active.model, size, current(), exec);
|
|
1776
|
+
}), active.provider, active.model, size, current(), exec, knownWorkspaceRoots);
|
|
1461
1777
|
},
|
|
1462
1778
|
presentResult: (_args, result) => imagePresentation(result)
|
|
1463
1779
|
}));
|
|
@@ -1545,6 +1861,7 @@ function imageOutput(verb) {
|
|
|
1545
1861
|
output: value.output,
|
|
1546
1862
|
...verb === "Edited" ? { operation: "edit" } : {},
|
|
1547
1863
|
...typeof value.savedTo === "string" ? { savedTo: value.savedTo } : {},
|
|
1864
|
+
...typeof value.seed === "number" ? { seed: value.seed } : {},
|
|
1548
1865
|
prompt: args.prompt
|
|
1549
1866
|
})
|
|
1550
1867
|
};
|
|
@@ -1563,7 +1880,7 @@ function attachmentMeta(ref) {
|
|
|
1563
1880
|
} }
|
|
1564
1881
|
};
|
|
1565
1882
|
}
|
|
1566
|
-
async function saveGenerated(ctx, generated, provider, model, output, config, exec) {
|
|
1883
|
+
async function saveGenerated(ctx, generated, provider, model, output, config, exec, knownRoots) {
|
|
1567
1884
|
if (!ctx.attachments.imageLimits.mediaTypes.includes(generated.mediaType)) throw new Error(`This DSH deployment does not accept ${generated.mediaType} generated images`);
|
|
1568
1885
|
const attachment = await ctx.attachments.saveImage({
|
|
1569
1886
|
data: generated.data,
|
|
@@ -1574,11 +1891,13 @@ async function saveGenerated(ctx, generated, provider, model, output, config, ex
|
|
|
1574
1891
|
attachment,
|
|
1575
1892
|
provider,
|
|
1576
1893
|
model,
|
|
1577
|
-
output
|
|
1894
|
+
output,
|
|
1895
|
+
...typeof generated.seed === "number" ? { seed: generated.seed } : {}
|
|
1578
1896
|
};
|
|
1579
1897
|
if (config.saveToWorkspace === false) return value;
|
|
1580
1898
|
const workspaceRoot = exec.agent?.session.header.cwd;
|
|
1581
1899
|
if (workspaceRoot === void 0) return value;
|
|
1900
|
+
knownRoots?.add(workspaceRoot);
|
|
1582
1901
|
try {
|
|
1583
1902
|
value.savedTo = await saveImageToWorkspace({
|
|
1584
1903
|
workspaceRoot,
|
|
@@ -1606,5 +1925,28 @@ function imagePresentation(result) {
|
|
|
1606
1925
|
}]
|
|
1607
1926
|
};
|
|
1608
1927
|
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Wire the settings namespace across both dsh-settings API generations.
|
|
1930
|
+
* A namespace import keeps module loading safe on either version; the branch
|
|
1931
|
+
* picks the service method (0.1.2+) or the legacy top-level relay (<= rc.2),
|
|
1932
|
+
* and falls back to the composition entry with a warning when neither exists
|
|
1933
|
+
* so an incompatible host degrades the settings UI instead of failing boot.
|
|
1934
|
+
*/
|
|
1935
|
+
function installImageSettings(ctx, config, hooks) {
|
|
1936
|
+
const namespace = dshSettings;
|
|
1937
|
+
if (typeof namespace.SettingsProvider?.prototype?.installSection === "function") {
|
|
1938
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
1939
|
+
settingsCtx.settings.installSection(ctx, IMAGE_GENERATION_NAMESPACE, Config, config, hooks);
|
|
1940
|
+
});
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
const legacyInstall = namespace.installSettingsSection;
|
|
1944
|
+
const legacyNamespace = namespace.settingsNamespace;
|
|
1945
|
+
if (typeof legacyInstall === "function" && typeof legacyNamespace === "function") {
|
|
1946
|
+
legacyInstall(ctx, legacyNamespace(IMAGE_GENERATION_NAMESPACE), Config, config, hooks);
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
ctx.logger.warn("dsh-image-gen: this DSH exposes neither settings API generation; settings UI stays on the composition entry");
|
|
1950
|
+
}
|
|
1609
1951
|
//#endregion
|
|
1610
|
-
export { Config, IMAGE_ROUTE, apply, imageAttachmentFromMeta, inject, name };
|
|
1952
|
+
export { Config, DELETE_ROUTE, IMAGE_ROUTE, apply, imageAttachmentFromMeta, inject, name };
|