feat: 按任务管理比特浏览器临时窗口
This commit is contained in:
@@ -2,7 +2,7 @@ const assert = require('assert');
|
||||
|
||||
const {
|
||||
setOpenBrowserConcurrency,
|
||||
_private: { Semaphore, openBrowserSemaphore, collectBrowserPages },
|
||||
_private: { Semaphore, openBrowserSemaphore, collectBrowserPages, parseAccountProxy },
|
||||
} = require('../src/core/bit_api');
|
||||
|
||||
(async () => {
|
||||
@@ -57,6 +57,16 @@ const {
|
||||
assert.strictEqual(largeBrowsers.length, 1250, '完整同步不能在1000个窗口处截断');
|
||||
assert.strictEqual(largePageCalls.length, 13, '1250个窗口应按100个一页读取完成');
|
||||
|
||||
assert.deepStrictEqual(parseAccountProxy(''), { proxyMethod: 2, proxyType: 'noproxy' });
|
||||
assert.deepStrictEqual(parseAccountProxy('127.0.0.1:1080:user:p:ass'), {
|
||||
proxyMethod: 2,
|
||||
proxyType: 'socks5',
|
||||
host: '127.0.0.1',
|
||||
port: '1080',
|
||||
proxyUserName: 'user',
|
||||
proxyPassword: 'p:ass',
|
||||
});
|
||||
|
||||
console.log('bit_api_open_concurrency.test.js: PASS');
|
||||
})().catch(error => {
|
||||
console.error(error);
|
||||
|
||||
@@ -97,6 +97,26 @@ function yieldEventLoop() {
|
||||
assert.strictEqual(deferredBatch.completed, 3, '延迟重试不应重复累计完成进度');
|
||||
assert.strictEqual(deferredBatch.results[0].attempt, 2, '延迟项目到期后应在原索引写入最终结果');
|
||||
|
||||
const heldSlotRuns = [];
|
||||
const heldSlotAttempts = new Map();
|
||||
const heldSlotBatch = await runConcurrentItems(['sleeping', 'next'], async item => {
|
||||
heldSlotRuns.push(item);
|
||||
const attempt = (heldSlotAttempts.get(item) || 0) + 1;
|
||||
heldSlotAttempts.set(item, attempt);
|
||||
if (item === 'sleeping' && attempt === 1) return { defer: true, delayMs: 20 };
|
||||
return { item, attempt };
|
||||
}, {
|
||||
concurrency: 1,
|
||||
deferReleasesSlot: false,
|
||||
getDeferDelay: result => (result && result.defer ? result.delayMs : 0),
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
heldSlotRuns,
|
||||
['sleeping', 'sleeping', 'next'],
|
||||
'临时比特浏览器任务延迟等待时必须占住串行槽,删除后才能轮到下一账号',
|
||||
);
|
||||
assert.strictEqual(heldSlotBatch.completed, 2, '占住串行槽时仍应完成全部项目');
|
||||
|
||||
console.log('concurrent_runner_resilience.test.js: PASS');
|
||||
})().catch(error => {
|
||||
console.error(error);
|
||||
|
||||
@@ -28,8 +28,10 @@ function responseStub() {
|
||||
'获取Cli凭证按钮必须位于获取凭证和链接提取按钮之间');
|
||||
assert.match(html, /Google 账号取号 API[\s\S]{0,2500}googleAccountCredentialForm\.value/,
|
||||
'设置页应提供加密取号 API 配置');
|
||||
assert.match(html, /处理账号数量(1~5000)[\s\S]{0,800}并发(1~30)/,
|
||||
'任务弹窗应同时提供数量和并发');
|
||||
assert.match(html, /处理账号数量(1~5000)[\s\S]{0,800}并发参数(兼容保留)/,
|
||||
'任务弹窗应说明并发参数只为兼容保留');
|
||||
assert.match(html, /临时比特浏览器[\s\S]{0,120}严格按邮箱串行处理:完成一个、删除一个,再开始下一个/,
|
||||
'任务弹窗应告知用户临时比特浏览器严格串行');
|
||||
assert(server.includes("/api/google-account-credentials"), '服务端应挂载取号凭据路由');
|
||||
assert(operations.includes("'google_account_ag_credential'"), '取号任务必须受本机凭据任务保护');
|
||||
assert(operations.includes('importAccountsFromText'), 'Worker 应通过主线程安全导入新账号');
|
||||
@@ -44,15 +46,18 @@ function responseStub() {
|
||||
'链接取号导入账号必须使用默认客户名“企业字-Free”');
|
||||
assert.match(worker, /fetchGoogleAccounts\(\{[\s\S]{0,220}quantity:\s*1/,
|
||||
'每次供应接口调用只能提取 1 个账号');
|
||||
assert(worker.includes('const taskConcurrency = 1;'),
|
||||
'浏览器账号必须固定串行,删除当前临时窗口后再处理下一个');
|
||||
assert(worker.includes('const taskResults = await runConcurrent(taskSlots'),
|
||||
'账号应由用户设定并发数驱动滑动队列,而不是写死固定工位数');
|
||||
'账号应通过滑动队列按顺序处理');
|
||||
const fetchOneAt = worker.indexOf('quantity: 1,');
|
||||
const createBrowserAt = worker.indexOf("createBrowserFromAccount(account, '默认模板'", fetchOneAt);
|
||||
const createBrowserAt = worker.indexOf('taskBrowserLifecycle.run(account', fetchOneAt);
|
||||
const acquireCredentialAt = worker.indexOf("taskType: 'ag_get_credential'", createBrowserAt);
|
||||
assert(fetchOneAt >= 0 && createBrowserAt > fetchOneAt && acquireCredentialAt > createBrowserAt,
|
||||
'每个工位必须按“取 1 个→建浏览器→获取反重力凭证”的顺序处理');
|
||||
assert.match(worker, /browserResult\.reused[\s\S]{0,500}proxyType:\s*'noproxy'/,
|
||||
'复用孤立浏览器时也必须明确切换为无代理');
|
||||
'每个工位必须按“取 1 个→准备临时比特窗口→获取反重力凭证”的顺序处理');
|
||||
const slotTaskEnd = worker.indexOf('}, taskConcurrency', createBrowserAt);
|
||||
assert.match(worker.slice(createBrowserAt, slotTaskEnd), /forceDirect:\s*true/,
|
||||
'取号任务的临时比特窗口必须明确使用直连模式');
|
||||
assert(worker.includes('ATOM_STEPS.ag_get_credential.step'),
|
||||
'取号后必须复用反重力获取凭证步骤');
|
||||
assert(!worker.includes('supplied.raw'), 'Worker 不得记录供应商原始响应');
|
||||
|
||||
@@ -90,8 +90,14 @@ async function waitUntil(predicate, timeoutMs = 1000) {
|
||||
const importStart = taskRunnerSource.indexOf("case 'import_setup'");
|
||||
const importEnd = taskRunnerSource.indexOf('\n default:', importStart);
|
||||
const importSection = taskRunnerSource.slice(importStart, importEnd);
|
||||
assert.ok(importSection.includes('runImportBrowserBatch(readyAccounts'), '导入流程应接入并发批处理器');
|
||||
assert.ok(!importSection.includes('setTimeout(r, 600)'), '导入流程不应再为每个账号固定等待 600ms');
|
||||
const disabledAt = importSection.indexOf('导入后自动创建比特浏览器已停用');
|
||||
const firstBreakAt = importSection.indexOf('break;');
|
||||
const legacyBrowserBatchAt = importSection.indexOf('runImportBrowserBatch(readyAccounts');
|
||||
assert.ok(disabledAt >= 0, '导入初始化任务必须明确停用浏览器创建');
|
||||
assert.ok(firstBreakAt > disabledAt, '停用提示后应立即结束导入初始化任务');
|
||||
assert.ok(legacyBrowserBatchAt === -1 || firstBreakAt < legacyBrowserBatchAt, '旧浏览器批处理代码不得再被执行');
|
||||
const appSource = fs.readFileSync(path.join(__dirname, '../public/app.js'), 'utf8');
|
||||
assert.ok(!appSource.includes('_runImportSetup('), '前端导入成功后不得启动浏览器初始化任务');
|
||||
|
||||
console.log('import_browser_batch_service.test.js PASS');
|
||||
})().catch(error => {
|
||||
|
||||
@@ -9,7 +9,7 @@ const vm = require('vm');
|
||||
{ email: 'first@example.com', password: 'p1', browser_id: 'stale-window-1' },
|
||||
{ email: 'second@example.com', password: 'p2', browser_id: '' },
|
||||
];
|
||||
let operationPayload = null;
|
||||
let operationCalls = 0;
|
||||
const context = vm.createContext({
|
||||
console, Set, Map, Date, JSON, Math, String, Number, Object, Array, RegExp, Promise, Uint8Array,
|
||||
setTimeout, clearTimeout,
|
||||
@@ -18,8 +18,8 @@ const vm = require('vm');
|
||||
return { json: async () => ({ success: true, count: 2, accounts: restoredAccounts, batchName: '待恢复' }) };
|
||||
}
|
||||
if (url === '/api/operations/run') {
|
||||
operationPayload = JSON.parse(options.body);
|
||||
return { json: async () => ({ success: true, taskId: 'restore-create-task' }) };
|
||||
operationCalls += 1;
|
||||
throw new Error('恢复账号不应启动浏览器任务');
|
||||
}
|
||||
throw new Error(`未处理的测试请求: ${url}`);
|
||||
},
|
||||
@@ -37,13 +37,10 @@ const vm = require('vm');
|
||||
|
||||
await app.restoreStagedBatch();
|
||||
|
||||
assert(operationPayload, '恢复后应自动启动创建浏览器任务');
|
||||
assert.strictEqual(operationPayload.taskType, 'create_accounts');
|
||||
assert.deepStrictEqual(Array.from(operationPayload.accounts, account => account.email), ['first@example.com', 'second@example.com']);
|
||||
assert.strictEqual(operationPayload.params.reuseExistingBrowsers, true, '已有同名窗口时应复用并修正本地 ID');
|
||||
assert.strictEqual(app.isRunning, true);
|
||||
assert.strictEqual(app.currentTaskId, 'restore-create-task');
|
||||
assert.strictEqual(app.__startedTaskId, 'restore-create-task');
|
||||
assert.strictEqual(operationCalls, 0, '恢复只回写账号,比特浏览器应等具体任务开始时再创建');
|
||||
assert.strictEqual(app.isRunning, false);
|
||||
assert.strictEqual(app.currentTaskId, null);
|
||||
assert.strictEqual(app.__startedTaskId, undefined);
|
||||
assert.strictEqual(app.stagedBatchDetail, null);
|
||||
|
||||
console.log('staging_restore_browser_creation.test.js: PASS');
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const { TaskBrowserLifecycle } = require('../src/services/task_browser_lifecycle');
|
||||
|
||||
function createHarness(initialBrowsers = []) {
|
||||
const order = [];
|
||||
const notifications = [];
|
||||
const rows = new Map();
|
||||
let createCount = 0;
|
||||
const lifecycle = new TaskBrowserLifecycle({
|
||||
getBrowserListSimple: async () => initialBrowsers,
|
||||
createBrowserFromAccount: async account => {
|
||||
createCount++;
|
||||
order.push(`create:${account.email}`);
|
||||
return { browserId: `created-${createCount}`, browserSeq: createCount };
|
||||
},
|
||||
getApi: () => ({
|
||||
updateBrowserProxy: async ids => {
|
||||
order.push(`proxy:${ids[0]}`);
|
||||
return { success: true };
|
||||
},
|
||||
getAliveBrowserPids: async () => ({ success: true, data: {} }),
|
||||
}),
|
||||
closeBrowser: async id => { order.push(`close:${id}`); return { success: true }; },
|
||||
deleteBrowsersBatch: async (ids, callback) => {
|
||||
order.push(`delete:${ids[0]}`);
|
||||
callback(ids[0], true, '');
|
||||
return { successCount: 1, total: 1 };
|
||||
},
|
||||
parseAccountProxy: value => (value
|
||||
? { proxyMethod: 2, proxyType: 'socks5', host: '127.0.0.1', port: '1080' }
|
||||
: { proxyMethod: 2, proxyType: 'noproxy' }),
|
||||
dbWrite: async (fn, args) => {
|
||||
if (fn === 'updateAccountBrowserId') {
|
||||
const previous = rows.get(args[0]) || { email: args[0] };
|
||||
rows.set(args[0], { ...previous, browser_id: args[1] || '', browser_seq: args[2] ?? null });
|
||||
order.push(`db:${args[0]}:${args[1] || 'clear'}`);
|
||||
return true;
|
||||
}
|
||||
if (fn === 'getAccount') return rows.get(args[0]) || null;
|
||||
throw new Error(`unexpected db call: ${fn}`);
|
||||
},
|
||||
updateMessage: async () => {},
|
||||
acquireRequestSlot: async () => { order.push('slot'); },
|
||||
notify: message => notifications.push(message),
|
||||
});
|
||||
return { lifecycle, order, notifications, rows, getCreateCount: () => createCount };
|
||||
}
|
||||
|
||||
(async () => {
|
||||
{
|
||||
const harness = createHarness([{ id: 'bit-existing', name: 'user@example.com', seq: 7 }]);
|
||||
const account = { email: 'user@example.com', browser_id: 'stale-id', proxy_ip: '127.0.0.1:1080' };
|
||||
const result = await harness.lifecycle.run(account, async prepared => {
|
||||
harness.order.push(`work:${prepared.browser_id}`);
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(result, { ok: true });
|
||||
assert.strictEqual(harness.getCreateCount(), 0, '同邮箱比特窗口应复用,旧 ID 不应阻止重绑');
|
||||
assert(harness.order.indexOf('proxy:bit-existing') < harness.order.indexOf('work:bit-existing'));
|
||||
assert(harness.order.indexOf('work:bit-existing') < harness.order.indexOf('delete:bit-existing'));
|
||||
assert.strictEqual(account.browser_id, '', '任务结束后应清空账号浏览器绑定');
|
||||
assert.strictEqual(harness.rows.get(account.email).browser_id, '');
|
||||
assert.deepStrictEqual(harness.notifications.map(item => item.action), ['prepared', 'released']);
|
||||
}
|
||||
|
||||
{
|
||||
const harness = createHarness([]);
|
||||
const account = { email: 'new@example.com', proxy_ip: '' };
|
||||
const first = await harness.lifecycle.run(account, async prepared => ({
|
||||
__defer: true,
|
||||
browserId: prepared.browser_id,
|
||||
}));
|
||||
assert.strictEqual(first.__defer, true);
|
||||
assert.strictEqual(account.browser_id, 'created-1', '延迟重试仍属于同一账号任务,不应提前删除窗口');
|
||||
assert.strictEqual(harness.order.filter(item => item.startsWith('delete:')).length, 0);
|
||||
|
||||
const second = await harness.lifecycle.run(account, async prepared => ({ browserId: prepared.browser_id }));
|
||||
assert.strictEqual(second.browserId, 'created-1');
|
||||
assert.strictEqual(harness.getCreateCount(), 1, '延迟重试应继续复用当前临时窗口');
|
||||
assert.strictEqual(account.browser_id, '');
|
||||
assert.strictEqual(harness.order.filter(item => item === 'delete:created-1').length, 1);
|
||||
}
|
||||
|
||||
console.log('task_browser_lifecycle.test.js PASS');
|
||||
})().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user