Files
go_bt/src/core/bit_api.js
T

723 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file bit_api.js
* @brief 比特浏览器 Local API 封装
* @details 基于官方文档 https://doc2.bitbrowser.cn/
* 所有接口使用 POST 方法,JSON 格式
*/
const axios = require('axios');
const { getSetting } = require('./database');
// ==================== 信号量(限制并发打开浏览器请求) ====================
/**
* 简单信号量,用于限制同时进行的 openBrowser API 请求数
* 避免比特浏览器因请求过于集中而返回 429
*/
class Semaphore {
constructor(max) {
this.max = max;
this.current = 0;
this.queue = [];
}
acquire() {
return new Promise(resolve => {
if (this.current < this.max) {
this.current++;
resolve();
} else {
this.queue.push(resolve);
}
});
}
release() {
this.current--;
if (this.queue.length > 0 && this.current < this.max) {
this.current++;
this.queue.shift()();
}
}
/**
* 动态调整并发上限,并立即释放新增的可用名额。
* @param {number} max - 新的并发上限
* @returns {number} 标准化后的并发上限
*/
setMax(max) {
this.max = Math.max(1, Math.min(100, parseInt(max, 10) || 1));
while (this.current < this.max && this.queue.length > 0) {
this.current++;
this.queue.shift()();
}
return this.max;
}
/** 返回当前信号量状态,供诊断和测试使用。 */
getStats() {
return { max: this.max, current: this.current, queued: this.queue.length };
}
}
// 初始值仅用于任务配置前;每个 Worker 启动后会同步为用户设置的任务并发数。
const _openBrowserSem = new Semaphore(2);
/**
* 让比特浏览器打开请求并发跟随任务并发。
* 每秒启动速度仍由 browser_lifecycle 的 openThrottle 独立控制。
* @param {number} concurrency - 当前任务并发数
* @returns {number} 实际采用的打开请求并发数
*/
function setOpenBrowserConcurrency(concurrency) {
return _openBrowserSem.setMax(concurrency);
}
// ==================== API 类 ====================
class BitBrowserAPI {
/**
* 构造函数
* @param {string} [baseUrl='http://127.0.0.1:54345'] - 比特浏览器 API 基础 URL
*/
constructor(baseUrl = 'http://127.0.0.1:54345') {
this.baseUrl = baseUrl;
this.headers = { 'Content-Type': 'application/json' };
}
/**
* 发送 POST 请求到比特浏览器 API
* @param {string} endpoint - API 路径(如 '/browser/open')
* @param {object} [data={}] - 请求体数据
* @param {number} [timeout=30000] - 请求超时时间(毫秒)
* @returns {Promise<object>} API 响应数据
*/
async request(endpoint, data = {}, timeout = 30000) {
const url = `${this.baseUrl}${endpoint}`;
for (let attempt = 0; attempt < 3; attempt++) {
try {
const response = await axios.post(url, data, {
headers: this.headers,
timeout,
});
return response.data;
} catch (e) {
if (e.response && e.response.status === 429 && attempt < 2) {
const delay = (attempt + 1) * 1000; // 1s, 2s
await new Promise(r => setTimeout(r, delay));
continue;
}
if (e.response) {
return { success: false, msg: `HTTP ${e.response.status}: ${e.response.statusText}` };
}
return { success: false, msg: `请求失败: ${e.message}` };
}
}
}
/**
* 检测比特浏览器是否在线
* @returns {Promise<object>} 健康检查响应
*/
async healthCheck() {
return this.request('/health');
}
/**
* 创建新的浏览器窗口
* @param {object} [options={}] - 创建选项
* @param {string} [options.name='new browser'] - 窗口名称
* @param {string} [options.groupId] - 分组 ID
* @param {object} [options.browserFingerprint] - 指纹配置
* @param {number} [options.proxyMethod=2] - 代理方式
* @param {string} [options.proxyType='noproxy'] - 代理类型
* @returns {Promise<object>} 创建结果,data.id 为新窗口 ID
*/
async createBrowser({ name = 'new browser', groupId, browserFingerprint, proxyMethod = 2, proxyType = 'noproxy', ...rest } = {}) {
const data = {
name,
proxyMethod,
proxyType,
browserFingerPrint: browserFingerprint || {},
...rest,
};
if (groupId) data.groupId = groupId;
return this.request('/browser/update', data);
}
/**
* 打开指定浏览器窗口
* @param {string} browserId - 浏览器窗口 ID
* @param {object} [options={}] - 打开选项
* @param {string[]} [options.args] - 启动参数
* @param {boolean} [options.queue=true] - 是否排队等待
* @param {boolean} [options.ignoreDefaultUrls] - 是否忽略默认标签页
* @param {string} [options.newPageUrl] - 打开后跳转到的 URL
* @returns {Promise<object>} 打开结果,data.ws 为 WebSocket 端点
*/
async openBrowser(browserId, { args, queue = true, ignoreDefaultUrls, newPageUrl } = {}) {
const data = { id: browserId, queue };
if (args) data.args = args;
if (ignoreDefaultUrls) data.ignoreDefaultUrls = ignoreDefaultUrls;
if (newPageUrl) data.newPageUrl = newPageUrl;
return this.request('/browser/open', data, 60000);
}
/**
* 关闭指定浏览器窗口
* @param {string} browserId - 浏览器窗口 ID
* @returns {Promise<object>} 关闭结果
*/
async closeBrowser(browserId) {
return this.request('/browser/close', { id: browserId });
}
/**
* 删除指定浏览器窗口(含数据)
* @param {string} browserId - 浏览器窗口 ID
* @returns {Promise<object>} 删除结果
*/
async deleteBrowser(browserId) {
return this.request('/browser/delete', { id: browserId });
}
/**
* 批量删除浏览器窗口(最多 100 个)
* @param {string[]} browserIds - 浏览器窗口 ID 数组
* @returns {Promise<object>} 删除结果
*/
async deleteBrowsers(browserIds) {
return this.request('/browser/delete/ids', { ids: browserIds.slice(0, 100) });
}
/**
* 获取浏览器窗口详情
* @param {string} browserId - 浏览器窗口 ID
* @returns {Promise<object>} 窗口详情
*/
async getBrowserDetail(browserId) {
return this.request('/browser/detail', { id: browserId });
}
/**
* 分页列举浏览器窗口
* @param {object} [options={}] - 查询选项
* @param {number} [options.page=0] - 页码
* @param {number} [options.pageSize=10] - 每页数量(最大 100)
* @param {string} [options.groupId] - 按分组过滤
* @param {string} [options.name] - 按名称过滤
* @param {string} [options.sort='desc'] - 排序方向
* @returns {Promise<object>} 窗口列表
*/
async listBrowsers({ page = 0, pageSize = 10, groupId, name, sort = 'desc' } = {}) {
const data = { page, pageSize: Math.min(pageSize, 100), sort };
if (groupId) data.groupId = groupId;
if (name) data.name = name;
return this.request('/browser/list', data);
}
/**
* 批量更新浏览器代理设置
* @param {string[]} browserIds - 浏览器窗口 ID 数组
* @param {object} [options={}] - 代理配置
* @param {number} [options.proxyMethod=2] - 代理方式
* @param {string} [options.proxyType='noproxy'] - 代理类型
* @returns {Promise<object>} 更新结果
*/
async updateBrowserProxy(browserIds, { proxyMethod = 2, proxyType = 'noproxy', ...rest } = {}) {
return this.request('/browser/proxy/update', {
ids: browserIds,
proxyMethod,
proxyType,
...rest,
});
}
/**
* 查询浏览器进程是否存活
* @param {string[]} browserIds - 浏览器窗口 ID 数组
* @returns {Promise<object>} 进程状态映射,data[id] 为进程 PID 或 0
*/
async getAliveBrowserPids(browserIds) {
return this.request('/browser/pids/alive', { ids: browserIds });
}
}
// ==================== 全局实例(单例) ====================
let _apiInstance = null;
/**
* 从数据库设置中读取比特浏览器端口号
* @returns {number} 端口号,默认 54345
*/
function getBitBrowserPort() {
try {
const portStr = getSetting('bit_browser_port', '54345');
return parseInt(portStr, 10) || 54345;
} catch (e) {
return 54345;
}
}
/**
* 获取 BitBrowserAPI 单例实例
* @param {boolean} [forceNew=false] - 是否强制创建新实例(用于重读端口配置)
* @returns {BitBrowserAPI} API 实例
*/
function getApi(forceNew = false) {
if (!_apiInstance || forceNew) {
const port = getBitBrowserPort();
_apiInstance = new BitBrowserAPI(`http://127.0.0.1:${port}`);
}
return _apiInstance;
}
/**
* 重置 API 单例,下次调用 getApi 时重新创建
*/
function resetApi() {
_apiInstance = null;
}
// ==================== 代理设置 ====================
/**
* 在打开浏览器前根据账号配置更新代理设置
* 若浏览器已在运行则跳过代理更新
* @param {string} browserId - 浏览器窗口 ID
* @param {string} [email=''] - 账号邮箱,用于从数据库读取代理配置
* @returns {Promise<{success: boolean, msg?: string}>} 操作结果
*/
async function applyProxyBeforeOpen(browserId, email = '') {
try {
if (!email) return { success: true };
const { getAccount } = require('./database');
const account = getAccount(email);
if (!account) {
console.log(`[Proxy] 未找到账号记录,跳过代理设置: ${email}`);
return { success: true };
}
const proxyIp = (account.proxy_ip || '').trim();
console.log(`[Proxy] 准备设置代理: email=${email}, proxy_ip='${proxyIp}'`);
const api = getApi();
// 检查浏览器是否已在运行
const pidsResult = await api.getAliveBrowserPids([browserId]);
if (pidsResult.success) {
const pidsData = pidsResult.data || {};
if (pidsData[browserId]) {
console.log(`[Proxy] 浏览器已在运行中,跳过代理更新: ${browserId}`);
return { success: true };
}
}
if (proxyIp) {
const lower = proxyIp.toLowerCase();
if (['noproxy', 'direct', 'none'].includes(lower)) {
const result = await api.updateBrowserProxy([browserId], { proxyMethod: 2, proxyType: 'noproxy' });
if (!result.success) return { success: false, msg: result.msg || '代理设置失败' };
console.log(`[Proxy] 已设置为直连: ${browserId}`);
return { success: true };
}
const parts = proxyIp.split(':');
if (parts.length < 2) {
console.log(`[Proxy] 代理格式错误: ${proxyIp}`);
return { success: false, msg: `代理格式错误: ${proxyIp}` };
}
const proxyConfig = {
proxyMethod: 2,
proxyType: 'socks5',
host: parts[0].trim(),
port: parts[1].trim(),
proxyUserName: parts[2] ? parts[2].trim() : '',
proxyPassword: parts[3] ? parts[3].trim() : '',
};
console.log(`[Proxy] 代理配置: host=${proxyConfig.host}, port=${proxyConfig.port}`);
const result = await api.updateBrowserProxy([browserId], proxyConfig);
if (!result.success) {
const msg = result.msg || '';
if (msg.includes('操作失败')) {
console.log(`[Proxy] 代理更新被拒绝(浏览器可能已运行),跳过: ${msg}`);
return { success: true };
}
return { success: false, msg: msg || '代理设置失败' };
}
console.log(`[Proxy] 设置代理成功: ${browserId}`);
} else {
console.log(`[Proxy] 未配置代理,跳过设置: ${browserId}`);
}
return { success: true };
} catch (e) {
console.log(`[Proxy] 设置代理异常: ${e.message}`);
return { success: false, msg: e.message };
}
}
// ==================== 便捷函数 ====================
/**
* 打开浏览器(含代理预设置)
* @param {string} browserId - 浏览器窗口 ID
* @param {object} [options={}] - 打开选项
* @param {string[]} [options.args] - 启动参数
* @param {boolean} [options.queue=true] - 是否排队
* @param {string} [options.email=''] - 账号邮箱,用于设置代理
* @param {string} [options.newPageUrl] - 打开后跳转 URL
* @returns {Promise<object>} 打开结果,data.ws 为 WebSocket 端点
*/
async function openBrowser(browserId, { args, queue = true, email = '', newPageUrl } = {}) {
const proxyResult = await applyProxyBeforeOpen(browserId, email);
if (!proxyResult.success) return proxyResult;
return getApi().openBrowser(browserId, { args, queue, newPageUrl });
}
/**
* 带重试机制的浏览器打开函数
* @param {string} browserId - 浏览器窗口 ID
* @param {object} [options={}] - 选项
* @param {string} [options.email=''] - 账号邮箱
* @param {number} [options.maxRetries=3] - 最大重试次数
* @param {number} [options.retryDelay=2000] - 重试间隔(毫秒)
* @param {Function} [options.logCallback] - 日志回调函数,接收一个字符串参数
* @returns {Promise<object>} 打开结果,data.ws 为 WebSocket 端点
*/
async function openBrowserWithRetry(browserId, { email = '', maxRetries = 3, retryDelay = 2000, logCallback, ...opts } = {}) {
const log = (msg) => { if (logCallback) logCallback(msg); };
let lastResult = { success: false, msg: '未知错误' };
const RATE_LIMIT_RE = /429|请求频繁|rate.?limit|too.?many/i;
for (let attempt = 0; attempt < maxRetries; attempt++) {
// 重试前等待:429 用更长的退避,其他错误用 retryDelay
if (attempt > 0) {
const isRateLimit = RATE_LIMIT_RE.test(lastResult.msg || '');
const delay = isRateLimit ? attempt * 5000 : retryDelay;
log(`打开失败: ${lastResult.msg || ''},${delay / 1000}秒后重试...`);
await new Promise(r => setTimeout(r, delay));
}
log(`尝试打开浏览器 (第${attempt + 1}/${maxRetries}次)`);
// 通过信号量排队,限制同时发往比特浏览器的 open 请求数
await _openBrowserSem.acquire();
try {
const result = await openBrowser(browserId, { email, ...opts });
if (result.success) {
const data = result.data;
if (data && data.ws) return result;
lastResult = { success: false, msg: `API返回成功但ws端点为空: ${JSON.stringify(data)}` };
} else {
lastResult = result;
}
} catch (e) {
lastResult = { success: false, msg: e.message };
} finally {
_openBrowserSem.release();
}
}
log(`浏览器打开失败(已重试${maxRetries}次): ${lastResult.msg || ''}`);
return lastResult;
}
/**
* 关闭指定浏览器窗口
* @param {string} browserId - 浏览器窗口 ID
* @returns {Promise<object>} 关闭结果
*/
async function closeBrowser(browserId) {
return getApi().closeBrowser(browserId);
}
/**
* 获取所有浏览器窗口的简单列表
* @param {object} [options={}] - 查询选项
* @param {number} [options.page=0] - 页码
* @param {number} [options.maxItems=10000] - 最多读取的窗口总数
* @param {number} [options.requestPageSize=100] - 每次向比特API请求的数量(API上限100)
* @param {number} [options.pageSize] - 旧参数,未传 maxItems 时作为总数上限兼容
* @returns {Promise<object[]>} 浏览器窗口对象数组
*/
async function collectBrowserPages(api, options = {}) {
const { page = 0 } = options || {};
const startPage = Math.max(0, parseInt(page, 10) || 0);
const legacyLimit = options && options.pageSize !== undefined ? options.pageSize : null;
const maxItems = Math.max(1, parseInt(
options && options.maxItems !== undefined ? options.maxItems : (legacyLimit || 10000),
10,
) || 10000);
const pageRequestSize = Math.max(1, Math.min(100, parseInt(
options && options.requestPageSize !== undefined ? options.requestPageSize : 100,
10,
) || 100));
const browsers = [];
const seenIds = new Set();
let currentPage = startPage;
let totalNum = null;
const maxPages = Math.max(1, Math.ceil(maxItems / pageRequestSize) + 1);
while (browsers.length < maxItems && currentPage < startPage + maxPages) {
const requestSize = Math.min(pageRequestSize, maxItems - browsers.length);
const response = await api.listBrowsers({ page: currentPage, pageSize: requestSize });
if (!response || !response.success) break;
const data = response.data;
const list = Array.isArray(data) ? data : (data && Array.isArray(data.list) ? data.list : []);
if (data && !Array.isArray(data) && Number.isFinite(Number(data.totalNum))) {
totalNum = Number(data.totalNum);
}
if (list.length === 0) break;
const countBeforePage = browsers.length;
for (const browser of list) {
const id = String((browser && (browser.id || browser.browserId || browser.browser_id)) || '');
if (id && seenIds.has(id)) continue;
if (id) seenIds.add(id);
// 下游统一按 id 建立映射,兼容部分比特版本返回 browserId/browser_id。
browsers.push(id && browser.id !== id ? { ...browser, id } : browser);
if (browsers.length >= maxItems) break;
}
if (totalNum !== null && browsers.length >= totalNum) break;
if (browsers.length === countBeforePage) break;
if (list.length < requestSize) break;
currentPage++;
}
return browsers;
}
async function getBrowserListSimple(options = {}) {
return collectBrowserPages(getApi(), options);
}
/**
* 将账号中保存的 host:port:user:password 代理转为比特浏览器代理配置。
* 空值表示直连。
*/
function parseAccountProxy(proxyValue) {
const value = String(proxyValue || '').trim();
if (!value) return { proxyMethod: 2, proxyType: 'noproxy' };
const parts = value.split(':');
const host = String(parts.shift() || '').trim();
const port = String(parts.shift() || '').trim();
if (!host || !port) return null;
const proxyUserName = String(parts.shift() || '').trim();
const proxyPassword = parts.join(':').trim();
return {
proxyMethod: 2,
proxyType: 'socks5',
host,
port,
proxyUserName,
proxyPassword,
};
}
/**
* 根据账号信息创建比特浏览器窗口
* @param {object} account - 账号信息,包含 email、proxy_ip 等字段
* @param {string} [browserName='默认模板'] - 窗口名称(账号无邮箱时使用)
* @param {object[]|null} [existingBrowsers=null] - 已有浏览器列表,为 null 时自动获取
* @param {boolean} [reuseExisting=false] - 同名窗口已存在时是否直接复用
* @returns {Promise<{browserId: string|null, error: string|null}>} 创建结果
*/
async function createBrowserFromAccount(account, browserName = '默认模板', existingBrowsers = null, reuseExisting = false) {
const api = getApi();
const jsonData = { ...DEFAULT_BROWSER_TEMPLATE };
jsonData.name = account.email || browserName;
// 设置代理
const proxyConfig = parseAccountProxy(account.proxy_ip);
if (!proxyConfig) return { browserId: null, error: `代理格式错误: ${account.proxy_ip}` };
Object.assign(jsonData, proxyConfig);
// 检查是否已存在该账号的窗口
const browsers = existingBrowsers !== null ? existingBrowsers : await getBrowserListSimple();
for (const b of browsers) {
const matchName = account.email && (b.name === account.email || b.userName === account.email);
if (matchName) {
// 检查是否为家庭组母号,如果是则复用
try {
const detail = await api.request('/browser/detail', { id: b.id });
if (detail.success && detail.data && detail.data.remark === '家庭组母号') {
console.log(`[BitAPI] 发现家庭组母号浏览器,复用: ${b.name} (${b.id})`);
const { updateAccountTag } = require('./database');
updateAccountTag(account.email, '家庭组母号');
return { browserId: b.id, browserSeq: b.seq ?? null, error: null, isFamily: true };
}
} catch (_) {}
if (reuseExisting) {
console.log(`[BitAPI] 发现已有同名窗口,直接复用: ${b.name} (${b.id})`);
return { browserId: b.id, browserSeq: b.seq ?? null, error: null, reused: true };
}
return { browserId: null, error: `该账号已有对应窗口: ${b.name} (ID: ${b.id})` };
}
}
try {
// 遇到"请求频繁"时最多重试 3 次,每次等待时间递增
const MAX_RETRIES = 3;
const RATE_LIMIT_PATTERN = /请求频繁|rate.?limit|too.?many.?request/i;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
const result = await api.request('/browser/update', jsonData, 30000);
if (result.success) {
const browserId = result.data && result.data.id;
if (!browserId) return { browserId: null, error: 'API返回成功但未获取到ID' };
const browserSeq = result.data.seq ?? result.data.browserSeq ?? result.data.browser_seq ?? null;
return { browserId, browserSeq, error: null };
}
const msg = result.msg || '';
if (RATE_LIMIT_PATTERN.test(msg) && attempt < MAX_RETRIES) {
const waitMs = attempt * 2000; // 2s, 4s
console.log(`[BitAPI] 创建浏览器请求频繁,${waitMs / 1000}s 后重试 (${attempt}/${MAX_RETRIES}): ${msg}`);
await new Promise(r => setTimeout(r, waitMs));
continue;
}
return { browserId: null, error: msg || '创建失败' };
}
return { browserId: null, error: '请求频繁,重试次数已耗尽' };
} catch (e) {
return { browserId: null, error: `请求异常: ${e.message}` };
}
}
/**
* 逐个删除浏览器窗口(带回调)
* @param {string[]} browserIds - 要删除的浏览器 ID 数组
* @param {Function|null} [callback=null] - 每次删除后的回调,接收 (browserId, success, msg)
* @returns {Promise<{successCount: number, total: number}>} 删除统计
*/
async function deleteBrowsersBatch(browserIds, callback = null) {
const api = getApi();
const normalizedIds = Array.from(new Set((Array.isArray(browserIds) ? browserIds : []).filter(Boolean)));
let successCount = 0;
for (let start = 0; start < normalizedIds.length; start += 100) {
const chunk = normalizedIds.slice(start, start + 100);
try {
// 中文注释:优先走比特浏览器原生批量接口,显著缩短大批量删除耗时。
const batchResult = await api.deleteBrowsers(chunk);
if (batchResult && batchResult.success === true) {
successCount += chunk.length;
if (callback) chunk.forEach(browserId => callback(browserId, true, ''));
continue;
}
} catch (_) {}
// 中文注释:批量接口不可用时再回退到单个删除,保持兼容旧环境。
for (let i = 0; i < chunk.length; i++) {
const browserId = chunk[i];
const result = await api.deleteBrowser(browserId);
const success = result.success === true;
if (success) successCount++;
if (callback) callback(browserId, success, success ? '' : (result.msg || ''));
if (i < chunk.length - 1) await new Promise(r => setTimeout(r, 80));
}
}
return { successCount, total: normalizedIds.length };
}
// ==================== 默认浏览器模板 ====================
const DEFAULT_BROWSER_TEMPLATE = {
browserFingerPrint: {
coreVersion: '148',
ostype: 'PC',
os: 'Win32',
architecture: 'x86',
osVersion: '',
platformVersion: '10.0.0',
version: '',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36',
isIpCreateTimeZone: true,
isIpCreatePosition: true,
isIpCreateDisplayLanguage: true,
isIpCreateLanguage: true,
webRTC: '3',
position: '1',
resolutionType: '0',
resolution: '1920 x 1080',
openWidth: 1280,
openHeight: 720,
fontType: '0',
canvas: '0',
webGL: '0',
webGLMeta: '0',
audioContext: '0',
mediaDevice: '1',
speechVoices: '0',
hardwareConcurrency: '6',
deviceMemory: '8',
deviceInfoEnabled: true,
clientRectNoiseEnabled: true,
doNotTrack: '0',
flash: '1',
portScanProtect: '0',
colorDepth: 24,
devicePixelRatio: 1,
enablePlugins: false,
windowSizeLimit: true,
coreProduct: 'chrome',
navigatorVendor: 'Google Inc.',
defaultAccuracy: 10,
},
proxyMethod: 2,
proxyType: 'noproxy',
ipCheckService: 'ip-api',
syncTabs: false,
syncCookies: true,
syncIndexedDb: true,
syncLocalStorage: true,
syncBookmarks: false,
syncAuthorization: true,
syncHistory: false,
syncGoogleAccount: false,
syncExtensions: false,
syncUserExtensions: false,
syncSessions: false,
allowedSignin: false,
clearCacheFilesBeforeLaunch: false,
clearCookiesBeforeLaunch: false,
clearHistoriesBeforeLaunch: false,
randomFingerprint: false,
muteAudio: false,
disableGpu: false,
enableBackgroundMode: false,
credentialsEnableService: false,
disableTranslatePopup: false,
disableClipboard: false,
disableNotifications: false,
memorySaver: false,
abortImage: false,
abortMedia: false,
stopWhileNetError: false,
stopWhileCountryChange: false,
stopWhileIpChange: false,
isRandomFinger: true,
remarkType: 1,
duplicateCheck: 0,
workbench: 'localserver',
};
module.exports = {
BitBrowserAPI,
getApi,
resetApi,
openBrowser,
openBrowserWithRetry,
setOpenBrowserConcurrency,
closeBrowser,
getBrowserListSimple,
createBrowserFromAccount,
deleteBrowsersBatch,
DEFAULT_BROWSER_TEMPLATE,
_private: { Semaphore, openBrowserSemaphore: _openBrowserSem, collectBrowserPages, parseAccountProxy },
};