Add text features: trusted triggers, context cache, response policy, MCP config tools
This commit is contained in:
@@ -6,6 +6,16 @@
|
||||
* `whitelist` commands and normalizes @mentions.
|
||||
*/
|
||||
|
||||
export interface CachedMessage {
|
||||
id: string;
|
||||
authorId: string;
|
||||
authorName: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
/** Attachment file names only. Bodies stay on Discord. */
|
||||
attachments?: string[];
|
||||
}
|
||||
|
||||
export interface PuppetMessage {
|
||||
id: string;
|
||||
channelId: string;
|
||||
@@ -18,6 +28,8 @@ export interface PuppetMessage {
|
||||
trusted: boolean;
|
||||
/** Parsed command text when the message is one. */
|
||||
command?: string;
|
||||
/** Prior messages for context. Present on mention triggers. */
|
||||
context?: CachedMessage[];
|
||||
}
|
||||
|
||||
export const TRUSTED_LOG_LIMIT = 100;
|
||||
@@ -81,3 +93,81 @@ export function isTrusted(
|
||||
export function randomActionDelay(): number {
|
||||
return 500 + Math.floor(Math.random() * 2301);
|
||||
}
|
||||
|
||||
/* Context assembly */
|
||||
|
||||
export interface ContextOptions {
|
||||
maxMessages?: number;
|
||||
maxChars?: number;
|
||||
/** Skip this message ID. Use it to keep the trigger out of its own context. */
|
||||
excludeId?: string;
|
||||
}
|
||||
|
||||
export interface AssembledContext {
|
||||
messages: CachedMessage[];
|
||||
chars: number;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export function formatContextLine(message: CachedMessage): string {
|
||||
const parts: string[] = [];
|
||||
if (message.content) parts.push(message.content);
|
||||
for (const name of message.attachments ?? []) parts.push(`[file: ${name}]`);
|
||||
return `${message.authorName}: ${parts.join(" ")}`.trim();
|
||||
}
|
||||
|
||||
/** Assemble the newest messages inside a message count limit and a character limit. */
|
||||
export function assembleContext(messages: readonly CachedMessage[], options: ContextOptions = {}): AssembledContext {
|
||||
const maxMessages = options.maxMessages ?? 15;
|
||||
const maxChars = options.maxChars ?? 4000;
|
||||
const selected: CachedMessage[] = [];
|
||||
|
||||
let chars = 0;
|
||||
let truncated = false;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
if (options.excludeId && message.id === options.excludeId) continue;
|
||||
if (selected.length >= maxMessages) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const line = formatContextLine(message);
|
||||
if (selected.length > 0 && chars + line.length > maxChars) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
|
||||
selected.push(message);
|
||||
chars += line.length;
|
||||
}
|
||||
|
||||
selected.reverse();
|
||||
return { messages: selected, chars, truncated };
|
||||
}
|
||||
|
||||
/** Compare two snowflake IDs. Falls back to string compare for invalid input. */
|
||||
export function compareSnowflakes(a: string, b: string): number {
|
||||
try {
|
||||
const left = BigInt(a);
|
||||
const right = BigInt(b);
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
} catch {
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert a core `message.fetch` or `message.read` message into a cached message. */
|
||||
export function toCachedMessage(raw: any): CachedMessage {
|
||||
return {
|
||||
id: String(raw.id),
|
||||
authorId: String(raw.author?.id ?? ""),
|
||||
authorName: String(raw.author?.username ?? raw.author?.id ?? "unknown"),
|
||||
content: String(raw.content ?? ""),
|
||||
timestamp: String(raw.timestamp ?? ""),
|
||||
attachments: Array.isArray(raw.attachments)
|
||||
? raw.attachments.map((attachment: any) => String(attachment?.filename ?? attachment)).filter(Boolean)
|
||||
: undefined
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user