@alfe.ai/openclaw 0.1.0 → 0.3.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/dist/plugin2.cjs +149 -33
- package/dist/plugin2.js +150 -34
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
package/dist/plugin2.cjs
CHANGED
|
@@ -31,6 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
}) : target, mod));
|
|
32
32
|
//#endregion
|
|
33
33
|
let node_path = require("node:path");
|
|
34
|
+
let node_fs_promises = require("node:fs/promises");
|
|
34
35
|
let node_os = require("node:os");
|
|
35
36
|
let node_module = require("node:module");
|
|
36
37
|
let _alfe_ai_config = require("@alfe.ai/config");
|
|
@@ -3089,6 +3090,29 @@ async function registerWithDaemon(client, log, pluginName = "@alfe.ai/openclaw")
|
|
|
3089
3090
|
const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
|
|
3090
3091
|
const DEFAULT_SOCKET_PATH = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "gateway.sock");
|
|
3091
3092
|
let ipcClient = null;
|
|
3093
|
+
/**
|
|
3094
|
+
* The local AI proxy (packages/ai-proxy-local) runs alongside the daemon on a
|
|
3095
|
+
* fixed loopback port in every runtime mode (managed VM / docker / self-hosted),
|
|
3096
|
+
* injects the agent's Alfe key, and forwards arbitrary paths to the cloud AI
|
|
3097
|
+
* proxy. openclaw-chat hardcodes the same host for /__alfe/set-identity.
|
|
3098
|
+
*/
|
|
3099
|
+
const LOCAL_AI_PROXY_URL = process.env.ALFE_AI_PROXY_URL ?? "http://127.0.0.1:18193";
|
|
3100
|
+
/** Max avatar upload size — mirrors the server-side finalize check. */
|
|
3101
|
+
const MAX_AVATAR_BYTES = 5 * 1024 * 1024;
|
|
3102
|
+
/** Image types the avatar presign/finalize flow accepts, keyed by file extension. */
|
|
3103
|
+
const AVATAR_MIME_BY_EXT = {
|
|
3104
|
+
".png": "image/png",
|
|
3105
|
+
".jpg": "image/jpeg",
|
|
3106
|
+
".jpeg": "image/jpeg",
|
|
3107
|
+
".webp": "image/webp"
|
|
3108
|
+
};
|
|
3109
|
+
/** Infer an avatar mime type from a file path, rejecting unsupported types. */
|
|
3110
|
+
function avatarMimeFromPath(path) {
|
|
3111
|
+
const ext = (0, node_path.extname)(path).toLowerCase();
|
|
3112
|
+
const mimeType = AVATAR_MIME_BY_EXT[ext];
|
|
3113
|
+
if (!mimeType) throw new Error(`unsupported image type "${ext || path}" — set_avatar accepts .png, .jpg/.jpeg, or .webp`);
|
|
3114
|
+
return mimeType;
|
|
3115
|
+
}
|
|
3092
3116
|
function ok(result) {
|
|
3093
3117
|
return { content: [{
|
|
3094
3118
|
type: "text",
|
|
@@ -3182,40 +3206,132 @@ function buildIntegrationTools(client) {
|
|
|
3182
3206
|
];
|
|
3183
3207
|
}
|
|
3184
3208
|
function buildVoiceTools(client) {
|
|
3185
|
-
return [
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
name: "set_voice",
|
|
3202
|
-
description: "Change this agent's OWN voice. Pass a `voiceId` from list_voices. Optionally set `enabled` to turn voice on/off. Returns the updated voice config.",
|
|
3203
|
-
parameters: Type.Object({
|
|
3204
|
-
voiceId: Type.String({ description: "The ElevenLabs voice id to use, from list_voices (e.g. the `id` field)." }),
|
|
3205
|
-
enabled: Type.Optional(Type.Boolean({ description: "Enable or disable voice. Omit to leave the current on/off state unchanged." }))
|
|
3209
|
+
return [
|
|
3210
|
+
defineTool({
|
|
3211
|
+
name: "list_voices",
|
|
3212
|
+
description: "List the available voices this agent can speak with (the ElevenLabs catalogue). Use this to pick or confirm a voice before calling set_voice with the chosen id.",
|
|
3213
|
+
parameters: Type.Object({}),
|
|
3214
|
+
handler: async () => {
|
|
3215
|
+
const { voices } = await client.listVoices();
|
|
3216
|
+
return { voices: voices.map((v) => ({
|
|
3217
|
+
id: v.id,
|
|
3218
|
+
name: v.name,
|
|
3219
|
+
description: v.description,
|
|
3220
|
+
labels: v.labels,
|
|
3221
|
+
category: v.category,
|
|
3222
|
+
previewUrl: v.previewUrl
|
|
3223
|
+
})) };
|
|
3224
|
+
}
|
|
3206
3225
|
}),
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3226
|
+
defineTool({
|
|
3227
|
+
name: "set_voice",
|
|
3228
|
+
description: "Change this agent's OWN voice. Pass a `voiceId` from list_voices. Optionally set `enabled` to turn voice on/off. Returns the updated voice config.",
|
|
3229
|
+
parameters: Type.Object({
|
|
3230
|
+
voiceId: Type.String({ description: "The ElevenLabs voice id to use, from list_voices (e.g. the `id` field)." }),
|
|
3231
|
+
enabled: Type.Optional(Type.Boolean({ description: "Enable or disable voice. Omit to leave the current on/off state unchanged." }))
|
|
3232
|
+
}),
|
|
3233
|
+
handler: async (params) => {
|
|
3234
|
+
const voiceId = params.voiceId;
|
|
3235
|
+
const enabled = params.enabled;
|
|
3236
|
+
return {
|
|
3237
|
+
updated: true,
|
|
3238
|
+
voiceConfig: (await client.updateSelf({ voiceConfig: {
|
|
3239
|
+
voiceId,
|
|
3240
|
+
...enabled === void 0 ? {} : { enabled }
|
|
3241
|
+
} })).voiceConfig
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
}),
|
|
3245
|
+
defineTool({
|
|
3246
|
+
name: "generate_avatar",
|
|
3247
|
+
description: "Generate and set THIS agent's own profile picture / avatar from a text prompt (e.g. a character description). Updates the agent's real profile picture shown in the dashboard, chat, and apps. Use this — do NOT use `config set ui.assistant.avatar`, which has no effect on the real profile.",
|
|
3248
|
+
parameters: Type.Object({ prompt: Type.String({ description: "Text description of the avatar to generate (e.g. a character or portrait description)." }) }),
|
|
3249
|
+
handler: async (params) => {
|
|
3250
|
+
const prompt = params.prompt;
|
|
3251
|
+
return {
|
|
3252
|
+
updated: true,
|
|
3253
|
+
avatarUrl: (await client.generateAvatar({ prompt })).avatarUrl,
|
|
3254
|
+
note: "This is now your live profile picture across the Alfe dashboard, chat, and apps. `config set ui.assistant.avatar` is NOT the way — it has no effect on the real profile."
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3257
|
+
}),
|
|
3258
|
+
defineTool({
|
|
3259
|
+
name: "set_avatar",
|
|
3260
|
+
description: "Set THIS agent's own profile picture from a local image file you created or downloaded (png/jpeg/webp, <=5MB). Updates the real profile picture shown in the dashboard, chat, and apps. Use this — do NOT use `config set ui.assistant.avatar`, which has no effect on the real profile.",
|
|
3261
|
+
parameters: Type.Object({ path: Type.String({ description: "Absolute path to a local image file (.png, .jpg/.jpeg, or .webp, <=5MB) to use as the avatar." }) }),
|
|
3262
|
+
handler: async (params) => {
|
|
3263
|
+
const path = params.path;
|
|
3264
|
+
const mimeType = avatarMimeFromPath(path);
|
|
3265
|
+
const bytes = await (0, node_fs_promises.readFile)(path);
|
|
3266
|
+
if (bytes.length > MAX_AVATAR_BYTES) throw new Error(`image is ${String(bytes.length)} bytes — the avatar limit is ${String(MAX_AVATAR_BYTES)} bytes (5MB)`);
|
|
3267
|
+
const { uploadUrl, s3Key } = await client.presignAvatar({
|
|
3268
|
+
mimeType,
|
|
3269
|
+
size: bytes.length
|
|
3270
|
+
});
|
|
3271
|
+
const putRes = await fetch(uploadUrl, {
|
|
3272
|
+
method: "PUT",
|
|
3273
|
+
headers: { "Content-Type": mimeType },
|
|
3274
|
+
body: bytes
|
|
3275
|
+
});
|
|
3276
|
+
if (!putRes.ok) throw new Error(`failed to upload the avatar image (${String(putRes.status)})`);
|
|
3277
|
+
return {
|
|
3278
|
+
updated: true,
|
|
3279
|
+
avatarUrl: (await client.finalizeAvatar(s3Key)).avatarUrl,
|
|
3280
|
+
note: "This is now your live profile picture across the Alfe dashboard, chat, and apps. `config set ui.assistant.avatar` is NOT the way — it has no effect on the real profile."
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
}),
|
|
3284
|
+
defineTool({
|
|
3285
|
+
name: "generate_image",
|
|
3286
|
+
description: "Generate an image from a text prompt. Returns a URL to the generated PNG. To show the image to the user, embed it in your reply as markdown: . Optional `model` picks the image model (default gpt-image-1).",
|
|
3287
|
+
parameters: Type.Object({
|
|
3288
|
+
prompt: Type.String({ description: "Text description of the image to generate." }),
|
|
3289
|
+
model: Type.Optional(Type.String({ description: "Image model id (default \"gpt-image-1\"). Others may be available, e.g. dall-e-3." })),
|
|
3290
|
+
size: Type.Optional(Type.String({ description: "Image size, e.g. \"1024x1024\" (provider-dependent)." })),
|
|
3291
|
+
quality: Type.Optional(Type.String({ description: "Image quality hint (provider-dependent)." }))
|
|
3292
|
+
}),
|
|
3293
|
+
handler: async (params) => {
|
|
3294
|
+
const prompt = params.prompt;
|
|
3295
|
+
const model = params.model ?? "gpt-image-1";
|
|
3296
|
+
const size = params.size;
|
|
3297
|
+
const quality = params.quality;
|
|
3298
|
+
const genRes = await fetch(`${LOCAL_AI_PROXY_URL}/v1/images/generations`, {
|
|
3299
|
+
method: "POST",
|
|
3300
|
+
headers: { "Content-Type": "application/json" },
|
|
3301
|
+
body: JSON.stringify({
|
|
3302
|
+
model,
|
|
3303
|
+
prompt,
|
|
3304
|
+
...size ? { size } : {},
|
|
3305
|
+
...quality ? { quality } : {}
|
|
3306
|
+
})
|
|
3307
|
+
});
|
|
3308
|
+
const genJson = await genRes.json().catch(() => ({}));
|
|
3309
|
+
if (!genRes.ok) throw new Error(`image generation failed (${String(genRes.status)}): ${genJson.message ?? genJson.error ?? "unknown error"}`);
|
|
3310
|
+
const b64 = genJson.data?.[0]?.b64_json;
|
|
3311
|
+
if (!b64) throw new Error("image generation returned no image data");
|
|
3312
|
+
const buffer = Buffer.from(b64, "base64");
|
|
3313
|
+
const filename = `generated-image-${String(Date.now())}.png`;
|
|
3314
|
+
const { attachments } = await client.presignAttachments([{
|
|
3315
|
+
filename,
|
|
3316
|
+
mimeType: "image/png",
|
|
3317
|
+
size: buffer.length
|
|
3318
|
+
}]);
|
|
3319
|
+
if (attachments.length === 0) throw new Error("failed to presign upload for the generated image");
|
|
3320
|
+
const att = attachments[0];
|
|
3321
|
+
const putRes = await fetch(att.uploadUrl, {
|
|
3322
|
+
method: "PUT",
|
|
3323
|
+
headers: { "Content-Type": "image/png" },
|
|
3324
|
+
body: buffer
|
|
3325
|
+
});
|
|
3326
|
+
if (!putRes.ok) throw new Error(`failed to upload the generated image (${String(putRes.status)})`);
|
|
3327
|
+
return {
|
|
3328
|
+
imageUrl: att.downloadUrl,
|
|
3329
|
+
model,
|
|
3330
|
+
instruction: "Show this image to the user by including it in your reply as markdown: ."
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
})
|
|
3334
|
+
];
|
|
3219
3335
|
}
|
|
3220
3336
|
const plugin = {
|
|
3221
3337
|
id: "@alfe.ai/openclaw",
|
package/dist/plugin2.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { extname, join } from "node:path";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { resolveConfig } from "@alfe.ai/config";
|
|
5
6
|
import { AgentApiClient } from "@alfe.ai/agent-api-client";
|
|
@@ -3090,6 +3091,29 @@ async function registerWithDaemon(client, log, pluginName = "@alfe.ai/openclaw")
|
|
|
3090
3091
|
const pkg = createRequire(import.meta.url)("../package.json");
|
|
3091
3092
|
const DEFAULT_SOCKET_PATH = join(homedir(), ".alfe", "gateway.sock");
|
|
3092
3093
|
let ipcClient = null;
|
|
3094
|
+
/**
|
|
3095
|
+
* The local AI proxy (packages/ai-proxy-local) runs alongside the daemon on a
|
|
3096
|
+
* fixed loopback port in every runtime mode (managed VM / docker / self-hosted),
|
|
3097
|
+
* injects the agent's Alfe key, and forwards arbitrary paths to the cloud AI
|
|
3098
|
+
* proxy. openclaw-chat hardcodes the same host for /__alfe/set-identity.
|
|
3099
|
+
*/
|
|
3100
|
+
const LOCAL_AI_PROXY_URL = process.env.ALFE_AI_PROXY_URL ?? "http://127.0.0.1:18193";
|
|
3101
|
+
/** Max avatar upload size — mirrors the server-side finalize check. */
|
|
3102
|
+
const MAX_AVATAR_BYTES = 5 * 1024 * 1024;
|
|
3103
|
+
/** Image types the avatar presign/finalize flow accepts, keyed by file extension. */
|
|
3104
|
+
const AVATAR_MIME_BY_EXT = {
|
|
3105
|
+
".png": "image/png",
|
|
3106
|
+
".jpg": "image/jpeg",
|
|
3107
|
+
".jpeg": "image/jpeg",
|
|
3108
|
+
".webp": "image/webp"
|
|
3109
|
+
};
|
|
3110
|
+
/** Infer an avatar mime type from a file path, rejecting unsupported types. */
|
|
3111
|
+
function avatarMimeFromPath(path) {
|
|
3112
|
+
const ext = extname(path).toLowerCase();
|
|
3113
|
+
const mimeType = AVATAR_MIME_BY_EXT[ext];
|
|
3114
|
+
if (!mimeType) throw new Error(`unsupported image type "${ext || path}" — set_avatar accepts .png, .jpg/.jpeg, or .webp`);
|
|
3115
|
+
return mimeType;
|
|
3116
|
+
}
|
|
3093
3117
|
function ok(result) {
|
|
3094
3118
|
return { content: [{
|
|
3095
3119
|
type: "text",
|
|
@@ -3183,40 +3207,132 @@ function buildIntegrationTools(client) {
|
|
|
3183
3207
|
];
|
|
3184
3208
|
}
|
|
3185
3209
|
function buildVoiceTools(client) {
|
|
3186
|
-
return [
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
name: "set_voice",
|
|
3203
|
-
description: "Change this agent's OWN voice. Pass a `voiceId` from list_voices. Optionally set `enabled` to turn voice on/off. Returns the updated voice config.",
|
|
3204
|
-
parameters: Type.Object({
|
|
3205
|
-
voiceId: Type.String({ description: "The ElevenLabs voice id to use, from list_voices (e.g. the `id` field)." }),
|
|
3206
|
-
enabled: Type.Optional(Type.Boolean({ description: "Enable or disable voice. Omit to leave the current on/off state unchanged." }))
|
|
3210
|
+
return [
|
|
3211
|
+
defineTool({
|
|
3212
|
+
name: "list_voices",
|
|
3213
|
+
description: "List the available voices this agent can speak with (the ElevenLabs catalogue). Use this to pick or confirm a voice before calling set_voice with the chosen id.",
|
|
3214
|
+
parameters: Type.Object({}),
|
|
3215
|
+
handler: async () => {
|
|
3216
|
+
const { voices } = await client.listVoices();
|
|
3217
|
+
return { voices: voices.map((v) => ({
|
|
3218
|
+
id: v.id,
|
|
3219
|
+
name: v.name,
|
|
3220
|
+
description: v.description,
|
|
3221
|
+
labels: v.labels,
|
|
3222
|
+
category: v.category,
|
|
3223
|
+
previewUrl: v.previewUrl
|
|
3224
|
+
})) };
|
|
3225
|
+
}
|
|
3207
3226
|
}),
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3227
|
+
defineTool({
|
|
3228
|
+
name: "set_voice",
|
|
3229
|
+
description: "Change this agent's OWN voice. Pass a `voiceId` from list_voices. Optionally set `enabled` to turn voice on/off. Returns the updated voice config.",
|
|
3230
|
+
parameters: Type.Object({
|
|
3231
|
+
voiceId: Type.String({ description: "The ElevenLabs voice id to use, from list_voices (e.g. the `id` field)." }),
|
|
3232
|
+
enabled: Type.Optional(Type.Boolean({ description: "Enable or disable voice. Omit to leave the current on/off state unchanged." }))
|
|
3233
|
+
}),
|
|
3234
|
+
handler: async (params) => {
|
|
3235
|
+
const voiceId = params.voiceId;
|
|
3236
|
+
const enabled = params.enabled;
|
|
3237
|
+
return {
|
|
3238
|
+
updated: true,
|
|
3239
|
+
voiceConfig: (await client.updateSelf({ voiceConfig: {
|
|
3240
|
+
voiceId,
|
|
3241
|
+
...enabled === void 0 ? {} : { enabled }
|
|
3242
|
+
} })).voiceConfig
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
}),
|
|
3246
|
+
defineTool({
|
|
3247
|
+
name: "generate_avatar",
|
|
3248
|
+
description: "Generate and set THIS agent's own profile picture / avatar from a text prompt (e.g. a character description). Updates the agent's real profile picture shown in the dashboard, chat, and apps. Use this — do NOT use `config set ui.assistant.avatar`, which has no effect on the real profile.",
|
|
3249
|
+
parameters: Type.Object({ prompt: Type.String({ description: "Text description of the avatar to generate (e.g. a character or portrait description)." }) }),
|
|
3250
|
+
handler: async (params) => {
|
|
3251
|
+
const prompt = params.prompt;
|
|
3252
|
+
return {
|
|
3253
|
+
updated: true,
|
|
3254
|
+
avatarUrl: (await client.generateAvatar({ prompt })).avatarUrl,
|
|
3255
|
+
note: "This is now your live profile picture across the Alfe dashboard, chat, and apps. `config set ui.assistant.avatar` is NOT the way — it has no effect on the real profile."
|
|
3256
|
+
};
|
|
3257
|
+
}
|
|
3258
|
+
}),
|
|
3259
|
+
defineTool({
|
|
3260
|
+
name: "set_avatar",
|
|
3261
|
+
description: "Set THIS agent's own profile picture from a local image file you created or downloaded (png/jpeg/webp, <=5MB). Updates the real profile picture shown in the dashboard, chat, and apps. Use this — do NOT use `config set ui.assistant.avatar`, which has no effect on the real profile.",
|
|
3262
|
+
parameters: Type.Object({ path: Type.String({ description: "Absolute path to a local image file (.png, .jpg/.jpeg, or .webp, <=5MB) to use as the avatar." }) }),
|
|
3263
|
+
handler: async (params) => {
|
|
3264
|
+
const path = params.path;
|
|
3265
|
+
const mimeType = avatarMimeFromPath(path);
|
|
3266
|
+
const bytes = await readFile(path);
|
|
3267
|
+
if (bytes.length > MAX_AVATAR_BYTES) throw new Error(`image is ${String(bytes.length)} bytes — the avatar limit is ${String(MAX_AVATAR_BYTES)} bytes (5MB)`);
|
|
3268
|
+
const { uploadUrl, s3Key } = await client.presignAvatar({
|
|
3269
|
+
mimeType,
|
|
3270
|
+
size: bytes.length
|
|
3271
|
+
});
|
|
3272
|
+
const putRes = await fetch(uploadUrl, {
|
|
3273
|
+
method: "PUT",
|
|
3274
|
+
headers: { "Content-Type": mimeType },
|
|
3275
|
+
body: bytes
|
|
3276
|
+
});
|
|
3277
|
+
if (!putRes.ok) throw new Error(`failed to upload the avatar image (${String(putRes.status)})`);
|
|
3278
|
+
return {
|
|
3279
|
+
updated: true,
|
|
3280
|
+
avatarUrl: (await client.finalizeAvatar(s3Key)).avatarUrl,
|
|
3281
|
+
note: "This is now your live profile picture across the Alfe dashboard, chat, and apps. `config set ui.assistant.avatar` is NOT the way — it has no effect on the real profile."
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
}),
|
|
3285
|
+
defineTool({
|
|
3286
|
+
name: "generate_image",
|
|
3287
|
+
description: "Generate an image from a text prompt. Returns a URL to the generated PNG. To show the image to the user, embed it in your reply as markdown: . Optional `model` picks the image model (default gpt-image-1).",
|
|
3288
|
+
parameters: Type.Object({
|
|
3289
|
+
prompt: Type.String({ description: "Text description of the image to generate." }),
|
|
3290
|
+
model: Type.Optional(Type.String({ description: "Image model id (default \"gpt-image-1\"). Others may be available, e.g. dall-e-3." })),
|
|
3291
|
+
size: Type.Optional(Type.String({ description: "Image size, e.g. \"1024x1024\" (provider-dependent)." })),
|
|
3292
|
+
quality: Type.Optional(Type.String({ description: "Image quality hint (provider-dependent)." }))
|
|
3293
|
+
}),
|
|
3294
|
+
handler: async (params) => {
|
|
3295
|
+
const prompt = params.prompt;
|
|
3296
|
+
const model = params.model ?? "gpt-image-1";
|
|
3297
|
+
const size = params.size;
|
|
3298
|
+
const quality = params.quality;
|
|
3299
|
+
const genRes = await fetch(`${LOCAL_AI_PROXY_URL}/v1/images/generations`, {
|
|
3300
|
+
method: "POST",
|
|
3301
|
+
headers: { "Content-Type": "application/json" },
|
|
3302
|
+
body: JSON.stringify({
|
|
3303
|
+
model,
|
|
3304
|
+
prompt,
|
|
3305
|
+
...size ? { size } : {},
|
|
3306
|
+
...quality ? { quality } : {}
|
|
3307
|
+
})
|
|
3308
|
+
});
|
|
3309
|
+
const genJson = await genRes.json().catch(() => ({}));
|
|
3310
|
+
if (!genRes.ok) throw new Error(`image generation failed (${String(genRes.status)}): ${genJson.message ?? genJson.error ?? "unknown error"}`);
|
|
3311
|
+
const b64 = genJson.data?.[0]?.b64_json;
|
|
3312
|
+
if (!b64) throw new Error("image generation returned no image data");
|
|
3313
|
+
const buffer = Buffer.from(b64, "base64");
|
|
3314
|
+
const filename = `generated-image-${String(Date.now())}.png`;
|
|
3315
|
+
const { attachments } = await client.presignAttachments([{
|
|
3316
|
+
filename,
|
|
3317
|
+
mimeType: "image/png",
|
|
3318
|
+
size: buffer.length
|
|
3319
|
+
}]);
|
|
3320
|
+
if (attachments.length === 0) throw new Error("failed to presign upload for the generated image");
|
|
3321
|
+
const att = attachments[0];
|
|
3322
|
+
const putRes = await fetch(att.uploadUrl, {
|
|
3323
|
+
method: "PUT",
|
|
3324
|
+
headers: { "Content-Type": "image/png" },
|
|
3325
|
+
body: buffer
|
|
3326
|
+
});
|
|
3327
|
+
if (!putRes.ok) throw new Error(`failed to upload the generated image (${String(putRes.status)})`);
|
|
3328
|
+
return {
|
|
3329
|
+
imageUrl: att.downloadUrl,
|
|
3330
|
+
model,
|
|
3331
|
+
instruction: "Show this image to the user by including it in your reply as markdown: ."
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
})
|
|
3335
|
+
];
|
|
3220
3336
|
}
|
|
3221
3337
|
const plugin = {
|
|
3222
3338
|
id: "@alfe.ai/openclaw",
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED