From 4a8c1f96b42a457f0ba73387b2ff995cc95ac2e1 Mon Sep 17 00:00:00 2001 From: Yousei Takahashi Date: Thu, 2 Apr 2026 12:31:32 +0900 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20conversations.info=E3=81=AE=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E3=83=AD=E3=82=B0=E8=BF=BD=E5=8A=A0=E3=81=A8?= =?UTF-8?q?=E3=83=81=E3=83=A3=E3=83=B3=E3=83=8D=E3=83=AB=E5=90=8D=E3=82=AD?= =?UTF-8?q?=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit conversations.infoが例外をスローした場合にBoltに飲まれて原因不明だった問題に対応。 try-catchでエラー内容をログ出力するようにした。 また、同一チャンネルへの重複API呼び出しを避けるためキャッシュを追加した。 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/handlers/message.ts | 56 +++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 7470ce7..94768cd 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -4,6 +4,43 @@ import { isForwardableMessage } from '../lib/messageFilter.ts'; import { buildMessageLink } from '../lib/messageLink.ts'; import { isTimesChannel } from '../lib/timesChannel.ts'; +type SlackClient = (AllMiddlewareArgs & + SlackEventMiddlewareArgs<'message'>)['client']; + +const channelNameCache = new Map(); + +async function getChannelName( + client: SlackClient, + channelId: string, +): Promise { + const cached = channelNameCache.get(channelId); + if (cached !== undefined) { + return cached; + } + try { + const channelInfo = await client.conversations.info({ + channel: channelId, + }); + const channelName = channelInfo.channel?.name; + if (!channelName) { + console.error('channel name not found', { + channel: channelId, + ok: channelInfo.ok, + error: channelInfo.error, + }); + return undefined; + } + channelNameCache.set(channelId, channelName); + return channelName; + } catch (error) { + console.error('conversations.info failed', { + channel: channelId, + error: error instanceof Error ? error.message : error, + }); + return undefined; + } +} + export function createMessageHandler( timesAllChannelId: string, workspaceUrl: string, @@ -12,17 +49,22 @@ export function createMessageHandler( message, client, }: AllMiddlewareArgs & SlackEventMiddlewareArgs<'message'>) => { - console.log('received message'); + console.log('received message', { + channel: message.channel, + subtype: message.subtype, + ts: message.ts, + }); if (!isForwardableMessage(message, timesAllChannelId)) { + console.log('skipped: not forwardable', { subtype: message.subtype }); return; } - const channelInfo = await client.conversations.info({ - channel: message.channel, - }); - const channelName = channelInfo.channel?.name; - if (!channelName || !isTimesChannel(channelName)) { - console.log(channelName); + const channelName = await getChannelName(client, message.channel); + if (!channelName) { + return; + } + if (!isTimesChannel(channelName)) { + console.log('skipped: not a times channel', { channelName }); return; } From d04cf285713ee996891917114095876c3bd06360 Mon Sep 17 00:00:00 2001 From: Yousei Takahashi Date: Thu, 2 Apr 2026 12:34:55 +0900 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20conversations.info=E5=A4=B1=E6=95=97?= =?UTF-8?q?=E6=99=82=E3=81=AE=E3=83=AA=E3=83=88=E3=83=A9=E3=82=A4=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一時的なSlack APIエラーで転送が失敗するケースに対応。 500ms間隔で最大2回リトライし、自動回復を試みる。 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/handlers/message.ts | 45 ++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 94768cd..7b8f5bb 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -9,6 +9,9 @@ type SlackClient = (AllMiddlewareArgs & const channelNameCache = new Map(); +const MAX_RETRIES = 2; +const RETRY_DELAY_MS = 500; + async function getChannelName( client: SlackClient, channelId: string, @@ -17,28 +20,34 @@ async function getChannelName( if (cached !== undefined) { return cached; } - try { - const channelInfo = await client.conversations.info({ - channel: channelId, - }); - const channelName = channelInfo.channel?.name; - if (!channelName) { - console.error('channel name not found', { + for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { + try { + const channelInfo = await client.conversations.info({ channel: channelId, - ok: channelInfo.ok, - error: channelInfo.error, }); - return undefined; + const channelName = channelInfo.channel?.name; + if (!channelName) { + console.error('channel name not found', { + channel: channelId, + ok: channelInfo.ok, + error: channelInfo.error, + }); + return undefined; + } + channelNameCache.set(channelId, channelName); + return channelName; + } catch (error) { + console.error('conversations.info failed', { + channel: channelId, + attempt: attempt + 1, + error: error instanceof Error ? error.message : error, + }); + if (attempt < MAX_RETRIES) { + await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS)); + } } - channelNameCache.set(channelId, channelName); - return channelName; - } catch (error) { - console.error('conversations.info failed', { - channel: channelId, - error: error instanceof Error ? error.message : error, - }); - return undefined; } + return undefined; } export function createMessageHandler( From 2a7d89a8042997da9301eae6bf5d138accaf3ee9 Mon Sep 17 00:00:00 2001 From: Yousei Takahashi Date: Thu, 2 Apr 2026 12:46:48 +0900 Subject: [PATCH 3/8] =?UTF-8?q?Revert=20"fix:=20conversations.info?= =?UTF-8?q?=E5=A4=B1=E6=95=97=E6=99=82=E3=81=AE=E3=83=AA=E3=83=88=E3=83=A9?= =?UTF-8?q?=E3=82=A4=E3=82=92=E8=BF=BD=E5=8A=A0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d04cf285713ee996891917114095876c3bd06360. --- src/handlers/message.ts | 45 +++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 7b8f5bb..94768cd 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -9,9 +9,6 @@ type SlackClient = (AllMiddlewareArgs & const channelNameCache = new Map(); -const MAX_RETRIES = 2; -const RETRY_DELAY_MS = 500; - async function getChannelName( client: SlackClient, channelId: string, @@ -20,34 +17,28 @@ async function getChannelName( if (cached !== undefined) { return cached; } - for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { - try { - const channelInfo = await client.conversations.info({ - channel: channelId, - }); - const channelName = channelInfo.channel?.name; - if (!channelName) { - console.error('channel name not found', { - channel: channelId, - ok: channelInfo.ok, - error: channelInfo.error, - }); - return undefined; - } - channelNameCache.set(channelId, channelName); - return channelName; - } catch (error) { - console.error('conversations.info failed', { + try { + const channelInfo = await client.conversations.info({ + channel: channelId, + }); + const channelName = channelInfo.channel?.name; + if (!channelName) { + console.error('channel name not found', { channel: channelId, - attempt: attempt + 1, - error: error instanceof Error ? error.message : error, + ok: channelInfo.ok, + error: channelInfo.error, }); - if (attempt < MAX_RETRIES) { - await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS)); - } + return undefined; } + channelNameCache.set(channelId, channelName); + return channelName; + } catch (error) { + console.error('conversations.info failed', { + channel: channelId, + error: error instanceof Error ? error.message : error, + }); + return undefined; } - return undefined; } export function createMessageHandler( From e013a3220b1f459022cb3b5c20843d45ab24f5c4 Mon Sep 17 00:00:00 2001 From: Yousei Takahashi Date: Thu, 2 Apr 2026 12:49:18 +0900 Subject: [PATCH 4/8] =?UTF-8?q?refactor:=20=E3=83=81=E3=83=A3=E3=83=B3?= =?UTF-8?q?=E3=83=8D=E3=83=AB=E5=90=8D=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E3=82=92createMessageHandler=E5=86=85=E3=81=AE?= =?UTF-8?q?=E3=82=AF=E3=83=AD=E3=83=BC=E3=82=B8=E3=83=A3=E3=81=AB=E7=A7=BB?= =?UTF-8?q?=E5=8B=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit モジュールスコープで無制限に増え続ける問題を解消するため、 ハンドラーのファクトリ関数内に閉じ込めた。 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/handlers/message.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 94768cd..07f8528 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -7,13 +7,12 @@ import { isTimesChannel } from '../lib/timesChannel.ts'; type SlackClient = (AllMiddlewareArgs & SlackEventMiddlewareArgs<'message'>)['client']; -const channelNameCache = new Map(); - async function getChannelName( client: SlackClient, channelId: string, + cache: Map, ): Promise { - const cached = channelNameCache.get(channelId); + const cached = cache.get(channelId); if (cached !== undefined) { return cached; } @@ -30,7 +29,7 @@ async function getChannelName( }); return undefined; } - channelNameCache.set(channelId, channelName); + cache.set(channelId, channelName); return channelName; } catch (error) { console.error('conversations.info failed', { @@ -45,6 +44,8 @@ export function createMessageHandler( timesAllChannelId: string, workspaceUrl: string, ) { + const channelNameCache = new Map(); + return async ({ message, client, @@ -59,7 +60,7 @@ export function createMessageHandler( return; } - const channelName = await getChannelName(client, message.channel); + const channelName = await getChannelName(client, message.channel, channelNameCache); if (!channelName) { return; } From 7793e08e854d1f5ba8edaefd02f09a9ae391f2d8 Mon Sep 17 00:00:00 2001 From: Yousei Takahashi Date: Thu, 2 Apr 2026 12:49:28 +0900 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20conversations.info=E5=A4=B1=E6=95=97?= =?UTF-8?q?=E6=99=82=E3=81=AE=E3=83=AD=E3=82=B0=E3=81=ABname/stack?= =?UTF-8?q?=E3=82=92=E5=90=AB=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit error.messageだけではエラー種別やスタックトレースが失われるため、 name, message, stackをすべて出力するようにした。 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/handlers/message.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 07f8528..0d2bec5 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -34,7 +34,10 @@ async function getChannelName( } catch (error) { console.error('conversations.info failed', { channel: channelId, - error: error instanceof Error ? error.message : error, + error: + error instanceof Error + ? { name: error.name, message: error.message, stack: error.stack } + : error, }); return undefined; } From fee0dc647d0b3f4814ce0d1f6b74cdec1b55d607 Mon Sep 17 00:00:00 2001 From: Yousei Takahashi Date: Thu, 2 Apr 2026 12:49:53 +0900 Subject: [PATCH 6/8] =?UTF-8?q?test:=20=E3=82=AD=E3=83=A3=E3=83=83?= =?UTF-8?q?=E3=82=B7=E3=83=A5=E3=81=A8=E4=BE=8B=E5=A4=96=E6=99=82=E3=81=AE?= =?UTF-8?q?=E6=8C=99=E5=8B=95=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 同一チャンネルでconversations.infoが1回だけ呼ばれること、 例外時にchat.postMessageが呼ばれないことを検証するテストを追加した。 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/handlers/__tests__/message.test.ts | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/handlers/__tests__/message.test.ts b/src/handlers/__tests__/message.test.ts index 56606c2..0ce2d63 100644 --- a/src/handlers/__tests__/message.test.ts +++ b/src/handlers/__tests__/message.test.ts @@ -102,4 +102,55 @@ describe('createMessageHandler', () => { assert.equal(args.client.conversations.info.mock.callCount(), 0); assert.equal(args.client.chat.postMessage.mock.callCount(), 0); }); + + it('同一チャンネルの2回目以降はconversations.infoを呼ばない', async () => { + const cachedHandler = createMessageHandler(timesAllChannelId, workspaceUrl); + const message = { + type: 'message', + subtype: undefined, + channel: 'C_CACHED', + channel_type: 'channel', + text: 'first', + ts: '1234567890.000001', + user: 'U12345', + }; + // biome-ignore lint/suspicious/noExplicitAny: テスト用のモック + const args1 = makeArgs(message) as any; + await cachedHandler(args1); + + const message2 = { ...message, text: 'second', ts: '1234567890.000002' }; + // biome-ignore lint/suspicious/noExplicitAny: テスト用のモック + const args2 = makeArgs(message2) as any; + await cachedHandler(args2); + + assert.equal(args1.client.conversations.info.mock.callCount(), 1); + assert.equal(args2.client.conversations.info.mock.callCount(), 0); + }); + + it('conversations.infoが例外をスローした場合は転送しない', async () => { + const message = { + type: 'message', + subtype: undefined, + channel: 'C_ERROR', + channel_type: 'channel', + text: 'hello', + ts: '1234567890.123456', + user: 'U12345', + }; + const args = { + message, + client: { + conversations: { + info: mock.fn(() => Promise.reject(new Error('platform_error'))), + }, + chat: { + postMessage: mock.fn(() => Promise.resolve()), + }, + }, + }; + // biome-ignore lint/suspicious/noExplicitAny: テスト用のモック + await createMessageHandler(timesAllChannelId, workspaceUrl)(args as any); + + assert.equal(args.client.chat.postMessage.mock.callCount(), 0); + }); }); From a738bf4bd1e72d59a1ad2001319e1db2071f32b6 Mon Sep 17 00:00:00 2001 From: Yousei Takahashi Date: Thu, 2 Apr 2026 12:51:48 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20biome=E3=83=95=E3=82=A9=E3=83=BC?= =?UTF-8?q?=E3=83=9E=E3=83=83=E3=83=88=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getChannelName呼び出しの引数を複数行に展開した。 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/handlers/message.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 0d2bec5..2e456fd 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -63,7 +63,11 @@ export function createMessageHandler( return; } - const channelName = await getChannelName(client, message.channel, channelNameCache); + const channelName = await getChannelName( + client, + message.channel, + channelNameCache, + ); if (!channelName) { return; } From 6694fd4d91ed192cd7a8bcbb4d3eab5f51531550 Mon Sep 17 00:00:00 2001 From: Yousei Takahashi Date: Thu, 2 Apr 2026 12:57:11 +0900 Subject: [PATCH 8/8] =?UTF-8?q?refactor:=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=94=E3=81=A8=E3=81=ABhandler=E3=82=92=E7=94=9F=E6=88=90?= =?UTF-8?q?=E3=81=97=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5=E3=81=AE?= =?UTF-8?q?=E7=8A=B6=E6=85=8B=E5=85=B1=E6=9C=89=E3=82=92=E9=98=B2=E3=81=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit describeスコープで共有していたhandlerを各itブロック内で createMessageHandlerから生成するように変更し、 テスト間のキャッシュ状態の依存を排除した。 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/handlers/__tests__/message.test.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/handlers/__tests__/message.test.ts b/src/handlers/__tests__/message.test.ts index 0ce2d63..cde03c1 100644 --- a/src/handlers/__tests__/message.test.ts +++ b/src/handlers/__tests__/message.test.ts @@ -5,7 +5,6 @@ import { createMessageHandler } from '../message.ts'; describe('createMessageHandler', () => { const timesAllChannelId = 'C_TIMES_ALL'; const workspaceUrl = 'https://workspace.slack.com'; - const handler = createMessageHandler(timesAllChannelId, workspaceUrl); function makeArgs( message: Record, @@ -29,6 +28,7 @@ describe('createMessageHandler', () => { } it('timesチャンネルのメッセージをtimes-allに転送する', async () => { + const handler = createMessageHandler(timesAllChannelId, workspaceUrl); const message = { type: 'message', subtype: undefined, @@ -53,6 +53,7 @@ describe('createMessageHandler', () => { }); it('times-以外のチャンネルは転送しない', async () => { + const handler = createMessageHandler(timesAllChannelId, workspaceUrl); const message = { type: 'message', subtype: undefined, @@ -70,6 +71,7 @@ describe('createMessageHandler', () => { }); it('subtypeを持つメッセージは転送しない', async () => { + const handler = createMessageHandler(timesAllChannelId, workspaceUrl); const message = { type: 'message', subtype: 'channel_join', @@ -86,6 +88,7 @@ describe('createMessageHandler', () => { }); it('DMメッセージは転送しない', async () => { + const handler = createMessageHandler(timesAllChannelId, workspaceUrl); const message = { type: 'message', subtype: undefined, @@ -104,7 +107,7 @@ describe('createMessageHandler', () => { }); it('同一チャンネルの2回目以降はconversations.infoを呼ばない', async () => { - const cachedHandler = createMessageHandler(timesAllChannelId, workspaceUrl); + const handler = createMessageHandler(timesAllChannelId, workspaceUrl); const message = { type: 'message', subtype: undefined, @@ -116,18 +119,19 @@ describe('createMessageHandler', () => { }; // biome-ignore lint/suspicious/noExplicitAny: テスト用のモック const args1 = makeArgs(message) as any; - await cachedHandler(args1); + await handler(args1); const message2 = { ...message, text: 'second', ts: '1234567890.000002' }; // biome-ignore lint/suspicious/noExplicitAny: テスト用のモック const args2 = makeArgs(message2) as any; - await cachedHandler(args2); + await handler(args2); assert.equal(args1.client.conversations.info.mock.callCount(), 1); assert.equal(args2.client.conversations.info.mock.callCount(), 0); }); it('conversations.infoが例外をスローした場合は転送しない', async () => { + const handler = createMessageHandler(timesAllChannelId, workspaceUrl); const message = { type: 'message', subtype: undefined, @@ -149,7 +153,7 @@ describe('createMessageHandler', () => { }, }; // biome-ignore lint/suspicious/noExplicitAny: テスト用のモック - await createMessageHandler(timesAllChannelId, workspaceUrl)(args as any); + await handler(args as any); assert.equal(args.client.chat.postMessage.mock.callCount(), 0); });