disclaw/src/router.ts
Nick Tabeling 57cd3d4a43 feat(DIS-102): sanitizeForDiscord strips paths and tokens before Discord send
Add sanitizeForDiscord() in src/runtime/sanitize.ts.
Router applies sanitizer to all channel.send() calls.
Prevents path/token leaks into Discord channels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:50:37 +02:00

140 lines
3.7 KiB
TypeScript

import * as os from "os";
import { Message, TextChannel } from "discord.js";
import { DisclawDatabase } from "./db/database";
import { DisclawConfig } from "./config/loader";
import { runAgent } from "./agent/runner";
import { sanitizeForDiscord } from "./runtime/sanitize";
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(sanitizeForDiscord(chunk, { home: os.homedir() }));
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";
const safeMsg = sanitizeForDiscord(`Fehler bei der Verarbeitung: ${msg}`, { home: os.homedir() });
await channel.send(safeMsg).catch(() => {});
} finally {
clearInterval(typingInterval);
}
return true;
}