@wu529778790/open-im 1.11.8-beta.6 → 1.11.8-beta.7
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/dist/commands/handler.d.ts +1 -0
- package/dist/commands/handler.js +34 -0
- package/dist/config/types.d.ts +19 -0
- package/dist/config-web.js +7 -0
- package/dist/config.js +20 -1
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +11 -0
- package/dist/platform/handle-ai-request.js +18 -1
- package/dist/shared/ai-task.d.ts +15 -0
- package/dist/shared/ai-task.js +171 -1
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ export declare class CommandHandler {
|
|
|
23
23
|
dispatch(text: string, chatId: string, userId: string, platform: Platform, handleClaudeRequest: ClaudeRequestHandler,
|
|
24
24
|
/** 若提供,本条消息的斜杠命令回复走此 sender(须与 handleTextFlow 的 sendTextReply 一致,如带 msgId)。 */
|
|
25
25
|
senderOverride?: MessageSender): Promise<boolean>;
|
|
26
|
+
private handleAutopilotStatus;
|
|
26
27
|
private handleHelp;
|
|
27
28
|
private handlePlugins;
|
|
28
29
|
private handleSessions;
|
package/dist/commands/handler.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolvePlatformAiCommand } from '../config.js';
|
|
2
2
|
import { escapePathForMarkdown } from '../shared/utils.js';
|
|
3
|
+
import { getAutopilotPendingStatus } from '../shared/ai-task.js';
|
|
3
4
|
import { TERMINAL_ONLY_COMMANDS } from '../constants.js';
|
|
4
5
|
import { createLogger } from '../logger.js';
|
|
5
6
|
import { ClaudeSDKAdapter } from '../adapters/claude-sdk-adapter.js';
|
|
@@ -103,6 +104,8 @@ export class CommandHandler {
|
|
|
103
104
|
return this.handlePwd(chatId, userId);
|
|
104
105
|
if (t === '/status')
|
|
105
106
|
return this.handleStatus(chatId, userId, platform);
|
|
107
|
+
if (t === '/autopilot')
|
|
108
|
+
return this.handleAutopilotStatus(chatId, userId);
|
|
106
109
|
// 快捷命令 — 直接发送预设 prompt 给 AI
|
|
107
110
|
if (t === '/git commit')
|
|
108
111
|
return this.handleQuickCommand(chatId, userId, 'git commit -m "AI generated commit"', platform);
|
|
@@ -133,6 +136,36 @@ export class CommandHandler {
|
|
|
133
136
|
}
|
|
134
137
|
return runBody();
|
|
135
138
|
}
|
|
139
|
+
async handleAutopilotStatus(chatId, userId) {
|
|
140
|
+
const ap = this.deps.config.autopilot;
|
|
141
|
+
const lines = [
|
|
142
|
+
'🤖 限流自动恢复 (Autopilot)',
|
|
143
|
+
'',
|
|
144
|
+
`状态: ${ap.enabled ? '✅ 已启用' : '❌ 已禁用'}`,
|
|
145
|
+
`最大重试: ${ap.maxRetries} 次`,
|
|
146
|
+
`默认等待: ${ap.defaultIntervalHours} 小时`,
|
|
147
|
+
`短延迟: ${ap.shortRetrySeconds} 秒`,
|
|
148
|
+
`恢复提示: "${ap.autoResumePrompt}"`,
|
|
149
|
+
];
|
|
150
|
+
const pending = getAutopilotPendingStatus(userId);
|
|
151
|
+
if (pending) {
|
|
152
|
+
const remaining = Math.max(pending.retryAt.getTime() - Date.now(), 0);
|
|
153
|
+
const hours = Math.floor(remaining / 3600000);
|
|
154
|
+
const minutes = Math.floor((remaining % 3600000) / 60000);
|
|
155
|
+
lines.push('');
|
|
156
|
+
lines.push('⏳ 当前等待中:');
|
|
157
|
+
lines.push(` 类型: ${pending.type}`);
|
|
158
|
+
lines.push(` 恢复时间: ${pending.retryAt.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`);
|
|
159
|
+
lines.push(` 剩余: ${hours > 0 ? `${hours}小时` : ''}${minutes}分钟`);
|
|
160
|
+
lines.push(` 重试次数: ${pending.retryCount}/${ap.maxRetries}`);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
lines.push('');
|
|
164
|
+
lines.push('当前无等待中的限流恢复任务。');
|
|
165
|
+
}
|
|
166
|
+
await this.replySender().sendTextReply(chatId, lines.join('\n'));
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
136
169
|
async handleHelp(chatId) {
|
|
137
170
|
const help = [
|
|
138
171
|
'📋 可用命令:',
|
|
@@ -151,6 +184,7 @@ export class CommandHandler {
|
|
|
151
184
|
'/status - 显示状态',
|
|
152
185
|
'/cd <路径> - 切换工作目录',
|
|
153
186
|
'/pwd - 当前工作目录',
|
|
187
|
+
'/autopilot - 查看限流自动恢复状态',
|
|
154
188
|
'',
|
|
155
189
|
'⚡ 快捷命令:',
|
|
156
190
|
'/git commit - 提交代码',
|
package/dist/config/types.d.ts
CHANGED
|
@@ -53,6 +53,17 @@ export interface Config {
|
|
|
53
53
|
/** 可选 Bearer,与采集端一致 */
|
|
54
54
|
token?: string;
|
|
55
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* 限流自动恢复(Autopilot)。
|
|
58
|
+
* 当 AI 工具遇到限流错误时,自动等待并在恢复后重试。
|
|
59
|
+
*/
|
|
60
|
+
autopilot: {
|
|
61
|
+
enabled: boolean;
|
|
62
|
+
maxRetries: number;
|
|
63
|
+
defaultIntervalHours: number;
|
|
64
|
+
shortRetrySeconds: number;
|
|
65
|
+
autoResumePrompt: string;
|
|
66
|
+
};
|
|
56
67
|
platforms: {
|
|
57
68
|
telegram?: {
|
|
58
69
|
enabled: boolean;
|
|
@@ -176,6 +187,14 @@ export interface FileToolClaude {
|
|
|
176
187
|
proxy?: string;
|
|
177
188
|
/** Claude API 配置(优先级:环境变量 > tools.claude.env > ~/.claude/settings.json) */
|
|
178
189
|
env?: Record<string, string>;
|
|
190
|
+
/** 限流自动恢复配置 */
|
|
191
|
+
autopilot?: {
|
|
192
|
+
enabled?: boolean;
|
|
193
|
+
maxRetries?: number;
|
|
194
|
+
defaultIntervalHours?: number;
|
|
195
|
+
shortRetrySeconds?: number;
|
|
196
|
+
autoResumePrompt?: string;
|
|
197
|
+
};
|
|
179
198
|
}
|
|
180
199
|
export interface FileToolCodex {
|
|
181
200
|
cliPath?: string;
|
package/dist/config-web.js
CHANGED
|
@@ -503,6 +503,13 @@ function createProbeConfig(values) {
|
|
|
503
503
|
logDir: "",
|
|
504
504
|
logLevel: "INFO",
|
|
505
505
|
telemetry: { enabled: true },
|
|
506
|
+
autopilot: {
|
|
507
|
+
enabled: true,
|
|
508
|
+
maxRetries: 5,
|
|
509
|
+
defaultIntervalHours: 5,
|
|
510
|
+
shortRetrySeconds: 60,
|
|
511
|
+
autoResumePrompt: "继续",
|
|
512
|
+
},
|
|
506
513
|
codebuddyCliPath: "codebuddy",
|
|
507
514
|
opencodeCliPath: "opencode",
|
|
508
515
|
platforms: {},
|
package/dist/config.js
CHANGED
|
@@ -8,7 +8,7 @@ import { accessSync, constants } from 'node:fs';
|
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
9
|
import { join, isAbsolute } from 'node:path';
|
|
10
10
|
import { createLogger } from './logger.js';
|
|
11
|
-
import { APP_HOME, } from './constants.js';
|
|
11
|
+
import { APP_HOME, AUTOPILOT_ENABLED_DEFAULT, AUTOPILOT_MAX_RETRIES, AUTOPILOT_DEFAULT_INTERVAL_HOURS, AUTOPILOT_SHORT_RETRY_SECONDS, AUTOPILOT_DEFAULT_PROMPT, } from './constants.js';
|
|
12
12
|
const log = createLogger('config');
|
|
13
13
|
import { isAiCommand, AI_TOOL_BY_ID } from './adapters/tool-registry.js';
|
|
14
14
|
/**
|
|
@@ -200,6 +200,24 @@ export function loadConfig() {
|
|
|
200
200
|
const tc = file.tools?.claude ?? {};
|
|
201
201
|
const tcod = file.tools?.codex ?? {};
|
|
202
202
|
const tcb = file.tools?.codebuddy ?? {};
|
|
203
|
+
// Autopilot(限流自动恢复)配置
|
|
204
|
+
const autopilotFile = tc.autopilot ?? {};
|
|
205
|
+
const autopilot = {
|
|
206
|
+
enabled: process.env.OPEN_IM_AUTOPILOT !== 'false'
|
|
207
|
+
&& (autopilotFile.enabled ?? AUTOPILOT_ENABLED_DEFAULT),
|
|
208
|
+
maxRetries: (() => {
|
|
209
|
+
const envVal = process.env.OPEN_IM_AUTOPILOT_MAX_RETRIES;
|
|
210
|
+
if (envVal !== undefined && envVal !== '') {
|
|
211
|
+
const n = Number.parseInt(envVal, 10);
|
|
212
|
+
if (Number.isFinite(n) && n >= 0)
|
|
213
|
+
return n;
|
|
214
|
+
}
|
|
215
|
+
return autopilotFile.maxRetries ?? AUTOPILOT_MAX_RETRIES;
|
|
216
|
+
})(),
|
|
217
|
+
defaultIntervalHours: autopilotFile.defaultIntervalHours ?? AUTOPILOT_DEFAULT_INTERVAL_HOURS,
|
|
218
|
+
shortRetrySeconds: autopilotFile.shortRetrySeconds ?? AUTOPILOT_SHORT_RETRY_SECONDS,
|
|
219
|
+
autoResumePrompt: autopilotFile.autoResumePrompt ?? AUTOPILOT_DEFAULT_PROMPT,
|
|
220
|
+
};
|
|
203
221
|
const claudeProxy = process.env.CLAUDE_PROXY ?? tc.proxy;
|
|
204
222
|
const codexProxy = process.env.CODEX_PROXY ?? tcod.proxy;
|
|
205
223
|
// Windows 下若 CLI 路径仍是默认名,尝试在 npm 全局目录定位 .cmd。
|
|
@@ -532,6 +550,7 @@ export function loadConfig() {
|
|
|
532
550
|
telemetry: {
|
|
533
551
|
enabled: telemetryEnabled,
|
|
534
552
|
},
|
|
553
|
+
autopilot,
|
|
535
554
|
platforms,
|
|
536
555
|
};
|
|
537
556
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -71,5 +71,15 @@ export declare const MAX_WORKBUDDY_MESSAGE_LENGTH = 2000;
|
|
|
71
71
|
export declare const MAX_CLAWBOT_MESSAGE_LENGTH = 2000;
|
|
72
72
|
/** ClawBot 长轮询间隔 */
|
|
73
73
|
export declare const CLAWBOT_POLL_INTERVAL_MS = 3000;
|
|
74
|
+
/** Autopilot 默认开启 */
|
|
75
|
+
export declare const AUTOPILOT_ENABLED_DEFAULT = true;
|
|
76
|
+
/** 最大连续自动重试次数 */
|
|
77
|
+
export declare const AUTOPILOT_MAX_RETRIES = 5;
|
|
78
|
+
/** 无法从错误消息提取重置时间时的默认等待小时数 */
|
|
79
|
+
export declare const AUTOPILOT_DEFAULT_INTERVAL_HOURS = 5;
|
|
80
|
+
/** 529/临时限流的短延迟秒数 */
|
|
81
|
+
export declare const AUTOPILOT_SHORT_RETRY_SECONDS = 60;
|
|
82
|
+
/** 自动恢复时发送的提示词 */
|
|
83
|
+
export declare const AUTOPILOT_DEFAULT_PROMPT = "\u7EE7\u7EED";
|
|
74
84
|
export declare const DEFAULT_OPEN_IM_COAUTHOR_ADDR = "529778790@qq.com";
|
|
75
85
|
export declare const TERMINAL_ONLY_COMMANDS: Set<string>;
|
package/dist/constants.js
CHANGED
|
@@ -90,6 +90,17 @@ export const MAX_CLAWBOT_MESSAGE_LENGTH = 2000;
|
|
|
90
90
|
// ─── 轮询间隔 ───
|
|
91
91
|
/** ClawBot 长轮询间隔 */
|
|
92
92
|
export const CLAWBOT_POLL_INTERVAL_MS = 3000;
|
|
93
|
+
// ─── Autopilot(限流自动恢复) ───
|
|
94
|
+
/** Autopilot 默认开启 */
|
|
95
|
+
export const AUTOPILOT_ENABLED_DEFAULT = true;
|
|
96
|
+
/** 最大连续自动重试次数 */
|
|
97
|
+
export const AUTOPILOT_MAX_RETRIES = 5;
|
|
98
|
+
/** 无法从错误消息提取重置时间时的默认等待小时数 */
|
|
99
|
+
export const AUTOPILOT_DEFAULT_INTERVAL_HOURS = 5;
|
|
100
|
+
/** 529/临时限流的短延迟秒数 */
|
|
101
|
+
export const AUTOPILOT_SHORT_RETRY_SECONDS = 60;
|
|
102
|
+
/** 自动恢复时发送的提示词 */
|
|
103
|
+
export const AUTOPILOT_DEFAULT_PROMPT = "继续";
|
|
93
104
|
// ─── Git 共同作者 ───
|
|
94
105
|
export const DEFAULT_OPEN_IM_COAUTHOR_ADDR = "529778790@qq.com";
|
|
95
106
|
// ─── 终端独占命令 ───
|
|
@@ -133,7 +133,24 @@ export function createPlatformAIRequestHandler(deps) {
|
|
|
133
133
|
};
|
|
134
134
|
// 7. Run AI task
|
|
135
135
|
try {
|
|
136
|
-
await runAITask({
|
|
136
|
+
await runAITask({
|
|
137
|
+
config,
|
|
138
|
+
sessionManager,
|
|
139
|
+
autopilot: {
|
|
140
|
+
onAutoPilotContinue: (continuePrompt) => {
|
|
141
|
+
log.info(`[${platform}] Autopilot: re-enqueuing "${continuePrompt}" for user ${userId}`);
|
|
142
|
+
handleAIRequest({
|
|
143
|
+
userId,
|
|
144
|
+
chatId,
|
|
145
|
+
prompt: continuePrompt,
|
|
146
|
+
workDir,
|
|
147
|
+
convId,
|
|
148
|
+
}).catch((err) => {
|
|
149
|
+
log.error(`[${platform}] Autopilot re-enqueue failed:`, err);
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}, {
|
|
137
154
|
userId,
|
|
138
155
|
chatId,
|
|
139
156
|
workDir,
|
package/dist/shared/ai-task.d.ts
CHANGED
|
@@ -4,9 +4,24 @@
|
|
|
4
4
|
import type { Config } from '../config.js';
|
|
5
5
|
import type { SessionManager } from '../session/session-manager.js';
|
|
6
6
|
import type { ToolAdapter } from '../adapters/tool-adapter.interface.js';
|
|
7
|
+
/** 查询指定用户的 autopilot 等待状态(供 /autopilot 命令使用)。 */
|
|
8
|
+
export declare function getAutopilotPendingStatus(userId: string): {
|
|
9
|
+
retryAt: Date;
|
|
10
|
+
type: string;
|
|
11
|
+
retryCount: number;
|
|
12
|
+
} | undefined;
|
|
13
|
+
/** 清除指定用户的 autopilot 状态(测试用)。 */
|
|
14
|
+
export declare function clearAutopilotState(userId: string): void;
|
|
7
15
|
export interface TaskDeps {
|
|
8
16
|
config: Config;
|
|
9
17
|
sessionManager: SessionManager;
|
|
18
|
+
/**
|
|
19
|
+
* Autopilot 回调:限流恢复后调用,将 autoResumePrompt 作为新消息
|
|
20
|
+
* 通过平台的请求队列重新入队。如果不提供,autopilot 不执行恢复动作。
|
|
21
|
+
*/
|
|
22
|
+
autopilot?: {
|
|
23
|
+
onAutoPilotContinue: (prompt: string) => void;
|
|
24
|
+
};
|
|
10
25
|
}
|
|
11
26
|
export interface TaskContext {
|
|
12
27
|
userId: string;
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -6,8 +6,99 @@ import { captureError } from './sentry.js';
|
|
|
6
6
|
import { formatToolStats, formatToolCallNotification, getContextWarning, getAIToolDisplayName, toReplyPlainText, } from './utils.js';
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
8
8
|
const log = createLogger('AITask');
|
|
9
|
+
/** 每用户连续 autopilot 重试计数。成功完成或遇到非限流错误时清零。 */
|
|
10
|
+
const autopilotRetryCount = new Map();
|
|
11
|
+
/** 当前等待中的 autopilot 定时器(用于 /autopilot 状态查询)。 */
|
|
12
|
+
const pendingAutopilotTimers = new Map();
|
|
13
|
+
/** 查询指定用户的 autopilot 等待状态(供 /autopilot 命令使用)。 */
|
|
14
|
+
export function getAutopilotPendingStatus(userId) {
|
|
15
|
+
return pendingAutopilotTimers.get(userId);
|
|
16
|
+
}
|
|
17
|
+
/** 清除指定用户的 autopilot 状态(测试用)。 */
|
|
18
|
+
export function clearAutopilotState(userId) {
|
|
19
|
+
autopilotRetryCount.delete(userId);
|
|
20
|
+
pendingAutopilotTimers.delete(userId);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 判断错误是否为限流类错误(用于决定是否保留 session)。
|
|
24
|
+
* 覆盖所有已知的限流模式:429/529/session limit/overloaded/temporary throttle。
|
|
25
|
+
*/
|
|
9
26
|
function isUsageLimitError(error) {
|
|
10
|
-
return /usage
|
|
27
|
+
return /usage\s*limit/i.test(error)
|
|
28
|
+
|| /try\s+again\s+at\s+\d{1,2}:\d{2}/i.test(error)
|
|
29
|
+
|| /rate\s*limit/i.test(error)
|
|
30
|
+
|| /\b429\b/.test(error)
|
|
31
|
+
|| /\b529\b/.test(error)
|
|
32
|
+
|| /overloaded/i.test(error)
|
|
33
|
+
|| /temporarily\s+limiting/i.test(error)
|
|
34
|
+
|| /session\s+limit/i.test(error)
|
|
35
|
+
|| /you['\u2019]ve\s+hit\s+your/i.test(error);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 从错误消息中提取限流详情。仅在 isUsageLimitError() 返回 true 后调用。
|
|
39
|
+
*/
|
|
40
|
+
function classifyRateLimit(error, config) {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const shortMs = config.autopilot.shortRetrySeconds * 1000;
|
|
43
|
+
const defaultMs = config.autopilot.defaultIntervalHours * 3600 * 1000;
|
|
44
|
+
// Session quota / usage limit(最常见,等待时间最长)
|
|
45
|
+
if (/session\s*limit|usage\s*limit|opus\s*limit|you['\u2019]ve\s+hit\s+your/i.test(error)) {
|
|
46
|
+
const timeMatch = error.match(/try\s+again\s+at\s+(\d{1,2}):(\d{2})\s*(AM|PM)?/i);
|
|
47
|
+
if (timeMatch) {
|
|
48
|
+
let hours = parseInt(timeMatch[1], 10);
|
|
49
|
+
const minutes = parseInt(timeMatch[2], 10);
|
|
50
|
+
const ampm = timeMatch[3]?.toUpperCase();
|
|
51
|
+
if (ampm === 'PM' && hours < 12)
|
|
52
|
+
hours += 12;
|
|
53
|
+
if (ampm === 'AM' && hours === 12)
|
|
54
|
+
hours = 0;
|
|
55
|
+
const retryAt = new Date();
|
|
56
|
+
retryAt.setHours(hours, minutes, 0, 0);
|
|
57
|
+
if (retryAt.getTime() <= now)
|
|
58
|
+
retryAt.setDate(retryAt.getDate() + 1);
|
|
59
|
+
return { detected: true, type: 'session_limit', retryAt, isLongWait: true };
|
|
60
|
+
}
|
|
61
|
+
return { detected: true, type: 'session_limit', retryAt: new Date(now + defaultMs), isLongWait: true };
|
|
62
|
+
}
|
|
63
|
+
// 529 overloaded / temporarily limiting(短延迟)
|
|
64
|
+
if (/\b529\b|overloaded|temporarily\s+limiting/i.test(error)) {
|
|
65
|
+
return {
|
|
66
|
+
detected: true,
|
|
67
|
+
type: /overloaded/i.test(error) ? 'overloaded' : 'temporary',
|
|
68
|
+
retryAt: new Date(now + shortMs),
|
|
69
|
+
isLongWait: false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// 429 rate limit(使用默认周期)
|
|
73
|
+
if (/\b429\b|rate\s*limit/i.test(error)) {
|
|
74
|
+
return { detected: true, type: 'api_rate_limit', retryAt: new Date(now + defaultMs), isLongWait: true };
|
|
75
|
+
}
|
|
76
|
+
return { detected: false, type: null, retryAt: null, isLongWait: false };
|
|
77
|
+
}
|
|
78
|
+
function rateLimitTypeLabel(type) {
|
|
79
|
+
switch (type) {
|
|
80
|
+
case 'session_limit': return '会话额度限制';
|
|
81
|
+
case 'api_rate_limit': return 'API 速率限制';
|
|
82
|
+
case 'overloaded': return '服务器过载';
|
|
83
|
+
case 'temporary': return '临时限流';
|
|
84
|
+
default: return '限流';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function formatTimeHHMM(date) {
|
|
88
|
+
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
|
89
|
+
}
|
|
90
|
+
function formatDuration(ms) {
|
|
91
|
+
if (ms <= 0)
|
|
92
|
+
return '即将';
|
|
93
|
+
const totalSec = Math.ceil(ms / 1000);
|
|
94
|
+
const hours = Math.floor(totalSec / 3600);
|
|
95
|
+
const minutes = Math.floor((totalSec % 3600) / 60);
|
|
96
|
+
const seconds = totalSec % 60;
|
|
97
|
+
if (hours > 0)
|
|
98
|
+
return `${hours}小时${minutes > 0 ? `${minutes}分钟` : ''}`;
|
|
99
|
+
if (minutes > 0)
|
|
100
|
+
return `${minutes}分钟${seconds > 0 ? `${seconds}秒` : ''}`;
|
|
101
|
+
return `${seconds}秒`;
|
|
11
102
|
}
|
|
12
103
|
/**
|
|
13
104
|
* 将 AI/CLI 错误文本归类为一个稳定的 errorType 字符串,用于遥测聚合。
|
|
@@ -211,6 +302,8 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
211
302
|
if (settled)
|
|
212
303
|
return;
|
|
213
304
|
settled = true;
|
|
305
|
+
// 成功完成 → 清除 autopilot 重试计数
|
|
306
|
+
autopilotRetryCount.delete(ctx.userId);
|
|
214
307
|
if (pendingUpdate) {
|
|
215
308
|
clearTimeout(pendingUpdate);
|
|
216
309
|
pendingUpdate = null;
|
|
@@ -263,6 +356,83 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
263
356
|
}
|
|
264
357
|
settled = true;
|
|
265
358
|
log.error(`Task error for user ${ctx.userId}: ${error}`);
|
|
359
|
+
// ── Autopilot 拦截 ──
|
|
360
|
+
const apConfig = config.autopilot;
|
|
361
|
+
const currentRetries = autopilotRetryCount.get(ctx.userId) ?? 0;
|
|
362
|
+
if (apConfig?.enabled &&
|
|
363
|
+
deps.autopilot &&
|
|
364
|
+
isUsageLimitError(error) &&
|
|
365
|
+
currentRetries < apConfig.maxRetries) {
|
|
366
|
+
const rateInfo = classifyRateLimit(error, config);
|
|
367
|
+
if (rateInfo.detected && rateInfo.retryAt) {
|
|
368
|
+
const retryCount = currentRetries + 1;
|
|
369
|
+
autopilotRetryCount.set(ctx.userId, retryCount);
|
|
370
|
+
// 记录等待状态
|
|
371
|
+
pendingAutopilotTimers.set(ctx.userId, {
|
|
372
|
+
retryAt: rateInfo.retryAt,
|
|
373
|
+
type: rateInfo.type,
|
|
374
|
+
retryCount,
|
|
375
|
+
});
|
|
376
|
+
// 通知用户
|
|
377
|
+
const typeLabel = rateLimitTypeLabel(rateInfo.type);
|
|
378
|
+
const timeStr = formatTimeHHMM(rateInfo.retryAt);
|
|
379
|
+
const remaining = formatDuration(rateInfo.retryAt.getTime() - Date.now());
|
|
380
|
+
const statusMsg = `⏳ 检测到${typeLabel},将在 ${timeStr}(${remaining}后)自动恢复 (${retryCount}/${apConfig.maxRetries})`;
|
|
381
|
+
log.info(`[Autopilot] ${statusMsg} for user ${ctx.userId}`);
|
|
382
|
+
try {
|
|
383
|
+
await platformAdapter.sendError(hadSessionInvalid
|
|
384
|
+
? '当前 Claude 会话已失效,已自动执行 /new 重置会话,请重新发送刚才的问题。'
|
|
385
|
+
: `${error}\n\n${statusMsg}`);
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
log.error('Failed to send autopilot status:', err);
|
|
389
|
+
}
|
|
390
|
+
cleanup();
|
|
391
|
+
resolve();
|
|
392
|
+
// 异步等待 → 恢复(不阻塞 Promise 解决)
|
|
393
|
+
const retryAt = rateInfo.retryAt;
|
|
394
|
+
const delayMs = Math.max(retryAt.getTime() - Date.now(), 0);
|
|
395
|
+
log.info(`[Autopilot] Scheduling retry in ${delayMs}ms for user ${ctx.userId}`);
|
|
396
|
+
// 分段定时器(setTimeout 最大 ~24.8 天)
|
|
397
|
+
const MAX_SEGMENT = 2_147_483_647; // 2^31 - 1
|
|
398
|
+
const scheduleRetry = (remaining) => {
|
|
399
|
+
if (remaining <= 0) {
|
|
400
|
+
// 定时器到期 → 清除状态 → 发送恢复消息
|
|
401
|
+
pendingAutopilotTimers.delete(ctx.userId);
|
|
402
|
+
log.info(`[Autopilot] Timer fired for user ${ctx.userId}, sending "${apConfig.autoResumePrompt}"`);
|
|
403
|
+
try {
|
|
404
|
+
platformAdapter.streamUpdate('', `🔄 限额已恢复,正在自动重试... (${retryCount})`);
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
/* ignore */
|
|
408
|
+
}
|
|
409
|
+
deps.autopilot.onAutoPilotContinue(apConfig.autoResumePrompt);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const segment = Math.min(remaining, MAX_SEGMENT);
|
|
413
|
+
const timer = setTimeout(() => scheduleRetry(remaining - segment), segment);
|
|
414
|
+
if (typeof timer === 'object' && 'unref' in timer)
|
|
415
|
+
timer.unref();
|
|
416
|
+
// 如果任务被 abort(/new、cancelUser),取消待执行的定时器
|
|
417
|
+
if (ctx.signal) {
|
|
418
|
+
if (ctx.signal.aborted) {
|
|
419
|
+
clearTimeout(timer);
|
|
420
|
+
pendingAutopilotTimers.delete(ctx.userId);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
ctx.signal.addEventListener('abort', () => {
|
|
424
|
+
clearTimeout(timer);
|
|
425
|
+
pendingAutopilotTimers.delete(ctx.userId);
|
|
426
|
+
log.info(`[Autopilot] Timer cancelled by abort for user ${ctx.userId}`);
|
|
427
|
+
}, { once: true });
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
scheduleRetry(delayMs);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// ── 非限流错误或 autopilot 未启用/耗尽重试 ──
|
|
435
|
+
autopilotRetryCount.delete(ctx.userId);
|
|
266
436
|
if (isUsageLimitError(error)) {
|
|
267
437
|
// Usage limit errors: keep session for all tools (user can retry later)
|
|
268
438
|
log.warn(`Keeping ${aiCommand} session for user ${ctx.userId} after usage limit error`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wu529778790/open-im",
|
|
3
|
-
"version": "1.11.8-beta.
|
|
3
|
+
"version": "1.11.8-beta.7",
|
|
4
4
|
"description": "Your AI coding assistant, in every chat app. Multi-platform IM bridge for Claude Code, Codex, and CodeBuddy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|