evolclaw 2.2.0 → 2.3.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/README.md +49 -27
- package/data/evolclaw.sample.json +6 -3
- package/dist/agents/claude-runner.js +125 -52
- package/dist/agents/codex-runner.js +10 -5
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +247 -84
- package/dist/channels/feishu.js +556 -96
- package/dist/channels/wechat.js +98 -74
- package/dist/cli.js +132 -50
- package/dist/config.js +185 -31
- package/dist/core/channel-loader.js +11 -4
- package/dist/core/command-handler.js +750 -209
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/{message-processor.js → message/message-processor.js} +386 -105
- package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
- package/dist/{utils → core/message}/stream-debouncer.js +1 -1
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/core/permission.js +212 -11
- package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
- package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/{utils → core/session}/session-file-health.js +1 -1
- package/dist/core/{session-manager.js → session/session-manager.js} +57 -11
- package/dist/index.js +138 -54
- package/dist/{core/ipc-server.js → ipc.js} +36 -1
- package/dist/types.js +3 -0
- package/dist/utils/cross-platform.js +38 -1
- package/dist/utils/error-utils.js +130 -5
- package/dist/utils/init-channel.js +649 -0
- package/dist/utils/init.js +55 -150
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/{core → utils}/stats-collector.js +16 -0
- package/package.json +3 -3
- package/dist/core/message-bridge.js +0 -187
- package/dist/utils/init-feishu.js +0 -263
- package/dist/utils/init-wechat.js +0 -172
- package/dist/utils/ipc-client.js +0 -36
- package/dist/utils/permission-utils.js +0 -71
- /package/dist/{utils → core/message}/message-cache.js +0 -0
- /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
- /package/dist/core/{session-file-adapter.js → session/session-file-adapter.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import net from 'net';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
import { logger } from '
|
|
3
|
+
import { logger } from './utils/logger.js';
|
|
4
4
|
export class IpcServer {
|
|
5
5
|
socketPath;
|
|
6
6
|
getStatus;
|
|
@@ -69,3 +69,38 @@ export class IpcServer {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Query the running EvolClaw daemon via Unix socket.
|
|
74
|
+
* Returns null if the service is not running or the socket is unreachable.
|
|
75
|
+
*/
|
|
76
|
+
export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const conn = net.connect(socketPath);
|
|
79
|
+
let buf = '';
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
conn.destroy();
|
|
82
|
+
resolve(null);
|
|
83
|
+
}, timeoutMs);
|
|
84
|
+
conn.on('connect', () => {
|
|
85
|
+
conn.write(JSON.stringify(cmd) + '\n');
|
|
86
|
+
});
|
|
87
|
+
conn.on('data', (data) => {
|
|
88
|
+
buf += data.toString();
|
|
89
|
+
const idx = buf.indexOf('\n');
|
|
90
|
+
if (idx !== -1) {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
try {
|
|
93
|
+
resolve(JSON.parse(buf.slice(0, idx)));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
resolve(null);
|
|
97
|
+
}
|
|
98
|
+
conn.destroy();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
conn.on('error', () => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
resolve(null);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -78,15 +78,52 @@ export function getProcessInfo(pid) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
else {
|
|
81
|
-
const
|
|
81
|
+
const etimes = execFileSync('ps', ['-p', String(pid), '-o', 'etimes='], { encoding: 'utf-8' }).trim();
|
|
82
82
|
const cpu = execFileSync('ps', ['-p', String(pid), '-o', '%cpu='], { encoding: 'utf-8' }).trim();
|
|
83
83
|
const mem = execFileSync('ps', ['-p', String(pid), '-o', 'rss='], { encoding: 'utf-8' }).trim();
|
|
84
|
+
const uptime = formatUptime(parseInt(etimes, 10));
|
|
84
85
|
return { uptime, cpu, memory: mem };
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
catch { }
|
|
88
89
|
return {};
|
|
89
90
|
}
|
|
91
|
+
function formatUptime(totalSeconds) {
|
|
92
|
+
if (isNaN(totalSeconds) || totalSeconds < 0)
|
|
93
|
+
return 'unknown';
|
|
94
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
95
|
+
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
|
96
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
97
|
+
const seconds = totalSeconds % 60;
|
|
98
|
+
const parts = [];
|
|
99
|
+
if (days > 0)
|
|
100
|
+
parts.push(`${days}d`);
|
|
101
|
+
if (hours > 0)
|
|
102
|
+
parts.push(`${hours}h`);
|
|
103
|
+
if (minutes > 0)
|
|
104
|
+
parts.push(`${minutes}m`);
|
|
105
|
+
if (parts.length === 0)
|
|
106
|
+
parts.push(`${seconds}s`);
|
|
107
|
+
return parts.join(' ');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Read a specific environment variable from a running process.
|
|
111
|
+
* Returns undefined if the process doesn't exist or the variable is not set.
|
|
112
|
+
* Linux: reads /proc/<pid>/environ; Windows: not supported (returns undefined).
|
|
113
|
+
*/
|
|
114
|
+
export function getProcessEnv(pid, varName) {
|
|
115
|
+
if (isWindows)
|
|
116
|
+
return undefined;
|
|
117
|
+
try {
|
|
118
|
+
const environ = fs.readFileSync(`/proc/${pid}/environ`, 'utf-8');
|
|
119
|
+
const prefix = `${varName}=`;
|
|
120
|
+
const entry = environ.split('\0').find(e => e.startsWith(prefix));
|
|
121
|
+
return entry ? entry.slice(prefix.length) : undefined;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
90
127
|
/**
|
|
91
128
|
* Cross-platform command existence check.
|
|
92
129
|
*/
|
|
@@ -2,11 +2,66 @@ export var ErrorType;
|
|
|
2
2
|
(function (ErrorType) {
|
|
3
3
|
ErrorType["SDK_TIMEOUT"] = "sdk_timeout";
|
|
4
4
|
ErrorType["API_ERROR"] = "api_error";
|
|
5
|
+
ErrorType["AUTH_ERROR"] = "auth_error";
|
|
5
6
|
ErrorType["FILE_CORRUPT"] = "file_corrupt";
|
|
6
7
|
ErrorType["STREAM_ERROR"] = "stream_error";
|
|
7
8
|
ErrorType["CONTEXT_TOO_LONG"] = "context_too_long";
|
|
8
9
|
ErrorType["UNKNOWN"] = "unknown";
|
|
9
10
|
})(ErrorType || (ErrorType = {}));
|
|
11
|
+
/**
|
|
12
|
+
* 错误来源前缀 — 区分基础设施异常 vs Agent 任务失败
|
|
13
|
+
*
|
|
14
|
+
* infra: 基础设施级(SDK 崩溃、API 不可用、文件损坏)— 应累计安全模式
|
|
15
|
+
* agent: Agent 任务级(权限拒绝、max turns、工具失败)— 仅统计,不累计安全模式
|
|
16
|
+
*/
|
|
17
|
+
export const ERROR_PREFIX = {
|
|
18
|
+
INFRA: 'infra',
|
|
19
|
+
AGENT: 'agent',
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* 判断 Agent complete.subtype 是否属于系统级故障(应累计安全模式)
|
|
23
|
+
*
|
|
24
|
+
* 非系统级(用户操作或任务边界):
|
|
25
|
+
* - end_turn / max_turns: Agent 正常结束或达到轮次上限
|
|
26
|
+
* - permission_denied: 用户主动拒绝权限
|
|
27
|
+
* - stop: 用户主动停止
|
|
28
|
+
*
|
|
29
|
+
* 系统级(SDK/模型/平台故障):
|
|
30
|
+
* - error_model / error_tool_use / error_api 等:基础设施异常
|
|
31
|
+
* - 未知 subtype:保守地视为系统级
|
|
32
|
+
*
|
|
33
|
+
* terminalReason 提供更精确的判断(SDK 0.2.100+):
|
|
34
|
+
* - rapid_refill_breaker: API 限流,不是代码问题
|
|
35
|
+
* - tool_deferred: 工具延迟,不是错误
|
|
36
|
+
* - stop_hook_prevented: Stop hook 阻止,不是错误
|
|
37
|
+
* - aborted_streaming / aborted_tools: 中断,已有中断处理逻辑
|
|
38
|
+
*/
|
|
39
|
+
const NON_INFRA_SUBTYPES = new Set([
|
|
40
|
+
'end_turn',
|
|
41
|
+
'max_turns',
|
|
42
|
+
'permission_denied',
|
|
43
|
+
'stop',
|
|
44
|
+
]);
|
|
45
|
+
const NON_INFRA_TERMINAL_REASONS = new Set([
|
|
46
|
+
'rapid_refill_breaker',
|
|
47
|
+
'tool_deferred',
|
|
48
|
+
'stop_hook_prevented',
|
|
49
|
+
'aborted_streaming',
|
|
50
|
+
'aborted_tools',
|
|
51
|
+
]);
|
|
52
|
+
export function isInfraError(subtype, terminalReason) {
|
|
53
|
+
// terminalReason 优先级更高(更精确)
|
|
54
|
+
if (terminalReason && NON_INFRA_TERMINAL_REASONS.has(terminalReason)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (!subtype)
|
|
58
|
+
return true; // 未知 subtype,保守视为系统级
|
|
59
|
+
return !NON_INFRA_SUBTYPES.has(subtype);
|
|
60
|
+
}
|
|
61
|
+
/** 为 errorType 添加来源前缀 */
|
|
62
|
+
export function prefixErrorType(prefix, errorType) {
|
|
63
|
+
return `${prefix}:${errorType}`;
|
|
64
|
+
}
|
|
10
65
|
export function classifyError(error) {
|
|
11
66
|
const msg = (error?.message || '').toLowerCase();
|
|
12
67
|
if (msg.includes('上下文过长') || msg.includes('context too long')
|
|
@@ -14,12 +69,21 @@ export function classifyError(error) {
|
|
|
14
69
|
|| msg.includes('prompt is too long') || msg.includes('context limit')) {
|
|
15
70
|
return ErrorType.CONTEXT_TOO_LONG;
|
|
16
71
|
}
|
|
72
|
+
// 认证错误(401 / Invalid API Key / key_not_found)— 不可恢复,不应触发安全模式
|
|
73
|
+
if (msg.includes('401') || msg.includes('invalid api key') || msg.includes('key_not_found')
|
|
74
|
+
|| msg.includes('authentication_error') || msg.includes('failed to authenticate')) {
|
|
75
|
+
return ErrorType.AUTH_ERROR;
|
|
76
|
+
}
|
|
17
77
|
if (msg.includes('timeout') || msg.includes('etimedout')) {
|
|
18
78
|
return ErrorType.SDK_TIMEOUT;
|
|
19
79
|
}
|
|
20
80
|
if (msg.includes('5') && (msg.includes('00') || msg.includes('02') || msg.includes('03') || msg.includes('04'))) {
|
|
21
81
|
return ErrorType.API_ERROR;
|
|
22
82
|
}
|
|
83
|
+
// "X is not valid JSON" — API 返回了非 JSON 响应(如算力池切换提示),属于 API 错误
|
|
84
|
+
if (msg.includes('is not valid json')) {
|
|
85
|
+
return ErrorType.API_ERROR;
|
|
86
|
+
}
|
|
23
87
|
if (msg.includes('enoent') || msg.includes('corrupt') || msg.includes('invalid json')) {
|
|
24
88
|
return ErrorType.FILE_CORRUPT;
|
|
25
89
|
}
|
|
@@ -28,7 +92,61 @@ export function classifyError(error) {
|
|
|
28
92
|
}
|
|
29
93
|
return ErrorType.UNKNOWN;
|
|
30
94
|
}
|
|
31
|
-
|
|
95
|
+
/**
|
|
96
|
+
* 判断错误是否可重试(暂时性 API 错误)
|
|
97
|
+
* 403 算力池切换、429 限流、5xx 服务端错误
|
|
98
|
+
* 注意:401 认证错误不可重试(API Key 无效不会因重试恢复)
|
|
99
|
+
*/
|
|
100
|
+
export function isRetryableError(error) {
|
|
101
|
+
const msg = error?.message || String(error);
|
|
102
|
+
const lower = msg.toLowerCase();
|
|
103
|
+
// 认证错误不可重试:重试不会恢复无效/缺失凭据
|
|
104
|
+
if (lower.includes('401')
|
|
105
|
+
|| lower.includes('invalid api key')
|
|
106
|
+
|| lower.includes('key_not_found')
|
|
107
|
+
|| lower.includes('authentication_error')
|
|
108
|
+
|| lower.includes('failed to authenticate')
|
|
109
|
+
|| (lower.includes('api error: 403') && (lower.includes('auth') || lower.includes('key') || lower.includes('token')))) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (msg.includes('API Error: 403'))
|
|
113
|
+
return true;
|
|
114
|
+
if (msg.includes('API Error: 429'))
|
|
115
|
+
return true;
|
|
116
|
+
if (msg.includes('API Error: 500'))
|
|
117
|
+
return true;
|
|
118
|
+
if (msg.includes('API Error: 502'))
|
|
119
|
+
return true;
|
|
120
|
+
if (msg.includes('API Error: 503'))
|
|
121
|
+
return true;
|
|
122
|
+
if (msg.includes('API Error: 504'))
|
|
123
|
+
return true;
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
export function getErrorMessage(error, terminalReason) {
|
|
127
|
+
// terminalReason 提供更精确的错误提示(SDK 0.2.100+)
|
|
128
|
+
if (terminalReason) {
|
|
129
|
+
switch (terminalReason) {
|
|
130
|
+
case 'max_turns':
|
|
131
|
+
return '❌ 任务达到最大轮次限制,请简化需求或分步执行';
|
|
132
|
+
case 'prompt_too_long':
|
|
133
|
+
return '⚠️ 输入过长,请精简提问或使用 /compact 压缩上下文';
|
|
134
|
+
case 'rapid_refill_breaker':
|
|
135
|
+
return '⚠️ API 限流中,请稍后重试';
|
|
136
|
+
case 'context_compact_failed':
|
|
137
|
+
return '⚠️ 上下文过长,自动压缩失败,请手动输入 /compact 重试';
|
|
138
|
+
case 'model_error':
|
|
139
|
+
return '❌ 模型服务异常,请稍后重试';
|
|
140
|
+
case 'tool_error':
|
|
141
|
+
return '❌ 工具执行失败,请检查操作或重试';
|
|
142
|
+
case 'permission_denied':
|
|
143
|
+
return '❌ 权限被拒绝,操作已取消';
|
|
144
|
+
case 'aborted_streaming':
|
|
145
|
+
case 'aborted_tools':
|
|
146
|
+
return '❌ 任务已中断';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// 回退到原有的错误消息匹配逻辑
|
|
32
150
|
const msg = error?.message || String(error);
|
|
33
151
|
if (msg.includes('CONTEXT_COMPACT_FAILED')) {
|
|
34
152
|
return '⚠️ 上下文过长,自动压缩失败,请手动输入 /compact 重试';
|
|
@@ -38,10 +156,17 @@ export function getErrorMessage(error) {
|
|
|
38
156
|
return '⚠️ 上下文过长,自动压缩重试失败,请手动输入 /compact 重试';
|
|
39
157
|
}
|
|
40
158
|
if (msg.includes('API Error: 400')) {
|
|
41
|
-
return '
|
|
159
|
+
return '❌ 请求格式错误,请检查输入内容';
|
|
160
|
+
}
|
|
161
|
+
if (msg.includes('401') || msg.includes('Invalid API key') || msg.includes('key_not_found')
|
|
162
|
+
|| msg.includes('authentication_error')) {
|
|
163
|
+
return '❌ API Key 无效,请检查密钥配置。使用 /status 查看当前配置';
|
|
42
164
|
}
|
|
43
165
|
if (msg.includes('API Error: 500')) {
|
|
44
|
-
return '
|
|
166
|
+
return '❌ API 服务暂时不可用,请稍后重试';
|
|
167
|
+
}
|
|
168
|
+
if (msg.includes('API Error: 403')) {
|
|
169
|
+
return '❌ API 认证失败,请检查密钥配置或稍后重试';
|
|
45
170
|
}
|
|
46
171
|
if (msg.includes('API Error: 429')) {
|
|
47
172
|
return '⚠️ 请求过于频繁,请稍后再试';
|
|
@@ -50,7 +175,7 @@ export function getErrorMessage(error) {
|
|
|
50
175
|
return '⚠️ 请求超时,请重试';
|
|
51
176
|
}
|
|
52
177
|
if (msg.includes('permission') || msg.includes('im:resource')) {
|
|
53
|
-
return '
|
|
178
|
+
return '❌ 权限不足,请联系管理员配置应用权限';
|
|
54
179
|
}
|
|
55
|
-
return '
|
|
180
|
+
return '❌ 处理消息时出错,请稍后重试';
|
|
56
181
|
}
|