import type { Client, Message } from 'discord.js' import { HTTPClient } from '@/http' import { getDefaultAgentId } from '@/app/api/nova-config' import { getConversationId, setConversationId } from './user-store' import { sendMessageAndWaitResponse } from '@/remote-control/shared/nova-bridge' import { uploadBotAttachments, type BotAttachment } from '@/remote-control/shared/file-uploader' import { BotLogger } from '@/remote-control/shared/logger' const oapiClient = new HTTPClient({ baseURL: process.env.NOVA_BASE_URL, headers: { 'Content-Type': 'application/json', 'Tenant-Id': process.env.NOVA_TENANT_ID!, 'Authorization': process.env.NOVA_ACCESS_KEY!, }, }) const userQueues = new Map>() function splitMessage(text: string, maxLength = 2000): string[] { if (text.length <= maxLength) return [text] const parts: string[] = [] let remaining = text while (remaining.length > 0) { if (remaining.length <= maxLength) { parts.push(remaining) break } let splitIndex = remaining.lastIndexOf('\n', maxLength) if (splitIndex <= 0) { splitIndex = maxLength } parts.push(remaining.slice(0, splitIndex)) remaining = remaining.slice(splitIndex).trimStart() } return parts } async function getOrCreateConversation(discordUserId: string): Promise { const existing = getConversationId(discordUserId) if (existing) return existing const res = await oapiClient.post<{ conversation_id: string }>( '/v1/oapi/super_agent/chat/create_conversation', { agent_id: getDefaultAgentId(), title: `Discord User ${discordUserId}`, }, ) const conversationId = res.conversation_id setConversationId(discordUserId, conversationId) return conversationId } async function processMessage(message: Message, onMessage?: () => void): Promise { const content = message.content .replace(/<@!?\d+>/g, '') .trim() // Extract attachments from Discord message const attachments: BotAttachment[] = Array.from(message.attachments.values()).map(att => ({ url: att.url, fileName: att.name ?? `discord_file_${Date.now()}`, mimeType: att.contentType ?? undefined, size: att.size, })) if (!content && attachments.length === 0) return BotLogger.log({ platform: 'discord', eventType: 'message_received', severity: 'info', message: `收到来自用户 ${message.author.username} 的消息 (${attachments.length} 附件)`, details: { userId: message.author.id, channelId: message.channelId }, }) let typingInterval: ReturnType | null = null try { const channel = message.channel if ('sendTyping' in channel) { await channel.sendTyping() } typingInterval = setInterval(() => { if ('sendTyping' in channel) { channel.sendTyping().catch(() => {}) } }, 8000) // Upload attachments if any let uploadFileIds: string[] | undefined if (attachments.length > 0) { uploadFileIds = await uploadBotAttachments(attachments) if (uploadFileIds.length === 0) uploadFileIds = undefined } const conversationId = await getOrCreateConversation(message.author.id) const response = await sendMessageAndWaitResponse( `discord:${message.author.id}`, conversationId, content || '', uploadFileIds, ) if (!response) { await message.reply('没有收到回复,请稍后重试。') return } const parts = splitMessage(response) for (const part of parts) { await message.reply(part) } if (onMessage) onMessage() BotLogger.log({ platform: 'discord', eventType: 'message_sent', severity: 'info', message: `已回复用户 ${message.author.username}`, }) } catch (error) { const errMsg = error instanceof Error ? error.message : '未知错误' console.error('[Discord Bot] 处理消息失败:', errMsg) await message.reply(`处理消息时出错: ${errMsg}`).catch(() => {}) BotLogger.log({ platform: 'discord', eventType: 'error', severity: 'error', message: `处理消息失败: ${errMsg}`, details: { userId: message.author.id }, }) } finally { if (typingInterval) clearInterval(typingInterval) } } export function registerHandlers(client: Client, onMessage?: () => void): void { client.on('messageCreate', (message: Message) => { if (message.author.bot) return if (!client.user || !message.mentions.has(client.user)) return const userId = message.author.id const currentQueue = userQueues.get(userId) ?? Promise.resolve() const newQueue = currentQueue.then(() => processMessage(message, onMessage)) userQueues.set(userId, newQueue) newQueue.finally(() => { if (userQueues.get(userId) === newQueue) { userQueues.delete(userId) } }) }) }