

























@@ -32,6 +32,9 @@ const SLACK_UPLOAD_SSRF_POLICY = {
3232allowRfc2544BenchmarkRange: true,
3333};
3434const SLACK_DM_CHANNEL_CACHE_MAX = 1024;
35+const SLACK_DNS_RETRY_CODES = new Set(["EAI_AGAIN", "ENOTFOUND", "UND_ERR_DNS_RESOLVE_FAILED"]);
36+const SLACK_DNS_RETRY_ATTEMPTS = 2;
37+const SLACK_DNS_RETRY_BASE_DELAY_MS = 250;
3538const slackDmChannelCache = new Map<string, string>();
3639const slackSendQueues = new Map<string, Promise<void>>();
3740@@ -147,6 +150,66 @@ function enrichSlackWebApiError(err: unknown): unknown {
147150return new Error(message);
148151}
149152153+function readSlackRequestErrorCode(value: unknown): string | undefined {
154+if (!value || typeof value !== "object") {
155+return undefined;
156+}
157+const code = (value as { code?: unknown }).code;
158+return typeof code === "string" ? code.toUpperCase() : undefined;
159+}
160+161+function readSlackRequestErrorMessage(value: unknown): string {
162+if (value instanceof Error) {
163+return value.message;
164+}
165+return typeof value === "string" ? value : "";
166+}
167+168+function hasSlackDnsRequestSignal(err: unknown): boolean {
169+let current: unknown = err;
170+const seen = new Set<unknown>();
171+for (let depth = 0; current && typeof current === "object" && depth < 6; depth += 1) {
172+if (seen.has(current)) {
173+return false;
174+}
175+seen.add(current);
176+const code = readSlackRequestErrorCode(current);
177+if (code && SLACK_DNS_RETRY_CODES.has(code)) {
178+return true;
179+}
180+const message = readSlackRequestErrorMessage(current);
181+if (/\b(EAI_AGAIN|ENOTFOUND|UND_ERR_DNS_RESOLVE_FAILED)\b/i.test(message)) {
182+return true;
183+}
184+current =
185+(current as { original?: unknown; cause?: unknown }).original ??
186+(current as { cause?: unknown }).cause;
187+}
188+return false;
189+}
190+191+function delaySlackDnsRetry(attempt: number): Promise<void> {
192+return new Promise((resolve) =>
193+setTimeout(resolve, SLACK_DNS_RETRY_BASE_DELAY_MS * Math.max(1, attempt)),
194+);
195+}
196+197+async function withSlackDnsRequestRetry<T>(operation: string, fn: () => Promise<T>): Promise<T> {
198+for (let attempt = 0; ; attempt += 1) {
199+try {
200+return await fn();
201+} catch (err) {
202+if (attempt >= SLACK_DNS_RETRY_ATTEMPTS || !hasSlackDnsRequestSignal(err)) {
203+throw err;
204+}
205+logVerbose(
206+`slack send: retrying ${operation} after transient DNS request error (${attempt + 1}/${SLACK_DNS_RETRY_ATTEMPTS})`,
207+);
208+await delaySlackDnsRetry(attempt + 1);
209+}
210+}
211+}
212+150213function isSlackCustomizeScopeError(err: unknown): boolean {
151214const data = getSlackWebApiErrorData(err);
152215const code = normalizeLowercaseStringOrEmpty(normalizeSlackApiString(data?.error));
@@ -182,30 +245,37 @@ async function postSlackMessageBestEffort(params: {
182245try {
183246// Slack Web API types model icon_url and icon_emoji as mutually exclusive.
184247// Build payloads in explicit branches so TS and runtime stay aligned.
185-if (params.identity?.iconUrl) {
186-return await postChatMessage({
187- ...basePayload,
188- ...(params.identity.username ? { username: params.identity.username } : {}),
189-icon_url: params.identity.iconUrl,
190-});
248+const identity = params.identity;
249+if (identity?.iconUrl) {
250+return await withSlackDnsRequestRetry("chat.postMessage", () =>
251+postChatMessage({
252+ ...basePayload,
253+ ...(identity.username ? { username: identity.username } : {}),
254+icon_url: identity.iconUrl,
255+}),
256+);
191257}
192-if (params.identity?.iconEmoji) {
193-return await postChatMessage({
194- ...basePayload,
195- ...(params.identity.username ? { username: params.identity.username } : {}),
196-icon_emoji: params.identity.iconEmoji,
197-});
258+if (identity?.iconEmoji) {
259+return await withSlackDnsRequestRetry("chat.postMessage", () =>
260+postChatMessage({
261+ ...basePayload,
262+ ...(identity.username ? { username: identity.username } : {}),
263+icon_emoji: identity.iconEmoji,
264+}),
265+);
198266}
199-return await postChatMessage({
200- ...basePayload,
201- ...(params.identity?.username ? { username: params.identity.username } : {}),
202-});
267+return await withSlackDnsRequestRetry("chat.postMessage", () =>
268+postChatMessage({
269+ ...basePayload,
270+ ...(identity?.username ? { username: identity.username } : {}),
271+}),
272+);
203273} catch (err) {
204274if (!hasCustomIdentity(params.identity) || !isSlackCustomizeScopeError(err)) {
205275throw err;
206276}
207277logVerbose("slack send: missing chat:write.customize, retrying without custom identity");
208-return postChatMessage(basePayload);
278+return withSlackDnsRequestRetry("chat.postMessage", () => postChatMessage(basePayload));
209279}
210280}
211281@@ -338,7 +408,9 @@ async function resolveChannelId(
338408if (cachedChannelId) {
339409return { channelId: cachedChannelId, isDm: true, cacheHit: true };
340410}
341-const response = await client.conversations.open({ users: recipient.id });
411+const response = await withSlackDnsRequestRetry("conversations.open", () =>
412+client.conversations.open({ users: recipient.id }),
413+);
342414const channelId = response.channel?.id;
343415if (!channelId) {
344416throw new Error("Failed to open Slack DM channel");
@@ -382,13 +454,16 @@ async function uploadSlackFile(params: {
382454// Use the 3-step upload flow (getUploadURLExternal -> POST -> completeUploadExternal)
383455// instead of files.uploadV2 which relies on the deprecated files.upload endpoint
384456// and can fail with missing_scope even when files:write is granted.
385-const uploadUrlResp = await params.client.files.getUploadURLExternal({
386-filename: uploadFileName,
387-length: buffer.length,
388-});
457+const uploadUrlResp = await withSlackDnsRequestRetry("files.getUploadURLExternal", () =>
458+params.client.files.getUploadURLExternal({
459+filename: uploadFileName,
460+length: buffer.length,
461+}),
462+);
389463if (!uploadUrlResp.ok || !uploadUrlResp.upload_url || !uploadUrlResp.file_id) {
390464throw new Error(`Failed to get upload URL: ${uploadUrlResp.error ?? "unknown error"}`);
391465}
466+const uploadFileId = uploadUrlResp.file_id;
392467393468// Upload the file content to the presigned URL
394469const uploadBody = new Uint8Array(buffer) as BodyInit;
@@ -413,17 +488,19 @@ async function uploadSlackFile(params: {
413488}
414489415490// Complete the upload and share to channel/thread
416-const completeResp = await params.client.files.completeUploadExternal({
417-files: [{ id: uploadUrlResp.file_id, title: uploadTitle }],
418-channel_id: params.channelId,
419- ...(params.caption ? { initial_comment: params.caption } : {}),
420- ...(params.threadTs ? { thread_ts: params.threadTs } : {}),
421-});
491+const completeResp = await withSlackDnsRequestRetry("files.completeUploadExternal", () =>
492+params.client.files.completeUploadExternal({
493+files: [{ id: uploadFileId, title: uploadTitle }],
494+channel_id: params.channelId,
495+ ...(params.caption ? { initial_comment: params.caption } : {}),
496+ ...(params.threadTs ? { thread_ts: params.threadTs } : {}),
497+}),
498+);
422499if (!completeResp.ok) {
423500throw new Error(`Failed to complete upload: ${completeResp.error ?? "unknown error"}`);
424501}
425502426-return uploadUrlResp.file_id;
503+return uploadFileId;
427504}
428505429506export async function sendMessageSlack(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。