Stabilize paired-room orchestration and Codex app-server flow

This commit is contained in:
Eyejoker
2026-03-19 02:57:14 +09:00
parent b710d4a33d
commit 779d57c392
32 changed files with 2517 additions and 185 deletions

View File

@@ -313,11 +313,55 @@ describe('DiscordChannel', () => {
expect.any(String),
expect.objectContaining({
content: 'I am a bot',
is_from_me: false,
is_bot_message: true,
}),
);
});
it('ignores self-authored bot messages', async () => {
const opts = createTestOpts();
const channel = new DiscordChannel('test-token', opts);
await channel.connect();
const msg = createMessage({
authorId: '999888777',
isBot: true,
content: 'I am this bot',
});
await triggerMessage(msg);
expect(opts.onMessage).not.toHaveBeenCalled();
});
it('identifies stored messages authored by the current bot', async () => {
const opts = createTestOpts();
const channel = new DiscordChannel('test-token', opts);
await channel.connect();
expect(
channel.isOwnMessage?.({
id: 'msg_bot',
chat_jid: 'dc:1234567890123456',
sender: '999888777',
sender_name: 'Andy',
content: 'bot message',
timestamp: '2024-01-01T00:00:00.000Z',
is_bot_message: true,
}),
).toBe(true);
expect(
channel.isOwnMessage?.({
id: 'msg_user',
chat_jid: 'dc:1234567890123456',
sender: '55512345',
sender_name: 'Alice',
content: 'user message',
timestamp: '2024-01-01T00:00:00.000Z',
}),
).toBe(false);
});
it('uses member displayName when available (server nickname)', async () => {
const opts = createTestOpts();
const channel = new DiscordChannel('test-token', opts);

View File

@@ -19,6 +19,7 @@ import {
} from '../config.js';
import { readEnvFile } from '../env.js';
import { logger } from '../logger.js';
import { formatOutbound } from '../router.js';
const ATTACHMENTS_DIR = path.join(DATA_DIR, 'attachments');
const TRANSCRIPTION_CACHE_DIR = path.join(CACHE_DIR, 'transcriptions');
@@ -158,6 +159,7 @@ import {
AgentType,
Channel,
ChannelMeta,
NewMessage,
OnChatMetadata,
OnInboundMessage,
RegisteredGroup,
@@ -446,6 +448,7 @@ export class DiscordChannel implements Channel {
for (const [name, id] of Object.entries(mentionMap)) {
cleaned = cleaned.replace(new RegExp(`@${name}`, 'g'), `<@${id}>`);
}
cleaned = formatOutbound(cleaned);
// Discord has a 2000 character limit per message — split if needed
const MAX_LENGTH = 2000;
@@ -454,6 +457,11 @@ export class DiscordChannel implements Channel {
name: path.basename(f),
}));
if (!cleaned && files.length === 0) {
logger.debug({ jid }, 'Skipping empty Discord outbound message');
return;
}
if (cleaned.length <= MAX_LENGTH) {
await textChannel.send({
content: cleaned || undefined,
@@ -490,6 +498,10 @@ export class DiscordChannel implements Channel {
return groupType === this.agentTypeFilter;
}
isOwnMessage(msg: NewMessage): boolean {
return !!this.client?.user?.id && msg.sender === this.client.user.id;
}
async disconnect(): Promise<void> {
if (this.client) {
this.client.destroy();