@hybridaione/hybridclaw 0.2.1 → 0.2.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/CHANGELOG.md +23 -0
- package/README.md +47 -15
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/browser-tools.ts +1 -1
- package/container/src/index.ts +243 -14
- package/container/src/token-usage.ts +18 -2
- package/container/src/tools.ts +339 -1
- package/container/src/types.ts +28 -2
- package/dist/agent.d.ts +2 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +2 -2
- package/dist/agent.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts +9 -0
- package/dist/channels/discord/attachments.d.ts.map +1 -0
- package/dist/channels/discord/attachments.js +245 -0
- package/dist/channels/discord/attachments.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +31 -0
- package/dist/channels/discord/delivery.d.ts.map +1 -0
- package/dist/channels/discord/delivery.js +60 -0
- package/dist/channels/discord/delivery.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +20 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -0
- package/dist/channels/discord/inbound.js +44 -0
- package/dist/channels/discord/inbound.js.map +1 -0
- package/dist/channels/discord/mentions.d.ts +14 -0
- package/dist/channels/discord/mentions.d.ts.map +1 -0
- package/dist/channels/discord/mentions.js +118 -0
- package/dist/channels/discord/mentions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +22 -0
- package/dist/channels/discord/runtime.d.ts.map +1 -0
- package/dist/channels/discord/runtime.js +972 -0
- package/dist/channels/discord/runtime.js.map +1 -0
- package/dist/channels/discord/stream.d.ts +32 -0
- package/dist/channels/discord/stream.d.ts.map +1 -0
- package/dist/channels/discord/stream.js +196 -0
- package/dist/channels/discord/stream.js.map +1 -0
- package/dist/channels/discord/tool-actions.d.ts +31 -0
- package/dist/channels/discord/tool-actions.d.ts.map +1 -0
- package/dist/channels/discord/tool-actions.js +268 -0
- package/dist/channels/discord/tool-actions.js.map +1 -0
- package/dist/container-runner.d.ts +2 -2
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +12 -2
- package/dist/container-runner.js.map +1 -1
- package/dist/discord.basic.test.d.ts +2 -0
- package/dist/discord.basic.test.d.ts.map +1 -0
- package/dist/discord.basic.test.js +38 -0
- package/dist/discord.basic.test.js.map +1 -0
- package/dist/discord.d.ts +5 -44
- package/dist/discord.d.ts.map +1 -1
- package/dist/discord.js +3 -1468
- package/dist/discord.js.map +1 -1
- package/dist/gateway-service.d.ts +7 -1
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +111 -2
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-service.media-routing.test.d.ts +2 -0
- package/dist/gateway-service.media-routing.test.d.ts.map +1 -0
- package/dist/gateway-service.media-routing.test.js +29 -0
- package/dist/gateway-service.media-routing.test.js.map +1 -0
- package/dist/gateway-types.d.ts +8 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +5 -2
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +1 -1
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +2 -0
- package/dist/heartbeat.js.map +1 -1
- package/dist/token-efficiency.basic.test.d.ts +2 -0
- package/dist/token-efficiency.basic.test.d.ts.map +1 -0
- package/dist/token-efficiency.basic.test.js +29 -0
- package/dist/token-efficiency.basic.test.js.map +1 -0
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js +18 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/types.d.ts +23 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -2
- package/src/agent.ts +11 -1
- package/src/channels/discord/attachments.ts +282 -0
- package/src/channels/discord/delivery.ts +99 -0
- package/src/channels/discord/inbound.ts +72 -0
- package/src/channels/discord/mentions.ts +130 -0
- package/src/{discord.ts → channels/discord/runtime.ts} +77 -615
- package/src/{discord-stream.ts → channels/discord/stream.ts} +2 -2
- package/src/channels/discord/tool-actions.ts +332 -0
- package/src/container-runner.ts +24 -1
- package/src/gateway-service.ts +125 -1
- package/src/gateway-types.ts +8 -0
- package/src/gateway.ts +5 -5
- package/src/health.ts +2 -1
- package/src/heartbeat.ts +2 -0
- package/src/token-efficiency.ts +17 -1
- package/src/types.ts +27 -1
- package/tests/discord.basic.test.ts +43 -0
- package/tests/gateway-service.media-routing.test.ts +33 -0
- package/tests/token-efficiency.basic.test.ts +32 -0
- package/vitest.e2e.config.ts +15 -0
- package/vitest.integration.config.ts +15 -0
- package/vitest.live.config.ts +16 -0
- package/vitest.unit.config.ts +15 -0
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ActivityType,
|
|
3
3
|
AttachmentBuilder,
|
|
4
|
-
type ChatInputCommandInteraction,
|
|
5
4
|
Client,
|
|
6
5
|
GatewayIntentBits,
|
|
7
|
-
type GuildMember,
|
|
8
6
|
type Message as DiscordMessage,
|
|
9
7
|
Partials,
|
|
10
8
|
} from 'discord.js';
|
|
@@ -17,10 +15,37 @@ import {
|
|
|
17
15
|
DISCORD_PREFIX,
|
|
18
16
|
DISCORD_RESPOND_TO_ALL_MESSAGES,
|
|
19
17
|
DISCORD_TOKEN,
|
|
20
|
-
} from '
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
18
|
+
} from '../../config.js';
|
|
19
|
+
import { buildAttachmentContext } from './attachments.js';
|
|
20
|
+
import {
|
|
21
|
+
buildSessionIdFromContext as buildSessionIdFromContextInbound,
|
|
22
|
+
cleanIncomingContent as cleanIncomingContentInbound,
|
|
23
|
+
hasPrefixInvocation as hasPrefixInvocationInbound,
|
|
24
|
+
isTrigger as isTriggerInbound,
|
|
25
|
+
parseCommand as parseCommandInbound,
|
|
26
|
+
type ParsedCommand,
|
|
27
|
+
} from './inbound.js';
|
|
28
|
+
import {
|
|
29
|
+
addMentionAlias,
|
|
30
|
+
extractMentionAliasHints,
|
|
31
|
+
normalizeMentionAlias,
|
|
32
|
+
type MentionLookup,
|
|
33
|
+
} from './mentions.js';
|
|
34
|
+
import {
|
|
35
|
+
formatError,
|
|
36
|
+
prepareChunkedPayloads,
|
|
37
|
+
sendChunkedDirectReply as sendChunkedDirectReplyFromDelivery,
|
|
38
|
+
sendChunkedInteractionReply as sendChunkedInteractionReplyFromDelivery,
|
|
39
|
+
sendChunkedReply as sendChunkedReplyFromDelivery,
|
|
40
|
+
} from './delivery.js';
|
|
41
|
+
import {
|
|
42
|
+
createDiscordToolActionRunner,
|
|
43
|
+
type CachedDiscordPresence,
|
|
44
|
+
type DiscordToolActionRequest,
|
|
45
|
+
} from './tool-actions.js';
|
|
46
|
+
import { DiscordStreamManager } from './stream.js';
|
|
47
|
+
import { logger } from '../../logger.js';
|
|
48
|
+
import type { MediaContextItem } from '../../types.js';
|
|
24
49
|
|
|
25
50
|
export type ReplyFn = (content: string, files?: AttachmentBuilder[]) => Promise<void>;
|
|
26
51
|
|
|
@@ -39,15 +64,6 @@ interface ParticipantInfo {
|
|
|
39
64
|
aliases: Set<string>;
|
|
40
65
|
}
|
|
41
66
|
|
|
42
|
-
export interface MentionLookup {
|
|
43
|
-
byAlias: Map<string, Set<string>>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface MentionAliasHint {
|
|
47
|
-
alias: string;
|
|
48
|
-
userId: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
67
|
export interface MessageRunContext {
|
|
52
68
|
sourceMessage: DiscordMessage;
|
|
53
69
|
batchedMessages: DiscordMessage[];
|
|
@@ -56,22 +72,6 @@ export interface MessageRunContext {
|
|
|
56
72
|
mentionLookup: MentionLookup;
|
|
57
73
|
}
|
|
58
74
|
|
|
59
|
-
export type DiscordToolAction = 'read' | 'member-info' | 'channel-info';
|
|
60
|
-
|
|
61
|
-
export interface DiscordToolActionRequest {
|
|
62
|
-
action: DiscordToolAction;
|
|
63
|
-
channelId?: string;
|
|
64
|
-
guildId?: string;
|
|
65
|
-
userId?: string;
|
|
66
|
-
username?: string;
|
|
67
|
-
user?: string;
|
|
68
|
-
memberId?: string;
|
|
69
|
-
limit?: number;
|
|
70
|
-
before?: string;
|
|
71
|
-
after?: string;
|
|
72
|
-
around?: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
75
|
export type MessageHandler = (
|
|
76
76
|
sessionId: string,
|
|
77
77
|
guildId: string | null,
|
|
@@ -79,6 +79,7 @@ export type MessageHandler = (
|
|
|
79
79
|
userId: string,
|
|
80
80
|
username: string,
|
|
81
81
|
content: string,
|
|
82
|
+
media: MediaContextItem[],
|
|
82
83
|
reply: ReplyFn,
|
|
83
84
|
context: MessageRunContext,
|
|
84
85
|
) => Promise<void>;
|
|
@@ -97,9 +98,6 @@ let commandHandler: CommandHandler;
|
|
|
97
98
|
let activeConversationRuns = 0;
|
|
98
99
|
let botMentionRegex: RegExp | null = null;
|
|
99
100
|
const MESSAGE_DEBOUNCE_MS = 2_500;
|
|
100
|
-
const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
101
|
-
const MAX_ATTACHMENT_CONTEXT_CHARS = 16_000;
|
|
102
|
-
const MAX_SINGLE_ATTACHMENT_CHARS = 8_000;
|
|
103
101
|
const DISCORD_RETRY_MAX_ATTEMPTS = 3;
|
|
104
102
|
const DISCORD_RETRY_BASE_DELAY_MS = 500;
|
|
105
103
|
const GUILD_INBOUND_HISTORY_LIMIT = 20;
|
|
@@ -108,21 +106,8 @@ const PARTICIPANT_CONTEXT_MAX_USERS = 30;
|
|
|
108
106
|
const PARTICIPANT_MEMORY_MAX_CHANNELS = 200;
|
|
109
107
|
const PARTICIPANT_MEMORY_MAX_USERS_PER_CHANNEL = 200;
|
|
110
108
|
const PARTICIPANT_MEMORY_MAX_ALIASES_PER_USER = 8;
|
|
111
|
-
const MENTION_ALIAS_LOOKUP_MAX = 8;
|
|
112
109
|
const MAX_PRESENCE_CACHE_USERS = 5_000;
|
|
113
110
|
|
|
114
|
-
interface CachedDiscordPresenceActivity {
|
|
115
|
-
type: number;
|
|
116
|
-
name: string;
|
|
117
|
-
state: string | null;
|
|
118
|
-
details: string | null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
interface CachedDiscordPresence {
|
|
122
|
-
status: string;
|
|
123
|
-
activities: CachedDiscordPresenceActivity[];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
111
|
const discordPresenceCache = new Map<string, CachedDiscordPresence>();
|
|
127
112
|
|
|
128
113
|
function setDiscordPresence(userId: string, data: CachedDiscordPresence): void {
|
|
@@ -139,54 +124,6 @@ function getDiscordPresence(userId: string): CachedDiscordPresence | undefined {
|
|
|
139
124
|
return discordPresenceCache.get(userId);
|
|
140
125
|
}
|
|
141
126
|
|
|
142
|
-
function normalizeMentionAlias(raw: string | null | undefined): string {
|
|
143
|
-
if (!raw) return '';
|
|
144
|
-
const trimmed = raw.trim().replace(/^@+/, '');
|
|
145
|
-
if (!trimmed) return '';
|
|
146
|
-
const lowered = trimmed.toLowerCase();
|
|
147
|
-
if (lowered === 'everyone' || lowered === 'here') return '';
|
|
148
|
-
if (!/^[\p{L}\p{N}._-]{2,32}$/u.test(trimmed)) return '';
|
|
149
|
-
return lowered;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function addMentionAlias(lookup: MentionLookup, rawAlias: string | null | undefined, userId: string): void {
|
|
153
|
-
const alias = normalizeMentionAlias(rawAlias);
|
|
154
|
-
if (!alias) return;
|
|
155
|
-
let ids = lookup.byAlias.get(alias);
|
|
156
|
-
if (!ids) {
|
|
157
|
-
ids = new Set<string>();
|
|
158
|
-
lookup.byAlias.set(alias, ids);
|
|
159
|
-
}
|
|
160
|
-
ids.add(userId);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function extractMentionAliasHints(text: string): MentionAliasHint[] {
|
|
164
|
-
if (!text) return [];
|
|
165
|
-
|
|
166
|
-
const hints = new Map<string, MentionAliasHint>();
|
|
167
|
-
const collect = (rawAlias: string | null | undefined, rawUserId: string | null | undefined): void => {
|
|
168
|
-
const userId = (rawUserId || '').trim();
|
|
169
|
-
if (!/^\d{16,22}$/.test(userId)) return;
|
|
170
|
-
const alias = normalizeMentionAlias(rawAlias);
|
|
171
|
-
if (!alias) return;
|
|
172
|
-
const key = `${alias}:${userId}`;
|
|
173
|
-
if (!hints.has(key)) hints.set(key, { alias, userId });
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const aliasToId = /(^|[\s,;:.!?])@?([\p{L}\p{N}._-]{2,32})\s*(?:ist|is|=|->|=>|means|heißt)\s*(?:<@!?(\d{16,22})>|(\d{16,22}))/giu;
|
|
177
|
-
let match: RegExpExecArray | null;
|
|
178
|
-
while ((match = aliasToId.exec(text)) !== null) {
|
|
179
|
-
collect(match[2], match[3] || match[4]);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const idToAlias = /(?:<@!?(\d{16,22})>|(\d{16,22}))\s*(?:ist|is|=|->|=>|means|heißt)\s*@?([\p{L}\p{N}._-]{2,32})/giu;
|
|
183
|
-
while ((match = idToAlias.exec(text)) !== null) {
|
|
184
|
-
collect(match[3], match[1] || match[2]);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return Array.from(hints.values());
|
|
188
|
-
}
|
|
189
|
-
|
|
190
127
|
function buildMentionLookup(
|
|
191
128
|
messages: DiscordMessage[],
|
|
192
129
|
pendingHistory: PendingGuildHistoryEntry[],
|
|
@@ -235,76 +172,6 @@ function buildMentionLookup(
|
|
|
235
172
|
return lookup;
|
|
236
173
|
}
|
|
237
174
|
|
|
238
|
-
export function rewriteUserMentions(text: string, lookup: MentionLookup): string {
|
|
239
|
-
if (!text) return text;
|
|
240
|
-
if (!lookup.byAlias.size) return text;
|
|
241
|
-
return text.replace(/(^|[\s([{:>])@([\p{L}\p{N}._-]{2,32})\b/gu, (full, prefix: string, rawAlias: string) => {
|
|
242
|
-
const alias = normalizeMentionAlias(rawAlias);
|
|
243
|
-
if (!alias) return full;
|
|
244
|
-
const ids = lookup.byAlias.get(alias);
|
|
245
|
-
if (!ids || ids.size !== 1) return full;
|
|
246
|
-
const [id] = Array.from(ids);
|
|
247
|
-
if (!id) return full;
|
|
248
|
-
return `${prefix}<@${id}>`;
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function extractMentionAliases(text: string): string[] {
|
|
253
|
-
if (!text) return [];
|
|
254
|
-
const aliases = new Set<string>();
|
|
255
|
-
const re = /(^|[\s([{:>])@([\p{L}\p{N}._-]{2,32})\b/gu;
|
|
256
|
-
let match: RegExpExecArray | null;
|
|
257
|
-
while ((match = re.exec(text)) !== null) {
|
|
258
|
-
const alias = normalizeMentionAlias(match[2]);
|
|
259
|
-
if (!alias) continue;
|
|
260
|
-
aliases.add(alias);
|
|
261
|
-
if (aliases.size >= MENTION_ALIAS_LOOKUP_MAX) break;
|
|
262
|
-
}
|
|
263
|
-
return Array.from(aliases);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
async function enrichMentionLookupFromGuild(
|
|
267
|
-
msg: DiscordMessage,
|
|
268
|
-
lookup: MentionLookup,
|
|
269
|
-
aliases: string[],
|
|
270
|
-
): Promise<void> {
|
|
271
|
-
if (!msg.guild || aliases.length === 0) return;
|
|
272
|
-
|
|
273
|
-
for (const alias of aliases) {
|
|
274
|
-
if (lookup.byAlias.has(alias)) continue;
|
|
275
|
-
try {
|
|
276
|
-
const members = await msg.guild.members.search({ query: alias, limit: 5 });
|
|
277
|
-
const exactMatches = Array.from(members.values()).filter((member) => {
|
|
278
|
-
const username = normalizeMentionAlias(member.user?.username || '');
|
|
279
|
-
const displayName = normalizeMentionAlias(member.displayName || '');
|
|
280
|
-
return username === alias || displayName === alias;
|
|
281
|
-
});
|
|
282
|
-
if (exactMatches.length !== 1) continue;
|
|
283
|
-
const match = exactMatches[0];
|
|
284
|
-
addMentionAlias(lookup, alias, match.id);
|
|
285
|
-
addMentionAlias(lookup, match.user?.username || '', match.id);
|
|
286
|
-
addMentionAlias(lookup, match.displayName || '', match.id);
|
|
287
|
-
} catch (error) {
|
|
288
|
-
logger.debug(
|
|
289
|
-
{ error, guildId: msg.guild.id, alias },
|
|
290
|
-
'Failed to resolve guild member alias for mention rewrite',
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export async function rewriteUserMentionsForMessage(
|
|
297
|
-
text: string,
|
|
298
|
-
msg: DiscordMessage,
|
|
299
|
-
lookup: MentionLookup,
|
|
300
|
-
): Promise<string> {
|
|
301
|
-
const aliases = extractMentionAliases(text);
|
|
302
|
-
if (aliases.length > 0) {
|
|
303
|
-
await enrichMentionLookupFromGuild(msg, lookup, aliases);
|
|
304
|
-
}
|
|
305
|
-
return rewriteUserMentions(text, lookup);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
175
|
function summarizePendingHistoryEntry(entry: PendingGuildHistoryEntry): string {
|
|
309
176
|
const author = entry.displayName || entry.username || 'user';
|
|
310
177
|
const authorLabel = entry.isBot ? `${author} [bot]` : author;
|
|
@@ -527,27 +394,6 @@ interface DiscordErrorLike {
|
|
|
527
394
|
};
|
|
528
395
|
}
|
|
529
396
|
|
|
530
|
-
/**
|
|
531
|
-
* Format an agent response as plain text.
|
|
532
|
-
* Appends a subtle tools line if any tools were used.
|
|
533
|
-
*/
|
|
534
|
-
export function buildResponseText(text: string, toolsUsed?: string[]): string {
|
|
535
|
-
let body = text;
|
|
536
|
-
if (toolsUsed && toolsUsed.length > 0) {
|
|
537
|
-
const toolsLine = `\n*Tools: ${toolsUsed.join(', ')}*`;
|
|
538
|
-
body = `${text}${toolsLine}`;
|
|
539
|
-
}
|
|
540
|
-
return body;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
export function formatInfo(title: string, body: string): string {
|
|
544
|
-
return `**${title}**\n${body}`;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
export function formatError(title: string, detail: string): string {
|
|
548
|
-
return `**${title}:** ${detail}`;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
397
|
function requireDiscordClientReady(): Client {
|
|
552
398
|
if (!client) {
|
|
553
399
|
throw new Error('Discord client is not initialized.');
|
|
@@ -558,303 +404,23 @@ function requireDiscordClientReady(): Client {
|
|
|
558
404
|
return client;
|
|
559
405
|
}
|
|
560
406
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
return value;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function normalizeDiscordUserLookupQuery(rawValue: string | undefined): string {
|
|
570
|
-
const trimmed = (rawValue || '').trim();
|
|
571
|
-
if (!trimmed) return '';
|
|
572
|
-
|
|
573
|
-
const mentionMatch = trimmed.match(/^<@!?(\d{16,22})>$/);
|
|
574
|
-
if (mentionMatch) return mentionMatch[1];
|
|
575
|
-
const prefixedId = trimmed.match(/^(?:user:|discord:)?(\d{16,22})$/i);
|
|
576
|
-
if (prefixedId) return prefixedId[1];
|
|
577
|
-
|
|
578
|
-
return trimmed.replace(/^@+/, '').trim();
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function scoreGuildMemberForLookup(member: GuildMember, query: string): number {
|
|
582
|
-
const q = query.toLowerCase();
|
|
583
|
-
const username = member.user.username?.toLowerCase() || '';
|
|
584
|
-
const globalName = member.user.globalName?.toLowerCase() || '';
|
|
585
|
-
const nickname = member.nickname?.toLowerCase() || '';
|
|
586
|
-
const displayName = member.displayName?.toLowerCase() || '';
|
|
587
|
-
const candidates = [username, globalName, nickname, displayName].filter(Boolean);
|
|
588
|
-
|
|
589
|
-
let score = 0;
|
|
590
|
-
if (candidates.some((value) => value === q)) score += 3;
|
|
591
|
-
if (candidates.some((value) => value.includes(q))) score += 1;
|
|
592
|
-
if (!member.user.bot) score += 1;
|
|
593
|
-
return score;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
async function resolveGuildMemberIdFromLookup(params: {
|
|
597
|
-
guildId: string;
|
|
598
|
-
rawUser: string;
|
|
599
|
-
}): Promise<{ userId: string; note?: string }> {
|
|
600
|
-
const activeClient = requireDiscordClientReady();
|
|
601
|
-
const guildId = sanitizeDiscordId(params.guildId, 'guildId');
|
|
602
|
-
const normalized = normalizeDiscordUserLookupQuery(params.rawUser);
|
|
603
|
-
if (!normalized) {
|
|
604
|
-
throw new Error('userId or username is required.');
|
|
605
|
-
}
|
|
606
|
-
if (/^\d{16,22}$/.test(normalized)) {
|
|
607
|
-
return { userId: normalized };
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const guild = await activeClient.guilds.fetch(guildId);
|
|
611
|
-
const searchQuery = normalized.slice(0, 32);
|
|
612
|
-
if (!searchQuery) {
|
|
613
|
-
throw new Error('username query is empty after normalization.');
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
let members: Map<string, GuildMember>;
|
|
617
|
-
try {
|
|
618
|
-
members = await guild.members.search({ query: searchQuery, limit: 25 });
|
|
619
|
-
} catch {
|
|
620
|
-
const fetched = await guild.members.fetch({ query: searchQuery, limit: 25 });
|
|
621
|
-
members = fetched;
|
|
622
|
-
}
|
|
623
|
-
let best: GuildMember | null = null;
|
|
624
|
-
let bestScore = 0;
|
|
625
|
-
let matchCount = 0;
|
|
626
|
-
for (const member of members.values()) {
|
|
627
|
-
const score = scoreGuildMemberForLookup(member, searchQuery);
|
|
628
|
-
if (score <= 0) continue;
|
|
629
|
-
matchCount += 1;
|
|
630
|
-
if (!best || score > bestScore) {
|
|
631
|
-
best = member;
|
|
632
|
-
bestScore = score;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (!best) {
|
|
637
|
-
throw new Error(`No guild member matched username "${searchQuery}".`);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
return {
|
|
641
|
-
userId: best.id,
|
|
642
|
-
note: matchCount > 1 ? 'multiple matches; chose best' : undefined,
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function normalizeDate(value: Date | null | undefined): string | null {
|
|
647
|
-
if (!value) return null;
|
|
648
|
-
const ms = value.getTime();
|
|
649
|
-
if (!Number.isFinite(ms)) return null;
|
|
650
|
-
return value.toISOString();
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
async function runDiscordReadAction(request: DiscordToolActionRequest): Promise<Record<string, unknown>> {
|
|
654
|
-
const activeClient = requireDiscordClientReady();
|
|
655
|
-
const channelId = sanitizeDiscordId(request.channelId, 'channelId');
|
|
656
|
-
const channel = await activeClient.channels.fetch(channelId);
|
|
657
|
-
if (!channel || !('messages' in channel)) {
|
|
658
|
-
throw new Error('Channel does not support message reads.');
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
const requestedLimit =
|
|
662
|
-
typeof request.limit === 'number' && Number.isFinite(request.limit)
|
|
663
|
-
? Math.floor(request.limit)
|
|
664
|
-
: 20;
|
|
665
|
-
const limit = Math.max(1, Math.min(100, requestedLimit));
|
|
666
|
-
const before = request.before?.trim();
|
|
667
|
-
const after = request.after?.trim();
|
|
668
|
-
const around = request.around?.trim();
|
|
669
|
-
|
|
670
|
-
const query: { limit: number; before?: string; after?: string; around?: string } = { limit };
|
|
671
|
-
if (before) query.before = before;
|
|
672
|
-
if (after) query.after = after;
|
|
673
|
-
if (around) query.around = around;
|
|
674
|
-
|
|
675
|
-
const fetched = await channel.messages.fetch(query);
|
|
676
|
-
const messages = Array.from(fetched.values())
|
|
677
|
-
.sort((a, b) => a.createdTimestamp - b.createdTimestamp || a.id.localeCompare(b.id))
|
|
678
|
-
.map((message) => ({
|
|
679
|
-
id: message.id,
|
|
680
|
-
channelId: message.channelId,
|
|
681
|
-
guildId: message.guildId ?? null,
|
|
682
|
-
content: message.content || '',
|
|
683
|
-
createdAt: new Date(message.createdTimestamp).toISOString(),
|
|
684
|
-
editedAt: normalizeDate(message.editedAt),
|
|
685
|
-
author: {
|
|
686
|
-
id: message.author?.id || 'unknown',
|
|
687
|
-
username: message.author?.username || 'unknown',
|
|
688
|
-
handle: message.author?.username ? `@${message.author.username}` : null,
|
|
689
|
-
globalName: message.author?.globalName || null,
|
|
690
|
-
bot: Boolean(message.author?.bot),
|
|
691
|
-
},
|
|
692
|
-
member: message.member
|
|
693
|
-
? {
|
|
694
|
-
id: message.member.id,
|
|
695
|
-
nickname: message.member.nickname || null,
|
|
696
|
-
displayName: message.member.displayName || null,
|
|
697
|
-
}
|
|
698
|
-
: null,
|
|
699
|
-
attachments: Array.from(message.attachments.values()).map((attachment) => ({
|
|
700
|
-
id: attachment.id,
|
|
701
|
-
name: attachment.name || null,
|
|
702
|
-
url: attachment.url,
|
|
703
|
-
contentType: attachment.contentType || null,
|
|
704
|
-
size: attachment.size,
|
|
705
|
-
})),
|
|
706
|
-
mentions: {
|
|
707
|
-
users: Array.from(message.mentions.users.values()).map((user) => ({
|
|
708
|
-
id: user.id,
|
|
709
|
-
username: user.username,
|
|
710
|
-
bot: Boolean(user.bot),
|
|
711
|
-
})),
|
|
712
|
-
roles: Array.from(message.mentions.roles.values()).map((role) => ({
|
|
713
|
-
id: role.id,
|
|
714
|
-
name: role.name,
|
|
715
|
-
})),
|
|
716
|
-
channels: Array.from(message.mentions.channels.values()).map((mentionedChannel) => ({
|
|
717
|
-
id: mentionedChannel.id,
|
|
718
|
-
name: 'name' in mentionedChannel && typeof mentionedChannel.name === 'string'
|
|
719
|
-
? mentionedChannel.name
|
|
720
|
-
: null,
|
|
721
|
-
})),
|
|
722
|
-
},
|
|
723
|
-
}));
|
|
724
|
-
|
|
725
|
-
return {
|
|
726
|
-
ok: true,
|
|
727
|
-
action: 'read',
|
|
728
|
-
channelId,
|
|
729
|
-
count: messages.length,
|
|
730
|
-
messages,
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
async function runDiscordMemberInfoAction(request: DiscordToolActionRequest): Promise<Record<string, unknown>> {
|
|
735
|
-
const activeClient = requireDiscordClientReady();
|
|
736
|
-
const guildId = sanitizeDiscordId(request.guildId, 'guildId');
|
|
737
|
-
const userLookupRaw =
|
|
738
|
-
request.userId
|
|
739
|
-
|| request.memberId
|
|
740
|
-
|| request.user
|
|
741
|
-
|| request.username;
|
|
742
|
-
const resolvedUser = await resolveGuildMemberIdFromLookup({
|
|
743
|
-
guildId,
|
|
744
|
-
rawUser: userLookupRaw || '',
|
|
745
|
-
});
|
|
746
|
-
const userId = sanitizeDiscordId(resolvedUser.userId, 'userId');
|
|
747
|
-
|
|
748
|
-
const guild = await activeClient.guilds.fetch(guildId);
|
|
749
|
-
const member = await guild.members.fetch(userId);
|
|
750
|
-
const presence = getDiscordPresence(userId);
|
|
751
|
-
|
|
752
|
-
const roles = member.roles.cache
|
|
753
|
-
.filter((role) => role.id !== guild.id)
|
|
754
|
-
.map((role) => ({
|
|
755
|
-
id: role.id,
|
|
756
|
-
name: role.name,
|
|
757
|
-
color: role.hexColor,
|
|
758
|
-
position: role.position,
|
|
759
|
-
}))
|
|
760
|
-
.sort((a, b) => b.position - a.position || a.name.localeCompare(b.name));
|
|
761
|
-
|
|
762
|
-
return {
|
|
763
|
-
ok: true,
|
|
764
|
-
action: 'member-info',
|
|
765
|
-
guildId,
|
|
766
|
-
userId,
|
|
767
|
-
...(resolvedUser.note ? { note: resolvedUser.note } : {}),
|
|
768
|
-
member: {
|
|
769
|
-
id: member.id,
|
|
770
|
-
username: member.user.username,
|
|
771
|
-
handle: member.user.username ? `@${member.user.username}` : null,
|
|
772
|
-
globalName: member.user.globalName || null,
|
|
773
|
-
bot: Boolean(member.user.bot),
|
|
774
|
-
displayName: member.displayName,
|
|
775
|
-
nickname: member.nickname || null,
|
|
776
|
-
joinedAt: normalizeDate(member.joinedAt),
|
|
777
|
-
premiumSince: normalizeDate(member.premiumSince),
|
|
778
|
-
communicationDisabledUntil: normalizeDate(member.communicationDisabledUntil),
|
|
779
|
-
roles,
|
|
780
|
-
},
|
|
781
|
-
...(presence
|
|
782
|
-
? {
|
|
783
|
-
status: presence.status,
|
|
784
|
-
activities: presence.activities,
|
|
785
|
-
}
|
|
786
|
-
: {}),
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
async function runDiscordChannelInfoAction(request: DiscordToolActionRequest): Promise<Record<string, unknown>> {
|
|
791
|
-
const activeClient = requireDiscordClientReady();
|
|
792
|
-
const channelId = sanitizeDiscordId(request.channelId, 'channelId');
|
|
793
|
-
const channel = await activeClient.channels.fetch(channelId);
|
|
794
|
-
if (!channel) {
|
|
795
|
-
throw new Error('Channel not found.');
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
const channelData: Record<string, unknown> = {
|
|
799
|
-
id: channel.id,
|
|
800
|
-
type: channel.type,
|
|
801
|
-
guildId: 'guildId' in channel ? channel.guildId || null : null,
|
|
802
|
-
name: 'name' in channel && typeof channel.name === 'string' ? channel.name : null,
|
|
803
|
-
parentId: 'parentId' in channel ? channel.parentId || null : null,
|
|
804
|
-
topic: 'topic' in channel && typeof channel.topic === 'string' ? channel.topic : null,
|
|
805
|
-
nsfw: 'nsfw' in channel && typeof channel.nsfw === 'boolean' ? channel.nsfw : null,
|
|
806
|
-
rateLimitPerUser:
|
|
807
|
-
'rateLimitPerUser' in channel && typeof channel.rateLimitPerUser === 'number'
|
|
808
|
-
? channel.rateLimitPerUser
|
|
809
|
-
: null,
|
|
810
|
-
isTextBased: typeof channel.isTextBased === 'function' ? channel.isTextBased() : false,
|
|
811
|
-
isDMBased: typeof channel.isDMBased === 'function' ? channel.isDMBased() : false,
|
|
812
|
-
isThread: typeof channel.isThread === 'function' ? channel.isThread() : false,
|
|
813
|
-
lastMessageId: 'lastMessageId' in channel ? channel.lastMessageId || null : null,
|
|
814
|
-
};
|
|
815
|
-
|
|
816
|
-
if (typeof channel.isThread === 'function' && channel.isThread()) {
|
|
817
|
-
channelData.archived =
|
|
818
|
-
'archived' in channel && typeof channel.archived === 'boolean' ? channel.archived : null;
|
|
819
|
-
channelData.locked =
|
|
820
|
-
'locked' in channel && typeof channel.locked === 'boolean' ? channel.locked : null;
|
|
821
|
-
channelData.ownerId = 'ownerId' in channel ? channel.ownerId || null : null;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
return {
|
|
825
|
-
ok: true,
|
|
826
|
-
action: 'channel-info',
|
|
827
|
-
channel: channelData,
|
|
828
|
-
};
|
|
829
|
-
}
|
|
407
|
+
const runDiscordToolActionInternal = createDiscordToolActionRunner({
|
|
408
|
+
requireDiscordClientReady,
|
|
409
|
+
getDiscordPresence,
|
|
410
|
+
});
|
|
830
411
|
|
|
831
412
|
export async function runDiscordToolAction(
|
|
832
413
|
request: DiscordToolActionRequest,
|
|
833
414
|
): Promise<Record<string, unknown>> {
|
|
834
|
-
|
|
835
|
-
case 'read':
|
|
836
|
-
return await runDiscordReadAction(request);
|
|
837
|
-
case 'member-info':
|
|
838
|
-
return await runDiscordMemberInfoAction(request);
|
|
839
|
-
case 'channel-info':
|
|
840
|
-
return await runDiscordChannelInfoAction(request);
|
|
841
|
-
default:
|
|
842
|
-
throw new Error(`Unsupported Discord action: ${request.action as string}`);
|
|
843
|
-
}
|
|
415
|
+
return await runDiscordToolActionInternal(request);
|
|
844
416
|
}
|
|
845
417
|
|
|
846
418
|
function getSessionId(msg: DiscordMessage): string {
|
|
847
419
|
return buildSessionIdFromContext(msg.guild?.id ?? null, msg.channelId, msg.author.id);
|
|
848
420
|
}
|
|
849
421
|
|
|
850
|
-
function stripBotMentions(text: string): string {
|
|
851
|
-
if (!botMentionRegex) return text;
|
|
852
|
-
return text.replace(botMentionRegex, '').trim();
|
|
853
|
-
}
|
|
854
|
-
|
|
855
422
|
function hasPrefixInvocation(content: string): boolean {
|
|
856
|
-
|
|
857
|
-
return text.startsWith(DISCORD_PREFIX);
|
|
423
|
+
return hasPrefixInvocationInbound(content, botMentionRegex, DISCORD_PREFIX);
|
|
858
424
|
}
|
|
859
425
|
|
|
860
426
|
function isAuthorizedCommandUserId(userId: string): boolean {
|
|
@@ -864,33 +430,23 @@ function isAuthorizedCommandUserId(userId: string): boolean {
|
|
|
864
430
|
}
|
|
865
431
|
|
|
866
432
|
function buildSessionIdFromContext(guildId: string | null, channelId: string, userId: string): string {
|
|
867
|
-
return guildId
|
|
433
|
+
return buildSessionIdFromContextInbound(guildId, channelId, userId);
|
|
868
434
|
}
|
|
869
435
|
|
|
870
436
|
function isTrigger(msg: DiscordMessage): boolean {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
437
|
+
return isTriggerInbound({
|
|
438
|
+
content: msg.content,
|
|
439
|
+
isDm: !msg.guild,
|
|
440
|
+
commandsOnly: DISCORD_COMMANDS_ONLY,
|
|
441
|
+
respondToAllMessages: DISCORD_RESPOND_TO_ALL_MESSAGES,
|
|
442
|
+
prefix: DISCORD_PREFIX,
|
|
443
|
+
botMentionRegex,
|
|
444
|
+
hasBotMention: Boolean(client.user && msg.mentions.has(client.user)),
|
|
445
|
+
});
|
|
877
446
|
}
|
|
878
447
|
|
|
879
|
-
function parseCommand(content: string):
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
if (text.startsWith(DISCORD_PREFIX)) {
|
|
883
|
-
text = text.slice(DISCORD_PREFIX.length).trim();
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
const parts = text.split(/\s+/);
|
|
887
|
-
const subcommands = ['bot', 'rag', 'model', 'sessions', 'audit', 'schedule', 'clear', 'help'];
|
|
888
|
-
|
|
889
|
-
if (parts.length > 0 && subcommands.includes(parts[0].toLowerCase())) {
|
|
890
|
-
return { isCommand: true, command: parts[0].toLowerCase(), args: parts.slice(1) };
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
return { isCommand: false, command: '', args: [] };
|
|
448
|
+
function parseCommand(content: string): ParsedCommand {
|
|
449
|
+
return parseCommandInbound(content, botMentionRegex, DISCORD_PREFIX);
|
|
894
450
|
}
|
|
895
451
|
|
|
896
452
|
function isRetryableDiscordError(error: unknown): boolean {
|
|
@@ -928,11 +484,7 @@ async function withDiscordRetry<T>(label: string, fn: () => Promise<T>): Promise
|
|
|
928
484
|
}
|
|
929
485
|
|
|
930
486
|
function cleanIncomingContent(content: string): string {
|
|
931
|
-
|
|
932
|
-
if (text.startsWith(DISCORD_PREFIX)) {
|
|
933
|
-
text = text.slice(DISCORD_PREFIX.length).trim();
|
|
934
|
-
}
|
|
935
|
-
return text;
|
|
487
|
+
return cleanIncomingContentInbound(content, botMentionRegex, DISCORD_PREFIX);
|
|
936
488
|
}
|
|
937
489
|
|
|
938
490
|
function summarizeContextMessage(msg: DiscordMessage): string {
|
|
@@ -969,25 +521,6 @@ function buildChannelInfoContext(msg: DiscordMessage): string {
|
|
|
969
521
|
return `${lines.join('\n')}\n`;
|
|
970
522
|
}
|
|
971
523
|
|
|
972
|
-
function looksLikeTextAttachment(name: string, contentType: string): boolean {
|
|
973
|
-
if (contentType.startsWith('text/')) return true;
|
|
974
|
-
if (contentType.includes('json') || contentType.includes('xml') || contentType.includes('yaml')) return true;
|
|
975
|
-
return /\.(txt|md|markdown|json|ya?ml|js|jsx|ts|tsx|py|rb|go|rs|java|c|cpp|h|hpp|cs|php|html?|css|scss|sql|log|csv)$/i.test(name);
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
async function fetchAttachmentText(url: string, maxChars: number): Promise<string | null> {
|
|
979
|
-
try {
|
|
980
|
-
const response = await fetch(url);
|
|
981
|
-
if (!response.ok) return null;
|
|
982
|
-
const text = await response.text();
|
|
983
|
-
if (!text) return null;
|
|
984
|
-
if (text.length <= maxChars) return text;
|
|
985
|
-
return `${text.slice(0, Math.max(1_000, maxChars - 32))}\n...[truncated]`;
|
|
986
|
-
} catch {
|
|
987
|
-
return null;
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
524
|
async function buildReplyContext(msg: DiscordMessage): Promise<string> {
|
|
992
525
|
const blocks: string[] = [];
|
|
993
526
|
|
|
@@ -1023,52 +556,6 @@ async function buildReplyContext(msg: DiscordMessage): Promise<string> {
|
|
|
1023
556
|
return `${blocks.join('\n\n')}\n\n`;
|
|
1024
557
|
}
|
|
1025
558
|
|
|
1026
|
-
async function buildAttachmentContext(messages: DiscordMessage[]): Promise<string> {
|
|
1027
|
-
const lines: string[] = [];
|
|
1028
|
-
let remainingChars = MAX_ATTACHMENT_CONTEXT_CHARS;
|
|
1029
|
-
|
|
1030
|
-
for (const msg of messages) {
|
|
1031
|
-
if (!msg.attachments || msg.attachments.size === 0) continue;
|
|
1032
|
-
for (const attachment of msg.attachments.values()) {
|
|
1033
|
-
const name = attachment.name || 'unnamed';
|
|
1034
|
-
const size = attachment.size || 0;
|
|
1035
|
-
const contentType = (attachment.contentType || '').toLowerCase();
|
|
1036
|
-
if (size > MAX_ATTACHMENT_BYTES) {
|
|
1037
|
-
lines.push(`- ${name}: skipped (size ${size} bytes exceeds 10MB limit)`);
|
|
1038
|
-
continue;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
if (contentType.startsWith('image/')) {
|
|
1042
|
-
lines.push(`- ${name}: image attachment (${size} bytes, ${contentType || 'unknown type'})`);
|
|
1043
|
-
continue;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
if (looksLikeTextAttachment(name, contentType)) {
|
|
1047
|
-
const maxChars = Math.min(MAX_SINGLE_ATTACHMENT_CHARS, Math.max(500, remainingChars));
|
|
1048
|
-
const text = await fetchAttachmentText(attachment.url, maxChars);
|
|
1049
|
-
if (!text) {
|
|
1050
|
-
lines.push(`- ${name}: text attachment (failed to read content)`);
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
const block = `- ${name} (text attachment):\n\`\`\`\n${text}\n\`\`\``;
|
|
1055
|
-
remainingChars -= block.length;
|
|
1056
|
-
lines.push(block);
|
|
1057
|
-
if (remainingChars <= 0) {
|
|
1058
|
-
lines.push('- Additional attachment content omitted (context budget reached).');
|
|
1059
|
-
return `[Attachments]\n${lines.join('\n')}\n\n`;
|
|
1060
|
-
}
|
|
1061
|
-
continue;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
lines.push(`- ${name}: attachment (${size} bytes, ${contentType || 'unknown type'})`);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
if (lines.length === 0) return '';
|
|
1069
|
-
return `[Attachments]\n${lines.join('\n')}\n\n`;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
559
|
async function addProcessingReaction(msg: DiscordMessage): Promise<() => Promise<void>> {
|
|
1073
560
|
if (!client.user) return async () => {};
|
|
1074
561
|
const botUserId = client.user.id;
|
|
@@ -1116,36 +603,19 @@ function startTypingLoop(msg: DiscordMessage): { stop: () => void } {
|
|
|
1116
603
|
};
|
|
1117
604
|
}
|
|
1118
605
|
|
|
1119
|
-
function prepareChunkedPayloads(
|
|
1120
|
-
text: string,
|
|
1121
|
-
files?: AttachmentBuilder[],
|
|
1122
|
-
mentionLookup?: MentionLookup,
|
|
1123
|
-
): { content: string; files?: AttachmentBuilder[] }[] {
|
|
1124
|
-
const prepared = mentionLookup ? rewriteUserMentions(text, mentionLookup) : text;
|
|
1125
|
-
const chunks = chunkMessage(prepared, { maxChars: 1_900, maxLines: 20 });
|
|
1126
|
-
const safeChunks = chunks.length > 0 ? chunks : ['(no content)'];
|
|
1127
|
-
return safeChunks.map((content, i) => ({
|
|
1128
|
-
content,
|
|
1129
|
-
...(i === safeChunks.length - 1 && files && files.length > 0 ? { files } : {}),
|
|
1130
|
-
}));
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
606
|
async function sendChunkedReply(
|
|
1134
607
|
msg: DiscordMessage,
|
|
1135
608
|
text: string,
|
|
1136
609
|
files?: AttachmentBuilder[],
|
|
1137
610
|
mentionLookup?: MentionLookup,
|
|
1138
611
|
): Promise<void> {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}).send(payloads[i]));
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
612
|
+
await sendChunkedReplyFromDelivery({
|
|
613
|
+
msg,
|
|
614
|
+
text,
|
|
615
|
+
withRetry: withDiscordRetry,
|
|
616
|
+
...(files ? { files } : {}),
|
|
617
|
+
...(mentionLookup ? { mentionLookup } : {}),
|
|
618
|
+
});
|
|
1149
619
|
}
|
|
1150
620
|
|
|
1151
621
|
async function sendChunkedDirectReply(
|
|
@@ -1154,31 +624,26 @@ async function sendChunkedDirectReply(
|
|
|
1154
624
|
files?: AttachmentBuilder[],
|
|
1155
625
|
mentionLookup?: MentionLookup,
|
|
1156
626
|
): Promise<void> {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
627
|
+
await sendChunkedDirectReplyFromDelivery({
|
|
628
|
+
msg,
|
|
629
|
+
text,
|
|
630
|
+
withRetry: withDiscordRetry,
|
|
631
|
+
...(files ? { files } : {}),
|
|
632
|
+
...(mentionLookup ? { mentionLookup } : {}),
|
|
633
|
+
});
|
|
1162
634
|
}
|
|
1163
635
|
|
|
1164
636
|
async function sendChunkedInteractionReply(
|
|
1165
|
-
interaction:
|
|
637
|
+
interaction: Parameters<typeof sendChunkedInteractionReplyFromDelivery>[0]['interaction'],
|
|
1166
638
|
text: string,
|
|
1167
639
|
files?: AttachmentBuilder[],
|
|
1168
640
|
): Promise<void> {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
} else {
|
|
1176
|
-
await withDiscordRetry('interaction-reply', () => interaction.reply(payload));
|
|
1177
|
-
}
|
|
1178
|
-
continue;
|
|
1179
|
-
}
|
|
1180
|
-
await withDiscordRetry('interaction-followup', () => interaction.followUp(payload));
|
|
1181
|
-
}
|
|
641
|
+
await sendChunkedInteractionReplyFromDelivery({
|
|
642
|
+
interaction,
|
|
643
|
+
text,
|
|
644
|
+
withRetry: withDiscordRetry,
|
|
645
|
+
...(files ? { files } : {}),
|
|
646
|
+
});
|
|
1182
647
|
}
|
|
1183
648
|
|
|
1184
649
|
async function ensureSlashStatusCommand(): Promise<void> {
|
|
@@ -1427,7 +892,7 @@ export function initDiscord(onMessage: MessageHandler, onCommand: CommandHandler
|
|
|
1427
892
|
inboundHistory.entries,
|
|
1428
893
|
rememberedParticipants,
|
|
1429
894
|
);
|
|
1430
|
-
const combinedContent = `${feedbackNote ? `[Reaction feedback]\n${feedbackNote}\n\n` : ''}${channelInfoContext}${replyContext}${inboundHistory.context}${attachmentContext}${participantContext}${batchedContent}`;
|
|
895
|
+
const combinedContent = `${feedbackNote ? `[Reaction feedback]\n${feedbackNote}\n\n` : ''}${channelInfoContext}${replyContext}${inboundHistory.context}${attachmentContext.context}${participantContext}${batchedContent}`;
|
|
1431
896
|
|
|
1432
897
|
const abortController = new AbortController();
|
|
1433
898
|
const typingLoop = startTypingLoop(msg);
|
|
@@ -1454,6 +919,7 @@ export function initDiscord(onMessage: MessageHandler, onCommand: CommandHandler
|
|
|
1454
919
|
userId,
|
|
1455
920
|
username,
|
|
1456
921
|
combinedContent,
|
|
922
|
+
attachmentContext.media,
|
|
1457
923
|
async (text, files) => {
|
|
1458
924
|
typingLoop.stop();
|
|
1459
925
|
await sendChunkedReply(msg, text, files, mentionLookup);
|
|
@@ -1714,16 +1180,12 @@ export function initDiscord(onMessage: MessageHandler, onCommand: CommandHandler
|
|
|
1714
1180
|
export async function sendToChannel(channelId: string, text: string, files?: AttachmentBuilder[]): Promise<void> {
|
|
1715
1181
|
const channel = await client.channels.fetch(channelId);
|
|
1716
1182
|
if (channel && 'send' in channel) {
|
|
1717
|
-
const
|
|
1718
|
-
const safeChunks = chunks.length > 0 ? chunks : ['(no content)'];
|
|
1183
|
+
const payloads = prepareChunkedPayloads(text, files);
|
|
1719
1184
|
const send = (channel as unknown as {
|
|
1720
1185
|
send: (payload: { content: string; files?: AttachmentBuilder[] }) => Promise<void>;
|
|
1721
1186
|
}).send;
|
|
1722
|
-
for (
|
|
1723
|
-
await withDiscordRetry('send-channel', () => send(
|
|
1724
|
-
content: safeChunks[i],
|
|
1725
|
-
...(i === safeChunks.length - 1 && files && files.length > 0 ? { files } : {}),
|
|
1726
|
-
}));
|
|
1187
|
+
for (const payload of payloads) {
|
|
1188
|
+
await withDiscordRetry('send-channel', () => send(payload));
|
|
1727
1189
|
}
|
|
1728
1190
|
}
|
|
1729
1191
|
}
|