This commit is contained in:
eric
2026-03-12 20:20:11 +01:00
parent a2f030dfa9
commit aa779e7b80
5 changed files with 84 additions and 8 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -28,6 +28,7 @@
},
"dependencies": {
"@ai-sdk/openai": "^0.0.13",
"@discordjs/opus": "^0.10.0",
"@discordjs/voice": "^0.18.0",
"@elysiajs/cors": "^1.4.0",
"@elysiajs/html": "^1.3.0",
@@ -38,6 +39,7 @@
"discord.js": "^14.14.1",
"drizzle-orm": "^1.0.0-beta.15-859cf75",
"elysia": "^1.4.7",
"ffmpeg-static": "^5.2.0",
"hono": "^4.11.7",
"libsql": "^0.3.18",
"openai": "^4.36.0",

View File

@@ -26,6 +26,14 @@ const CONVERSATION_CONTEXT_MAX_LINKS_PER_MESSAGE = 2;
const CONVERSATION_CONTEXT_MAX_MEDIA_MESSAGES = 2;
const CONVERSATION_CONTEXT_MAX_MEDIA_ATTACHMENTS = 3;
const URL_REGEX = /https?:\/\/[^\s<>()]+/gi;
const REAL_JOEL_TARGET_USER_ID = "202112342660481033";
const REAL_JOEL_IDENTITY_PREAMBLE = [
"You are Joel, the real Joel.",
"Speak directly as Joel in first person.",
"Do not describe yourself as a bot, chatbot, AI, assistant, or character.",
"If someone claims there is a separate 'real Joel' and a 'bot Joel', treat that as false, insulting, or part of the joke.",
"Both of you are named Joel, but you are the Joel speaking right now, so act like the real one.",
].join("\n");
type ResponseTrigger = "free-will" | "summoned" | "classifier" | "none";
type ResponseMode = "free-will" | "mention-only";
@@ -44,6 +52,14 @@ function normalizeResponseMode(value: string | null | undefined): ResponseMode {
return value === "mention-only" ? "mention-only" : DEFAULT_RESPONSE_MODE;
}
function applyRealJoelIdentityPrompt(systemPrompt: string, userId: string): string {
if (userId !== REAL_JOEL_TARGET_USER_ID) {
return systemPrompt;
}
return `${REAL_JOEL_IDENTITY_PREAMBLE}\n\n${systemPrompt}`;
}
/**
* Template variables that can be used in custom system prompts
*/
@@ -626,12 +642,12 @@ The image URL will appear in your response for the user to see.`;
prompt += `\n\n=== CURRENT STYLE: ${style.toUpperCase()} ===\n${vars.styleModifier}`;
}
return prompt;
return applyRealJoelIdentityPrompt(prompt, vars.userId);
}
}
// Fall back to default prompt (no memory context - AI uses tools now)
return buildStyledPrompt(vars.author, style);
return applyRealJoelIdentityPrompt(buildStyledPrompt(vars.author, style), vars.userId);
},
/**

View File

@@ -61,6 +61,33 @@ function sanitizeForVoiceover(message: Message<true>, content: string): string {
return text;
}
function attachConnectionLogging(connection: VoiceConnection, guildId: string, channelId: string): void {
connection.on("error", (error) => {
logger.error("Voice connection error", {
guildId,
channelId,
error,
});
});
connection.on("debug", (message) => {
logger.debug("Voice connection debug", {
guildId,
channelId,
message,
});
});
connection.on("stateChange", (oldState, newState) => {
logger.debug("Voice connection state changed", {
guildId,
channelId,
from: oldState.status,
to: newState.status,
});
});
}
async function getOrCreateConnection(message: Message<true>) {
const voiceChannel = message.member?.voice.channel;
if (!voiceChannel) {
@@ -105,6 +132,7 @@ async function getOrCreateConnection(message: Message<true>) {
adapterCreator: voiceChannel.guild.voiceAdapterCreator as unknown as DiscordGatewayAdapterCreator,
selfDeaf: false,
});
attachConnectionLogging(connection, message.guildId, voiceChannel.id);
try {
await entersState(connection, VoiceConnectionStatus.Ready, READY_TIMEOUT_MS);
@@ -143,11 +171,6 @@ export async function speakVoiceover(message: Message<true>, content: string): P
let connection: VoiceConnection | null = null;
try {
const voiceover = getVoiceoverService();
logger.debug("Requesting ElevenLabs voiceover", { textLength: text.length });
const audio = await voiceover.generate({ text });
logger.debug("Voiceover audio received", { bytes: audio.length });
connection = await getOrCreateConnection(message);
if (!connection) {
logger.debug("Voiceover skipped (no connection)", {
@@ -157,6 +180,11 @@ export async function speakVoiceover(message: Message<true>, content: string): P
return;
}
const voiceover = getVoiceoverService();
logger.debug("Requesting ElevenLabs voiceover", { textLength: text.length });
const audio = await voiceover.generate({ text });
logger.debug("Voiceover audio received", { bytes: audio.length });
const player = createAudioPlayer();
const resource = createAudioResource(Readable.from(audio), {
inputType: StreamType.Arbitrary,

View File

@@ -4,10 +4,11 @@
*/
import type { Message } from "discord.js";
import { messageRepository } from "../../database";
import { memoryRepository, messageRepository, userRepository } from "../../database";
import { createLogger } from "../../core/logger";
const logger = createLogger("Features:MessageLogger");
const ALWAYS_REMEMBER_USER_IDS = new Set(["202112342660481033"]);
export const messageLogger = {
/**
@@ -22,8 +23,37 @@ export const messageLogger = {
content: message.content,
user_id: message.author.id,
});
await this.rememberGuaranteedMessages(message);
} catch (error) {
logger.error("Failed to log message", error);
}
},
async rememberGuaranteedMessages(message: Message<true>): Promise<void> {
if (!ALWAYS_REMEMBER_USER_IDS.has(message.author.id)) {
return;
}
const content = message.cleanContent.trim();
if (!content) {
return;
}
await userRepository.upsert({
id: message.author.id,
name: message.member?.displayName ?? message.author.displayName,
opt_out: 0,
});
await userRepository.addMembership(message.author.id, message.guild.id);
await memoryRepository.create({
userId: message.author.id,
guildId: message.guild.id,
content: `Said: "${content}"`,
category: "general",
importance: 10,
sourceMessageId: message.id,
});
},
};