@grasplabs/grasp
Version:
TypeScript SDK for browser automation and secure command execution in highly available and scalable cloud browser environments
133 lines • 6.19 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CodeRunner = void 0;
const logger_1 = require("../utils/logger");
const detect_code_1 = require("../utils/detect-code");
const uuid_1 = require("uuid");
class CodeRunner {
/**
* Gets or creates a default logger instance
* @returns Logger instance
*/
getDefaultLogger() {
try {
return (0, logger_1.getLogger)().child('RunnerService');
}
catch (error) {
// If logger is not initialized, create a default one
const defaultLogger = new logger_1.Logger({
level: 'info',
console: true,
});
return defaultLogger.child('RunnerService');
}
}
constructor(session) {
this.logger = this.getDefaultLogger();
this.session = session;
this.logger.info('CodeRunner initialized');
}
injectCode(code, type) {
const wsUrl = this.session.browser.getEndpoint();
if (type === 'py') {
// 替换 python 调用
code = code.replace(/\.launch\s*\(/gm, `.connect_over_cdp('${wsUrl}',`);
code = code.replace(/\.launch_persistent_context\s*\(/gm, `.connect_over_cdp('${wsUrl}',`);
// 移除 launch_persistent_context 调用中的 user_data_dir 参数(位置参数)
code = code.replace(/\.connect_over_cdp\('([^']+)',\s*'[^']*',/gm, `.connect_over_cdp('$1',`);
code = code.replace(/\.connect_over_cdp\('([^']+)',\s*"[^"]*",/gm, `.connect_over_cdp('$1',`);
// 移除单独的位置参数(没有其他参数的情况)
code = code.replace(/\.connect_over_cdp\('([^']+)',\s*'[^']*'\s*\)/gm, `.connect_over_cdp('$1')`);
code = code.replace(/\.connect_over_cdp\('([^']+)',\s*"[^"]*"\s*\)/gm, `.connect_over_cdp('$1')`);
// 只保留 slow_mo 和 timeout 参数,其他所有具名参数都删除
const paramsToReserve = ['slow_mo', 'timeout'];
// 移除所有不在保留列表中的具名参数
// 使用通用正则表达式匹配所有具名参数,然后只保留指定的参数
code = code.replace(/\.connect_over_cdp\('([^']+)'([^)]*?)\)/gm, (match, url, params) => {
if (!params.trim()) {
return `.connect_over_cdp('${url}')`;
}
// 提取所有参数
const reservedParams = [];
paramsToReserve.forEach(param => {
// 匹配简单值参数
const simpleMatch = params.match(new RegExp(`\\b${param}\\s*=\\s*[^,)\\n{\\[]+`, 'g'));
if (simpleMatch) {
reservedParams.push(...simpleMatch);
}
// 匹配字典参数
const dictMatch = params.match(new RegExp(`\\b${param}\\s*=\\s*\\{[^}]*\\}`, 'g'));
if (dictMatch) {
reservedParams.push(...dictMatch);
}
// 匹配列表参数
const listMatch = params.match(new RegExp(`\\b${param}\\s*=\\s*\\[[^\\]]*\\]`, 'g'));
if (listMatch) {
reservedParams.push(...listMatch);
}
});
// 构建结果字符串
let result;
if (reservedParams.length === 0) {
result = `.connect_over_cdp('${url}')`;
}
else {
result = `.connect_over_cdp('${url}', ${reservedParams.join(', ')})`;
}
// 只对当前匹配的函数调用进行清理,避免影响其他代码
result = result.replace(/,\s*\)/g, ')');
result = result.replace(/\(\s*,/g, '(');
result = result.replace(/,\s*,/g, ',');
// 处理只有路径参数的情况,移除多余的逗号
result = result.replace(/\.connect_over_cdp\('([^']+)',\s*\)/g, `.connect_over_cdp('$1')`);
return result;
});
}
else {
// 替换 js 调用
code = code.replace(/\.launch\s*\(/gm, `.connectOverCDP('${wsUrl}',`);
code = code.replace(/\.launchPersistentContext\s*\([^,]*?,/gm, `.connectOverCDP('${wsUrl}',`);
}
return code;
}
async run(code, options = {}) {
const type = (0, detect_code_1.detectCodeType)(code);
if (options.inject) {
code = this.injectCode(code, type);
}
const scriptFile = `/home/user/codes/${(0, uuid_1.v4)()}-script.${type === 'esm' ? 'mjs' : type}`;
await this.session.files.writeFile(scriptFile, code);
const terminal = this.session.terminal;
const timeout = options.timeout || 300000;
const result = await terminal.runCommand(`${type === 'py' ? 'python3' : 'node'} ${scriptFile}`);
result.stderr.pipe(process.stderr);
result.stdout.pipe(process.stdout);
const timer = setTimeout(() => {
this.logger.error('🕖 code execution timeout');
result.kill();
}, timeout);
return result
.json()
.then(async (result) => {
clearTimeout(timer);
let localPath = null;
try {
this.logger.info('Script execution completed, syncing downloads directory');
localPath = await this.session.files.syncDownloadsDirectory(options.syncDir || './code-runner', './code-runner');
this.logger.info('Downloads directory synced successfully');
}
catch (error) {
this.logger.error('Failed to sync downloads directory:', error);
}
finally {
// eslint-disable-next-line no-unsafe-finally
return {
...result,
outputDir: localPath,
};
}
});
}
}
exports.CodeRunner = CodeRunner;
//# sourceMappingURL=code-runner.js.map