mioku-plugin-chat 2.9.11 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/configs/base.ts CHANGED
@@ -4,4 +4,4 @@ export const BASE_CONFIG = {
4
4
  historyCount: 100,
5
5
  maxIterations: 20,
6
6
  enableMediaRecognition: true,
7
- };
7
+ };
package/core/chat-turn.ts CHANGED
@@ -351,7 +351,7 @@ export async function processChat(
351
351
  }
352
352
  : undefined,
353
353
  ),
354
- { userId, groupId, label: isGroup ? "group-chat" : "private-chat" },
354
+ { userId, groupId, label: isGroup ? "group-chat" : "private-chat", skipRetryOnRateLimit: true },
355
355
  );
356
356
 
357
357
  if (!result) {
@@ -104,7 +104,7 @@ export async function handleIdleCheckDebug(
104
104
  pluginCtx.humanize,
105
105
  pluginCtx.skillManager,
106
106
  ),
107
- { groupId: targetGroupId, label: "idle-check" },
107
+ { groupId: targetGroupId, label: "idle-check", skipRetryOnRateLimit: true },
108
108
  );
109
109
  if (!result) {
110
110
  await e.reply(`[空闲检测] 群 ${targetGroupId} 因限流被跳过`);
@@ -98,6 +98,7 @@ export function createMessageHandler(
98
98
  userId,
99
99
  groupId,
100
100
  label: "history-media",
101
+ skipRetryOnRateLimit: true,
101
102
  }),
102
103
  )
103
104
  : undefined;
@@ -114,6 +115,7 @@ export function createMessageHandler(
114
115
  userId,
115
116
  groupId,
116
117
  label: "image-analysis",
118
+ skipRetryOnRateLimit: true,
117
119
  }),
118
120
  }).catch((err) =>
119
121
  ctx.logger.error(`[image-analyzer] Failed: ${err}`),
package/handlers/poke.ts CHANGED
@@ -120,7 +120,7 @@ export function createPokeHandler(
120
120
  ],
121
121
  },
122
122
  ),
123
- { userId, groupId, label: "poke" },
123
+ { userId, groupId, label: "poke", skipRetryOnRateLimit: true },
124
124
  );
125
125
  if (!result) return;
126
126
 
package/index.ts CHANGED
@@ -342,7 +342,7 @@ export default definePlugin({
342
342
  return;
343
343
  }
344
344
 
345
- roleModels = resolved.models;
345
+ roleModels = resolved.models;
346
346
  roleIsMultimodal = resolved.isMultimodal;
347
347
  await refreshBaseCache();
348
348
 
@@ -277,7 +277,7 @@ export class CooldownManager {
277
277
  ),
278
278
  },
279
279
  ),
280
- { userId: targetMessage.userId, groupId, label: "cooldown" },
280
+ { userId: targetMessage.userId, groupId, label: "cooldown", skipRetryOnRateLimit: true },
281
281
  );
282
282
  if (!result) return;
283
283
 
@@ -414,7 +414,7 @@ export class CooldownManager {
414
414
  ),
415
415
  },
416
416
  ),
417
- { userId: targetMessage.userId, groupId, label: "cooldown-planner" },
417
+ { userId: targetMessage.userId, groupId, label: "cooldown-planner", skipRetryOnRateLimit: true },
418
418
  );
419
419
  if (!result) {
420
420
  this.ctx.logger.warn(
@@ -277,7 +277,7 @@ export class IdleCheckManager {
277
277
  this.humanize,
278
278
  this.skillManager,
279
279
  ),
280
- { groupId, label: "idle-check" },
280
+ { groupId, label: "idle-check", skipRetryOnRateLimit: true },
281
281
  );
282
282
  if (!result) {
283
283
  this.groupMessageCount.set(groupSessionId, 0);
@@ -199,7 +199,7 @@ export class QueueProcessor {
199
199
  }, this.humanize, this.skillManager, {
200
200
  manager: this.groupStructuredHistory, ttlMs: cfg.groupStructuredHistoryTtlMs, currentUserInputs: structuredUserInputs,
201
201
  }),
202
- { userId: targetMessage.userId, groupId, label: "dynamic-delay" },
202
+ { userId: targetMessage.userId, groupId, label: "dynamic-delay", skipRetryOnRateLimit: true },
203
203
  );
204
204
  if (!result) return;
205
205
 
@@ -280,7 +280,7 @@ export class QueueProcessor {
280
280
  }, this.humanize, this.skillManager, {
281
281
  manager: this.groupStructuredHistory, ttlMs: cfg.groupStructuredHistoryTtlMs, currentUserInputs: structuredUserInputs,
282
282
  }),
283
- { userId: targetMessage.userId, groupId, label: "queue" },
283
+ { userId: targetMessage.userId, groupId, label: "queue", skipRetryOnRateLimit: true },
284
284
  );
285
285
  if (!result) return;
286
286
 
@@ -1,12 +1,38 @@
1
1
  import type { RateLimiter } from "./rate-limiter";
2
2
 
3
- const RATE_LIMIT_RETRY_DELAY_MS = 5_000;
4
- const RATE_LIMIT_MAX_RETRIES = 2;
3
+ const RATE_LIMIT_RETRY_DELAYS_MS = [5_000, 10_000, 30_000, 60_000];
5
4
 
6
5
  type Logger = { warn: (...args: unknown[]) => void };
7
6
 
8
- // Wraps an AI request with global rate-limit blocking + per-user/group RPM
9
- // limiting. On a 429 it backs off and retries up to RATE_LIMIT_MAX_RETRIES.
7
+ function isRateLimitError(err: unknown): boolean {
8
+ if (err === null || err === undefined) return false;
9
+ const s = String(err).toLowerCase();
10
+ return s.includes("429") || s.includes("rate limit") || s.includes("rate_limit");
11
+ }
12
+
13
+ async function retryOn429<T>(
14
+ fn: () => Promise<T>,
15
+ log: Logger,
16
+ label?: string,
17
+ ): Promise<T> {
18
+ let attempt = 0;
19
+ while (true) {
20
+ try {
21
+ return await fn();
22
+ } catch (err) {
23
+ if (!isRateLimitError(err)) throw err;
24
+ if (attempt >= RATE_LIMIT_RETRY_DELAYS_MS.length) throw err;
25
+ const delay = RATE_LIMIT_RETRY_DELAYS_MS[attempt];
26
+ const attemptLabel = `${attempt + 1}/${RATE_LIMIT_RETRY_DELAYS_MS.length}`;
27
+ log.warn(
28
+ `[Chat]${label ? ` ${label}` : ""} 命中 429,等待 ${delay / 1000}s 后第 ${attemptLabel} 次重试: ${err}`,
29
+ );
30
+ attempt += 1;
31
+ await new Promise((resolve) => setTimeout(resolve, delay));
32
+ }
33
+ }
34
+ }
35
+
10
36
  export class RateLimitGuard {
11
37
  private blockedUntil = 0;
12
38
 
@@ -20,17 +46,17 @@ export class RateLimitGuard {
20
46
  }
21
47
 
22
48
  private markBlocked(): void {
23
- this.blockedUntil = Date.now() + RATE_LIMIT_RETRY_DELAY_MS;
24
- }
25
-
26
- private isRateLimitError(err: unknown): boolean {
27
- const s = String(err).toLowerCase();
28
- return s.includes("429") || s.includes("rate limit");
49
+ this.blockedUntil = Date.now() + RATE_LIMIT_RETRY_DELAYS_MS[0];
29
50
  }
30
51
 
31
52
  async run<T>(
32
53
  request: () => Promise<T>,
33
- opts?: { userId?: number; groupId?: number; label?: string },
54
+ opts?: {
55
+ userId?: number;
56
+ groupId?: number;
57
+ label?: string;
58
+ skipRetryOnRateLimit?: boolean;
59
+ },
34
60
  ): Promise<T | null> {
35
61
  if (this.isBlocked()) {
36
62
  this.log.warn(
@@ -46,25 +72,15 @@ export class RateLimitGuard {
46
72
  }
47
73
  this.rateLimiter.recordAIRequest(opts?.userId, opts?.groupId);
48
74
 
49
- let retries = 0;
50
- while (true) {
51
- try {
52
- const result = await request();
53
- this.blockedUntil = 0;
54
- return result;
55
- } catch (err) {
56
- if (!this.isRateLimitError(err)) throw err;
57
- this.markBlocked();
58
- if (retries >= RATE_LIMIT_MAX_RETRIES) throw err;
59
- retries += 1;
60
- this.log.warn(
61
- `[Chat] Rate limit hit, waiting ${RATE_LIMIT_RETRY_DELAY_MS / 1000}s...`,
62
- );
63
- await new Promise((resolve) =>
64
- setTimeout(resolve, RATE_LIMIT_RETRY_DELAY_MS),
65
- );
66
- if (this.isBlocked()) this.blockedUntil = 0;
67
- }
75
+ if (opts?.skipRetryOnRateLimit) {
76
+ return await request();
77
+ }
78
+
79
+ try {
80
+ return await retryOn429(request, this.log, opts?.label);
81
+ } catch (err) {
82
+ this.markBlocked();
83
+ throw err;
68
84
  }
69
85
  }
70
- }
86
+ }
package/manage/types.ts CHANGED
@@ -21,7 +21,12 @@ export type HistoryMediaOptions = {
21
21
 
22
22
  export type RunRateLimitGuard = <T>(
23
23
  request: () => Promise<T>,
24
- opts?: { userId?: number; groupId?: number; label?: string },
24
+ opts?: {
25
+ userId?: number;
26
+ groupId?: number;
27
+ label?: string;
28
+ skipRetryOnRateLimit?: boolean;
29
+ },
25
30
  ) => Promise<T | null>;
26
31
 
27
32
  export type GetGroupHistoryMessages = (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-chat",
3
- "version": "2.9.11",
3
+ "version": "2.10.0",
4
4
  "description": "AI 智能聊天插件",
5
5
  "main": "index.ts",
6
6
  "type": "module",