evolclaw 2.3.0 → 2.5.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.
@@ -1,3 +1,7 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { resolvePaths } from '../paths.js';
4
+ import { logger } from './logger.js';
1
5
  export var ErrorType;
2
6
  (function (ErrorType) {
3
7
  ErrorType["SDK_TIMEOUT"] = "sdk_timeout";
@@ -62,32 +66,152 @@ export function isInfraError(subtype, terminalReason) {
62
66
  export function prefixErrorType(prefix, errorType) {
63
67
  return `${prefix}:${errorType}`;
64
68
  }
69
+ // ── 错误字典 ──────────────────────────────────────────────────────────
70
+ const VALID_ACTIONS = new Set(['retry', 'stop', 'ignore']);
71
+ let _dictPath = null;
72
+ let _rules = [];
73
+ let _lastMtime = 0;
74
+ function getDictPath() {
75
+ if (!_dictPath) {
76
+ _dictPath = path.join(resolvePaths().dataDir, 'error-dict.json');
77
+ }
78
+ return _dictPath;
79
+ }
80
+ /** 校验单条规则,返回错误原因(null = 合法) */
81
+ function validateRule(r, index) {
82
+ if (!r || typeof r !== 'object')
83
+ return `rules[${index}]: 不是对象`;
84
+ if (!r.id || typeof r.id !== 'string')
85
+ return `rules[${index}]: 缺少 id 或类型不是 string`;
86
+ if (!r.match || typeof r.match !== 'string')
87
+ return `rules[${index}] (${r.id}): 缺少 match 或类型不是 string`;
88
+ if (!VALID_ACTIONS.has(r.action))
89
+ return `rules[${index}] (${r.id}): action 无效 "${r.action}",允许值: retry/stop/ignore`;
90
+ if (r.type !== undefined && typeof r.type !== 'string')
91
+ return `rules[${index}] (${r.id}): type 类型不是 string`;
92
+ if (r.message !== undefined && typeof r.message !== 'string')
93
+ return `rules[${index}] (${r.id}): message 类型不是 string`;
94
+ return null;
95
+ }
96
+ /**
97
+ * 刷新字典:检查文件 mtime,有变化则重新读取并校验。
98
+ * 校验不通过 → 不更新内存数据,记录告警日志。
99
+ */
100
+ function refreshDict() {
101
+ const dictPath = getDictPath();
102
+ let stat;
103
+ try {
104
+ stat = fs.statSync(dictPath);
105
+ }
106
+ catch {
107
+ // 文件不存在 → 清空规则(首次)或保持不变(之前有数据且文件被删)
108
+ if (_rules.length > 0) {
109
+ logger.warn('[error-dict] 字典文件已删除,清空规则: %s', dictPath);
110
+ _rules = [];
111
+ _lastMtime = 0;
112
+ }
113
+ return;
114
+ }
115
+ const mtime = stat.mtimeMs;
116
+ if (mtime === _lastMtime)
117
+ return; // 文件未变化,跳过
118
+ // 文件有变化,尝试加载
119
+ try {
120
+ const content = fs.readFileSync(dictPath, 'utf-8');
121
+ let raw;
122
+ try {
123
+ raw = JSON.parse(content);
124
+ }
125
+ catch (parseErr) {
126
+ logger.warn('[error-dict] JSON 解析失败,保留原有规则: %s — %s', dictPath, parseErr.message);
127
+ _lastMtime = mtime; // 标记已检查,避免重复读取
128
+ return;
129
+ }
130
+ if (!raw || typeof raw !== 'object' || !Array.isArray(raw.rules)) {
131
+ logger.warn('[error-dict] 字典格式错误(缺少 rules 数组),保留原有规则: %s', dictPath);
132
+ _lastMtime = mtime;
133
+ return;
134
+ }
135
+ // 逐条校验
136
+ const errors = [];
137
+ for (let i = 0; i < raw.rules.length; i++) {
138
+ const err = validateRule(raw.rules[i], i);
139
+ if (err)
140
+ errors.push(err);
141
+ }
142
+ if (errors.length > 0) {
143
+ logger.warn('[error-dict] 字典校验失败(%d 条错误),保留原有规则:\n %s', errors.length, errors.join('\n '));
144
+ _lastMtime = mtime;
145
+ return;
146
+ }
147
+ // 全部通过,更新
148
+ _rules = raw.rules;
149
+ _lastMtime = mtime;
150
+ logger.info('[error-dict] 已加载 %d 条规则: %s', _rules.length, dictPath);
151
+ }
152
+ catch (err) {
153
+ logger.warn('[error-dict] 读取失败,保留原有规则: %s — %s', dictPath, err.message);
154
+ }
155
+ }
156
+ /**
157
+ * 匹配错误消息,返回首条命中的规则。
158
+ * 每次调用自动检查文件变化(基于 mtime,无变化零开销)。
159
+ * @param errorMessage 已 toLowerCase 的错误消息
160
+ */
161
+ export function matchErrorRule(errorMessage) {
162
+ refreshDict();
163
+ for (const rule of _rules) {
164
+ if (errorMessage.includes(rule.match.toLowerCase())) {
165
+ return rule;
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+ /** 获取当前已加载的规则数量(供测试和状态查询使用) */
171
+ export function getLoadedRuleCount() {
172
+ refreshDict();
173
+ return _rules.length;
174
+ }
175
+ /** 重置字典状态(仅供测试使用) */
176
+ export function _resetDict() {
177
+ _rules = [];
178
+ _lastMtime = 0;
179
+ _dictPath = null;
180
+ }
181
+ /** 设置字典文件路径(仅供测试使用) */
182
+ export function _setDictPath(p) {
183
+ _dictPath = p;
184
+ _lastMtime = 0; // 强制下次刷新
185
+ }
186
+ // ── 错误分类 / 重试 / 消息 ──────────────────────────────────────────
65
187
  export function classifyError(error) {
66
188
  const msg = (error?.message || '').toLowerCase();
67
- if (msg.includes('上下文过长') || msg.includes('context too long')
68
- || msg.includes('context_length_exceeded') || msg.includes('context_compact_failed')
69
- || msg.includes('prompt is too long') || msg.includes('context limit')) {
189
+ // 字典优先 命中则直接返回
190
+ const rule = matchErrorRule(msg);
191
+ if (rule) {
192
+ if (rule.type)
193
+ return rule.type;
194
+ if (rule.action === 'retry')
195
+ return ErrorType.API_ERROR;
196
+ if (rule.action === 'stop')
197
+ return ErrorType.AUTH_ERROR;
198
+ return ErrorType.UNKNOWN;
199
+ }
200
+ // 内置兜底规则(结构性、稳定的错误模式)
201
+ if (msg.includes('context_length_exceeded') || msg.includes('context_compact_failed')
202
+ || msg.includes('context limit')) {
70
203
  return ErrorType.CONTEXT_TOO_LONG;
71
204
  }
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')) {
205
+ if (msg.includes('401') || msg.includes('authentication_error')) {
75
206
  return ErrorType.AUTH_ERROR;
76
207
  }
77
208
  if (msg.includes('timeout') || msg.includes('etimedout')) {
78
209
  return ErrorType.SDK_TIMEOUT;
79
210
  }
80
- if (msg.includes('5') && (msg.includes('00') || msg.includes('02') || msg.includes('03') || msg.includes('04'))) {
81
- return ErrorType.API_ERROR;
82
- }
83
- // "X is not valid JSON" — API 返回了非 JSON 响应(如算力池切换提示),属于 API 错误
84
- if (msg.includes('is not valid json')) {
85
- return ErrorType.API_ERROR;
86
- }
87
- if (msg.includes('enoent') || msg.includes('corrupt') || msg.includes('invalid json')) {
211
+ if (msg.includes('enoent') || msg.includes('corrupt')) {
88
212
  return ErrorType.FILE_CORRUPT;
89
213
  }
90
- if (msg.includes('stream') || msg.includes('aborted') || msg.includes('interrupted')) {
214
+ if (msg.includes('aborted') || msg.includes('interrupted')) {
91
215
  return ErrorType.STREAM_ERROR;
92
216
  }
93
217
  return ErrorType.UNKNOWN;
@@ -100,26 +224,16 @@ export function classifyError(error) {
100
224
  export function isRetryableError(error) {
101
225
  const msg = error?.message || String(error);
102
226
  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;
227
+ // 字典优先 — 命中则直接返回
228
+ const rule = matchErrorRule(lower);
229
+ if (rule)
230
+ return rule.action === 'retry';
231
+ // 内置兜底规则(结构性错误码)
232
+ if (lower.includes('401') || lower.includes('authentication_error')) {
233
+ return false; // 认证错误不可重试
111
234
  }
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'))
235
+ // HTTP 5xx / 429 — 标准可重试状态码
236
+ if (/api error: (429|5\d{2})\b/.test(lower))
123
237
  return true;
124
238
  return false;
125
239
  }
@@ -148,34 +262,20 @@ export function getErrorMessage(error, terminalReason) {
148
262
  }
149
263
  // 回退到原有的错误消息匹配逻辑
150
264
  const msg = error?.message || String(error);
151
- if (msg.includes('CONTEXT_COMPACT_FAILED')) {
265
+ // 字典优先 — 命中且有自定义消息则直接返回
266
+ const rule = matchErrorRule(msg.toLowerCase());
267
+ if (rule?.message)
268
+ return rule.message;
269
+ // 内置兜底规则(结构性错误)
270
+ if (msg.includes('CONTEXT_COMPACT_FAILED') || msg.includes('context_length_exceeded')
271
+ || msg.includes('Context limit')) {
152
272
  return '⚠️ 上下文过长,自动压缩失败,请手动输入 /compact 重试';
153
273
  }
154
- if (msg.includes('上下文过长') || msg.includes('context too long') || msg.includes('context_length_exceeded')
155
- || msg.includes('Prompt is too long') || msg.includes('Context limit')) {
156
- return '⚠️ 上下文过长,自动压缩重试失败,请手动输入 /compact 重试';
157
- }
158
- if (msg.includes('API Error: 400')) {
159
- return '❌ 请求格式错误,请检查输入内容';
160
- }
161
- if (msg.includes('401') || msg.includes('Invalid API key') || msg.includes('key_not_found')
162
- || msg.includes('authentication_error')) {
274
+ if (msg.includes('401') || msg.includes('authentication_error')) {
163
275
  return '❌ API Key 无效,请检查密钥配置。使用 /status 查看当前配置';
164
276
  }
165
- if (msg.includes('API Error: 500')) {
166
- return '❌ API 服务暂时不可用,请稍后重试';
167
- }
168
- if (msg.includes('API Error: 403')) {
169
- return '❌ API 认证失败,请检查密钥配置或稍后重试';
170
- }
171
- if (msg.includes('API Error: 429')) {
172
- return '⚠️ 请求过于频繁,请稍后再试';
173
- }
174
277
  if (msg.includes('timeout')) {
175
278
  return '⚠️ 请求超时,请重试';
176
279
  }
177
- if (msg.includes('permission') || msg.includes('im:resource')) {
178
- return '❌ 权限不足,请联系管理员配置应用权限';
179
- }
180
280
  return '❌ 处理消息时出错,请稍后重试';
181
281
  }
@@ -0,0 +1,32 @@
1
+ // ── Markdown → Plain Text ───────────────────────────────────────────────────
2
+ export function markdownToPlainText(text) {
3
+ let result = text;
4
+ // Code blocks: strip fences, keep content
5
+ result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code) => code.trim());
6
+ // Images: remove entirely
7
+ result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, '');
8
+ // Links: keep display text only
9
+ result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1');
10
+ // Tables: remove separator rows
11
+ result = result.replace(/^\|[\s:|-]+\|$/gm, '');
12
+ result = result.replace(/^\|(.+)\|$/gm, (_, inner) => inner.split('|').map(cell => cell.trim()).join(' '));
13
+ // Bold/italic
14
+ result = result.replace(/\*\*(.+?)\*\*/g, '$1');
15
+ result = result.replace(/\*(.+?)\*/g, '$1');
16
+ result = result.replace(/__(.+?)__/g, '$1');
17
+ result = result.replace(/_(.+?)_/g, '$1');
18
+ // Strikethrough
19
+ result = result.replace(/~~(.+?)~~/g, '$1');
20
+ // Inline code
21
+ result = result.replace(/`([^`]+)`/g, '$1');
22
+ // Headers
23
+ result = result.replace(/^#{1,6}\s+/gm, '');
24
+ // Blockquotes
25
+ result = result.replace(/^>\s?/gm, '');
26
+ // Horizontal rules
27
+ result = result.replace(/^[-*_]{3,}$/gm, '');
28
+ // List markers
29
+ result = result.replace(/^(\s*)[-*+]\s/gm, '$1');
30
+ result = result.replace(/^(\s*)\d+\.\s/gm, '$1');
31
+ return result.trim();
32
+ }