foliko 1.1.63 → 1.1.64

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,12 +1,42 @@
1
1
  /**
2
2
  * Foliko Retry Strategy - 重试策略系统
3
3
  * 提供多种重试策略,支持指数退避、抖动等
4
+ *
5
+ * 统一的重试入口,取代以下分散的重试逻辑:
6
+ * - ChatQueueManager.executeWithRetry() (utils/chat-queue.js)
7
+ * - Subagent.chat() 中的重试循环 (core/subagent.js)
8
+ * - utils/index.js 中的 retry() 函数
4
9
  */
5
10
 
6
11
  const { logger } = require('./logger');
7
12
 
8
13
  const log = logger.child('RetryStrategy');
9
14
 
15
+ /**
16
+ * 网络错误码列表
17
+ */
18
+ const NETWORK_ERROR_CODES = [
19
+ 'ECONNREFUSED',
20
+ 'ECONNRESET',
21
+ 'ETIMEDOUT',
22
+ 'ENOTFOUND',
23
+ 'EAI_AGAIN',
24
+ 'EPIPE',
25
+ 'ERR_CONNECTION_REFUSED',
26
+ 'ERR_CONNECTION_RESET',
27
+ 'ERR_CONNECTION_TIMEOUT',
28
+ ];
29
+
30
+ /**
31
+ * API 可重试的 HTTP 状态码
32
+ */
33
+ const RETRYABLE_HTTP_CODES = [429, 500, 502, 503, 504];
34
+
35
+ /**
36
+ * AI SDK 错误名称(可重试)
37
+ */
38
+ const AI_RETRYABLE_ERRORS = ['AI_RetryError', 'RetryError', 'AI_APICallError'];
39
+
10
40
  /**
11
41
  * 重试策略配置
12
42
  * @typedef {Object} RetryConfig
@@ -63,16 +93,32 @@ const PRESETS = {
63
93
  factor: 1.5,
64
94
  jitter: 0.05,
65
95
  },
66
- /** 空闲连接保活(适合长时间运行的连接检查) */
67
- idle: {
96
+ /** AI API 重试(包含 AI SDK 特定错误判断) */
97
+ ai: {
68
98
  maxAttempts: 3,
69
- baseDelay: 5000,
99
+ baseDelay: 2000,
70
100
  maxDelay: 15000,
71
- factor: 1.5,
72
- jitter: 0.1,
101
+ factor: 2,
102
+ jitter: 0.2,
103
+ shouldRetry: (error) => {
104
+ if (!error) return false;
105
+ const name = error.name || '';
106
+ if (AI_RETRYABLE_ERRORS.includes(name)) return true;
107
+ // 也检查标准网络错误
108
+ return isNetworkError(error);
109
+ },
110
+ },
111
+ /** API 调用重试(适合第三方API) */
112
+ api: {
113
+ maxAttempts: 4,
114
+ baseDelay: 500,
115
+ maxDelay: 8000,
116
+ factor: 2,
117
+ jitter: 0.15,
73
118
  shouldRetry: (error) => {
74
- const networkCodes = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'];
75
- return error.code && networkCodes.includes(error.code);
119
+ const status = error.status || error.statusCode;
120
+ if (status && (status >= 500 || status === 429)) return true;
121
+ return isNetworkError(error);
76
122
  },
77
123
  },
78
124
  /** 数据库重试(适合数据库连接) */
@@ -104,19 +150,119 @@ const PRESETS = {
104
150
  return dbKeywords.some((k) => msg.includes(k));
105
151
  },
106
152
  },
107
- /** API调用重试(适合第三方API) */
108
- api: {
109
- maxAttempts: 4,
110
- baseDelay: 500,
111
- maxDelay: 8000,
112
- factor: 2,
113
- jitter: 0.15,
114
- shouldRetry: (error) => {
115
- const status = error.status || error.statusCode;
116
- if (status && (status >= 500 || status === 429)) return true;
117
- const networkCodes = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'];
118
- if (error.code && networkCodes.includes(error.code)) return true;
119
- return false;
120
- },
121
- },
153
+ };
154
+
155
+ /**
156
+ * 判断错误是否由网络/AI服务问题引起(统一入口)
157
+ * 替代 ChatQueueManager.isRetryableError() 和 Subagent 中的分类逻辑
158
+ * @param {Error} err
159
+ * @returns {boolean}
160
+ */
161
+ function isNetworkError(err) {
162
+ if (!err) return false;
163
+
164
+ const code = err.code || err.status || err.statusCode;
165
+ const message = (err.message || '').toLowerCase();
166
+ const name = err.name || '';
167
+
168
+ // 检查网络错误码
169
+ if (code && NETWORK_ERROR_CODES.includes(code)) return true;
170
+
171
+ // 检查可重试 HTTP 状态码
172
+ if (typeof code === 'number' && RETRYABLE_HTTP_CODES.includes(code)) return true;
173
+
174
+ // 检查 AI SDK 重试错误
175
+ if (AI_RETRYABLE_ERRORS.includes(name)) return true;
176
+
177
+ // 检查消息关键词
178
+ const retryableKeywords = [
179
+ 'timeout', 'network', 'econnrefused', 'econnreset', 'etimedout',
180
+ 'enotfound', 'rate limit', '负载较高', '暂时不可用',
181
+ '429', '500', '502', '503', '504',
182
+ ];
183
+ return retryableKeywords.some((kw) => message.includes(kw));
184
+ }
185
+
186
+ /**
187
+ * 计算下次重试延迟(指数退避 + 随机抖动)
188
+ * @param {number} attempt - 当前尝试次数(从 1 开始)
189
+ * @param {RetryConfig} config - 重试配置
190
+ * @returns {number} 延迟毫秒数
191
+ */
192
+ function calculateDelay(attempt, config = {}) {
193
+ const factor = config.factor || 2;
194
+ const baseDelay = config.baseDelay || 1000;
195
+ const maxDelay = config.maxDelay || 30000;
196
+ const jitter = config.jitter ?? 0.2;
197
+
198
+ // 指数退避: baseDelay * factor ^ (attempt - 1)
199
+ const exponentialDelay = baseDelay * Math.pow(factor, attempt - 1);
200
+
201
+ // 加上抖动: delay * (1 + random(-jitter, +jitter))
202
+ const jitterAmount = exponentialDelay * jitter * (Math.random() * 2 - 1);
203
+ const delay = Math.min(exponentialDelay + jitterAmount, maxDelay);
204
+
205
+ return Math.max(delay, 0);
206
+ }
207
+
208
+ /**
209
+ * 带重试策略的函数执行
210
+ * @param {Function} fn - 要执行的异步函数
211
+ * @param {RetryConfig} [config] - 重试配置(默认 standard)
212
+ * @returns {Promise<any>}
213
+ * @throws {Error} 所有重试耗尽后抛出最后一次错误
214
+ */
215
+ async function withRetry(fn, config = {}) {
216
+ const merged = { ...PRESETS.standard, ...config };
217
+ const { maxAttempts, onRetry } = merged;
218
+
219
+ let lastError;
220
+
221
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
222
+ try {
223
+ return await fn();
224
+ } catch (err) {
225
+ lastError = err;
226
+
227
+ // 判断是否应重试
228
+ const shouldRetry = merged.shouldRetry || isNetworkError;
229
+ if (!shouldRetry(err)) {
230
+ throw err; // 不可重试的错误,立即抛出
231
+ }
232
+
233
+ if (attempt < maxAttempts) {
234
+ const delay = calculateDelay(attempt, merged);
235
+ if (onRetry) {
236
+ try { onRetry(err, attempt, delay); } catch { /* ignore */ }
237
+ }
238
+ await new Promise((resolve) => setTimeout(resolve, delay));
239
+ }
240
+ }
241
+ }
242
+
243
+ throw lastError;
244
+ }
245
+
246
+ /**
247
+ * 装饰器:使函数可重试
248
+ * @param {RetryConfig} [config]
249
+ * @returns {Function} 装饰后的函数
250
+ */
251
+ function retryable(config = {}) {
252
+ return function (fn) {
253
+ return async (...args) => {
254
+ return withRetry(() => fn(...args), config);
255
+ };
256
+ };
257
+ }
258
+
259
+ module.exports = {
260
+ withRetry,
261
+ retryable,
262
+ isNetworkError,
263
+ calculateDelay,
264
+ PRESETS,
265
+ NETWORK_ERROR_CODES,
266
+ RETRYABLE_HTTP_CODES,
267
+ AI_RETRYABLE_ERRORS,
122
268
  };
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * 安全沙箱工具
3
- * 用于替代 new Function() 执行不可信代码
3
+ * 使用 Node.js 原生 vm 模块(替代已废弃的 vm2)
4
4
  * 提供安全的代码执行环境,限制对系统资源的访问
5
5
  */
6
6
 
7
- const { VM, VMScript } = require('vm2')
7
+ const vm = require('vm');
8
8
 
9
9
  /**
10
10
  * 创建安全的沙箱上下文
@@ -13,272 +13,125 @@ const { VM, VMScript } = require('vm2')
13
13
  * @returns {Object} 沙箱上下文
14
14
  */
15
15
  function createSandboxContext(customContext = {}, options = {}) {
16
- const {
17
- timeout = 5000, // 超时时间(毫秒)
18
- allowSync = false, // 是否允许同步执行
19
- mockConsole = true // 是否模拟 console
20
- } = options
16
+ const { mockConsole = true } = options;
21
17
 
22
- // 创建安全的 console 对象
23
- const safeConsole = mockConsole ? {
24
- log: (...args) => { /* 静默执行,阻止日志泄露 */ },
25
- error: (...args) => { /* 静默执行 */ },
26
- warn: (...args) => { /* 静默执行 */ },
27
- info: (...args) => { /* 静默执行 */ },
28
- debug: (...args) => { /* 静默执行 */ }
29
- } : console
18
+ const safeConsole = mockConsole
19
+ ? { log() {}, error() {}, warn() {}, info() {}, debug() {} }
20
+ : console;
30
21
 
31
22
  return {
32
- // 用户提供的上下文
33
23
  ...customContext,
34
- // 安全的 console
35
24
  console: safeConsole,
36
- // 安全的 Math 对象
37
25
  Math,
38
- // 安全的 JSON 对象
39
- JSON: {
40
- parse: JSON.parse,
41
- stringify: JSON.stringify
42
- },
43
- // 安全的 Array 对象(限制方法)
44
- Array: {
45
- isArray: Array.isArray,
46
- from: Array.from,
47
- of: Array.of
48
- },
49
- // 安全的 String 对象
50
- String: {
51
- fromCharCode: String.fromCharCode,
52
- fromCodePoint: String.fromCodePoint,
53
- raw: String.raw
54
- },
55
- // 安全的 Number 对象
56
- Number: {
57
- isNaN: Number.isNaN,
58
- isFinite: Number.isFinite,
59
- parseInt: Number.parseInt,
60
- parseFloat: Number.parseFloat
61
- },
62
- // 安全的 Boolean 对象
26
+ JSON: { parse: JSON.parse, stringify: JSON.stringify },
27
+ Array: { isArray: Array.isArray, from: Array.from, of: Array.of },
28
+ String: { fromCharCode: String.fromCharCode, fromCodePoint: String.fromCodePoint, raw: String.raw },
29
+ Number: { isNaN: Number.isNaN, isFinite: Number.isFinite, parseInt: Number.parseInt, parseFloat: Number.parseFloat },
63
30
  Boolean,
64
- // 安全的 Object 对象
65
- Object: {
66
- keys: Object.keys,
67
- values: Object.values,
68
- entries: Object.entries,
69
- assign: Object.assign,
70
- create: Object.create,
71
- freeze: Object.freeze
72
- },
73
- // 安全的 Date 对象
74
- Date: {
75
- now: Date.now
76
- },
77
- // 安全的 RegExp 对象
31
+ Object: { keys: Object.keys, values: Object.values, entries: Object.entries, assign: Object.assign, create: Object.create, freeze: Object.freeze },
32
+ Date: { now: Date.now },
78
33
  RegExp,
79
- // 安全的 Error 对象
80
34
  Error,
81
- // 安全的 Promise 对象
82
35
  Promise,
83
- // 安全的 Set 对象
84
36
  Set,
85
- // 安全的 Map 对象
86
37
  Map,
87
- // 安全的 WeakMap 对象
88
38
  WeakMap,
89
- // 安全的 WeakSet 对象
90
39
  WeakSet,
91
- // 安全的 Symbol
92
40
  Symbol,
93
- // 安全的 Proxy
94
41
  Proxy,
95
- // 安全的 Reflect
96
- Reflect
97
- }
42
+ Reflect,
43
+ };
98
44
  }
99
45
 
100
46
  /**
101
- * 在沙箱中执行异步代码
47
+ * 在沙箱中执行代码
102
48
  * @param {string} code - 要执行的代码
103
49
  * @param {Object} context - 上下文变量
104
50
  * @param {Object} options - 配置选项
51
+ * @param {number} options.timeout - 超时毫秒
105
52
  * @returns {Promise<any>} 执行结果
106
53
  */
107
54
  async function runInSandbox(code, context = {}, options = {}) {
108
- const { timeout = 5000 } = options
109
-
55
+ const { timeout = 5000 } = options;
110
56
  try {
111
- const vm = new VM({
112
- timeout,
113
- sandbox: createSandboxContext(context, options),
114
- eval: false,
115
- wasm: false
116
- })
117
-
118
- // 包装代码为异步函数
119
- const wrappedCode = `(async () => { ${code} })()`
120
- return await vm.run(wrappedCode)
57
+ const ctx = vm.createContext(createSandboxContext(context, options));
58
+ const script = new vm.Script(`(async () => { ${code} })()`, { filename: 'sandbox.js' });
59
+ return await script.runInContext(ctx, { timeout });
121
60
  } catch (error) {
122
- throw new Error(`Sandbox execution error: ${error.message}`)
61
+ throw new Error(`Sandbox execution error: ${error.message}`);
123
62
  }
124
63
  }
125
64
 
126
65
  /**
127
66
  * 在沙箱中执行表达式(同步)
128
- * @param {string} expression - 要执行的表达式
129
- * @param {Object} context - 上下文变量
130
- * @param {Object} options - 配置选项
131
- * @returns {any} 执行结果
132
67
  */
133
68
  function evaluateInSandbox(expression, context = {}, options = {}) {
134
- const { timeout = 5000 } = options
135
-
69
+ const { timeout = 5000 } = options;
136
70
  try {
137
- const vm = new VM({
138
- timeout,
139
- sandbox: createSandboxContext(context, options),
140
- eval: false,
141
- wasm: false
142
- })
143
-
144
- return vm.run(expression)
71
+ const ctx = vm.createContext(createSandboxContext(context, options));
72
+ return vm.runInContext(expression, ctx, { timeout });
145
73
  } catch (error) {
146
- throw new Error(`Sandbox evaluation error: ${error.message}`)
74
+ throw new Error(`Sandbox evaluation error: ${error.message}`);
147
75
  }
148
76
  }
149
77
 
150
78
  /**
151
79
  * 安全执行脚本(用于工作流中的用户脚本)
152
- * @param {string} script - 脚本代码
153
- * @param {Object} context - 上下文变量
154
- * @param {Object} options - 配置选项
155
- * @returns {Promise<any>} 执行结果
156
80
  */
157
81
  async function runScriptSafely(script, context = {}, options = {}) {
158
- const { timeout = 10000 } = options
159
-
82
+ const { timeout = 10000 } = options;
160
83
  try {
161
- const vm = new VM({
162
- timeout,
163
- sandbox: createSandboxContext(context, options),
164
- eval: false,
165
- wasm: false
166
- })
167
-
168
- // 将脚本包装在异步 IIFE 中
169
- const wrappedCode = `
170
- (async () => {
171
- const { input, input_data, variables, previousResult, console } = context;
172
- ${script}
173
- })()
174
- `
175
-
176
- return await vm.run(wrappedCode)
84
+ const ctx = vm.createContext(createSandboxContext(context, options));
85
+ const wrapped = `(async () => { ${script} })()`;
86
+ const s = new vm.Script(wrapped, { filename: 'script.js' });
87
+ return await s.runInContext(ctx, { timeout });
177
88
  } catch (error) {
178
- throw new Error(`Script execution error: ${error.message}`)
89
+ throw new Error(`Script execution error: ${error.message}`);
179
90
  }
180
91
  }
181
92
 
182
93
  /**
183
94
  * 安全执行工作流定义
184
- * @param {string} workflowCode - 工作流代码
185
- * @param {Object} engine - 工作流引擎
186
- * @param {Object} options - 配置选项
187
- * @returns {any} 执行结果
188
95
  */
189
96
  function runWorkflowSafely(workflowCode, engine, options = {}) {
190
- const { timeout = 5000 } = options
191
-
97
+ const { timeout = 5000 } = options;
192
98
  try {
193
- const vm = new VM({
194
- timeout,
195
- sandbox: {
196
- engine,
197
- console: {
198
- log: () => {},
199
- error: () => {},
200
- warn: () => {},
201
- info: () => {}
202
- },
203
- Math,
204
- JSON,
205
- Promise,
206
- Array,
207
- Object,
208
- Date,
209
- RegExp,
210
- Error,
211
- Symbol,
212
- Map,
213
- Set
214
- },
215
- eval: false,
216
- wasm: false
217
- })
218
-
219
- return vm.run(`(function(engine) { return (${workflowCode}) })(engine)`)
99
+ const ctx = vm.createContext({
100
+ engine,
101
+ console: { log() {}, error() {}, warn() {}, info() {} },
102
+ Math, JSON, Promise, Array, Object, Date, RegExp, Error, Symbol, Map, Set,
103
+ });
104
+ const wrapped = `(function(engine) { return (${workflowCode}) })(engine)`;
105
+ return vm.runInContext(wrapped, ctx, { timeout });
220
106
  } catch (error) {
221
- throw new Error(`Workflow execution error: ${error.message}`)
107
+ throw new Error(`Workflow execution error: ${error.message}`);
222
108
  }
223
109
  }
224
110
 
225
111
  /**
226
112
  * 安全执行工作流加载脚本
227
- * @param {string} content - 文件内容
228
- * @param {Object} options - 配置选项
229
- * @returns {Object} module.exports
230
113
  */
231
114
  function runWorkflowFileSafely(content, options = {}) {
232
- const { timeout = 10000 } = options
233
-
115
+ const { timeout = 10000 } = options;
234
116
  try {
235
- const vm = new VM({
236
- timeout,
237
- sandbox: {
238
- module: { exports: {} },
239
- exports: {},
240
- require: () => { throw new Error('require is not allowed in sandbox') },
241
- process: {
242
- env: {},
243
- cwd: () => '/'
244
- },
245
- console: {
246
- log: () => {},
247
- error: () => {},
248
- warn: () => {},
249
- info: () => {}
250
- },
251
- __dirname: '/',
252
- __filename: '/workflow.js',
253
- Math,
254
- JSON,
255
- Promise,
256
- Array,
257
- Object,
258
- Date,
259
- RegExp,
260
- Error,
261
- Symbol,
262
- Map,
263
- Set
264
- },
265
- eval: false,
266
- wasm: false
267
- })
268
-
269
- const wrappedCode = `
270
- (function(module, exports, require, process, console, __dirname, __filename) {
271
- ${content}
272
- })
273
- `
274
-
275
- const fn = vm.run(wrappedCode)
276
- const moduleObj = { exports: {} }
277
- fn(moduleObj, moduleObj.exports, () => {}, { env: {}, cwd: () => '/' }, { log: () => {}, error: () => {}, warn: () => {}, info: () => {} }, '/', '/workflow.js')
278
-
279
- return moduleObj.exports.default || moduleObj.exports
117
+ const ctx = vm.createContext({
118
+ module: { exports: {} },
119
+ exports: {},
120
+ require: () => { throw new Error('require is not allowed in sandbox'); },
121
+ process: { env: {}, cwd: () => '/' },
122
+ console: { log() {}, error() {}, warn() {}, info() {} },
123
+ __dirname: '/',
124
+ __filename: '/workflow.js',
125
+ Math, JSON, Promise, Array, Object, Date, RegExp, Error, Symbol, Map, Set,
126
+ });
127
+
128
+ const wrapped = `(function(module, exports, require, process, console, __dirname, __filename) { ${content} })`;
129
+ const fn = vm.runInContext(wrapped, ctx, { timeout });
130
+ const m = { exports: {} };
131
+ fn(m, m.exports, () => {}, { env: {}, cwd: () => '/' }, { log() {}, error() {}, warn() {}, info() {} }, '/', '/workflow.js');
132
+ return m.exports.default || m.exports;
280
133
  } catch (error) {
281
- throw new Error(`Workflow file execution error: ${error.message}`)
134
+ throw new Error(`Workflow file execution error: ${error.message}`);
282
135
  }
283
136
  }
284
137
 
@@ -288,5 +141,5 @@ module.exports = {
288
141
  evaluateInSandbox,
289
142
  runScriptSafely,
290
143
  runWorkflowSafely,
291
- runWorkflowFileSafely
292
- }
144
+ runWorkflowFileSafely,
145
+ };
@@ -1,106 +0,0 @@
1
- /**
2
- * 防抖函数 - 等待 delay 时间内没有新调用才执行
3
- * @param {Function} fn - 要防抖的函数
4
- * @param {number} delay - 延迟毫秒
5
- * @returns {{run: Function, cancel: Function}}
6
- */
7
- function debounce(fn, delay) {
8
- let timer = null;
9
-
10
- const debounced = (...args) => {
11
- if (timer) {
12
- clearTimeout(timer);
13
- }
14
- timer = setTimeout(() => {
15
- fn(...args);
16
- timer = null;
17
- }, delay);
18
- };
19
-
20
- const cancel = () => {
21
- if (timer) {
22
- clearTimeout(timer);
23
- timer = null;
24
- }
25
- };
26
-
27
- return { run: debounced, cancel };
28
- }
29
-
30
- function throttle(fn, delay = 300, options = {}) {
31
- let timer = null;
32
- let lastTime = 0;
33
- const { leading = true, trailing = true } = options;
34
-
35
- return function (...args) {
36
- const now = Date.now();
37
-
38
- // 首次调用且不允许立即执行
39
- if (!leading && lastTime === 0) {
40
- lastTime = now;
41
- }
42
-
43
- // 距离下次执行还需要等待的时间
44
- const remaining = delay - (now - lastTime);
45
-
46
- if (remaining <= 0 || remaining > delay) {
47
- // 可以立即执行
48
- if (timer) {
49
- clearTimeout(timer);
50
- timer = null;
51
- }
52
- fn.apply(this, args);
53
- lastTime = now;
54
- } else if (!timer && trailing) {
55
- // 设置定时器,确保最后一次调用能执行
56
- timer = setTimeout(() => {
57
- fn.apply(this, args);
58
- lastTime = leading ? Date.now() : 0;
59
- timer = null;
60
- }, remaining);
61
- }
62
- };
63
- }
64
-
65
- /**
66
- * 间隔执行函数 - 每隔 interval 毫秒执行一次,直到被取消
67
- * @param {Function} fn - 要执行的函数
68
- * @param {number} interval - 间隔毫秒
69
- * @param {boolean} [lazy=false] - 如果为 true,start() 不会立即执行,只开始计时
70
- * @returns {{run: Function, cancel: Function}}
71
- */
72
- function interval(fn, intervalMs, lazy = false) {
73
- let timer = null;
74
-
75
- const start = () => {
76
- // 先清除已存在的定时器
77
- if (timer) {
78
- clearInterval(timer);
79
- }
80
-
81
- // 如果不是 lazy 模式,立即执行一次
82
- if (!lazy) {
83
- fn();
84
- }
85
-
86
- // 设置间隔定时器
87
- timer = setInterval(() => {
88
- fn();
89
- }, intervalMs);
90
- };
91
-
92
- const stop = () => {
93
- if (timer) {
94
- clearInterval(timer);
95
- timer = null;
96
- }
97
- };
98
-
99
- return { start, stop };
100
- }
101
-
102
- module.exports = {
103
- debounce,
104
- throttle,
105
- interval,
106
- };