












@@ -16,6 +16,7 @@ import { z } from "zod";
16161717const MIN_SEND_INTERVAL_MS = 500;
1818let lastSendTime = 0;
19+let sendQueue: Promise<void> = Promise.resolve();
19202021// --- Chat user_id resolution ---
2122// Synology Chat uses two different user_id spaces:
@@ -94,21 +95,14 @@ export async function sendMessage(
9495// The @mention is optional but user_ids is mandatory
9596const body = buildWebhookBody({ text }, userId);
969797-// Internal rate limit: min 500ms between sends
98-const now = Date.now();
99-const elapsed = now - lastSendTime;
100-if (elapsed < MIN_SEND_INTERVAL_MS) {
101-await sleep(MIN_SEND_INTERVAL_MS - elapsed);
102-}
103-10498// Retry with exponential backoff (3 attempts, 300ms base)
10599const maxRetries = 3;
106100const baseDelay = 300;
107101108102for (let attempt = 0; attempt < maxRetries; attempt++) {
109103try {
104+await waitForSendSlot();
110105const ok = await doPost(incomingUrl, body, allowInsecureSsl);
111-lastSendTime = Date.now();
112106if (ok) {
113107return true;
114108}
@@ -137,14 +131,8 @@ export async function sendFileUrl(
137131const safeFileUrl = await assertSafeWebhookFileUrl(fileUrl);
138132const body = buildWebhookBody({ file_url: safeFileUrl }, userId);
139133140-const now = Date.now();
141-const elapsed = now - lastSendTime;
142-if (elapsed < MIN_SEND_INTERVAL_MS) {
143-await sleep(MIN_SEND_INTERVAL_MS - elapsed);
144-}
145-134+await waitForSendSlot();
146135const ok = await doPost(incomingUrl, body, allowInsecureSsl);
147-lastSendTime = Date.now();
148136return ok;
149137} catch {
150138return false;
@@ -171,19 +159,27 @@ export async function fetchChatUsers(
171159}
172160173161return new Promise((resolve) => {
162+let settled = false;
163+const finish = (users: ChatUser[]) => {
164+if (settled) {
165+return;
166+}
167+settled = true;
168+resolve(users);
169+};
174170let parsedUrl: URL;
175171try {
176172parsedUrl = new URL(listUrl);
177173} catch {
178174log?.warn("fetchChatUsers: invalid user_list URL, using cached data");
179-resolve(cached?.users ?? []);
175+finish(cached?.users ?? []);
180176return;
181177}
182178const transport = parsedUrl.protocol === "https:" ? https : http;
183179const requestOptions: http.RequestOptions | https.RequestOptions =
184180parsedUrl.protocol === "https:" ? { rejectUnauthorized: !allowInsecureSsl } : {};
185181186-transport
182+const req = transport
187183.get(listUrl, requestOptions, (res) => {
188184let data = "";
189185res.on("data", (c: Buffer) => {
@@ -193,7 +189,7 @@ export async function fetchChatUsers(
193189const result = safeParseJsonWithSchema(ChatUserListResponseSchema, data);
194190if (!result) {
195191log?.warn("fetchChatUsers: failed to parse user_list response");
196-resolve(cached?.users ?? []);
192+finish(cached?.users ?? []);
197193return;
198194}
199195@@ -203,19 +199,36 @@ export async function fetchChatUsers(
203199 users,
204200cachedAt: now,
205201});
206-resolve(users);
202+finish(users);
207203return;
208204}
209205210206log?.warn(`fetchChatUsers: API returned success=${result.success}, using cached data`);
211-resolve(cached?.users ?? []);
207+finish(cached?.users ?? []);
212208});
213209})
214210.on("error", (err) => {
215211log?.warn(`fetchChatUsers: HTTP error — ${err instanceof Error ? err.message : err}`);
216-resolve(cached?.users ?? []);
212+finish(cached?.users ?? []);
217213});
214+req.setTimeout?.(15_000, () => {
215+log?.warn("fetchChatUsers: request timed out, using cached data");
216+req.destroy?.();
217+finish(cached?.users ?? []);
218+});
219+});
220+}
221+222+async function waitForSendSlot(): Promise<void> {
223+const next = sendQueue.then(async () => {
224+const elapsed = Date.now() - lastSendTime;
225+if (elapsed < MIN_SEND_INTERVAL_MS) {
226+await sleep(MIN_SEND_INTERVAL_MS - elapsed);
227+}
228+lastSendTime = Date.now();
218229});
230+sendQueue = next.catch(() => {});
231+await next;
219232}
220233221234async function assertSafeWebhookFileUrl(fileUrl: string): Promise<string> {
此內容由慣性聚合(RSS閱讀器)自動聚合整理,僅供閱讀參考。 原文來自 — 版權歸原作者所有。