dsh-image-gen 0.1.7 → 0.2.0
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 +171 -167
- package/README.md +187 -170
- package/README.zh-CN.md +171 -170
- package/lib/client.js +6 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +648 -195
- package/lib/types/dashscope.d.ts +12 -3
- package/lib/types/google.d.ts +14 -3
- package/lib/types/index.d.ts +0 -3
- package/lib/types/openai-compatible.d.ts +15 -3
- package/lib/types/reference-image.d.ts +38 -0
- package/lib/types/seedream.d.ts +17 -0
- package/lib/types/shared.d.ts +1 -1
- package/package.json +37 -34
package/lib/index.js
CHANGED
|
@@ -2,9 +2,9 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
|
2
2
|
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
3
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
|
-
import {
|
|
6
|
-
import { mkdir, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { mkdir, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
7
6
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
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. */
|
|
@@ -27,7 +27,7 @@ const DEFAULT_DASHSCOPE_ENDPOINT = "https://dashscope.aliyuncs.com/api/v1";
|
|
|
27
27
|
const DEFAULT_GOOGLE_MODEL = "gemini-3.1-flash-image";
|
|
28
28
|
const DEFAULT_OPENAI_MODEL = "gpt-image-2";
|
|
29
29
|
const DEFAULT_SEEDREAM_MODEL = "doubao-seedream-5-0-260128";
|
|
30
|
-
const DEFAULT_DASHSCOPE_MODEL = "
|
|
30
|
+
const DEFAULT_DASHSCOPE_MODEL = "qwen-image-3.0";
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region lib/types/config.js
|
|
33
33
|
/** User-facing configuration for supported image providers. */
|
|
@@ -75,7 +75,7 @@ function resolveProvider(config) {
|
|
|
75
75
|
case "dashscope": return {
|
|
76
76
|
provider: "dashscope",
|
|
77
77
|
apiKeyEnv: DASHSCOPE_API_KEY_ENV,
|
|
78
|
-
model: config.dashscopeModel ?? "
|
|
78
|
+
model: config.dashscopeModel ?? "qwen-image-3.0",
|
|
79
79
|
endpoint: config.dashscopeEndpoint ?? "https://dashscope.aliyuncs.com/api/v1",
|
|
80
80
|
imageSize: "1024*1024"
|
|
81
81
|
};
|
|
@@ -91,83 +91,75 @@ function resolveProvider(config) {
|
|
|
91
91
|
}
|
|
92
92
|
//#endregion
|
|
93
93
|
//#region lib/types/dashscope.js
|
|
94
|
-
/** Call DashScope image synthesis/generation API and handle both synchronous output & async task polling. */
|
|
95
94
|
async function generateDashScopeImage(options) {
|
|
95
|
+
assertQwenImageModel(options.model);
|
|
96
|
+
const formattedSize = formatSize(options.size);
|
|
97
|
+
return requestQwenImage({
|
|
98
|
+
...options,
|
|
99
|
+
requestBody: {
|
|
100
|
+
model: options.model,
|
|
101
|
+
input: { messages: [{
|
|
102
|
+
role: "user",
|
|
103
|
+
content: [{ text: options.prompt }]
|
|
104
|
+
}] },
|
|
105
|
+
parameters: { ...formattedSize === void 0 ? {} : { size: formattedSize } }
|
|
106
|
+
},
|
|
107
|
+
operation: "generation"
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async function editDashScopeImage(options) {
|
|
111
|
+
assertQwenImageModel(options.model);
|
|
112
|
+
const formattedSize = formatSize(options.size);
|
|
113
|
+
return requestQwenImage({
|
|
114
|
+
...options,
|
|
115
|
+
requestBody: {
|
|
116
|
+
model: options.model,
|
|
117
|
+
input: { messages: [{
|
|
118
|
+
role: "user",
|
|
119
|
+
content: [{ image: toDataUrl$1(options.sourceImage) }, { text: options.prompt }]
|
|
120
|
+
}] },
|
|
121
|
+
parameters: {
|
|
122
|
+
prompt_extend: true,
|
|
123
|
+
...formattedSize === void 0 ? {} : { size: formattedSize }
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
operation: "editing"
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function assertQwenImageModel(model) {
|
|
130
|
+
if (!model.toLowerCase().startsWith("qwen-image")) throw new Error(`Unsupported DashScope image model ${model}. Configure a qwen-image model.`);
|
|
131
|
+
}
|
|
132
|
+
function formatSize(size) {
|
|
133
|
+
if (size === void 0 || size.length === 0) return void 0;
|
|
134
|
+
return size.replace("x", "*");
|
|
135
|
+
}
|
|
136
|
+
function toDataUrl$1(image) {
|
|
137
|
+
return `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
|
|
138
|
+
}
|
|
139
|
+
async function requestQwenImage(options) {
|
|
96
140
|
const base = options.endpoint.replace(/\/+$/, "");
|
|
97
|
-
const
|
|
98
|
-
const isQwenImage = options.model.toLowerCase().startsWith("qwen-image");
|
|
99
|
-
const submitUrl = isQwenImage ? `${base}/services/aigc/multimodal-generation/generation` : `${base}/services/aigc/text2image/image-synthesis`;
|
|
100
|
-
const requestBody = isQwenImage ? {
|
|
101
|
-
model: options.model,
|
|
102
|
-
input: { messages: [{
|
|
103
|
-
role: "user",
|
|
104
|
-
content: [{ text: options.prompt }]
|
|
105
|
-
}] },
|
|
106
|
-
parameters: { size: formattedSize }
|
|
107
|
-
} : {
|
|
108
|
-
model: options.model,
|
|
109
|
-
input: { prompt: options.prompt },
|
|
110
|
-
parameters: {
|
|
111
|
-
size: formattedSize,
|
|
112
|
-
n: 1
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
const submitResponse = await fetch(submitUrl, {
|
|
141
|
+
const response = await fetch(`${base}/services/aigc/multimodal-generation/generation`, {
|
|
116
142
|
method: "POST",
|
|
117
143
|
...options.signal ? { signal: options.signal } : {},
|
|
118
144
|
headers: {
|
|
119
145
|
"content-type": "application/json",
|
|
120
|
-
authorization: `Bearer ${options.apiKey}
|
|
121
|
-
"X-DashScope-Async": "enable"
|
|
146
|
+
authorization: `Bearer ${options.apiKey}`
|
|
122
147
|
},
|
|
123
|
-
body: JSON.stringify(requestBody)
|
|
148
|
+
body: JSON.stringify(options.requestBody)
|
|
124
149
|
});
|
|
125
|
-
if (!
|
|
126
|
-
const errorText = await
|
|
127
|
-
throw new Error(`DashScope image
|
|
128
|
-
}
|
|
129
|
-
const submitResult = await submitResponse.json();
|
|
130
|
-
const directImageUrl = extractImageUrl(submitResult);
|
|
131
|
-
if (directImageUrl !== void 0) return downloadImageBlob(directImageUrl, options);
|
|
132
|
-
const taskId = submitResult.output?.task_id;
|
|
133
|
-
if (taskId === void 0 || taskId.length === 0) throw new Error(`DashScope did not return an image URL or task_id: ${submitResult.message ?? JSON.stringify(submitResult)}`);
|
|
134
|
-
const taskQueryUrl = `${base}/tasks/${taskId}`;
|
|
135
|
-
const startTime = Date.now();
|
|
136
|
-
const timeoutMs = 6e4;
|
|
137
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
138
|
-
if (options.signal?.aborted) throw new Error("DashScope image task polling aborted");
|
|
139
|
-
await new Promise((resolve, reject) => {
|
|
140
|
-
const timer = setTimeout(resolve, 1500);
|
|
141
|
-
if (options.signal !== void 0) options.signal.addEventListener("abort", () => {
|
|
142
|
-
clearTimeout(timer);
|
|
143
|
-
reject(/* @__PURE__ */ new Error("DashScope image generation aborted"));
|
|
144
|
-
}, { once: true });
|
|
145
|
-
});
|
|
146
|
-
const taskResponse = await fetch(taskQueryUrl, {
|
|
147
|
-
method: "GET",
|
|
148
|
-
...options.signal ? { signal: options.signal } : {},
|
|
149
|
-
headers: { authorization: `Bearer ${options.apiKey}` }
|
|
150
|
-
});
|
|
151
|
-
if (!taskResponse.ok) {
|
|
152
|
-
const errorText = await taskResponse.text();
|
|
153
|
-
throw new Error(`DashScope task query failed (${String(taskResponse.status)}): ${errorText}`);
|
|
154
|
-
}
|
|
155
|
-
const taskResult = await taskResponse.json();
|
|
156
|
-
const status = taskResult.output?.task_status;
|
|
157
|
-
if (status === "SUCCEEDED") {
|
|
158
|
-
const imageUrl = extractImageUrl(taskResult);
|
|
159
|
-
if (imageUrl === void 0 || imageUrl.length === 0) throw new Error("DashScope task succeeded but returned no image URL");
|
|
160
|
-
return downloadImageBlob(imageUrl, options);
|
|
161
|
-
}
|
|
162
|
-
if (status === "FAILED") throw new Error(`DashScope image generation failed: ${taskResult.output?.message ?? taskResult.message ?? "Unknown error"}`);
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
const errorText = await response.text();
|
|
152
|
+
throw new Error(`DashScope image ${options.operation} failed (${String(response.status)}): ${errorText}`);
|
|
163
153
|
}
|
|
164
|
-
|
|
154
|
+
const payload = await response.json();
|
|
155
|
+
const imageUrl = extractImageUrl(payload);
|
|
156
|
+
if (imageUrl === void 0) throw new Error(`DashScope image ${options.operation} returned no image URL: ${payload.message ?? JSON.stringify(payload)}`);
|
|
157
|
+
return downloadImageBlob(imageUrl, options);
|
|
165
158
|
}
|
|
166
159
|
function extractImageUrl(response) {
|
|
167
|
-
const resultUrl = response.output?.results?.[0]?.url;
|
|
168
|
-
if (resultUrl !== void 0 && resultUrl.length > 0) return resultUrl;
|
|
169
160
|
const contents = response.output?.choices?.[0]?.message?.content;
|
|
170
|
-
if (Array.isArray(contents))
|
|
161
|
+
if (!Array.isArray(contents)) return void 0;
|
|
162
|
+
for (const item of contents) {
|
|
171
163
|
if (item.image !== void 0 && item.image.length > 0) return item.image;
|
|
172
164
|
if (item.image_url !== void 0 && item.image_url.length > 0) return item.image_url;
|
|
173
165
|
if (item.url !== void 0 && item.url.length > 0) return item.url;
|
|
@@ -187,10 +179,34 @@ async function downloadImageBlob(imageUrl, options) {
|
|
|
187
179
|
}
|
|
188
180
|
//#endregion
|
|
189
181
|
//#region lib/types/google.js
|
|
190
|
-
const ERROR_LIMIT$
|
|
182
|
+
const ERROR_LIMIT$2 = 4096;
|
|
191
183
|
const REQUESTED_MEDIA_TYPE = "image/jpeg";
|
|
192
|
-
/** Send one native Google image request
|
|
193
|
-
|
|
184
|
+
/** Send one native Google text-to-image request. */
|
|
185
|
+
function generateGoogleImage(input) {
|
|
186
|
+
return requestGoogleImage({
|
|
187
|
+
...input,
|
|
188
|
+
operation: "generation",
|
|
189
|
+
interactionInput: input.prompt
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/** Send one native Google image-editing request using already-resolved bytes. */
|
|
193
|
+
function editGoogleImage(input) {
|
|
194
|
+
return requestGoogleImage({
|
|
195
|
+
...input,
|
|
196
|
+
operation: "editing",
|
|
197
|
+
interactionInput: [{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: input.prompt
|
|
200
|
+
}, {
|
|
201
|
+
type: "image",
|
|
202
|
+
mime_type: input.sourceImage.mediaType,
|
|
203
|
+
data: Buffer.from(input.sourceImage.data).toString("base64")
|
|
204
|
+
}]
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/** Shared Google request, response parsing, decoding, and size enforcement. */
|
|
208
|
+
async function requestGoogleImage(input) {
|
|
209
|
+
const label = `Google image ${input.operation}`;
|
|
194
210
|
const response = await fetch(input.endpoint, {
|
|
195
211
|
method: "POST",
|
|
196
212
|
redirect: "error",
|
|
@@ -201,7 +217,7 @@ async function generateGoogleImage(input) {
|
|
|
201
217
|
},
|
|
202
218
|
body: JSON.stringify({
|
|
203
219
|
model: input.model,
|
|
204
|
-
input: input.
|
|
220
|
+
input: input.interactionInput,
|
|
205
221
|
response_format: {
|
|
206
222
|
type: "image",
|
|
207
223
|
mime_type: REQUESTED_MEDIA_TYPE,
|
|
@@ -210,33 +226,33 @@ async function generateGoogleImage(input) {
|
|
|
210
226
|
}
|
|
211
227
|
})
|
|
212
228
|
});
|
|
213
|
-
const text = await readBoundedText$
|
|
214
|
-
if (!response.ok) throw new Error(
|
|
229
|
+
const text = await readBoundedText$2(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT$2, label);
|
|
230
|
+
if (!response.ok) throw new Error(`${label} failed (${response.status}): ${text.slice(0, ERROR_LIMIT$2)}`);
|
|
215
231
|
let payload;
|
|
216
232
|
try {
|
|
217
233
|
payload = JSON.parse(text);
|
|
218
234
|
} catch {
|
|
219
|
-
throw new Error(
|
|
235
|
+
throw new Error(`${label} returned invalid JSON`);
|
|
220
236
|
}
|
|
221
237
|
const image = outputImage(payload);
|
|
222
|
-
if (image === void 0) throw new Error(
|
|
238
|
+
if (image === void 0) throw new Error(`${label} returned no image: ${text.slice(0, ERROR_LIMIT$2)}`);
|
|
223
239
|
const mediaType = mediaTypeOf(image.mime_type ?? REQUESTED_MEDIA_TYPE);
|
|
224
|
-
if (mediaType === void 0) throw new Error(
|
|
225
|
-
const data = decodeBase64$
|
|
226
|
-
if (data.byteLength > input.maxBytes) throw new Error(
|
|
240
|
+
if (mediaType === void 0) throw new Error(`${label} returned unsupported media type ${JSON.stringify(image.mime_type)}`);
|
|
241
|
+
const data = decodeBase64$2(image.data, label);
|
|
242
|
+
if (data.byteLength > input.maxBytes) throw new Error(`${label} exceeded the ${String(input.maxBytes)} byte image limit`);
|
|
227
243
|
return {
|
|
228
244
|
data,
|
|
229
245
|
mediaType
|
|
230
246
|
};
|
|
231
247
|
}
|
|
232
248
|
function outputImage(value) {
|
|
233
|
-
const interaction = record$
|
|
249
|
+
const interaction = record$2(value);
|
|
234
250
|
if (interaction === void 0) return void 0;
|
|
235
251
|
const direct = imageContent(interaction.output_image, false);
|
|
236
252
|
if (direct !== void 0) return direct;
|
|
237
253
|
if (!Array.isArray(interaction.steps)) return void 0;
|
|
238
254
|
for (const step of interaction.steps) {
|
|
239
|
-
const modelOutput = record$
|
|
255
|
+
const modelOutput = record$2(step);
|
|
240
256
|
if (modelOutput?.type !== "model_output" || !Array.isArray(modelOutput.content)) continue;
|
|
241
257
|
for (const content of modelOutput.content) {
|
|
242
258
|
const image = imageContent(content, true);
|
|
@@ -245,27 +261,27 @@ function outputImage(value) {
|
|
|
245
261
|
}
|
|
246
262
|
}
|
|
247
263
|
function imageContent(value, requiresImageType) {
|
|
248
|
-
const image = record$
|
|
264
|
+
const image = record$2(value);
|
|
249
265
|
if (image === void 0 || requiresImageType && image.type !== "image" || typeof image.data !== "string") return void 0;
|
|
250
266
|
return {
|
|
251
267
|
data: image.data,
|
|
252
268
|
mime_type: image.mime_type
|
|
253
269
|
};
|
|
254
270
|
}
|
|
255
|
-
function record$
|
|
271
|
+
function record$2(value) {
|
|
256
272
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
257
273
|
}
|
|
258
274
|
function mediaTypeOf(value) {
|
|
259
275
|
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif" ? value : void 0;
|
|
260
276
|
}
|
|
261
|
-
function decodeBase64$
|
|
277
|
+
function decodeBase64$2(data, label) {
|
|
262
278
|
const clean = data.replace(/\s+/g, "");
|
|
263
|
-
if (clean.length === 0) throw new Error(
|
|
279
|
+
if (clean.length === 0) throw new Error(`${label} returned invalid base64 image data`);
|
|
264
280
|
const decoded = Buffer.from(clean, "base64");
|
|
265
|
-
if (decoded.length === 0) throw new Error(
|
|
281
|
+
if (decoded.length === 0) throw new Error(`${label} returned invalid base64 image data`);
|
|
266
282
|
return new Uint8Array(decoded);
|
|
267
283
|
}
|
|
268
|
-
async function readBoundedText$
|
|
284
|
+
async function readBoundedText$2(response, maxBytes, label) {
|
|
269
285
|
if (response.body === null) return "";
|
|
270
286
|
const reader = response.body.getReader();
|
|
271
287
|
const chunks = [];
|
|
@@ -275,7 +291,7 @@ async function readBoundedText$1(response, maxBytes) {
|
|
|
275
291
|
const next = await reader.read();
|
|
276
292
|
if (next.done) break;
|
|
277
293
|
bytes += next.value.byteLength;
|
|
278
|
-
if (bytes > maxBytes) throw new Error(
|
|
294
|
+
if (bytes > maxBytes) throw new Error(`${label} response exceeded the ${String(maxBytes)} byte limit`);
|
|
279
295
|
chunks.push(next.value);
|
|
280
296
|
}
|
|
281
297
|
} finally {
|
|
@@ -290,6 +306,156 @@ async function readBoundedText$1(response, maxBytes) {
|
|
|
290
306
|
return new TextDecoder().decode(joined);
|
|
291
307
|
}
|
|
292
308
|
//#endregion
|
|
309
|
+
//#region lib/types/reference-image.js
|
|
310
|
+
/** Latest-DSH compatibility boundary for resolving conversation image references. */
|
|
311
|
+
/**
|
|
312
|
+
* Resolve and read the image to edit from the current effective conversation.
|
|
313
|
+
*
|
|
314
|
+
* This is the only module allowed to know how DSH sessions expose effective
|
|
315
|
+
* messages or how nested model-facing content stores durable image refs.
|
|
316
|
+
*/
|
|
317
|
+
async function resolveReferenceImage(input) {
|
|
318
|
+
if (input.sourceAttachmentId !== void 0 && input.sourcePath !== void 0) throw new Error("edit_image accepts either source_attachment_id or source_path, not both");
|
|
319
|
+
if (input.sourcePath !== void 0) return readWorkspaceReferenceImage({
|
|
320
|
+
sourcePath: input.sourcePath,
|
|
321
|
+
...input.agent?.session.header?.cwd === void 0 ? {} : { workspaceRoot: input.agent.session.header.cwd },
|
|
322
|
+
...input.maxBytes === void 0 ? {} : { maxBytes: input.maxBytes },
|
|
323
|
+
signal: input.signal
|
|
324
|
+
});
|
|
325
|
+
if (input.agent === void 0) throw new Error("edit_image requires an active DSH agent session to resolve a reference image");
|
|
326
|
+
const ref = findReferenceImage(input.agent.session.deriveMessages(), input.sourceAttachmentId);
|
|
327
|
+
if (ref === void 0) {
|
|
328
|
+
if (input.sourceAttachmentId !== void 0) throw new Error(`edit_image could not find image attachment ${input.sourceAttachmentId} in the current conversation`);
|
|
329
|
+
throw new Error("edit_image requires an image in the current conversation; upload or generate an image first");
|
|
330
|
+
}
|
|
331
|
+
const stored = await input.attachments.readImage(ref, input.signal);
|
|
332
|
+
return {
|
|
333
|
+
data: stored.data,
|
|
334
|
+
mediaType: stored.ref.mediaType
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Read an explicitly named workspace image without exposing filesystem or DSH
|
|
339
|
+
* details to provider adapters. Both lexical and real-path containment are
|
|
340
|
+
* enforced so absolute paths, parent traversal, and symlink escapes fail.
|
|
341
|
+
*/
|
|
342
|
+
async function readWorkspaceReferenceImage(input) {
|
|
343
|
+
const requested = input.sourcePath.trim();
|
|
344
|
+
if (requested.length === 0) throw new Error("edit_image source_path must not be empty");
|
|
345
|
+
if (input.workspaceRoot === void 0) throw new Error("edit_image source_path requires an active DSH session workspace");
|
|
346
|
+
const root = resolve(input.workspaceRoot);
|
|
347
|
+
const candidate = isAbsolute(requested) ? resolve(requested) : resolve(root, requested);
|
|
348
|
+
if (!containsPath$1(root, candidate)) throw new Error("edit_image source_path must stay inside the session workspace: " + requested);
|
|
349
|
+
let realRoot;
|
|
350
|
+
let realCandidate;
|
|
351
|
+
try {
|
|
352
|
+
[realRoot, realCandidate] = await Promise.all([realpath(root), realpath(candidate)]);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (error.code === "ENOENT") throw new Error("edit_image could not find workspace image: " + requested);
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
if (!containsPath$1(realRoot, realCandidate)) throw new Error("edit_image source_path resolves outside the session workspace: " + requested);
|
|
358
|
+
const file = await stat(realCandidate);
|
|
359
|
+
if (!file.isFile()) throw new Error("edit_image source_path is not a file: " + requested);
|
|
360
|
+
if (input.maxBytes !== void 0 && file.size > input.maxBytes) throw new Error("edit_image source image is too large (" + file.size + " bytes; maximum " + input.maxBytes + ")");
|
|
361
|
+
const data = await readFile(realCandidate, { signal: input.signal });
|
|
362
|
+
const mediaType = detectImageMediaType(data);
|
|
363
|
+
if (mediaType === void 0) throw new Error("edit_image source_path is not a supported PNG, JPEG, WebP, or GIF image: " + requested);
|
|
364
|
+
return {
|
|
365
|
+
data: new Uint8Array(data),
|
|
366
|
+
mediaType
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/** Find the newest matching image in the effective, replacement-aware history. */
|
|
370
|
+
function findReferenceImage(messages, sourceAttachmentId) {
|
|
371
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
372
|
+
const found = findInBlocks(messages[index]?.content ?? [], sourceAttachmentId);
|
|
373
|
+
if (found !== void 0) return found;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** Validate an untrusted serialized image reference at an HTTP/UI boundary. */
|
|
377
|
+
function parseImageAttachmentRef(value) {
|
|
378
|
+
const ref = record$1(value);
|
|
379
|
+
if (ref === void 0) return void 0;
|
|
380
|
+
if (typeof ref.attachmentId !== "string" || !imageMediaType$2(ref.mediaType)) return void 0;
|
|
381
|
+
if (!nonNegativeInteger(ref.bytes) || !positiveInteger(ref.width) || !positiveInteger(ref.height)) return void 0;
|
|
382
|
+
if (ref.name !== void 0 && typeof ref.name !== "string") return void 0;
|
|
383
|
+
const originalDimensions = ref.originalDimensions === void 0 ? void 0 : parseDimensions(ref.originalDimensions);
|
|
384
|
+
if (ref.originalDimensions !== void 0 && originalDimensions === void 0) return void 0;
|
|
385
|
+
return {
|
|
386
|
+
attachmentId: ref.attachmentId,
|
|
387
|
+
mediaType: ref.mediaType,
|
|
388
|
+
bytes: ref.bytes,
|
|
389
|
+
width: ref.width,
|
|
390
|
+
height: ref.height,
|
|
391
|
+
...typeof ref.name === "string" ? { name: ref.name } : {},
|
|
392
|
+
...originalDimensions === void 0 ? {} : { originalDimensions }
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function findInBlocks(blocks, sourceAttachmentId) {
|
|
396
|
+
for (let index = blocks.length - 1; index >= 0; index -= 1) {
|
|
397
|
+
const block = blocks[index];
|
|
398
|
+
if (block === void 0) continue;
|
|
399
|
+
if (block.type === "image") {
|
|
400
|
+
if (sourceAttachmentId === void 0 || String(block.attachment.attachmentId) === sourceAttachmentId) return block.attachment;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (block.type === "tool-result") {
|
|
404
|
+
const nested = findInBlocks(block.content, sourceAttachmentId);
|
|
405
|
+
if (nested !== void 0) return nested;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function parseDimensions(value) {
|
|
410
|
+
const dimensions = record$1(value);
|
|
411
|
+
if (dimensions === void 0 || !positiveInteger(dimensions.width) || !positiveInteger(dimensions.height)) return void 0;
|
|
412
|
+
return {
|
|
413
|
+
width: dimensions.width,
|
|
414
|
+
height: dimensions.height
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function imageMediaType$2(value) {
|
|
418
|
+
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
419
|
+
}
|
|
420
|
+
function detectImageMediaType(data) {
|
|
421
|
+
if (startsWith(data, [
|
|
422
|
+
137,
|
|
423
|
+
80,
|
|
424
|
+
78,
|
|
425
|
+
71,
|
|
426
|
+
13,
|
|
427
|
+
10,
|
|
428
|
+
26,
|
|
429
|
+
10
|
|
430
|
+
])) return "image/png";
|
|
431
|
+
if (startsWith(data, [
|
|
432
|
+
255,
|
|
433
|
+
216,
|
|
434
|
+
255
|
|
435
|
+
])) return "image/jpeg";
|
|
436
|
+
if (ascii(data, 0, 6) === "GIF87a" || ascii(data, 0, 6) === "GIF89a") return "image/gif";
|
|
437
|
+
if (ascii(data, 0, 4) === "RIFF" && ascii(data, 8, 4) === "WEBP") return "image/webp";
|
|
438
|
+
}
|
|
439
|
+
function startsWith(data, signature) {
|
|
440
|
+
return signature.every((byte, index) => data[index] === byte);
|
|
441
|
+
}
|
|
442
|
+
function ascii(data, offset, length) {
|
|
443
|
+
return String.fromCharCode(...data.subarray(offset, offset + length));
|
|
444
|
+
}
|
|
445
|
+
function containsPath$1(parent, child) {
|
|
446
|
+
const rel = relative(parent, child);
|
|
447
|
+
return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute(rel);
|
|
448
|
+
}
|
|
449
|
+
function positiveInteger(value) {
|
|
450
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
451
|
+
}
|
|
452
|
+
function nonNegativeInteger(value) {
|
|
453
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
454
|
+
}
|
|
455
|
+
function record$1(value) {
|
|
456
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
457
|
+
}
|
|
458
|
+
//#endregion
|
|
293
459
|
//#region lib/types/image-route.js
|
|
294
460
|
const MAX_BODY_BYTES = 4096;
|
|
295
461
|
/** Serve one verified durable image reference to a same-origin browser request. */
|
|
@@ -324,20 +490,10 @@ async function serveImage(req, res, deps) {
|
|
|
324
490
|
function imageAttachmentFromMeta(meta) {
|
|
325
491
|
const value = record(meta);
|
|
326
492
|
if (value?.kind !== "dsh-image-gen") return void 0;
|
|
327
|
-
return
|
|
493
|
+
return parseImageAttachmentRef(value.attachment);
|
|
328
494
|
}
|
|
329
495
|
function attachmentFromRequest(value) {
|
|
330
|
-
return
|
|
331
|
-
}
|
|
332
|
-
function imageAttachment(value) {
|
|
333
|
-
const ref = record(value);
|
|
334
|
-
if (ref === void 0) return void 0;
|
|
335
|
-
if (typeof ref.attachmentId !== "string" || !mediaType(ref.mediaType) || typeof ref.bytes !== "number" || typeof ref.width !== "number" || typeof ref.height !== "number") return void 0;
|
|
336
|
-
if (ref.name !== void 0 && typeof ref.name !== "string") return void 0;
|
|
337
|
-
return ref;
|
|
338
|
-
}
|
|
339
|
-
function mediaType(value) {
|
|
340
|
-
return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
|
|
496
|
+
return parseImageAttachmentRef(record(value)?.attachment);
|
|
341
497
|
}
|
|
342
498
|
function record(value) {
|
|
343
499
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
@@ -362,10 +518,9 @@ function jsonError(res, status, code) {
|
|
|
362
518
|
}
|
|
363
519
|
//#endregion
|
|
364
520
|
//#region lib/types/openai-compatible.js
|
|
365
|
-
const ERROR_LIMIT = 4096;
|
|
366
|
-
/** Generate an image from an OpenAI-compatible endpoint. */
|
|
521
|
+
const ERROR_LIMIT$1 = 4096;
|
|
367
522
|
async function generateOpenAICompatibleImage(input) {
|
|
368
|
-
|
|
523
|
+
return parseImageResponse(await fetch(imageEndpoint$1(input.baseURL, "generations"), {
|
|
369
524
|
method: "POST",
|
|
370
525
|
redirect: "error",
|
|
371
526
|
signal: input.signal,
|
|
@@ -379,20 +534,174 @@ async function generateOpenAICompatibleImage(input) {
|
|
|
379
534
|
size: input.size,
|
|
380
535
|
...input.provider === "seedream" ? { response_format: "url" } : {}
|
|
381
536
|
})
|
|
537
|
+
}), input.provider, input);
|
|
538
|
+
}
|
|
539
|
+
async function editOpenAICompatibleImage(input) {
|
|
540
|
+
const form = new FormData();
|
|
541
|
+
const uploadBytes = new Uint8Array(input.sourceImage.data);
|
|
542
|
+
form.append("image", new Blob([uploadBytes], { type: input.sourceImage.mediaType }), `reference.${extensionOf(input.sourceImage.mediaType)}`);
|
|
543
|
+
form.append("prompt", input.prompt);
|
|
544
|
+
form.append("model", input.model);
|
|
545
|
+
if (input.size !== void 0 && input.size.length > 0) form.append("size", input.size);
|
|
546
|
+
return parseImageResponse(await fetch(imageEndpoint$1(input.baseURL, "edits"), {
|
|
547
|
+
method: "POST",
|
|
548
|
+
redirect: "error",
|
|
549
|
+
signal: input.signal,
|
|
550
|
+
headers: { authorization: `Bearer ${input.apiKey}` },
|
|
551
|
+
body: form
|
|
552
|
+
}), "openai", input);
|
|
553
|
+
}
|
|
554
|
+
async function parseImageResponse(response, provider, input) {
|
|
555
|
+
const text = await readBoundedText$1(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT$1);
|
|
556
|
+
if (!response.ok) throw new Error(`${provider} image request failed (${response.status}): ${text.slice(0, ERROR_LIMIT$1)}`);
|
|
557
|
+
let payload;
|
|
558
|
+
try {
|
|
559
|
+
payload = JSON.parse(text);
|
|
560
|
+
} catch {
|
|
561
|
+
throw new Error(`${provider} image request returned invalid JSON`);
|
|
562
|
+
}
|
|
563
|
+
const image = firstImage$1(payload);
|
|
564
|
+
if (image === void 0) throw new Error(`${provider} image request returned no image: ${text.slice(0, ERROR_LIMIT$1)}`);
|
|
565
|
+
if (image.b64_json !== void 0) return {
|
|
566
|
+
data: decodeBase64$1(image.b64_json, provider),
|
|
567
|
+
mediaType: imageMediaType$1(image.mime_type) ?? "image/png"
|
|
568
|
+
};
|
|
569
|
+
return downloadImage$1(image.url, provider, input);
|
|
570
|
+
}
|
|
571
|
+
function imageEndpoint$1(baseURL, operation) {
|
|
572
|
+
try {
|
|
573
|
+
return new URL(`images/${operation}`, baseURL.endsWith("/") ? baseURL : `${baseURL}/`).toString();
|
|
574
|
+
} catch {
|
|
575
|
+
throw new Error("Image endpoint must be an absolute URL");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function firstImage$1(value) {
|
|
579
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
580
|
+
const record = value;
|
|
581
|
+
const data = Array.isArray(record.data) ? record.data : Array.isArray(record.images) ? record.images : Array.isArray(record.output) ? record.output : void 0;
|
|
582
|
+
if (data === void 0 || data.length === 0) return void 0;
|
|
583
|
+
const candidate = data[0];
|
|
584
|
+
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return void 0;
|
|
585
|
+
const item = candidate;
|
|
586
|
+
const mime = typeof item.mime_type === "string" ? item.mime_type : typeof item.mime === "string" ? item.mime : void 0;
|
|
587
|
+
return typeof item.b64_json === "string" ? {
|
|
588
|
+
b64_json: item.b64_json,
|
|
589
|
+
...mime === void 0 ? {} : { mime_type: mime }
|
|
590
|
+
} : typeof item.url === "string" ? {
|
|
591
|
+
url: item.url,
|
|
592
|
+
...mime === void 0 ? {} : { mime_type: mime }
|
|
593
|
+
} : void 0;
|
|
594
|
+
}
|
|
595
|
+
async function downloadImage$1(url, provider, input) {
|
|
596
|
+
if (url === void 0) throw new Error(`${provider} image request returned no image data`);
|
|
597
|
+
if (url.startsWith("data:")) {
|
|
598
|
+
const parsed = parseDataUrl(url);
|
|
599
|
+
if (parsed === void 0) throw new Error(`${provider} image request returned invalid data URL`);
|
|
600
|
+
return {
|
|
601
|
+
data: decodeBase64$1(parsed.base64, provider),
|
|
602
|
+
mediaType: imageMediaType$1(parsed.mediaType) ?? "image/png"
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const response = await fetch(url, {
|
|
606
|
+
redirect: "follow",
|
|
607
|
+
signal: input.signal,
|
|
608
|
+
...input.apiKey === void 0 ? {} : { headers: { authorization: `Bearer ${input.apiKey}` } }
|
|
609
|
+
});
|
|
610
|
+
if (!response.ok) throw new Error(`${provider} image download failed (${response.status})`);
|
|
611
|
+
const mediaType = imageMediaType$1(response.headers.get("content-type"));
|
|
612
|
+
if (mediaType === void 0) throw new Error(`${provider} image download returned unsupported content type`);
|
|
613
|
+
return {
|
|
614
|
+
data: await readBoundedBytes$1(response, input.maxBytes),
|
|
615
|
+
mediaType
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function parseDataUrl(value) {
|
|
619
|
+
const match = /^data:([^;,]+);base64,(.*)$/s.exec(value.trim());
|
|
620
|
+
return match?.[1] !== void 0 && match[2] !== void 0 ? {
|
|
621
|
+
mediaType: match[1],
|
|
622
|
+
base64: match[2]
|
|
623
|
+
} : void 0;
|
|
624
|
+
}
|
|
625
|
+
function extensionOf(mediaType) {
|
|
626
|
+
switch (mediaType) {
|
|
627
|
+
case "image/jpeg": return "jpg";
|
|
628
|
+
case "image/webp": return "webp";
|
|
629
|
+
case "image/gif": return "gif";
|
|
630
|
+
case "image/png": return "png";
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function decodeBase64$1(value, provider) {
|
|
634
|
+
const clean = (parseDataUrl(value)?.base64 ?? value).replace(/\s+/g, "");
|
|
635
|
+
if (clean.length === 0) throw new Error(`${provider} image request returned invalid base64 image data`);
|
|
636
|
+
const decoded = Buffer.from(clean, "base64");
|
|
637
|
+
if (decoded.length === 0) throw new Error(`${provider} image request returned invalid base64 image data`);
|
|
638
|
+
return new Uint8Array(decoded);
|
|
639
|
+
}
|
|
640
|
+
function imageMediaType$1(value) {
|
|
641
|
+
const mediaType = value?.split(";", 1)[0]?.trim().toLowerCase();
|
|
642
|
+
return mediaType === "image/png" || mediaType === "image/jpeg" || mediaType === "image/webp" || mediaType === "image/gif" ? mediaType : void 0;
|
|
643
|
+
}
|
|
644
|
+
async function readBoundedText$1(response, maxBytes) {
|
|
645
|
+
return new TextDecoder().decode(await readBoundedBytes$1(response, maxBytes));
|
|
646
|
+
}
|
|
647
|
+
async function readBoundedBytes$1(response, maxBytes) {
|
|
648
|
+
if (response.body === null) return /* @__PURE__ */ new Uint8Array();
|
|
649
|
+
const reader = response.body.getReader();
|
|
650
|
+
const chunks = [];
|
|
651
|
+
let bytes = 0;
|
|
652
|
+
try {
|
|
653
|
+
for (;;) {
|
|
654
|
+
const next = await reader.read();
|
|
655
|
+
if (next.done) break;
|
|
656
|
+
bytes += next.value.byteLength;
|
|
657
|
+
if (bytes > maxBytes) throw new Error(`Image response exceeded the ${String(maxBytes)} byte limit`);
|
|
658
|
+
chunks.push(next.value);
|
|
659
|
+
}
|
|
660
|
+
} finally {
|
|
661
|
+
reader.releaseLock();
|
|
662
|
+
}
|
|
663
|
+
const joined = new Uint8Array(bytes);
|
|
664
|
+
let offset = 0;
|
|
665
|
+
for (const chunk of chunks) {
|
|
666
|
+
joined.set(chunk, offset);
|
|
667
|
+
offset += chunk.byteLength;
|
|
668
|
+
}
|
|
669
|
+
return joined;
|
|
670
|
+
}
|
|
671
|
+
//#endregion
|
|
672
|
+
//#region lib/types/seedream.js
|
|
673
|
+
const ERROR_LIMIT = 4096;
|
|
674
|
+
/** Edit one image through Ark ImageGenerations using a data-URL reference. */
|
|
675
|
+
async function editSeedreamImage(input) {
|
|
676
|
+
const response = await fetch(imageEndpoint(input.baseURL), {
|
|
677
|
+
method: "POST",
|
|
678
|
+
redirect: "error",
|
|
679
|
+
signal: input.signal,
|
|
680
|
+
headers: {
|
|
681
|
+
authorization: `Bearer ${input.apiKey}`,
|
|
682
|
+
"content-type": "application/json"
|
|
683
|
+
},
|
|
684
|
+
body: JSON.stringify({
|
|
685
|
+
model: input.model,
|
|
686
|
+
prompt: input.prompt,
|
|
687
|
+
image: [toDataUrl(input.sourceImage)],
|
|
688
|
+
...input.size === void 0 || input.size.length === 0 ? {} : { size: input.size },
|
|
689
|
+
response_format: "b64_json"
|
|
690
|
+
})
|
|
382
691
|
});
|
|
383
692
|
const text = await readBoundedText(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT);
|
|
384
|
-
if (!response.ok) throw new Error(
|
|
693
|
+
if (!response.ok) throw new Error(`seedream image editing failed (${response.status}): ${text.slice(0, ERROR_LIMIT)}`);
|
|
385
694
|
let payload;
|
|
386
695
|
try {
|
|
387
696
|
payload = JSON.parse(text);
|
|
388
697
|
} catch {
|
|
389
|
-
throw new Error(
|
|
698
|
+
throw new Error("seedream image editing returned invalid JSON");
|
|
390
699
|
}
|
|
391
700
|
const image = firstImage(payload);
|
|
392
|
-
if (image === void 0) throw new Error(
|
|
701
|
+
if (image === void 0) throw new Error(`seedream image editing returned no image: ${text.slice(0, ERROR_LIMIT)}`);
|
|
393
702
|
if (image.b64_json !== void 0) return {
|
|
394
|
-
data: decodeBase64(image.b64_json
|
|
395
|
-
mediaType: "image/png"
|
|
703
|
+
data: decodeBase64(image.b64_json),
|
|
704
|
+
mediaType: imageMediaType(image.mime_type) ?? "image/png"
|
|
396
705
|
};
|
|
397
706
|
return downloadImage(image.url, input);
|
|
398
707
|
}
|
|
@@ -400,37 +709,48 @@ function imageEndpoint(baseURL) {
|
|
|
400
709
|
try {
|
|
401
710
|
return new URL("images/generations", baseURL.endsWith("/") ? baseURL : `${baseURL}/`).toString();
|
|
402
711
|
} catch {
|
|
403
|
-
throw new Error("
|
|
712
|
+
throw new Error("Seedream image endpoint must be an absolute URL");
|
|
404
713
|
}
|
|
405
714
|
}
|
|
715
|
+
function toDataUrl(image) {
|
|
716
|
+
return `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
|
|
717
|
+
}
|
|
406
718
|
function firstImage(value) {
|
|
407
719
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
408
|
-
const
|
|
409
|
-
|
|
720
|
+
const record = value;
|
|
721
|
+
const data = Array.isArray(record.data) ? record.data : Array.isArray(record.images) ? record.images : Array.isArray(record.output) ? record.output : void 0;
|
|
722
|
+
if (data === void 0 || data.length === 0) return void 0;
|
|
410
723
|
const candidate = data[0];
|
|
411
724
|
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return void 0;
|
|
412
725
|
const item = candidate;
|
|
413
|
-
|
|
726
|
+
const mime = typeof item.mime_type === "string" ? item.mime_type : typeof item.mime === "string" ? item.mime : void 0;
|
|
727
|
+
return typeof item.b64_json === "string" ? {
|
|
728
|
+
b64_json: item.b64_json,
|
|
729
|
+
...mime === void 0 ? {} : { mime_type: mime }
|
|
730
|
+
} : typeof item.url === "string" ? {
|
|
731
|
+
url: item.url,
|
|
732
|
+
...mime === void 0 ? {} : { mime_type: mime }
|
|
733
|
+
} : void 0;
|
|
414
734
|
}
|
|
415
735
|
async function downloadImage(url, input) {
|
|
416
|
-
if (url === void 0) throw new Error(
|
|
736
|
+
if (url === void 0) throw new Error("seedream image editing returned no image data");
|
|
417
737
|
const response = await fetch(url, {
|
|
418
738
|
redirect: "follow",
|
|
419
739
|
signal: input.signal
|
|
420
740
|
});
|
|
421
|
-
if (!response.ok) throw new Error(
|
|
741
|
+
if (!response.ok) throw new Error(`seedream image download failed (${response.status})`);
|
|
422
742
|
const mediaType = imageMediaType(response.headers.get("content-type"));
|
|
423
|
-
if (mediaType === void 0) throw new Error(
|
|
743
|
+
if (mediaType === void 0) throw new Error("seedream image download returned unsupported content type");
|
|
424
744
|
return {
|
|
425
745
|
data: await readBoundedBytes(response, input.maxBytes),
|
|
426
746
|
mediaType
|
|
427
747
|
};
|
|
428
748
|
}
|
|
429
|
-
function decodeBase64(value
|
|
749
|
+
function decodeBase64(value) {
|
|
430
750
|
const clean = value.replace(/\s+/g, "");
|
|
431
|
-
if (clean.length === 0) throw new Error(
|
|
751
|
+
if (clean.length === 0) throw new Error("seedream image editing returned invalid base64 image data");
|
|
432
752
|
const decoded = Buffer.from(clean, "base64");
|
|
433
|
-
if (decoded.length === 0) throw new Error(
|
|
753
|
+
if (decoded.length === 0) throw new Error("seedream image editing returned invalid base64 image data");
|
|
434
754
|
return new Uint8Array(decoded);
|
|
435
755
|
}
|
|
436
756
|
function imageMediaType(value) {
|
|
@@ -582,16 +902,13 @@ async function saveImageToWorkspace(options) {
|
|
|
582
902
|
}
|
|
583
903
|
//#endregion
|
|
584
904
|
//#region lib/types/index.js
|
|
585
|
-
/** Cordis plugin name. */
|
|
586
905
|
const name = "dsh-image-gen";
|
|
587
|
-
/** Host services required by the Bundle. */
|
|
588
906
|
const inject = [
|
|
589
907
|
"tools",
|
|
590
908
|
"attachments",
|
|
591
909
|
"credentials",
|
|
592
910
|
"webServer"
|
|
593
911
|
];
|
|
594
|
-
/** Register settings, the image route, and the model-callable tool. */
|
|
595
912
|
function apply(ctx, config = {}) {
|
|
596
913
|
let current = () => config;
|
|
597
914
|
installSettingsSection(ctx, settingsNamespace(IMAGE_GENERATION_NAMESPACE), Config, config, {
|
|
@@ -607,7 +924,7 @@ function apply(ctx, config = {}) {
|
|
|
607
924
|
}), "dsh-image-gen: image route");
|
|
608
925
|
ctx.tools.register(defineTool({
|
|
609
926
|
name: "generate_image",
|
|
610
|
-
description: "Generate
|
|
927
|
+
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.",
|
|
611
928
|
parameters: {
|
|
612
929
|
prompt: {
|
|
613
930
|
type: "string",
|
|
@@ -641,72 +958,7 @@ function apply(ctx, config = {}) {
|
|
|
641
958
|
description: "Optional dimensions or size tier for OpenAI, Seedream, or DashScope."
|
|
642
959
|
}
|
|
643
960
|
},
|
|
644
|
-
output:
|
|
645
|
-
schema: {
|
|
646
|
-
type: "object",
|
|
647
|
-
additionalProperties: false,
|
|
648
|
-
properties: {
|
|
649
|
-
attachment: {
|
|
650
|
-
type: "object",
|
|
651
|
-
required: true,
|
|
652
|
-
additionalProperties: false,
|
|
653
|
-
properties: {
|
|
654
|
-
attachmentId: {
|
|
655
|
-
type: "string",
|
|
656
|
-
required: true
|
|
657
|
-
},
|
|
658
|
-
mediaType: {
|
|
659
|
-
type: "string",
|
|
660
|
-
required: true
|
|
661
|
-
},
|
|
662
|
-
bytes: {
|
|
663
|
-
type: "integer",
|
|
664
|
-
required: true
|
|
665
|
-
},
|
|
666
|
-
width: {
|
|
667
|
-
type: "integer",
|
|
668
|
-
required: true
|
|
669
|
-
},
|
|
670
|
-
height: {
|
|
671
|
-
type: "integer",
|
|
672
|
-
required: true
|
|
673
|
-
},
|
|
674
|
-
name: { type: "string" }
|
|
675
|
-
}
|
|
676
|
-
},
|
|
677
|
-
provider: {
|
|
678
|
-
type: "string",
|
|
679
|
-
required: true
|
|
680
|
-
},
|
|
681
|
-
model: {
|
|
682
|
-
type: "string",
|
|
683
|
-
required: true
|
|
684
|
-
},
|
|
685
|
-
output: {
|
|
686
|
-
type: "string",
|
|
687
|
-
required: true
|
|
688
|
-
},
|
|
689
|
-
savedTo: { type: "string" },
|
|
690
|
-
saveError: { type: "string" }
|
|
691
|
-
}
|
|
692
|
-
},
|
|
693
|
-
render: (_args, value) => {
|
|
694
|
-
const saved = typeof value.savedTo === "string" ? ` It was also saved to the workspace as ${value.savedTo}.` : typeof value.saveError === "string" ? ` Saving it to the workspace failed: ${value.saveError}.` : " It has no local file path.";
|
|
695
|
-
return [{
|
|
696
|
-
type: "text",
|
|
697
|
-
text: `Generated one image with ${value.provider}/${value.model} (${value.output}). It is already attached to the conversation.${saved} Respond to the user without reading or searching for the image.`
|
|
698
|
-
}];
|
|
699
|
-
},
|
|
700
|
-
presentationMeta: (args, value) => ({
|
|
701
|
-
kind: "dsh-image-gen",
|
|
702
|
-
attachment: value.attachment,
|
|
703
|
-
provider: value.provider,
|
|
704
|
-
model: value.model,
|
|
705
|
-
output: value.output,
|
|
706
|
-
...typeof value.savedTo === "string" ? { savedTo: value.savedTo } : {},
|
|
707
|
-
prompt: args.prompt
|
|
708
|
-
})
|
|
709
|
-
},
|
|
961
|
+
output: imageOutput("Generated"),
|
|
710
962
|
async execute(args, exec) {
|
|
711
963
|
const active = resolveProvider(current());
|
|
712
964
|
const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
|
|
@@ -751,13 +1003,214 @@ function apply(ctx, config = {}) {
|
|
|
751
1003
|
},
|
|
752
1004
|
presentResult: (_args, result) => imagePresentation(result)
|
|
753
1005
|
}));
|
|
1006
|
+
ctx.tools.register(defineTool({
|
|
1007
|
+
name: "edit_image",
|
|
1008
|
+
description: "Edit or restyle an existing image with the configured provider. For a named workspace file, pass its exact path as source_path. For a specific image still attached to the current conversation, pass source_attachment_id. Never provide both. Omit both only when the user clearly means the newest conversation image. If the user identifies an older image but its exact path or attachment id is unknown, do not silently edit the newest image: first use an image-reading tool to put the intended file into the conversation, then call edit_image without a source.",
|
|
1009
|
+
parameters: {
|
|
1010
|
+
prompt: {
|
|
1011
|
+
type: "string",
|
|
1012
|
+
required: true,
|
|
1013
|
+
description: "Describe the changes to make while preserving everything else that should remain."
|
|
1014
|
+
},
|
|
1015
|
+
source_attachment_id: {
|
|
1016
|
+
type: "string",
|
|
1017
|
+
description: "Optional attachment id of a specific image already present in the current conversation."
|
|
1018
|
+
},
|
|
1019
|
+
source_path: {
|
|
1020
|
+
type: "string",
|
|
1021
|
+
description: "Optional absolute or workspace-relative path of a specific image file inside the active session workspace. Prefer this when the user names a saved file."
|
|
1022
|
+
},
|
|
1023
|
+
aspect_ratio: {
|
|
1024
|
+
type: "string",
|
|
1025
|
+
enum: [
|
|
1026
|
+
"1:1",
|
|
1027
|
+
"3:2",
|
|
1028
|
+
"2:3",
|
|
1029
|
+
"4:3",
|
|
1030
|
+
"3:4",
|
|
1031
|
+
"16:9",
|
|
1032
|
+
"9:16"
|
|
1033
|
+
],
|
|
1034
|
+
description: "Optional output aspect ratio for Google Gemini."
|
|
1035
|
+
},
|
|
1036
|
+
image_size: {
|
|
1037
|
+
type: "string",
|
|
1038
|
+
enum: [
|
|
1039
|
+
"1K",
|
|
1040
|
+
"2K",
|
|
1041
|
+
"4K"
|
|
1042
|
+
],
|
|
1043
|
+
description: "Optional output resolution for Google Gemini."
|
|
1044
|
+
},
|
|
1045
|
+
size: {
|
|
1046
|
+
type: "string",
|
|
1047
|
+
description: "Optional output size for OpenAI, Seedream, or DashScope."
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
output: imageOutput("Edited"),
|
|
1051
|
+
async execute(args, exec) {
|
|
1052
|
+
const active = resolveProvider(current());
|
|
1053
|
+
const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
|
|
1054
|
+
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.`);
|
|
1055
|
+
const sourceImage = await resolveReferenceImage({
|
|
1056
|
+
...exec.agent === void 0 ? {} : { agent: exec.agent },
|
|
1057
|
+
attachments: ctx.attachments,
|
|
1058
|
+
...typeof args.source_attachment_id === "string" ? { sourceAttachmentId: args.source_attachment_id } : {},
|
|
1059
|
+
...typeof args.source_path === "string" ? { sourcePath: args.source_path } : {},
|
|
1060
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1061
|
+
signal: exec.signal
|
|
1062
|
+
});
|
|
1063
|
+
if (active.provider === "google") {
|
|
1064
|
+
const aspectRatio = args.aspect_ratio ?? active.aspectRatio;
|
|
1065
|
+
const imageSize = args.image_size ?? active.imageSize;
|
|
1066
|
+
return saveGenerated(ctx, await editGoogleImage({
|
|
1067
|
+
apiKey: credential.value,
|
|
1068
|
+
endpoint: active.endpoint,
|
|
1069
|
+
model: active.model,
|
|
1070
|
+
prompt: args.prompt,
|
|
1071
|
+
sourceImage,
|
|
1072
|
+
aspectRatio,
|
|
1073
|
+
imageSize,
|
|
1074
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1075
|
+
signal: exec.signal
|
|
1076
|
+
}), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec);
|
|
1077
|
+
}
|
|
1078
|
+
const size = args.size ?? active.imageSize;
|
|
1079
|
+
if (active.provider === "openai") return saveGenerated(ctx, await editOpenAICompatibleImage({
|
|
1080
|
+
apiKey: credential.value,
|
|
1081
|
+
baseURL: active.baseURL,
|
|
1082
|
+
model: active.model,
|
|
1083
|
+
prompt: args.prompt,
|
|
1084
|
+
sourceImage,
|
|
1085
|
+
size,
|
|
1086
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1087
|
+
signal: exec.signal
|
|
1088
|
+
}), active.provider, active.model, size, current(), exec);
|
|
1089
|
+
if (active.provider === "seedream") return saveGenerated(ctx, await editSeedreamImage({
|
|
1090
|
+
apiKey: credential.value,
|
|
1091
|
+
baseURL: active.baseURL,
|
|
1092
|
+
model: active.model,
|
|
1093
|
+
prompt: args.prompt,
|
|
1094
|
+
sourceImage,
|
|
1095
|
+
size,
|
|
1096
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1097
|
+
signal: exec.signal
|
|
1098
|
+
}), active.provider, active.model, size, current(), exec);
|
|
1099
|
+
return saveGenerated(ctx, await editDashScopeImage({
|
|
1100
|
+
apiKey: credential.value,
|
|
1101
|
+
endpoint: active.endpoint,
|
|
1102
|
+
model: active.model,
|
|
1103
|
+
prompt: args.prompt,
|
|
1104
|
+
sourceImage,
|
|
1105
|
+
size,
|
|
1106
|
+
maxBytes: ctx.attachments.imageLimits.maxImageBytes,
|
|
1107
|
+
signal: exec.signal
|
|
1108
|
+
}), active.provider, active.model, size, current(), exec);
|
|
1109
|
+
},
|
|
1110
|
+
presentResult: (_args, result) => imagePresentation(result)
|
|
1111
|
+
}));
|
|
1112
|
+
}
|
|
1113
|
+
function imageOutput(verb) {
|
|
1114
|
+
return {
|
|
1115
|
+
schema: {
|
|
1116
|
+
type: "object",
|
|
1117
|
+
additionalProperties: false,
|
|
1118
|
+
properties: {
|
|
1119
|
+
attachment: {
|
|
1120
|
+
type: "object",
|
|
1121
|
+
required: true,
|
|
1122
|
+
additionalProperties: false,
|
|
1123
|
+
properties: {
|
|
1124
|
+
attachmentId: {
|
|
1125
|
+
type: "string",
|
|
1126
|
+
required: true
|
|
1127
|
+
},
|
|
1128
|
+
mediaType: {
|
|
1129
|
+
type: "string",
|
|
1130
|
+
required: true
|
|
1131
|
+
},
|
|
1132
|
+
bytes: {
|
|
1133
|
+
type: "integer",
|
|
1134
|
+
required: true
|
|
1135
|
+
},
|
|
1136
|
+
width: {
|
|
1137
|
+
type: "integer",
|
|
1138
|
+
required: true
|
|
1139
|
+
},
|
|
1140
|
+
height: {
|
|
1141
|
+
type: "integer",
|
|
1142
|
+
required: true
|
|
1143
|
+
},
|
|
1144
|
+
name: { type: "string" },
|
|
1145
|
+
originalDimensions: {
|
|
1146
|
+
type: "object",
|
|
1147
|
+
additionalProperties: false,
|
|
1148
|
+
properties: {
|
|
1149
|
+
width: {
|
|
1150
|
+
type: "integer",
|
|
1151
|
+
required: true
|
|
1152
|
+
},
|
|
1153
|
+
height: {
|
|
1154
|
+
type: "integer",
|
|
1155
|
+
required: true
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
provider: {
|
|
1162
|
+
type: "string",
|
|
1163
|
+
required: true
|
|
1164
|
+
},
|
|
1165
|
+
model: {
|
|
1166
|
+
type: "string",
|
|
1167
|
+
required: true
|
|
1168
|
+
},
|
|
1169
|
+
output: {
|
|
1170
|
+
type: "string",
|
|
1171
|
+
required: true
|
|
1172
|
+
},
|
|
1173
|
+
savedTo: { type: "string" },
|
|
1174
|
+
saveError: { type: "string" }
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
render: (_args, value) => {
|
|
1178
|
+
const saved = typeof value.savedTo === "string" ? ` It was also saved to the workspace as ${value.savedTo}.` : typeof value.saveError === "string" ? ` Saving it to the workspace failed: ${value.saveError}.` : " It has no local file path.";
|
|
1179
|
+
const action = verb === "Generated" ? "It is already attached to the conversation." : "The edited image is attached to the conversation.";
|
|
1180
|
+
return [{
|
|
1181
|
+
type: "text",
|
|
1182
|
+
text: `${verb} one image with ${value.provider}/${value.model} (${value.output}). Attachment ID: ${String(value.attachment.attachmentId)}. ${action}${saved} Respond to the user without reading or searching for the image.`
|
|
1183
|
+
}, {
|
|
1184
|
+
type: "image",
|
|
1185
|
+
attachment: value.attachment
|
|
1186
|
+
}];
|
|
1187
|
+
},
|
|
1188
|
+
presentationMeta: (args, value) => ({
|
|
1189
|
+
kind: "dsh-image-gen",
|
|
1190
|
+
attachment: attachmentMeta(value.attachment),
|
|
1191
|
+
provider: value.provider,
|
|
1192
|
+
model: value.model,
|
|
1193
|
+
output: value.output,
|
|
1194
|
+
...verb === "Edited" ? { operation: "edit" } : {},
|
|
1195
|
+
...typeof value.savedTo === "string" ? { savedTo: value.savedTo } : {},
|
|
1196
|
+
prompt: args.prompt
|
|
1197
|
+
})
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
function attachmentMeta(ref) {
|
|
1201
|
+
return {
|
|
1202
|
+
attachmentId: String(ref.attachmentId),
|
|
1203
|
+
mediaType: ref.mediaType,
|
|
1204
|
+
bytes: ref.bytes,
|
|
1205
|
+
width: ref.width,
|
|
1206
|
+
height: ref.height,
|
|
1207
|
+
...ref.name === void 0 ? {} : { name: ref.name },
|
|
1208
|
+
...ref.originalDimensions === void 0 ? {} : { originalDimensions: {
|
|
1209
|
+
width: ref.originalDimensions.width,
|
|
1210
|
+
height: ref.originalDimensions.height
|
|
1211
|
+
} }
|
|
1212
|
+
};
|
|
754
1213
|
}
|
|
755
|
-
/**
|
|
756
|
-
* Persist the generated image as a durable attachment, then — when workspace
|
|
757
|
-
* saving is enabled — also write it as a file under the calling agent's
|
|
758
|
-
* session workspace. A workspace write failure never discards the generated
|
|
759
|
-
* attachment: it is reported through `saveError` instead.
|
|
760
|
-
*/
|
|
761
1214
|
async function saveGenerated(ctx, generated, provider, model, output, config, exec) {
|
|
762
1215
|
if (!ctx.attachments.imageLimits.mediaTypes.includes(generated.mediaType)) throw new Error(`This DSH deployment does not accept ${generated.mediaType} generated images`);
|
|
763
1216
|
const attachment = await ctx.attachments.saveImage({
|