discoclaw 0.8.0 → 0.8.2
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/cron/executor.js +2 -1
- package/dist/discord/actions-config.js +28 -2
- package/dist/discord/actions-config.test.js +4 -4
- package/dist/discord/actions-imagegen.js +155 -30
- package/dist/discord/actions-imagegen.test.js +608 -24
- package/dist/discord/actions-messaging.js +40 -4
- package/dist/discord/actions-messaging.test.js +169 -0
- package/dist/discord/actions.js +6 -3
- package/dist/discord/deferred-runner.js +2 -0
- package/dist/discord/image-download.js +54 -66
- package/dist/discord/image-download.test.js +156 -144
- package/dist/discord/message-coordinator.js +54 -13
- package/dist/discord/message-coordinator.test.js +206 -1
- package/dist/discord/message-history.js +21 -9
- package/dist/discord/message-history.test.js +98 -15
- package/dist/discord/models-command.js +3 -3
- package/dist/discord/models-command.test.js +3 -4
- package/dist/discord/reaction-handler.js +4 -1
- package/dist/discord/reaction-handler.test.js +4 -1
- package/dist/discord/reply-reference.test.js +2 -0
- package/dist/discord/streaming-progress.js +55 -0
- package/dist/discord/thread-context.js +61 -27
- package/dist/discord/thread-context.test.js +180 -1
- package/dist/image/url-safety.js +159 -0
- package/dist/image/url-safety.test.js +193 -0
- package/dist/index.js +10 -0
- package/dist/runtime/model-tiers.js +1 -1
- package/dist/runtime/model-tiers.test.js +4 -4
- package/dist/runtime/tools/image-download.js +65 -26
- package/dist/runtime/tools/image-download.test.js +9 -3
- package/package.json +1 -1
package/dist/cron/executor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveDefaultModel as resolveImagegenDefaultModel } from '../discord/actions-imagegen.js';
|
|
1
2
|
import { acquireCronLock, releaseCronLock } from './job-lock.js';
|
|
2
3
|
import { resolveChannel } from '../discord/action-utils.js';
|
|
3
4
|
import { DiscordTransportClient } from '../discord/transport-client.js';
|
|
@@ -211,7 +212,7 @@ export async function executeCronJob(job, ctx) {
|
|
|
211
212
|
});
|
|
212
213
|
// Inject tiered action schema documentation when discord actions are enabled.
|
|
213
214
|
if (ctx.discordActionsEnabled) {
|
|
214
|
-
const actionSelection = buildTieredDiscordActionsPromptSection(cronActionFlags, ctx.botDisplayName, { userText: job.def.prompt });
|
|
215
|
+
const actionSelection = buildTieredDiscordActionsPromptSection(cronActionFlags, ctx.botDisplayName, { userText: job.def.prompt, imagegenDefaultModel: ctx.imagegenCtx ? resolveImagegenDefaultModel(ctx.imagegenCtx) : undefined });
|
|
215
216
|
if (actionSelection.prompt) {
|
|
216
217
|
prompt += '\n\n---\n' + actionSelection.prompt;
|
|
217
218
|
}
|
|
@@ -21,6 +21,7 @@ const ROLE_DESCRIPTIONS = {
|
|
|
21
21
|
cron: 'Cron auto-tagging and model classification',
|
|
22
22
|
'cron-exec': 'Default model for cron job execution (overridden by per-job settings)',
|
|
23
23
|
voice: 'Voice channel AI responses',
|
|
24
|
+
imagegen: 'Default model for image generation',
|
|
24
25
|
};
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
27
|
// Executor
|
|
@@ -229,6 +230,15 @@ export function executeConfigAction(action, configCtx) {
|
|
|
229
230
|
return { ok: false, error: 'Voice subsystem not configured' };
|
|
230
231
|
}
|
|
231
232
|
break;
|
|
233
|
+
case 'imagegen':
|
|
234
|
+
if (bp.imagegenCtx) {
|
|
235
|
+
bp.imagegenCtx.defaultModel = model;
|
|
236
|
+
changes.push(`imagegen → ${model}`);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
return { ok: false, error: 'Imagegen subsystem not configured' };
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
232
242
|
default:
|
|
233
243
|
return { ok: false, error: `Unknown role: ${String(action.role)}` };
|
|
234
244
|
}
|
|
@@ -338,6 +348,21 @@ export function executeConfigAction(action, configCtx) {
|
|
|
338
348
|
}
|
|
339
349
|
}
|
|
340
350
|
break;
|
|
351
|
+
case 'imagegen':
|
|
352
|
+
if (bp.imagegenCtx) {
|
|
353
|
+
// Restore the fallback-resolved default (env setting or provider-based fallback).
|
|
354
|
+
const igEnvDefault = defaults['imagegen'];
|
|
355
|
+
if (igEnvDefault) {
|
|
356
|
+
bp.imagegenCtx.defaultModel = igEnvDefault;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
// Clear override so resolveDefaultModel falls back to provider detection.
|
|
360
|
+
bp.imagegenCtx.defaultModel = undefined;
|
|
361
|
+
}
|
|
362
|
+
const resolvedIg = resolveDefaultModel(bp.imagegenCtx);
|
|
363
|
+
resetChanges.push(`imagegen → ${resolvedIg}`);
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
341
366
|
}
|
|
342
367
|
// Clear the override marker regardless of whether we had a default.
|
|
343
368
|
if (configCtx.overrideSources) {
|
|
@@ -382,7 +407,7 @@ export function executeConfigAction(action, configCtx) {
|
|
|
382
407
|
if (bp.imagegenCtx) {
|
|
383
408
|
const igModel = resolveDefaultModel(bp.imagegenCtx);
|
|
384
409
|
const igProvider = resolveProvider(igModel);
|
|
385
|
-
rows.push(['imagegen', igModel, `Image generation (${igProvider})`, '']);
|
|
410
|
+
rows.push(['imagegen', igModel, `Image generation (${igProvider})`, ovr('imagegen')]);
|
|
386
411
|
}
|
|
387
412
|
else {
|
|
388
413
|
rows.push(['imagegen', 'setup-required', 'Image generation (setup required)', '']);
|
|
@@ -474,7 +499,7 @@ export function configActionsPromptSection() {
|
|
|
474
499
|
<discord-action>{"type":"modelSet","role":"chat","model":"sonnet"}</discord-action>
|
|
475
500
|
<discord-action>{"type":"modelSet","role":"fast","model":"haiku"}</discord-action>
|
|
476
501
|
\`\`\`
|
|
477
|
-
- \`role\` (required): One of \`chat\`, \`plan-run\`, \`fast\`, \`forge-drafter\`, \`forge-auditor\`, \`summary\`, \`cron\`, \`cron-exec\`, \`voice\`.
|
|
502
|
+
- \`role\` (required): One of \`chat\`, \`plan-run\`, \`fast\`, \`forge-drafter\`, \`forge-auditor\`, \`summary\`, \`cron\`, \`cron-exec\`, \`voice\`, \`imagegen\`.
|
|
478
503
|
- \`model\` (required): Model tier (\`fast\`, \`capable\`, \`deep\`), concrete model name (\`haiku\`, \`sonnet\`, \`opus\`), runtime name (\`openrouter\`, \`gemini\` — for \`chat\` and \`voice\` roles, swaps the active runtime adapter independently), or \`default\` (for cron-exec only, to revert to the startup default for that role). For the \`voice\` role, setting a model name that belongs to a different provider's tier map (e.g. \`sonnet\` while voice is on Gemini) will auto-switch the voice runtime to match.
|
|
479
504
|
|
|
480
505
|
**Roles:**
|
|
@@ -489,6 +514,7 @@ export function configActionsPromptSection() {
|
|
|
489
514
|
| \`cron\` | Cron auto-tagging and model classification (overrides fast) |
|
|
490
515
|
| \`cron-exec\` | Default model for cron job execution; per-job overrides (via \`cronUpdate\`) take priority |
|
|
491
516
|
| \`voice\` | Voice channel AI responses |
|
|
517
|
+
| \`imagegen\` | Default model for image generation |
|
|
492
518
|
|
|
493
519
|
Changes are **persisted** to \`models.json\` and survive restart. Use \`!models reset\` to clear overrides and revert to defaults.
|
|
494
520
|
|
|
@@ -243,7 +243,7 @@ describe('modelShow imagegen row', () => {
|
|
|
243
243
|
if (!result.ok)
|
|
244
244
|
return;
|
|
245
245
|
expect(result.summary).toContain('imagegen');
|
|
246
|
-
expect(result.summary).toContain('
|
|
246
|
+
expect(result.summary).toContain('gemini-3.1-flash-image-preview');
|
|
247
247
|
expect(result.summary).toContain('gemini');
|
|
248
248
|
});
|
|
249
249
|
it('respects explicit defaultModel', () => {
|
|
@@ -267,7 +267,7 @@ describe('modelShow imagegen row', () => {
|
|
|
267
267
|
expect(result.summary).toContain('setup-required');
|
|
268
268
|
expect(result.summary).toContain('Image generation (setup required)');
|
|
269
269
|
});
|
|
270
|
-
it('defaults to
|
|
270
|
+
it('defaults to native Gemini when both apiKey and geminiApiKey are set', () => {
|
|
271
271
|
const imagegenCtx = { apiKey: 'sk-test', geminiApiKey: 'gk-test' };
|
|
272
272
|
const ctx = makeCtx({ imagegenCtx });
|
|
273
273
|
const result = executeConfigAction({ type: 'modelShow' }, ctx);
|
|
@@ -275,8 +275,8 @@ describe('modelShow imagegen row', () => {
|
|
|
275
275
|
if (!result.ok)
|
|
276
276
|
return;
|
|
277
277
|
expect(result.summary).toContain('imagegen');
|
|
278
|
-
expect(result.summary).toContain('
|
|
279
|
-
expect(result.summary).toContain('
|
|
278
|
+
expect(result.summary).toContain('gemini-3.1-flash-image-preview');
|
|
279
|
+
expect(result.summary).toContain('gemini');
|
|
280
280
|
});
|
|
281
281
|
});
|
|
282
282
|
// ---------------------------------------------------------------------------
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AttachmentBuilder } from 'discord.js';
|
|
2
2
|
import { resolveChannel, findChannelRaw, describeChannelType } from './action-utils.js';
|
|
3
3
|
import { NO_MENTIONS } from './allowed-mentions.js';
|
|
4
|
+
import { downloadMessageImages, downloadImageUrl } from './image-download.js';
|
|
4
5
|
const IMAGEGEN_TYPE_MAP = {
|
|
5
6
|
generateImage: true,
|
|
6
7
|
};
|
|
@@ -15,14 +16,19 @@ const GPT_IMAGE_VALID_SIZES = new Set(['1024x1024', '1024x1792', '1792x1024', 'a
|
|
|
15
16
|
const GEMINI_VALID_SIZES = new Set(['1:1', '3:4', '4:3', '9:16', '16:9']);
|
|
16
17
|
const VALID_QUALITY = new Set(['standard', 'hd']);
|
|
17
18
|
const DISCORD_MAX_CONTENT = 2000;
|
|
19
|
+
// Progress UX
|
|
20
|
+
export const TYPING_INTERVAL_MS = 8_000;
|
|
21
|
+
export const DOT_CYCLE_INTERVAL_MS = 3_000;
|
|
22
|
+
export const REQUEST_TIMEOUT_MS = 120_000;
|
|
23
|
+
const DOT_STATES = ['On it.', 'On it..', 'On it...'];
|
|
18
24
|
// ---------------------------------------------------------------------------
|
|
19
25
|
// Provider resolution
|
|
20
26
|
// ---------------------------------------------------------------------------
|
|
21
27
|
export function resolveDefaultModel(imagegenCtx) {
|
|
22
28
|
if (imagegenCtx.defaultModel)
|
|
23
29
|
return imagegenCtx.defaultModel;
|
|
24
|
-
if (imagegenCtx.geminiApiKey
|
|
25
|
-
return '
|
|
30
|
+
if (imagegenCtx.geminiApiKey)
|
|
31
|
+
return 'gemini-3.1-flash-image-preview';
|
|
26
32
|
return 'dall-e-3';
|
|
27
33
|
}
|
|
28
34
|
export function resolveProvider(model, explicit) {
|
|
@@ -37,7 +43,7 @@ export function resolveProvider(model, explicit) {
|
|
|
37
43
|
// ---------------------------------------------------------------------------
|
|
38
44
|
// API callers
|
|
39
45
|
// ---------------------------------------------------------------------------
|
|
40
|
-
async function callOpenAI(prompt, model, size, quality, apiKey, baseUrl) {
|
|
46
|
+
async function callOpenAI(prompt, model, size, quality, apiKey, baseUrl, signal) {
|
|
41
47
|
const body = {
|
|
42
48
|
model,
|
|
43
49
|
prompt,
|
|
@@ -57,6 +63,7 @@ async function callOpenAI(prompt, model, size, quality, apiKey, baseUrl) {
|
|
|
57
63
|
'Content-Type': 'application/json',
|
|
58
64
|
},
|
|
59
65
|
body: JSON.stringify(body),
|
|
66
|
+
signal,
|
|
60
67
|
});
|
|
61
68
|
}
|
|
62
69
|
catch (err) {
|
|
@@ -87,7 +94,7 @@ async function callOpenAI(prompt, model, size, quality, apiKey, baseUrl) {
|
|
|
87
94
|
}
|
|
88
95
|
return { ok: true, b64: imageItem.b64_json };
|
|
89
96
|
}
|
|
90
|
-
async function callGemini(prompt, model, size, geminiApiKey) {
|
|
97
|
+
async function callGemini(prompt, model, size, geminiApiKey, signal) {
|
|
91
98
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:predict`;
|
|
92
99
|
const body = {
|
|
93
100
|
instances: [{ prompt }],
|
|
@@ -105,6 +112,7 @@ async function callGemini(prompt, model, size, geminiApiKey) {
|
|
|
105
112
|
'Content-Type': 'application/json',
|
|
106
113
|
},
|
|
107
114
|
body: JSON.stringify(body),
|
|
115
|
+
signal,
|
|
108
116
|
});
|
|
109
117
|
}
|
|
110
118
|
catch (err) {
|
|
@@ -135,10 +143,15 @@ async function callGemini(prompt, model, size, geminiApiKey) {
|
|
|
135
143
|
}
|
|
136
144
|
return { ok: true, b64 };
|
|
137
145
|
}
|
|
138
|
-
async function callGeminiNative(prompt, model, geminiApiKey) {
|
|
146
|
+
async function callGeminiNative(prompt, model, geminiApiKey, sourceImage, signal) {
|
|
139
147
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
|
|
148
|
+
const parts = [];
|
|
149
|
+
if (sourceImage) {
|
|
150
|
+
parts.push({ inlineData: { mimeType: sourceImage.mediaType, data: sourceImage.base64 } });
|
|
151
|
+
}
|
|
152
|
+
parts.push({ text: prompt });
|
|
140
153
|
const body = {
|
|
141
|
-
contents: [{ parts
|
|
154
|
+
contents: [{ parts }],
|
|
142
155
|
generationConfig: { responseModalities: ['TEXT', 'IMAGE'] },
|
|
143
156
|
};
|
|
144
157
|
let response;
|
|
@@ -150,6 +163,7 @@ async function callGeminiNative(prompt, model, geminiApiKey) {
|
|
|
150
163
|
'Content-Type': 'application/json',
|
|
151
164
|
},
|
|
152
165
|
body: JSON.stringify(body),
|
|
166
|
+
signal,
|
|
153
167
|
});
|
|
154
168
|
}
|
|
155
169
|
catch (err) {
|
|
@@ -174,14 +188,57 @@ async function callGeminiNative(prompt, model, geminiApiKey) {
|
|
|
174
188
|
catch {
|
|
175
189
|
return { ok: false, error: 'generateImage: failed to parse API response' };
|
|
176
190
|
}
|
|
177
|
-
const
|
|
178
|
-
const imagePart =
|
|
191
|
+
const responseParts = data.candidates?.[0]?.content?.parts ?? [];
|
|
192
|
+
const imagePart = responseParts.find(p => p.inlineData?.mimeType?.startsWith('image/'));
|
|
179
193
|
if (!imagePart?.inlineData?.data) {
|
|
180
194
|
return { ok: false, error: 'generateImage: API returned no image data' };
|
|
181
195
|
}
|
|
182
196
|
return { ok: true, b64: imagePart.inlineData.data };
|
|
183
197
|
}
|
|
184
198
|
// ---------------------------------------------------------------------------
|
|
199
|
+
// Source image resolution
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
async function resolveSourceImage(sourceImage, ctx) {
|
|
202
|
+
if (sourceImage.type === 'url') {
|
|
203
|
+
const dlResult = await downloadImageUrl(sourceImage.url);
|
|
204
|
+
if (!dlResult.ok) {
|
|
205
|
+
return { ok: false, error: `generateImage: ${dlResult.error}` };
|
|
206
|
+
}
|
|
207
|
+
return { ok: true, base64: dlResult.image.base64, mediaType: dlResult.image.mediaType };
|
|
208
|
+
}
|
|
209
|
+
const channelId = sourceImage.channelId ?? ctx.channelId;
|
|
210
|
+
const messageId = sourceImage.messageId ?? ctx.messageId;
|
|
211
|
+
const attachmentIndex = sourceImage.attachmentIndex ?? 0;
|
|
212
|
+
let channel;
|
|
213
|
+
try {
|
|
214
|
+
channel = await ctx.client.channels.fetch(channelId);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return { ok: false, error: `generateImage: could not fetch channel "${channelId}"` };
|
|
218
|
+
}
|
|
219
|
+
if (!channel || !('messages' in channel)) {
|
|
220
|
+
return { ok: false, error: `generateImage: channel "${channelId}" is not a text channel` };
|
|
221
|
+
}
|
|
222
|
+
let message;
|
|
223
|
+
try {
|
|
224
|
+
message = await channel.messages.fetch(messageId);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return { ok: false, error: `generateImage: could not fetch message "${messageId}"` };
|
|
228
|
+
}
|
|
229
|
+
const attachments = [...message.attachments.values()];
|
|
230
|
+
if (attachmentIndex < 0 || attachmentIndex >= attachments.length) {
|
|
231
|
+
return { ok: false, error: `generateImage: no attachment at index ${attachmentIndex} (message has ${attachments.length} attachment${attachments.length === 1 ? '' : 's'})` };
|
|
232
|
+
}
|
|
233
|
+
const target = attachments[attachmentIndex];
|
|
234
|
+
const result = await downloadMessageImages([target], 1);
|
|
235
|
+
if (result.images.length === 0) {
|
|
236
|
+
const reason = result.errors.length > 0 ? `: ${result.errors[0]}` : '';
|
|
237
|
+
return { ok: false, error: `generateImage: source image attachment rejected${reason}` };
|
|
238
|
+
}
|
|
239
|
+
return { ok: true, base64: result.images[0].base64, mediaType: result.images[0].mediaType };
|
|
240
|
+
}
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
185
242
|
// Executor
|
|
186
243
|
// ---------------------------------------------------------------------------
|
|
187
244
|
export async function executeImagegenAction(action, ctx, imagegenCtx) {
|
|
@@ -238,38 +295,85 @@ export async function executeImagegenAction(action, ctx, imagegenCtx) {
|
|
|
238
295
|
return { ok: false, error: 'generateImage: apiKey is required for OpenAI provider' };
|
|
239
296
|
}
|
|
240
297
|
}
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
298
|
+
// Source image model gate (sync check, before placeholder)
|
|
299
|
+
if (action.sourceImage && !model.startsWith('gemini-')) {
|
|
300
|
+
return { ok: false, error: `generateImage: sourceImage is only supported with native Gemini models (gemini-*), not "${model}"` };
|
|
301
|
+
}
|
|
302
|
+
// --- Progress UX lifecycle ---
|
|
303
|
+
const placeholder = await channel.send({ content: DOT_STATES[0], allowedMentions: NO_MENTIONS });
|
|
304
|
+
channel.sendTyping().catch(() => { });
|
|
305
|
+
let dotIndex = 0;
|
|
306
|
+
const typingInterval = setInterval(() => { channel.sendTyping().catch(() => { }); }, TYPING_INTERVAL_MS);
|
|
307
|
+
const dotInterval = setInterval(() => {
|
|
308
|
+
dotIndex = (dotIndex + 1) % DOT_STATES.length;
|
|
309
|
+
placeholder.edit(DOT_STATES[dotIndex]).catch(() => { });
|
|
310
|
+
}, DOT_CYCLE_INTERVAL_MS);
|
|
311
|
+
const controller = new AbortController();
|
|
312
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
313
|
+
try {
|
|
314
|
+
// Resolve source image if provided
|
|
315
|
+
let resolvedSourceImage;
|
|
316
|
+
if (action.sourceImage) {
|
|
317
|
+
const srcResult = await resolveSourceImage(action.sourceImage, ctx);
|
|
318
|
+
if (!srcResult.ok) {
|
|
319
|
+
return { ok: false, error: srcResult.error };
|
|
320
|
+
}
|
|
321
|
+
resolvedSourceImage = { base64: srcResult.base64, mediaType: srcResult.mediaType };
|
|
322
|
+
}
|
|
323
|
+
// Call provider
|
|
324
|
+
let result;
|
|
325
|
+
if (provider === 'gemini') {
|
|
326
|
+
if (model.startsWith('gemini-')) {
|
|
327
|
+
result = await callGeminiNative(action.prompt.trim(), model, imagegenCtx.geminiApiKey, resolvedSourceImage, controller.signal);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
result = await callGemini(action.prompt.trim(), model, size, imagegenCtx.geminiApiKey, controller.signal);
|
|
331
|
+
}
|
|
246
332
|
}
|
|
247
333
|
else {
|
|
248
|
-
|
|
334
|
+
const baseUrl = imagegenCtx.baseUrl ?? 'https://api.openai.com/v1';
|
|
335
|
+
result = await callOpenAI(action.prompt.trim(), model, size, quality, imagegenCtx.apiKey, baseUrl, controller.signal);
|
|
249
336
|
}
|
|
337
|
+
if (controller.signal.aborted) {
|
|
338
|
+
return { ok: false, error: 'generateImage: request timed out' };
|
|
339
|
+
}
|
|
340
|
+
if (!result.ok) {
|
|
341
|
+
return { ok: false, error: result.error };
|
|
342
|
+
}
|
|
343
|
+
// Stop progress before final Discord mutations
|
|
344
|
+
clearInterval(typingInterval);
|
|
345
|
+
clearInterval(dotInterval);
|
|
346
|
+
clearTimeout(timeoutId);
|
|
347
|
+
await placeholder.delete().catch(() => { });
|
|
348
|
+
const buf = Buffer.from(result.b64, 'base64');
|
|
349
|
+
const attachment = new AttachmentBuilder(buf, { name: 'image-1.png' });
|
|
350
|
+
const sendOpts = { files: [attachment], allowedMentions: NO_MENTIONS };
|
|
351
|
+
if (action.caption) {
|
|
352
|
+
sendOpts.content = action.caption;
|
|
353
|
+
}
|
|
354
|
+
await channel.send(sendOpts);
|
|
355
|
+
return { ok: true, summary: `Generated image posted to #${channel.name}` };
|
|
250
356
|
}
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
if (!result.ok) {
|
|
256
|
-
return { ok: false, error: result.error };
|
|
357
|
+
catch (err) {
|
|
358
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
359
|
+
return { ok: false, error: `generateImage: ${msg}` };
|
|
257
360
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
361
|
+
finally {
|
|
362
|
+
clearInterval(typingInterval);
|
|
363
|
+
clearInterval(dotInterval);
|
|
364
|
+
clearTimeout(timeoutId);
|
|
365
|
+
await placeholder.delete().catch(() => { });
|
|
263
366
|
}
|
|
264
|
-
await channel.send(sendOpts);
|
|
265
|
-
return { ok: true, summary: `Generated image posted to #${channel.name}` };
|
|
266
367
|
}
|
|
267
368
|
}
|
|
268
369
|
}
|
|
269
370
|
// ---------------------------------------------------------------------------
|
|
270
371
|
// Prompt section
|
|
271
372
|
// ---------------------------------------------------------------------------
|
|
272
|
-
export function imagegenActionsPromptSection() {
|
|
373
|
+
export function imagegenActionsPromptSection(resolvedDefaultModel) {
|
|
374
|
+
const modelFieldDoc = resolvedDefaultModel
|
|
375
|
+
? `- \`model\` (optional): Default is \`${resolvedDefaultModel}\`. Omit this field to use the default; only set it when a different model is explicitly needed. Supported families/examples:`
|
|
376
|
+
: `- \`model\` (optional): Default depends on configuration. Supported families/examples:`;
|
|
273
377
|
return `### Image Generation
|
|
274
378
|
|
|
275
379
|
**generateImage** — Generate an image and post it to a channel:
|
|
@@ -278,7 +382,7 @@ export function imagegenActionsPromptSection() {
|
|
|
278
382
|
\`\`\`
|
|
279
383
|
- \`prompt\` (required): Text description of the image to generate.
|
|
280
384
|
- \`channel\` (optional): Channel name (with or without #) or channel ID to post the image to. Defaults to the current channel/thread if omitted.
|
|
281
|
-
|
|
385
|
+
${modelFieldDoc}
|
|
282
386
|
- OpenAI: \`dall-e-3\`, \`gpt-image-1\`
|
|
283
387
|
- Gemini (Imagen): \`imagen-4.0-generate-001\`, \`imagen-4.0-fast-generate-001\`, \`imagen-4.0-ultra-generate-001\`
|
|
284
388
|
- Gemini (native): \`gemini-3.1-flash-image-preview\`, \`gemini-3-pro-image-preview\`
|
|
@@ -289,5 +393,26 @@ export function imagegenActionsPromptSection() {
|
|
|
289
393
|
- Gemini (Imagen): aspect ratios — \`1:1\` (default), \`3:4\`, \`4:3\`, \`9:16\`, \`16:9\`
|
|
290
394
|
- Gemini (native): size/aspect-ratio params do not apply — omit \`size\` for these models
|
|
291
395
|
- \`quality\` (optional): \`standard\` (default) or \`hd\` — applies to OpenAI dall-e-3 only.
|
|
292
|
-
- \`caption\` (optional): Text message to accompany the image in the channel
|
|
396
|
+
- \`caption\` (optional): Text message to accompany the image in the channel.
|
|
397
|
+
- \`sourceImage\` (optional): Provide a source image for image-to-image editing. **Only supported with native Gemini models** (\`gemini-*\`). Two forms:
|
|
398
|
+
- **Attachment form** — reference a Discord message attachment:
|
|
399
|
+
- \`type\` (required): \`"attachment"\`
|
|
400
|
+
- \`channelId\` (optional): Channel ID of the message containing the image. Defaults to the current channel.
|
|
401
|
+
- \`messageId\` (optional): Message ID containing the image attachment. Defaults to the current message.
|
|
402
|
+
- \`attachmentIndex\` (optional): Zero-based index of the attachment to use. Defaults to \`0\` (first attachment).
|
|
403
|
+
- Example — edit the image from the current message:
|
|
404
|
+
\`\`\`
|
|
405
|
+
<discord-action>{"type":"generateImage","prompt":"Make this image look like a watercolor painting","model":"gemini-3.1-flash-image-preview","sourceImage":{"type":"attachment"}}</discord-action>
|
|
406
|
+
\`\`\`
|
|
407
|
+
- Example — edit an image from a specific message:
|
|
408
|
+
\`\`\`
|
|
409
|
+
<discord-action>{"type":"generateImage","prompt":"Add a sunset sky","model":"gemini-3.1-flash-image-preview","sourceImage":{"type":"attachment","channelId":"123","messageId":"456","attachmentIndex":1}}</discord-action>
|
|
410
|
+
\`\`\`
|
|
411
|
+
- **URL form** — provide a public http(s) image URL directly:
|
|
412
|
+
- \`type\` (required): \`"url"\`
|
|
413
|
+
- \`url\` (required): A public \`http(s)\` image URL (PNG, JPEG, GIF, or WebP).
|
|
414
|
+
- Example:
|
|
415
|
+
\`\`\`
|
|
416
|
+
<discord-action>{"type":"generateImage","prompt":"Make this photo a pencil sketch","model":"gemini-3.1-flash-image-preview","sourceImage":{"type":"url","url":"https://example.com/photo.jpg"}}</discord-action>
|
|
417
|
+
\`\`\``;
|
|
293
418
|
}
|