feat(discord): make gateway optional with REST send path and send logs

Gateway is now an opt-in feature (DISCORD_GATEWAY_ENABLED); when disabled,
messages are delivered via the Discord REST API (discord-rest.ts) instead of
the Durable Object. Record per-route delivery results (send-log.ts) for the
WebUI. Configure assets binding, DO sqlite migration and cron trigger.
This commit is contained in:
RhenCloud 2026-08-02 06:50:09 +08:00
parent 4d5c90773d
commit cfdf37e273
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
4 changed files with 184 additions and 56 deletions

49
src/discord-rest.ts Normal file
View file

@ -0,0 +1,49 @@
import { log } from "./log";
const DISCORD_API = "https://discord.com/api/v10";
export async function sendMessage(
token: string,
channelId: string,
message: unknown,
threadId?: string,
): Promise<{ ok: boolean; error?: string }> {
const url = threadId
? `${DISCORD_API}/channels/${threadId}/messages`
: `${DISCORD_API}/channels/${channelId}/messages`;
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bot ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(message),
});
if (res.status === 429) {
const rateLimit = (await res.json()) as { retry_after?: number };
const retryAfter = (rateLimit.retry_after ?? 1) * 1000;
log.warn({ retryAfter, attempt }, "Rate limited");
await new Promise((r) => setTimeout(r, retryAfter));
continue;
}
if (!res.ok) {
const err = await res.text();
log.error({ status: res.status, err, channelId }, "Discord API error");
return { ok: false, error: err };
}
return { ok: true };
} catch (err) {
log.error({ err, attempt, channelId }, "Failed to send message");
if (attempt === 2) return { ok: false, error: String(err) };
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
}
}
return { ok: false, error: "Max retries exceeded" };
}