

























@@ -0,0 +1,265 @@
1+import { inspect } from "node:util";
2+import { formatDurationSeconds } from "openclaw/plugin-sdk/runtime-env";
3+import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
4+import { RateLimitError } from "../internal/discord.js";
5+6+const DISCORD_DEPLOY_REJECTED_ENTRY_LIMIT = 3;
7+8+export type DiscordDeployErrorLike = {
9+status?: unknown;
10+statusCode?: unknown;
11+discordCode?: unknown;
12+retryAfter?: unknown;
13+scope?: unknown;
14+rawBody?: unknown;
15+deployRequestBody?: unknown;
16+};
17+18+export type DiscordDeployRateLimitDetails = {
19+status?: number;
20+retryAfterMs?: number;
21+scope?: string;
22+discordCode?: number | string;
23+};
24+25+export function attachDiscordDeployRequestBody(err: unknown, body: unknown) {
26+if (!err || typeof err !== "object" || body === undefined) {
27+return;
28+}
29+const deployErr = err as DiscordDeployErrorLike;
30+if (deployErr.deployRequestBody === undefined) {
31+deployErr.deployRequestBody = body;
32+}
33+}
34+35+function stringifyDiscordDeployField(value: unknown): string {
36+if (typeof value === "string") {
37+return JSON.stringify(value);
38+}
39+try {
40+return JSON.stringify(value);
41+} catch {
42+return inspect(value, { depth: 2, breakLength: 120 });
43+}
44+}
45+46+function readDiscordDeployRejectedFields(value: unknown): string[] {
47+if (Array.isArray(value)) {
48+return value.filter((entry): entry is string => typeof entry === "string").slice(0, 6);
49+}
50+if (!value || typeof value !== "object") {
51+return [];
52+}
53+return Object.keys(value).slice(0, 6);
54+}
55+56+function resolveDiscordRejectedDeployEntriesSource(
57+rawBody: unknown,
58+): Record<string, unknown> | null {
59+if (!rawBody || typeof rawBody !== "object") {
60+return null;
61+}
62+const payload = rawBody as { errors?: unknown };
63+const errors = payload.errors && typeof payload.errors === "object" ? payload.errors : undefined;
64+const source = errors ?? rawBody;
65+return source && typeof source === "object" ? (source as Record<string, unknown>) : null;
66+}
67+68+function readDiscordDeployObjectField(value: unknown, field: string): unknown {
69+return value && typeof value === "object" && field in value
70+ ? (value as Record<string, unknown>)[field]
71+ : undefined;
72+}
73+74+function readFiniteNumber(value: unknown): number | undefined {
75+if (typeof value === "number" && Number.isFinite(value)) {
76+return value;
77+}
78+if (typeof value === "string" && value.trim().length > 0) {
79+const parsed = Number(value);
80+return Number.isFinite(parsed) ? parsed : undefined;
81+}
82+return undefined;
83+}
84+85+export function resolveDiscordDeployRateLimitDetails(
86+err: unknown,
87+): DiscordDeployRateLimitDetails | undefined {
88+if (!err || typeof err !== "object") {
89+return undefined;
90+}
91+const deployErr = err as DiscordDeployErrorLike;
92+const status = readFiniteNumber(deployErr.status) ?? readFiniteNumber(deployErr.statusCode);
93+const retryAfterSeconds =
94+readFiniteNumber(deployErr.retryAfter) ??
95+readFiniteNumber(readDiscordDeployObjectField(deployErr.rawBody, "retry_after"));
96+const isRateLimit =
97+err instanceof RateLimitError || status === 429 || retryAfterSeconds !== undefined;
98+if (!isRateLimit) {
99+return undefined;
100+}
101+const rawGlobal = readDiscordDeployObjectField(deployErr.rawBody, "global");
102+const scope =
103+typeof deployErr.scope === "string" && deployErr.scope.trim().length > 0
104+ ? deployErr.scope
105+ : rawGlobal === true
106+ ? "global"
107+ : rawGlobal === false
108+ ? "route"
109+ : undefined;
110+const discordCode =
111+typeof deployErr.discordCode === "number" || typeof deployErr.discordCode === "string"
112+ ? deployErr.discordCode
113+ : undefined;
114+return {
115+ status,
116+retryAfterMs:
117+retryAfterSeconds === undefined ? undefined : Math.max(0, retryAfterSeconds * 1000),
118+ scope,
119+ discordCode,
120+};
121+}
122+123+export function formatDiscordDeployRateLimitDetails(err: unknown): string {
124+const rateLimit = resolveDiscordDeployRateLimitDetails(err);
125+if (!rateLimit) {
126+return "";
127+}
128+const details: string[] = [];
129+if (typeof rateLimit.status === "number") {
130+details.push(`status=${rateLimit.status}`);
131+}
132+if (typeof rateLimit.retryAfterMs === "number") {
133+details.push(
134+`retryAfter=${formatDurationSeconds(rateLimit.retryAfterMs, {
135+ decimals: 1,
136+ })}`,
137+);
138+}
139+if (rateLimit.scope) {
140+details.push(`scope=${rateLimit.scope}`);
141+}
142+if (typeof rateLimit.discordCode === "number" || typeof rateLimit.discordCode === "string") {
143+details.push(`code=${rateLimit.discordCode}`);
144+}
145+return details.length > 0 ? ` (${details.join(", ")})` : "";
146+}
147+148+export function formatDiscordDeployRateLimitWarning(
149+err: unknown,
150+accountId: string,
151+): string | undefined {
152+const rateLimit = resolveDiscordDeployRateLimitDetails(err);
153+if (!rateLimit) {
154+return undefined;
155+}
156+const parts = [`discord: native slash command deploy rate limited for ${accountId}`];
157+if (typeof rateLimit.retryAfterMs === "number") {
158+parts.push(
159+`retry after ${formatDurationSeconds(rateLimit.retryAfterMs, {
160+ decimals: 1,
161+ })}`,
162+);
163+}
164+if (rateLimit.scope) {
165+parts.push(`scope=${rateLimit.scope}`);
166+}
167+if (typeof rateLimit.discordCode === "number" || typeof rateLimit.discordCode === "string") {
168+parts.push(`code=${rateLimit.discordCode}`);
169+}
170+return `${parts.join("; ")}. Existing slash commands stay active. Message send/receive is unaffected.`;
171+}
172+173+function formatDiscordRejectedDeployEntries(params: {
174+rawBody: unknown;
175+requestBody: unknown;
176+}): string[] {
177+const requestBody = Array.isArray(params.requestBody) ? params.requestBody : null;
178+const rejectedEntriesSource = resolveDiscordRejectedDeployEntriesSource(params.rawBody);
179+if (!rejectedEntriesSource || !requestBody || requestBody.length === 0) {
180+return [];
181+}
182+const rawEntries = Object.entries(rejectedEntriesSource).filter(([key]) => /^\d+$/.test(key));
183+return rawEntries.slice(0, DISCORD_DEPLOY_REJECTED_ENTRY_LIMIT).flatMap(([key, value]) => {
184+const index = Number.parseInt(key, 10);
185+if (!Number.isFinite(index) || index < 0 || index >= requestBody.length) {
186+return [];
187+}
188+const command = requestBody[index];
189+if (!command || typeof command !== "object") {
190+return [`#${index} fields=${readDiscordDeployRejectedFields(value).join("|") || "unknown"}`];
191+}
192+const payload = command as {
193+name?: unknown;
194+description?: unknown;
195+options?: unknown;
196+};
197+const parts = [
198+`#${index}`,
199+`fields=${readDiscordDeployRejectedFields(value).join("|") || "unknown"}`,
200+];
201+if (typeof payload.name === "string" && payload.name.trim().length > 0) {
202+parts.push(`name=${payload.name}`);
203+}
204+if (payload.description !== undefined) {
205+parts.push(`description=${stringifyDiscordDeployField(payload.description)}`);
206+}
207+if (Array.isArray(payload.options) && payload.options.length > 0) {
208+parts.push(`options=${payload.options.length}`);
209+}
210+return [parts.join(" ")];
211+});
212+}
213+214+export function formatDiscordDeployErrorDetails(err: unknown): string {
215+if (!err || typeof err !== "object") {
216+return "";
217+}
218+const rateLimitDetails = formatDiscordDeployRateLimitDetails(err);
219+if (rateLimitDetails) {
220+return rateLimitDetails;
221+}
222+const status = (err as DiscordDeployErrorLike).status;
223+const discordCode = (err as DiscordDeployErrorLike).discordCode;
224+const rawBody = (err as DiscordDeployErrorLike).rawBody;
225+const requestBody = (err as DiscordDeployErrorLike).deployRequestBody;
226+const details: string[] = [];
227+if (typeof status === "number") {
228+details.push(`status=${status}`);
229+}
230+if (typeof discordCode === "number" || typeof discordCode === "string") {
231+details.push(`code=${discordCode}`);
232+}
233+if (rawBody !== undefined) {
234+let bodyText = "";
235+try {
236+bodyText = JSON.stringify(rawBody);
237+} catch {
238+bodyText =
239+typeof rawBody === "string" ? rawBody : inspect(rawBody, { depth: 3, breakLength: 120 });
240+}
241+if (bodyText) {
242+const maxLen = 800;
243+const trimmed = bodyText.length > maxLen ? `${bodyText.slice(0, maxLen)}...` : bodyText;
244+details.push(`body=${trimmed}`);
245+}
246+}
247+const rejectedEntries = formatDiscordRejectedDeployEntries({ rawBody, requestBody });
248+if (rejectedEntries.length > 0) {
249+details.push(`rejected=${rejectedEntries.join("; ")}`);
250+}
251+return details.length > 0 ? ` (${details.join(", ")})` : "";
252+}
253+254+export function isDiscordDeployDailyCreateLimit(err: unknown): boolean {
255+if (!err || typeof err !== "object") {
256+return false;
257+}
258+const deployErr = err as DiscordDeployErrorLike;
259+const discordCode = readFiniteNumber(deployErr.discordCode);
260+const rawCode = readFiniteNumber(readDiscordDeployObjectField(deployErr.rawBody, "code"));
261+return (
262+(discordCode === 30034 || rawCode === 30034) &&
263+/daily application command creates/i.test(formatErrorMessage(err))
264+);
265+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。