137 lines
3.5 KiB
TypeScript
137 lines
3.5 KiB
TypeScript
import { Message, TextChannel } from "discord.js";
|
|
import { DisclawDatabase } from "./db/database";
|
|
import { DisclawConfig } from "./config/loader";
|
|
import { runAgent } from "./agent/runner";
|
|
|
|
const DISCORD_MAX_LENGTH = 2000;
|
|
|
|
/**
|
|
* Splits a message at line breaks so each chunk stays under the Discord
|
|
* character limit. If a single line exceeds the limit, it is split at
|
|
* the character boundary as a last resort.
|
|
*/
|
|
function splitMessage(text: string): string[] {
|
|
if (text.length <= DISCORD_MAX_LENGTH) {
|
|
return [text];
|
|
}
|
|
|
|
const chunks: string[] = [];
|
|
const lines = text.split("\n");
|
|
let current = "";
|
|
|
|
for (const line of lines) {
|
|
// If adding this line would exceed the limit, flush current chunk
|
|
if (current.length + line.length + 1 > DISCORD_MAX_LENGTH) {
|
|
if (current.length > 0) {
|
|
chunks.push(current);
|
|
current = "";
|
|
}
|
|
|
|
// Handle lines that are themselves too long
|
|
if (line.length > DISCORD_MAX_LENGTH) {
|
|
let remaining = line;
|
|
while (remaining.length > DISCORD_MAX_LENGTH) {
|
|
chunks.push(remaining.slice(0, DISCORD_MAX_LENGTH));
|
|
remaining = remaining.slice(DISCORD_MAX_LENGTH);
|
|
}
|
|
if (remaining.length > 0) {
|
|
current = remaining;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
current += (current.length > 0 ? "\n" : "") + line;
|
|
}
|
|
|
|
if (current.length > 0) {
|
|
chunks.push(current);
|
|
}
|
|
|
|
return chunks;
|
|
}
|
|
|
|
/**
|
|
* Routes a Discord message to the correct agent based on channel_id.
|
|
* Returns true if the message was handled, false if the channel is not
|
|
* mapped to any agent.
|
|
*/
|
|
export async function routeMessage(
|
|
message: Message,
|
|
db: DisclawDatabase,
|
|
config: DisclawConfig,
|
|
managementChannelId: string | null
|
|
): Promise<boolean> {
|
|
const channelId = message.channelId;
|
|
|
|
// Ignore messages in the management channel (commands only)
|
|
if (channelId === managementChannelId) {
|
|
return false;
|
|
}
|
|
|
|
// Look up workspace for this channel
|
|
const workspace = db.getWorkspaceByChannelId(channelId);
|
|
if (!workspace) {
|
|
return false;
|
|
}
|
|
|
|
const channel = message.channel as TextChannel;
|
|
|
|
// Show typing indicator while the agent works
|
|
const typingInterval = setInterval(() => {
|
|
channel.sendTyping().catch(() => {});
|
|
}, 5000);
|
|
// Send immediately as well
|
|
await channel.sendTyping().catch(() => {});
|
|
|
|
try {
|
|
// Load recent conversation history for context
|
|
const history = db.getConversationHistory(workspace.id, 30);
|
|
|
|
// Run the agent
|
|
const response = await runAgent({
|
|
workspacePath: workspace.workspace_path,
|
|
channelName: channel.name,
|
|
userMessage: message.content,
|
|
conversationHistory: history,
|
|
});
|
|
|
|
// Store user message
|
|
db.addConversation(
|
|
workspace.id,
|
|
message.id,
|
|
"user",
|
|
message.content,
|
|
message.author.username
|
|
);
|
|
|
|
// Split and send response
|
|
const chunks = splitMessage(response);
|
|
let firstMsgId: string | null = null;
|
|
|
|
for (const chunk of chunks) {
|
|
const sent = await channel.send(chunk);
|
|
if (!firstMsgId) {
|
|
firstMsgId = sent.id;
|
|
}
|
|
}
|
|
|
|
// Store assistant response (use first message id for dedup)
|
|
if (firstMsgId) {
|
|
db.addConversation(
|
|
workspace.id,
|
|
firstMsgId,
|
|
"assistant",
|
|
response,
|
|
null
|
|
);
|
|
}
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : "Unbekannter Fehler";
|
|
await channel.send(`Fehler bei der Verarbeitung: ${msg}`).catch(() => {});
|
|
} finally {
|
|
clearInterval(typingInterval);
|
|
}
|
|
|
|
return true;
|
|
}
|