refactor: land approved runtime cleanup and room binding cutover

This commit is contained in:
ejclaw
2026-04-10 21:04:23 +09:00
parent ce3854745d
commit 4e05f60400
45 changed files with 636 additions and 637 deletions

View File

@@ -133,7 +133,7 @@ function createTestOpts(
return {
onMessage: vi.fn(),
onChatMetadata: vi.fn(),
registeredGroups: vi.fn(() => ({
roomBindings: vi.fn(() => ({
'dc:1234567890123456': {
name: 'Test Server #general',
folder: 'test-server',
@@ -441,7 +441,7 @@ describe('DiscordChannel', () => {
it('uses sender name for DM chats (no guild)', async () => {
const opts = createTestOpts({
registeredGroups: vi.fn(() => ({
roomBindings: vi.fn(() => ({
'dc:1234567890123456': {
name: 'DM',
folder: 'dm',
@@ -491,10 +491,10 @@ describe('DiscordChannel', () => {
});
});
// --- @mention translation ---
// --- bot mention handling ---
describe('@mention translation', () => {
it('translates <@botId> mention to trigger format', async () => {
describe('bot mention handling', () => {
it('passes through <@botId> mentions without rewriting them', async () => {
const opts = createTestOpts();
const channel = new DiscordChannel('test-token', opts);
await channel.connect();
@@ -509,12 +509,12 @@ describe('DiscordChannel', () => {
expect(opts.onMessage).toHaveBeenCalledWith(
'dc:1234567890123456',
expect.objectContaining({
content: '@Andy what time is it?',
content: '<@999888777> what time is it?',
}),
);
});
it('does not translate if message already matches trigger', async () => {
it('leaves mixed text and mentions untouched', async () => {
const opts = createTestOpts();
const channel = new DiscordChannel('test-token', opts);
await channel.connect();
@@ -531,7 +531,7 @@ describe('DiscordChannel', () => {
expect(opts.onMessage).toHaveBeenCalledWith(
'dc:1234567890123456',
expect.objectContaining({
content: '@Andy hello',
content: '@Andy hello <@999888777>',
}),
);
});
@@ -555,7 +555,7 @@ describe('DiscordChannel', () => {
);
});
it('handles <@!botId> (nickname mention format)', async () => {
it('passes through <@!botId> nickname mentions without rewriting them', async () => {
const opts = createTestOpts();
const channel = new DiscordChannel('test-token', opts);
await channel.connect();
@@ -570,7 +570,7 @@ describe('DiscordChannel', () => {
expect(opts.onMessage).toHaveBeenCalledWith(
'dc:1234567890123456',
expect.objectContaining({
content: '@Andy check this',
content: '<@!999888777> check this',
}),
);
});

View File

@@ -208,7 +208,7 @@ import {
export interface DiscordChannelOpts {
onMessage: OnInboundMessage;
onChatMetadata: OnChatMetadata;
registeredGroups: () => Record<string, RegisteredGroup>;
roomBindings: () => Record<string, RegisteredGroup>;
}
export class DiscordChannel implements Channel {
@@ -279,29 +279,6 @@ export class DiscordChannel implements Channel {
chatName = senderName;
}
// Translate Discord @bot mentions into TRIGGER_PATTERN format.
// Discord mentions look like <@botUserId> — these won't match
// TRIGGER_PATTERN (e.g., ^@Andy\b), so we prepend the trigger
// when the bot is @mentioned.
if (this.client?.user) {
const botId = this.client.user.id;
const isBotMentioned =
message.mentions.users.has(botId) ||
content.includes(`<@${botId}>`) ||
content.includes(`<@!${botId}>`);
if (isBotMentioned) {
// Strip the <@botId> mention to avoid visual clutter
content = content
.replace(new RegExp(`<@!?${botId}>`, 'g'), '')
.trim();
// Prepend trigger if not already present
if (!TRIGGER_PATTERN.test(content)) {
content = `@${ASSISTANT_NAME} ${content}`;
}
}
}
// Handle attachments — transcribe voice messages, download files
const isVoiceMessage = message.flags.has(MessageFlags.IsVoiceMessage);
if (message.attachments.size > 0) {
@@ -403,7 +380,7 @@ export class DiscordChannel implements Channel {
// Only deliver full message for registered groups. Secondary role bots
// are configured as outbound-only, while the owner bot receives inbound.
const group = this.opts.registeredGroups()[chatJid];
const group = this.opts.roomBindings()[chatJid];
if (!group) {
logger.debug(
{ chatJid, chatName },
@@ -627,7 +604,7 @@ export class DiscordChannel implements Channel {
if (!jid.startsWith('dc:')) return false;
if (!this.ownsDiscordJids) return false;
if (!this.agentTypeFilter) return true;
const group = this.opts.registeredGroups()[jid];
const group = this.opts.roomBindings()[jid];
if (!group) return false;
const groupType = group.agentType || 'claude-code';
return groupType === this.agentTypeFilter;

View File

@@ -8,7 +8,7 @@ import {
export interface ChannelOpts {
onMessage: OnInboundMessage;
onChatMetadata: OnChatMetadata;
registeredGroups: () => Record<string, RegisteredGroup>;
roomBindings: () => Record<string, RegisteredGroup>;
}
export type ChannelFactory = (opts: ChannelOpts) => Channel | null;