rl-rockcli 0.0.2 → 0.0.4
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 +400 -0
- package/index.js +51 -21
- package/package.json +3 -2
- package/commands/log/core/constants.js +0 -237
- package/commands/log/core/display.js +0 -370
- package/commands/log/core/search.js +0 -330
- package/commands/log/core/tail.js +0 -216
- package/commands/log/core/utils.js +0 -424
- package/commands/log.js +0 -298
- package/commands/sandbox/core/log-bridge.js +0 -119
- package/commands/sandbox/core/replay/analyzer.js +0 -311
- package/commands/sandbox/core/replay/batch-orchestrator.js +0 -536
- package/commands/sandbox/core/replay/batch-task.js +0 -369
- package/commands/sandbox/core/replay/concurrent-display.js +0 -70
- package/commands/sandbox/core/replay/concurrent-orchestrator.js +0 -170
- package/commands/sandbox/core/replay/data-source.js +0 -86
- package/commands/sandbox/core/replay/display.js +0 -231
- package/commands/sandbox/core/replay/executor.js +0 -634
- package/commands/sandbox/core/replay/history-fetcher.js +0 -124
- package/commands/sandbox/core/replay/index.js +0 -338
- package/commands/sandbox/core/replay/loghouse-data-source.js +0 -177
- package/commands/sandbox/core/replay/pid-mapping.js +0 -26
- package/commands/sandbox/core/replay/request.js +0 -109
- package/commands/sandbox/core/replay/worker.js +0 -166
- package/commands/sandbox/core/session.js +0 -346
- package/commands/sandbox/log-bridge.js +0 -2
- package/commands/sandbox/ray.js +0 -2
- package/commands/sandbox/replay/analyzer.js +0 -311
- package/commands/sandbox/replay/batch-orchestrator.js +0 -536
- package/commands/sandbox/replay/batch-task.js +0 -369
- package/commands/sandbox/replay/concurrent-display.js +0 -70
- package/commands/sandbox/replay/concurrent-orchestrator.js +0 -170
- package/commands/sandbox/replay/display.js +0 -231
- package/commands/sandbox/replay/executor.js +0 -634
- package/commands/sandbox/replay/history-fetcher.js +0 -118
- package/commands/sandbox/replay/index.js +0 -338
- package/commands/sandbox/replay/pid-mapping.js +0 -26
- package/commands/sandbox/replay/request.js +0 -109
- package/commands/sandbox/replay/worker.js +0 -166
- package/commands/sandbox/replay.js +0 -2
- package/commands/sandbox/session.js +0 -2
- package/commands/sandbox-original.js +0 -1393
- package/commands/sandbox.js +0 -499
- package/help/help.json +0 -1071
- package/help/middleware.js +0 -71
- package/help/renderer.js +0 -800
- package/lib/plugin-context.js +0 -40
- package/sdks/sandbox/core/client.js +0 -845
- package/sdks/sandbox/core/config.js +0 -70
- package/sdks/sandbox/core/types.js +0 -74
- package/sdks/sandbox/httpLogger.js +0 -251
- package/sdks/sandbox/index.js +0 -9
- package/utils/asciiArt.js +0 -138
- package/utils/bun-compat.js +0 -59
- package/utils/ciPipelines.js +0 -138
- package/utils/cli.js +0 -17
- package/utils/command-router.js +0 -79
- package/utils/configManager.js +0 -503
- package/utils/dependency-resolver.js +0 -135
- package/utils/eagleeye_traceid.js +0 -151
- package/utils/envDetector.js +0 -78
- package/utils/execution_logger.js +0 -415
- package/utils/featureManager.js +0 -68
- package/utils/firstTimeTip.js +0 -44
- package/utils/hook-manager.js +0 -125
- package/utils/http-logger.js +0 -264
- package/utils/i18n.js +0 -139
- package/utils/image-progress.js +0 -159
- package/utils/logger.js +0 -154
- package/utils/plugin-loader.js +0 -124
- package/utils/plugin-manager.js +0 -348
- package/utils/ray_cli_wrapper.js +0 -746
- package/utils/sandbox-client.js +0 -419
- package/utils/terminal.js +0 -32
- package/utils/tips.js +0 -106
|
@@ -1,424 +0,0 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
|
|
4
|
-
// 加载集群表映射配置
|
|
5
|
-
const clusterMappingPath = path.join(__dirname, '../../../config/cluster_table_mapping.json');
|
|
6
|
-
let clusterTableMapping = { clusters: {} };
|
|
7
|
-
if (fs.existsSync(clusterMappingPath)) {
|
|
8
|
-
clusterTableMapping = JSON.parse(fs.readFileSync(clusterMappingPath, 'utf8'));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 检查字段是否应该在格式化模式下被过滤
|
|
13
|
-
* @param {string} fieldName - 字段名
|
|
14
|
-
* @returns {boolean} 是否应该被过滤
|
|
15
|
-
*/
|
|
16
|
-
function isBlacklisted(fieldName) {
|
|
17
|
-
const { BLACKLIST_FIELDS } = require('./constants');
|
|
18
|
-
// 黑名单所有以 @ 开头的字段(仅在格式化模式下)
|
|
19
|
-
if (fieldName.startsWith('@')) return true;
|
|
20
|
-
// 黑名单 BLACKLIST_FIELDS 中的字段
|
|
21
|
-
if (BLACKLIST_FIELDS.includes(fieldName)) return true;
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 解析时间字符串为时间戳
|
|
27
|
-
* @param {string} timeStr - 时间字符串
|
|
28
|
-
* @returns {number|null} 时间戳(毫秒)
|
|
29
|
-
*/
|
|
30
|
-
function parseTime(timeStr) {
|
|
31
|
-
if (!timeStr) return null;
|
|
32
|
-
|
|
33
|
-
// 如果是纯数字,智能判断是秒级还是毫秒级时间戳
|
|
34
|
-
if (/^\d+$/.test(timeStr)) {
|
|
35
|
-
const timestamp = parseInt(timeStr);
|
|
36
|
-
// Unix 时间戳标准是秒级的(10位数字),毫秒级通常是13位数字
|
|
37
|
-
// 如果数字长度 <= 10,认为是秒级时间戳,需要转换为毫秒
|
|
38
|
-
// 如果数字长度 > 10,认为是毫秒级时间戳,直接使用
|
|
39
|
-
if (timestamp <= 9999999999) {
|
|
40
|
-
return timestamp * 1000; // 秒级 -> 毫秒级
|
|
41
|
-
} else {
|
|
42
|
-
return timestamp; // 已经是毫秒级
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// 解析相对时间格式(如 "15m", "1h", "2d")
|
|
47
|
-
const relativeMatch = timeStr.match(/^(\d+)([mhd])$/);
|
|
48
|
-
if (relativeMatch) {
|
|
49
|
-
const value = parseInt(relativeMatch[1]);
|
|
50
|
-
const unit = relativeMatch[2];
|
|
51
|
-
const now = Date.now();
|
|
52
|
-
switch (unit) {
|
|
53
|
-
case 'm': return now - value * 60 * 1000; // 分钟
|
|
54
|
-
case 'h': return now - value * 60 * 60 * 1000; // 小时
|
|
55
|
-
case 'd': return now - value * 24 * 60 * 60 * 1000; // 天
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 尝试解析为日期字符串
|
|
60
|
-
const date = new Date(timeStr);
|
|
61
|
-
if (!isNaN(date.getTime())) {
|
|
62
|
-
return date.getTime();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
throw new Error(`无法解析时间格式: ${timeStr}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* 生成日志的唯一键,用于去重
|
|
70
|
-
* @param {Object} log - 日志对象
|
|
71
|
-
* @returns {string} 唯一键
|
|
72
|
-
*/
|
|
73
|
-
function getLogUniqueKey(log) {
|
|
74
|
-
const source = log['@source'] || '';
|
|
75
|
-
const filename = log['@filename'] || '';
|
|
76
|
-
const packId = log['@packId'] || 0;
|
|
77
|
-
const logGroupOrder = log['@logGroupOrder'] || 0;
|
|
78
|
-
return `${source}:${filename}:${packId}:${logGroupOrder}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* 从字段过滤器中提取 @source 和 @filename 的值
|
|
83
|
-
* @param {Array} fieldFilters - 字段过滤器数组
|
|
84
|
-
* @returns {Object} 包含 source 和 filename 的对象
|
|
85
|
-
*/
|
|
86
|
-
function extractSourceAndFilename(fieldFilters) {
|
|
87
|
-
let source = null;
|
|
88
|
-
let filename = null;
|
|
89
|
-
|
|
90
|
-
if (fieldFilters && Array.isArray(fieldFilters)) {
|
|
91
|
-
fieldFilters.forEach(f => {
|
|
92
|
-
const sourceMatch = f.match(/^@source=(.+)$/);
|
|
93
|
-
const filenameMatch = f.match(/^@filename=(.+)$/);
|
|
94
|
-
|
|
95
|
-
if (sourceMatch) {
|
|
96
|
-
source = sourceMatch[1];
|
|
97
|
-
}
|
|
98
|
-
if (filenameMatch) {
|
|
99
|
-
filename = filenameMatch[1];
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return { source, filename };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* 检查两条日志是否连续(同IP、同文件,且序号相邻)
|
|
109
|
-
* @param {Object} log1 - 前一条日志
|
|
110
|
-
* @param {Object} log2 - 后一条日志
|
|
111
|
-
* @returns {boolean} 是否连续
|
|
112
|
-
*/
|
|
113
|
-
function areLogsConsecutive(log1, log2) {
|
|
114
|
-
if (!log1 || !log2) return false;
|
|
115
|
-
|
|
116
|
-
const source1 = log1['@source'] || '';
|
|
117
|
-
const source2 = log2['@source'] || '';
|
|
118
|
-
const filename1 = log1['@filename'] || '';
|
|
119
|
-
const filename2 = log2['@filename'] || '';
|
|
120
|
-
|
|
121
|
-
// 必须是同一IP和文件
|
|
122
|
-
if (source1 !== source2 || filename1 !== filename2) return false;
|
|
123
|
-
|
|
124
|
-
const packId1 = parseInt(log1['@packId']) || 0;
|
|
125
|
-
const packId2 = parseInt(log2['@packId']) || 0;
|
|
126
|
-
const order1 = parseInt(log1['@logGroupOrder']) || 0;
|
|
127
|
-
const order2 = parseInt(log2['@logGroupOrder']) || 0;
|
|
128
|
-
|
|
129
|
-
// 同一个packId,order差1
|
|
130
|
-
if (packId1 === packId2 && order2 - order1 === 1) {
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// packId差1,且log1是某packId的最后一条,log2是下一个packId的第一条
|
|
135
|
-
// 简化判断:packId差1且order2=0
|
|
136
|
-
if (packId2 - packId1 === 1 && order2 === 0) {
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* 构建查询字符串
|
|
145
|
-
* @param {Object} options - 查询选项
|
|
146
|
-
* @param {string} options.keyword - 关键词
|
|
147
|
-
* @param {Array} options.field - 字段过滤条件
|
|
148
|
-
* @param {string} options.query - 直接查询字符串
|
|
149
|
-
* @param {string} options.sandboxId - 沙箱ID
|
|
150
|
-
* @param {string} options.logFile - 日志文件名
|
|
151
|
-
* @param {string} options.role - 用户角色(用于权限控制)
|
|
152
|
-
* @returns {string} 查询字符串
|
|
153
|
-
*/
|
|
154
|
-
function buildQueryString({ keyword, field, query, sandboxId, logFile, role }) {
|
|
155
|
-
// 条件引用:内网专有模块(开源版不存在)
|
|
156
|
-
let Roles, ALLOWED_LOG_FILES;
|
|
157
|
-
try {
|
|
158
|
-
const access_control = require('../../../lib/access_control');
|
|
159
|
-
Roles = access_control.Roles;
|
|
160
|
-
ALLOWED_LOG_FILES = access_control.ALLOWED_LOG_FILES;
|
|
161
|
-
} catch (e) {
|
|
162
|
-
// 开源版:无权限控制,使用空实现
|
|
163
|
-
Roles = {};
|
|
164
|
-
ALLOWED_LOG_FILES = [];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let queryString = '';
|
|
168
|
-
|
|
169
|
-
// 如果提供了 --query,直接使用
|
|
170
|
-
if (query) {
|
|
171
|
-
queryString = query;
|
|
172
|
-
} else {
|
|
173
|
-
// 否则从 keyword 和 field 构建
|
|
174
|
-
const queryParts = [];
|
|
175
|
-
|
|
176
|
-
// 添加关键词搜索
|
|
177
|
-
if (keyword) {
|
|
178
|
-
const keywords = Array.isArray(keyword) ? keyword : [keyword];
|
|
179
|
-
keywords.forEach(kw => {
|
|
180
|
-
if (kw && typeof kw === 'string') {
|
|
181
|
-
// 直接使用关键词,不使用引号包裹
|
|
182
|
-
queryParts.push(kw);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// 添加字段搜索
|
|
188
|
-
if (field && field.length > 0) {
|
|
189
|
-
field.forEach(f => {
|
|
190
|
-
const negationMatch = f.match(/^([^!=]+)!=(.*)$/);
|
|
191
|
-
// 比较运算符匹配:>=, <=, >, < (必须在等值运算符之前检查,避免错误的匹配)
|
|
192
|
-
// 使用 \w+ 来匹配字段名,避免匹配到运算符字符
|
|
193
|
-
const greaterEqualMatch = f.match(/^(\w+)>=(.*)$/);
|
|
194
|
-
const lessEqualMatch = f.match(/^(\w+)<=(.*)$/);
|
|
195
|
-
const greaterMatch = f.match(/^(\w+)>(.*)$/);
|
|
196
|
-
const lessMatch = f.match(/^(\w+)<(.*)$/);
|
|
197
|
-
const equalMatch = f.match(/^([^=]+)=(.*)$/);
|
|
198
|
-
|
|
199
|
-
if (negationMatch) {
|
|
200
|
-
const [, fieldName, fieldValue] = negationMatch;
|
|
201
|
-
// 暂时注释掉 _exists_ 逻辑
|
|
202
|
-
// queryParts.push(`_exists_:${fieldName} and not ${fieldName}:${fieldValue}`);
|
|
203
|
-
queryParts.push(`not ${fieldName}:${fieldValue}`);
|
|
204
|
-
} else if (greaterEqualMatch) {
|
|
205
|
-
// status>=500 → status:[500 TO *}
|
|
206
|
-
const [, fieldName, fieldValue] = greaterEqualMatch;
|
|
207
|
-
queryParts.push(`${fieldName}:[${fieldValue} TO *}`);
|
|
208
|
-
} else if (lessEqualMatch) {
|
|
209
|
-
// status<=500 → status:{* TO 500]
|
|
210
|
-
const [, fieldName, fieldValue] = lessEqualMatch;
|
|
211
|
-
queryParts.push(`${fieldName}:{* TO ${fieldValue}]`);
|
|
212
|
-
} else if (greaterMatch) {
|
|
213
|
-
// status>500 → status:{500 TO *}
|
|
214
|
-
const [, fieldName, fieldValue] = greaterMatch;
|
|
215
|
-
queryParts.push(`${fieldName}:{${fieldValue} TO *}`);
|
|
216
|
-
} else if (lessMatch) {
|
|
217
|
-
// status<500 → status:{* TO 500}
|
|
218
|
-
const [, fieldName, fieldValue] = lessMatch;
|
|
219
|
-
queryParts.push(`${fieldName}:{* TO ${fieldValue}}`);
|
|
220
|
-
} else if (equalMatch) {
|
|
221
|
-
const [, fieldName, fieldValue] = equalMatch;
|
|
222
|
-
// 暂时注释掉 _exists_ 逻辑
|
|
223
|
-
// queryParts.push(`${fieldName}:${fieldValue} and _exists_:${fieldName}`);
|
|
224
|
-
queryParts.push(`${fieldName}:${fieldValue}`);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
queryString = queryParts.join(' and ');
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// 添加沙箱ID过滤
|
|
233
|
-
if (sandboxId) {
|
|
234
|
-
let sandboxFilter;
|
|
235
|
-
if (logFile) {
|
|
236
|
-
// 指定了 logFile,精确匹配
|
|
237
|
-
sandboxFilter = `@filename:/data/logs/${sandboxId}/${logFile}`;
|
|
238
|
-
} else if (role === Roles.SUPER_ADMIN) {
|
|
239
|
-
// 管理员:保持现有逻辑,可匹配多种来源(包含 swe-rex.log)
|
|
240
|
-
sandboxFilter = `(sandbox_id:${sandboxId} or ${sandboxId} or x22${sandboxId} or x0A${sandboxId} or @filename:/data/logs/${sandboxId}/command.log or @filename:/data/logs/${sandboxId}/swe-rex.log or @filename:/data/logs/${sandboxId}/rocklet.log)`;
|
|
241
|
-
} else {
|
|
242
|
-
// 普通用户:只能通过 @filename 白名单路径查询
|
|
243
|
-
const allowedPaths = ALLOWED_LOG_FILES
|
|
244
|
-
.map(file => `@filename:/data/logs/${sandboxId}/${file}`)
|
|
245
|
-
.join(' or ');
|
|
246
|
-
sandboxFilter = `(${allowedPaths})`;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (queryString) {
|
|
250
|
-
queryString = `${sandboxFilter} and ${queryString}`;
|
|
251
|
-
} else {
|
|
252
|
-
queryString = sandboxFilter;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return queryString;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* @param {string|null} cluster - 指定的集群名称
|
|
261
|
-
* @returns {Array} 查询列表 [{ clusterName, logType, tableName }]
|
|
262
|
-
*/
|
|
263
|
-
function getQueriesToRun(cluster) {
|
|
264
|
-
// 条件引用:内网专有模块(开源版不存在)
|
|
265
|
-
let unifiedClient;
|
|
266
|
-
try {
|
|
267
|
-
const unified_log_client = require('../../../utils/unified_log_client');
|
|
268
|
-
unifiedClient = unified_log_client.client;
|
|
269
|
-
} catch (e) {
|
|
270
|
-
// 开源版:无统一日志客户端,使用空实现
|
|
271
|
-
unifiedClient = null;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const queriesToRun = [];
|
|
275
|
-
const queriedTables = new Set();
|
|
276
|
-
const clustersToQuery = cluster
|
|
277
|
-
? [cluster]
|
|
278
|
-
: Object.keys(clusterTableMapping.clusters);
|
|
279
|
-
|
|
280
|
-
clustersToQuery.forEach(clusterName => {
|
|
281
|
-
const clusterConfig = clusterTableMapping.clusters[clusterName];
|
|
282
|
-
if (clusterConfig) {
|
|
283
|
-
Object.keys(clusterConfig).forEach(logType => {
|
|
284
|
-
const tableName = clusterConfig[logType];
|
|
285
|
-
if (tableName && !queriedTables.has(tableName)) {
|
|
286
|
-
queriedTables.add(tableName);
|
|
287
|
-
queriesToRun.push({ clusterName, logType, tableName });
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
} else if (clusterName === 'nt-c') {
|
|
291
|
-
const tableName = unifiedClient.defaultTableName;
|
|
292
|
-
if (!queriedTables.has(tableName)) {
|
|
293
|
-
queriedTables.add(tableName);
|
|
294
|
-
queriesToRun.push({ clusterName, logType: 'log', tableName });
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
return queriesToRun;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* 反转义日志字符串
|
|
304
|
-
* 参考: python3 -c "import sys; print(sys.stdin.read().encode().decode('unicode_escape').encode('latin-1').decode('utf-8'))"
|
|
305
|
-
* @param {string} str - 需要反转义的字符串
|
|
306
|
-
* @returns {string|null} 反转义后的字符串
|
|
307
|
-
*/
|
|
308
|
-
function unescapeLogString(str) {
|
|
309
|
-
if (!str || str === '-') return null;
|
|
310
|
-
|
|
311
|
-
// 处理 \xXX 格式的转义字符
|
|
312
|
-
const hexEscaped = str.replace(/\\x([0-9A-Fa-f]{2})/g, (match, hex) => {
|
|
313
|
-
return String.fromCharCode(parseInt(hex, 16));
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
// 处理 Unicode 转义序列 \uXXXX
|
|
317
|
-
const unicodeEscaped = hexEscaped.replace(/\\u([0-9A-Fa-f]{4})/g, (match, hex) => {
|
|
318
|
-
return String.fromCharCode(parseInt(hex, 16));
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// 处理常见的转义字符
|
|
322
|
-
const commonEscaped = unicodeEscaped
|
|
323
|
-
.replace(/\\n/g, '\n')
|
|
324
|
-
.replace(/\\r/g, '\r')
|
|
325
|
-
.replace(/\\t/g, '\t')
|
|
326
|
-
.replace(/\\"/g, '"')
|
|
327
|
-
.replace(/\\\\/g, '\\')
|
|
328
|
-
.replace(/\\'/g, "'");
|
|
329
|
-
|
|
330
|
-
return commonEscaped;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* 从 nginx 日志中提取沙箱ID
|
|
335
|
-
* 优先级:
|
|
336
|
-
* 1. 从 URI 参数中提取 sandbox_id
|
|
337
|
-
* 2. 从反转义后的 request_body 中提取(JSON 或表单格式)
|
|
338
|
-
* @param {Object} log - nginx 日志对象
|
|
339
|
-
* @returns {string|null} 沙箱ID,未找到返回 null
|
|
340
|
-
*/
|
|
341
|
-
function extractSandboxIdFromNginxLog(log) {
|
|
342
|
-
// 1. 从 URI 参数中提取 sandbox_id
|
|
343
|
-
const uri = log.request_uri || '';
|
|
344
|
-
const uriMatch = uri.match(/[?&]sandbox_id=([^&]+)/);
|
|
345
|
-
if (uriMatch) {
|
|
346
|
-
return uriMatch[1];
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// 2. 从 request_body 中提取
|
|
350
|
-
const requestBody = log.request_body || '';
|
|
351
|
-
if (!requestBody || requestBody === '-') {
|
|
352
|
-
return null;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
try {
|
|
356
|
-
// 反转义 request_body
|
|
357
|
-
const unescapedBody = unescapeLogString(requestBody);
|
|
358
|
-
if (!unescapedBody) {
|
|
359
|
-
return null;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// 尝试解析为 JSON
|
|
363
|
-
try {
|
|
364
|
-
const jsonData = JSON.parse(unescapedBody);
|
|
365
|
-
if (jsonData.sandbox_id) {
|
|
366
|
-
return jsonData.sandbox_id;
|
|
367
|
-
}
|
|
368
|
-
} catch (e) {
|
|
369
|
-
// 不是 JSON,尝试解析为表单格式
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// 尝试解析为表单格式 (application/x-www-form-urlencoded)
|
|
373
|
-
const formDataPairs = unescapedBody.split('&');
|
|
374
|
-
for (const pair of formDataPairs) {
|
|
375
|
-
const [key, value] = pair.split('=');
|
|
376
|
-
if (key === 'sandbox_id' && value) {
|
|
377
|
-
return decodeURIComponent(value);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// 尝试解析为 multipart/form-data 格式
|
|
382
|
-
const formDataMatch = unescapedBody.match(/name="sandbox_id"[\s\S]*?\r?\n\r?\n([\s\S]*?)(?:\r?\n--|$)/);
|
|
383
|
-
if (formDataMatch) {
|
|
384
|
-
return formDataMatch[1].trim();
|
|
385
|
-
}
|
|
386
|
-
} catch (error) {
|
|
387
|
-
// 解析失败,返回 null
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return null;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* 移除 URI 的前缀(如 /apis/envs/sandbox/v1/)
|
|
395
|
-
* @param {string} uri - 原始 URI
|
|
396
|
-
* @param {string} prefix - 要移除的前缀,默认为 /apis/envs/sandbox/v1/
|
|
397
|
-
* @returns {string} 移除前缀后的 URI
|
|
398
|
-
*/
|
|
399
|
-
function removeUriPrefix(uri, prefix = '/apis/envs/sandbox/v1/') {
|
|
400
|
-
if (!uri || typeof uri !== 'string') {
|
|
401
|
-
return uri || '';
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// 移除前缀
|
|
405
|
-
if (uri.startsWith(prefix)) {
|
|
406
|
-
return uri.substring(prefix.length);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return uri;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
module.exports = {
|
|
413
|
-
clusterTableMapping,
|
|
414
|
-
isBlacklisted,
|
|
415
|
-
parseTime,
|
|
416
|
-
getLogUniqueKey,
|
|
417
|
-
extractSourceAndFilename,
|
|
418
|
-
areLogsConsecutive,
|
|
419
|
-
buildQueryString,
|
|
420
|
-
getQueriesToRun,
|
|
421
|
-
unescapeLogString,
|
|
422
|
-
extractSandboxIdFromNginxLog,
|
|
423
|
-
removeUriPrefix,
|
|
424
|
-
};
|