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.
- package/.agent/sessions/cli_default.json +119 -301
- package/cli/bin/foliko.js +2 -2
- package/cli/src/commands/chat.js +15 -26
- package/cli/src/ui/chat-ui.js +102 -165
- package/cli/src/ui/footer-bar.js +7 -32
- package/cli/src/ui/message-bubble.js +24 -2
- package/cli/src/ui/status-bar.js +177 -0
- package/package.json +1 -2
- package/plugins/qq-plugin.js +1 -1
- package/src/core/agent-chat.js +50 -17
- package/src/core/agent.js +17 -27
- package/src/core/chat-session.js +7 -161
- package/src/core/constants.js +198 -0
- package/src/core/context-compressor.js +6 -181
- package/src/core/framework.js +125 -6
- package/src/core/plugin-base.js +7 -5
- package/src/core/provider.js +6 -0
- package/src/core/subagent.js +16 -135
- package/src/core/tool-executor.js +2 -70
- package/src/executors/mcp-executor.js +1 -1
- package/src/utils/chat-queue.js +11 -22
- package/src/utils/download.js +5 -4
- package/src/utils/message-validator.js +283 -0
- package/src/utils/retry.js +168 -22
- package/src/utils/sandbox.js +60 -207
- package/cli/src/utils/debounce.js +0 -106
package/src/utils/retry.js
CHANGED
|
@@ -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
|
-
|
|
96
|
+
/** AI API 重试(包含 AI SDK 特定错误判断) */
|
|
97
|
+
ai: {
|
|
68
98
|
maxAttempts: 3,
|
|
69
|
-
baseDelay:
|
|
99
|
+
baseDelay: 2000,
|
|
70
100
|
maxDelay: 15000,
|
|
71
|
-
factor:
|
|
72
|
-
jitter: 0.
|
|
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
|
|
75
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
};
|
package/src/utils/sandbox.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 安全沙箱工具
|
|
3
|
-
*
|
|
3
|
+
* 使用 Node.js 原生 vm 模块(替代已废弃的 vm2)
|
|
4
4
|
* 提供安全的代码执行环境,限制对系统资源的访问
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
138
|
-
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
};
|