fix 一键组合只上传邮箱列表,服务端再从数据库加载完整账号

This commit is contained in:
2026-09-24 16:01:34 +08:00
parent 4767981b42
commit 243ded46e9
10 changed files with 582 additions and 11 deletions
+14 -5
View File
@@ -3025,14 +3025,23 @@ function geminiApp() {
this.addLog(`🚀 启动任务: ${taskType}, 账号数: ${requestedCount}, 并发: ${taskThreadCount}${monitorText}`, 'info');
try {
// AI Studio 一键组合可能一次选择上万个账号。这里只提交邮箱引用,
// 完整账号由服务端从数据库重新加载,避免请求体超过 Express 的 10MB 限制。
const taskRequestBody = taskType === 'aistudio_combo'
? {
taskType,
accountEmails: taskAccounts.map(account => account.email),
params: taskParams,
}
: {
taskType,
accounts: taskAccounts,
params: taskParams,
};
const res = await fetch('/api/operations/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
taskType,
accounts: taskAccounts,
params: taskParams,
}),
body: JSON.stringify(taskRequestBody),
});
const data = await res.json();
if (!data.success) {
+140
View File
@@ -0,0 +1,140 @@
[CmdletBinding()]
param(
[string]$SourceApp = 'C:\Program Files\bitbrowser',
[string]$OutputDirectory = '',
[switch]$SkipArchive
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
function Copy-Tree {
param(
[Parameter(Mandatory = $true)][string]$Source,
[Parameter(Mandatory = $true)][string]$Destination,
[string[]]$ExcludeFiles = @()
)
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
$arguments = @($Source, $Destination, '/E', '/COPY:DAT', '/DCOPY:DAT', '/R:2', '/W:1', '/XJ', '/NFL', '/NDL', '/NP')
if ($ExcludeFiles.Count -gt 0) {
$arguments += '/XF'
$arguments += $ExcludeFiles
}
& robocopy.exe @arguments
$code = $LASTEXITCODE
if ($code -gt 7) {
throw "Copy failed with robocopy exit code $code ($Source)"
}
}
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$templateRoot = Join-Path $scriptRoot 'bitbrowser-clean-package'
$workspaceRoot = Split-Path -Parent $scriptRoot
$sourceExe = Join-Path $SourceApp ([string][char]0x6BD4 + [char]0x7279 + [char]0x6D4F + [char]0x89C8 + [char]0x5668 + '.exe')
if (-not (Test-Path -LiteralPath $sourceExe -PathType Leaf)) {
throw "BitBrowser executable was not found: $sourceExe"
}
if (-not $OutputDirectory) {
$OutputDirectory = Split-Path -Parent $workspaceRoot
}
$outputBase = [System.IO.Path]::GetFullPath($OutputDirectory)
New-Item -ItemType Directory -Path $outputBase -Force | Out-Null
$version = (Get-Item -LiteralPath $sourceExe).VersionInfo.FileVersion
if (-not $version) { $version = 'unknown' }
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$packageName = "BitBrowser-Clean-Share-$version-$stamp"
$packageRoot = Join-Path $outputBase $packageName
if (Test-Path -LiteralPath $packageRoot) {
throw "Refusing to overwrite an existing package: $packageRoot"
}
$appDestination = Join-Path $packageRoot 'App'
$seedRoaming = Join-Path $packageRoot 'Seed\Roaming\BitBrowser'
$seedLocal = Join-Path $packageRoot 'Seed\Local\BitBrowser'
New-Item -ItemType Directory -Path $seedRoaming -Force | Out-Null
New-Item -ItemType Directory -Path $seedLocal -Force | Out-Null
Write-Host "Copying application files from $SourceApp"
Copy-Tree -Source $SourceApp -Destination $appDestination -ExcludeFiles @('Uninstall BitBrowser.exe', 'Uninstall *.exe', '*.lnk')
$sourceRoaming = Join-Path $env:APPDATA 'BitBrowser'
$sourceDb = Join-Path $sourceRoaming 'db.sqlite'
if (-not (Test-Path -LiteralPath $sourceDb -PathType Leaf)) {
throw "BitBrowser database was not found: $sourceDb"
}
foreach ($directory in @('BitExtensions', 'Chrome-bin', 'chromedriver')) {
$source = Join-Path $sourceRoaming $directory
if (Test-Path -LiteralPath $source -PathType Container) {
Write-Host "Copying $directory"
Copy-Tree -Source $source -Destination (Join-Path $seedRoaming $directory)
}
}
$cleanDb = Join-Path $seedRoaming 'db.sqlite'
$dbTool = Join-Path $templateRoot 'make_clean_db.py'
$python = Get-Command py.exe -ErrorAction SilentlyContinue
if (-not $python) { $python = Get-Command python.exe -ErrorAction SilentlyContinue }
if (-not $python) { throw 'Python 3 is required to build the sanitized SQLite database.' }
Write-Host 'Creating sanitized database...'
if ($python.Name -ieq 'py.exe') {
& $python.Source -3 $dbTool --source $sourceDb --output $cleanDb
} else {
& $python.Source $dbTool --source $sourceDb --output $cleanDb
}
if ($LASTEXITCODE -ne 0) { throw 'Sanitized database creation failed.' }
Copy-Item -LiteralPath (Join-Path $templateRoot 'Install-Clean.ps1') -Destination $packageRoot
Copy-Item -LiteralPath (Join-Path $templateRoot 'Install-Clean.cmd') -Destination $packageRoot
Copy-Item -LiteralPath (Join-Path $templateRoot 'README.txt') -Destination $packageRoot
$manifestPath = Join-Path $packageRoot 'SHA256SUMS.txt'
Write-Host 'Computing SHA-256 manifest...'
$manifestLines = Get-ChildItem -LiteralPath $packageRoot -File -Recurse -Force |
Where-Object { $_.FullName -ne $manifestPath } |
Sort-Object FullName |
ForEach-Object {
$relative = $_.FullName.Substring($packageRoot.Length + 1).Replace('\', '/')
$hash = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
"$hash $relative"
}
[System.IO.File]::WriteAllLines($manifestPath, $manifestLines, [System.Text.UTF8Encoding]::new($false))
$allFiles = Get-ChildItem -LiteralPath $packageRoot -File -Recurse -Force
$totalBytes = ($allFiles | Measure-Object Length -Sum).Sum
$result = [ordered]@{
PackageDirectory = $packageRoot
Version = $version
FileCount = $allFiles.Count
UncompressedBytes = $totalBytes
Archive = $null
ArchiveSha256 = $null
}
if (-not $SkipArchive) {
$sevenZip = Join-Path $SourceApp 'resources\7zip-bin\7za.exe'
if (-not (Test-Path -LiteralPath $sevenZip -PathType Leaf)) {
throw "7-Zip command was not found: $sevenZip"
}
$archivePath = Join-Path $outputBase ($packageName + '.zip')
Write-Host "Creating archive $archivePath"
Push-Location $outputBase
try {
& $sevenZip a -tzip -mx=5 -mmt=on $archivePath $packageName
if ($LASTEXITCODE -ne 0) { throw "7-Zip failed with exit code $LASTEXITCODE" }
& $sevenZip t $archivePath
if ($LASTEXITCODE -ne 0) { throw "Archive verification failed with exit code $LASTEXITCODE" }
} finally {
Pop-Location
}
$result.Archive = $archivePath
$result.ArchiveSha256 = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
}
Write-Host ''
Write-Host 'Build complete:'
$result | ConvertTo-Json -Depth 3
@@ -0,0 +1,12 @@
@echo off
setlocal
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0Install-Clean.ps1"
if errorlevel 1 (
echo.
echo Installation failed. Read the message above, then try again.
pause
exit /b 1
)
echo.
echo Installation completed successfully.
pause
@@ -0,0 +1,136 @@
[CmdletBinding()]
param(
[switch]$NoLaunch,
[switch]$SkipShortcuts,
[switch]$SkipProcessCheck,
[string]$InstallRoot = '',
[string]$RoamingRoot = '',
[string]$LocalRoot = ''
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
function Copy-Tree {
param(
[Parameter(Mandatory = $true)][string]$Source,
[Parameter(Mandatory = $true)][string]$Destination
)
if (-not (Test-Path -LiteralPath $Source -PathType Container)) {
throw "Required source directory is missing: $Source"
}
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
& robocopy.exe $Source $Destination /E /COPY:DAT /DCOPY:DAT /R:2 /W:1 /XJ /NFL /NDL /NP
$code = $LASTEXITCODE
if ($code -gt 7) {
throw "Copy failed with robocopy exit code $code ($Source)"
}
}
function Move-ToBackup {
param([Parameter(Mandatory = $true)][string]$Path)
if (-not (Test-Path -LiteralPath $Path)) {
return $null
}
$parent = Split-Path -Parent $Path
$leaf = Split-Path -Leaf $Path
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$suffix = [Guid]::NewGuid().ToString('N').Substring(0, 6)
$backup = Join-Path $parent ($leaf + '.backup-' + $stamp + '-' + $suffix)
Move-Item -LiteralPath $Path -Destination $backup
return $backup
}
$packageRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$sourceApp = Join-Path $packageRoot 'App'
$sourceRoaming = Join-Path $packageRoot 'Seed\Roaming\BitBrowser'
$sourceLocal = Join-Path $packageRoot 'Seed\Local\BitBrowser'
$targetApp = if ($InstallRoot) { [System.IO.Path]::GetFullPath($InstallRoot) } else { Join-Path $env:LOCALAPPDATA 'Programs\BitBrowser' }
$targetRoaming = if ($RoamingRoot) { [System.IO.Path]::GetFullPath($RoamingRoot) } else { Join-Path $env:APPDATA 'BitBrowser' }
$targetLocal = if ($LocalRoot) { [System.IO.Path]::GetFullPath($LocalRoot) } else { Join-Path $env:LOCALAPPDATA 'BitBrowser' }
$targetExe = Join-Path $targetApp ([string][char]0x6BD4 + [char]0x7279 + [char]0x6D4F + [char]0x89C8 + [char]0x5668 + '.exe')
$expectedExtensionId = 'jiofmdifioeejeilfkpegipdjiopiekl'
$sourceDb = Join-Path $sourceRoaming 'db.sqlite'
$sourceExtensionManifest = Join-Path $sourceRoaming "BitExtensions\$expectedExtensionId\manifest.json"
if (-not $SkipProcessCheck) {
$running = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {
$_.Name -eq ([string][char]0x6BD4 + [char]0x7279 + [char]0x6D4F + [char]0x89C8 + [char]0x5668 + '.exe') -or
($_.ExecutablePath -and $_.ExecutablePath.StartsWith($targetApp, [System.StringComparison]::OrdinalIgnoreCase))
})
if ($running.Count -gt 0) {
throw 'BitBrowser is running. Close it and run this installer again.'
}
}
if (-not (Test-Path -LiteralPath $sourceDb -PathType Leaf)) {
throw 'The clean seed database is missing from this package.'
}
if (-not (Test-Path -LiteralPath $sourceExtensionManifest -PathType Leaf)) {
throw 'The YesCaptcha extension is missing from this package.'
}
Write-Host 'Installing BitBrowser application files...'
Copy-Tree -Source $sourceApp -Destination $targetApp
$backups = @()
$backup = Move-ToBackup -Path $targetRoaming
if ($backup) { $backups += $backup }
$backup = Move-ToBackup -Path $targetLocal
if ($backup) { $backups += $backup }
Write-Host 'Installing clean user data and preserved extensions...'
Copy-Tree -Source $sourceRoaming -Destination $targetRoaming
if (Test-Path -LiteralPath $sourceLocal -PathType Container) {
Copy-Tree -Source $sourceLocal -Destination $targetLocal
}
if (-not (Test-Path -LiteralPath $targetExe -PathType Leaf)) {
throw "Installed executable was not found: $targetExe"
}
$targetDb = Join-Path $targetRoaming 'db.sqlite'
$targetExtensionManifest = Join-Path $targetRoaming "BitExtensions\$expectedExtensionId\manifest.json"
if (-not (Test-Path -LiteralPath $targetExtensionManifest -PathType Leaf)) {
throw "YesCaptcha was not installed: $targetExtensionManifest"
}
if ((Get-FileHash -LiteralPath $sourceDb -Algorithm SHA256).Hash -ne (Get-FileHash -LiteralPath $targetDb -Algorithm SHA256).Hash) {
throw 'The clean database copy failed verification.'
}
if ((Get-FileHash -LiteralPath $sourceExtensionManifest -Algorithm SHA256).Hash -ne (Get-FileHash -LiteralPath $targetExtensionManifest -Algorithm SHA256).Hash) {
throw 'The YesCaptcha extension copy failed verification.'
}
if (-not $SkipShortcuts) {
$shell = New-Object -ComObject WScript.Shell
$desktop = [Environment]::GetFolderPath('Desktop')
$shortcutPath = Join-Path $desktop 'BitBrowser.lnk'
$shortcut = $shell.CreateShortcut($shortcutPath)
$shortcut.TargetPath = $targetExe
$shortcut.WorkingDirectory = $targetApp
$shortcut.IconLocation = $targetExe + ',0'
$shortcut.Save()
$startMenu = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs'
$startShortcutPath = Join-Path $startMenu 'BitBrowser.lnk'
$startShortcut = $shell.CreateShortcut($startShortcutPath)
$startShortcut.TargetPath = $targetExe
$startShortcut.WorkingDirectory = $targetApp
$startShortcut.IconLocation = $targetExe + ',0'
$startShortcut.Save()
}
Write-Host ''
Write-Host "Installed application: $targetApp"
Write-Host "Installed clean data: $targetRoaming"
Write-Host "Verified extension: YesCaptcha 1.4.7 ($expectedExtensionId)"
if ($backups.Count -gt 0) {
Write-Host 'Previous data was preserved at:'
foreach ($item in $backups) { Write-Host " $item" }
}
Write-Host 'No browser profiles, cookies, history, cache, proxy settings, or login token were installed.'
if (-not $NoLaunch) {
Start-Process -FilePath $targetExe -WorkingDirectory $targetApp
}
@@ -0,0 +1,35 @@
比特浏览器干净初始化分享包
==========================
使用方法
--------
1. 解压整个 ZIP,不要只在压缩包里直接运行。
2. 先关闭比特浏览器。
3. 双击“一键安装_保留YesCaptcha人机助手.cmd”(不要直接运行 App 目录里的 EXE)。
4. 首次打开后,使用新机器自己的比特账号登录。
包内保留
--------
- 比特浏览器程序本体
- Chromium 浏览器内核和 chromedriver
- YesCaptcha 验证码助手 1.4.7
- 比特的“服务助手(勿关闭)” 2.0.6
- 插件和内核必需的本地登记
已清空/未包含
------------
- 比特账号登录令牌
- 浏览器环境和窗口列表
- Cookie、历史记录、缓存、会话和站点存储
- 代理设置、日志和任务记录
- 插件的登录状态和使用数据
安全说明
--------
- 如果目标机器已有比特用户数据,安装脚本会将其改名为 BitBrowser.backup-时间戳-随机码,不会直接删除。
- 安装脚本不会复制原机器的 BrowserCache,因此不会携带原有环境资料。
- 程序默认安装到 %LOCALAPPDATA%\Programs\BitBrowser。
- 干净用户数据安装到 %APPDATA%\BitBrowser。
- 已有的外置缓存目录(例如 D:\bitbrowser-cache)不会被删除,新安装也不会引用它。
首次登录后建议新建一个浏览器环境,确认 YesCaptcha 插件已加载。插件本身的账号需要在新机器重新登录。
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Create a sanitized BitBrowser database while preserving extensions and kernels."""
from __future__ import annotations
import argparse
import json
import os
import sqlite3
from pathlib import Path
def quote_identifier(value: str) -> str:
return '"' + value.replace('"', '""') + '"'
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--source", required=True)
parser.add_argument("--output", required=True)
args = parser.parse_args()
source_path = Path(args.source).resolve()
output_path = Path(args.output).resolve()
if source_path == output_path:
raise RuntimeError("The output database must differ from the source database")
if not source_path.is_file():
raise FileNotFoundError(source_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
if output_path.exists():
output_path.unlink()
source = sqlite3.connect(f"file:{source_path.as_posix()}?mode=ro", uri=True)
target = sqlite3.connect(str(output_path))
try:
source.backup(target)
target.execute("PRAGMA foreign_keys = OFF")
tables = [
row[0]
for row in target.execute(
"SELECT name FROM sqlite_master "
"WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
)
]
extension_rows = []
if "local_extensions" in tables:
extension_rows = target.execute(
"SELECT name, desc, extension_id, icon, version, is_use "
"FROM local_extensions ORDER BY id"
).fetchall()
chrome_rows = []
if "chrome_versions" in tables:
chrome_rows = target.execute(
"SELECT chrome, version FROM chrome_versions ORDER BY id"
).fetchall()
firefox_rows = []
if "firefox_versions" in tables:
firefox_rows = target.execute(
"SELECT firefox, version FROM firefox_versions ORDER BY id"
).fetchall()
for table in tables:
target.execute(f"DELETE FROM {quote_identifier(table)}")
target.execute("DELETE FROM sqlite_sequence")
if extension_rows:
target.executemany(
"INSERT INTO local_extensions "
"(name, desc, extension_id, icon, version, is_use, cloud_id, main_user_id) "
"VALUES (?, ?, ?, ?, ?, ?, NULL, NULL)",
extension_rows,
)
if chrome_rows:
target.executemany(
"INSERT INTO chrome_versions (chrome, version) VALUES (?, ?)",
chrome_rows,
)
if firefox_rows:
target.executemany(
"INSERT INTO firefox_versions (firefox, version) VALUES (?, ?)",
firefox_rows,
)
target.commit()
target.execute("VACUUM")
target.execute("PRAGMA optimize")
counts = {
table: target.execute(
f"SELECT COUNT(*) FROM {quote_identifier(table)}"
).fetchone()[0]
for table in tables
}
allowed_nonempty = {"local_extensions", "chrome_versions", "firefox_versions"}
unexpected = {
table: count
for table, count in counts.items()
if count and table not in allowed_nonempty
}
if unexpected:
raise RuntimeError(f"Sanitization failed; unexpected rows remain: {unexpected}")
integrity = target.execute("PRAGMA integrity_check").fetchone()[0]
if integrity != "ok":
raise RuntimeError(f"SQLite integrity check failed: {integrity}")
report = {
"source": str(source_path),
"output": str(output_path),
"extensions": [
{
"name": row[0],
"extension_id": row[2],
"version": row[4],
"enabled": bool(row[5]),
}
for row in extension_rows
],
"chrome_versions": [
{"chrome": row[0], "version": row[1]} for row in chrome_rows
],
"firefox_versions": [
{"firefox": row[0], "version": row[1]} for row in firefox_rows
],
"table_counts": counts,
"integrity_check": integrity,
"size_bytes": os.path.getsize(output_path),
}
print(json.dumps(report, ensure_ascii=False, indent=2))
finally:
target.close()
source.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,12 @@
@echo off
setlocal
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0Install-Clean.ps1"
if errorlevel 1 (
echo.
echo Installation failed. Close BitBrowser and run this file again.
pause
exit /b 1
)
echo.
echo BitBrowser and YesCaptcha were installed and verified.
pause
+20 -3
View File
@@ -954,10 +954,27 @@ function getAccount(email) {
* @returns {object[]} 账号记录数组
*/
function getAccountsByEmails(emails) {
const normalizedEmails = Array.from(new Set((Array.isArray(emails) ? emails : []).filter(Boolean)));
const normalizedEmails = Array.from(new Set((Array.isArray(emails) ? emails : [])
.map(email => String(email || '').trim())
.filter(Boolean)));
if (normalizedEmails.length === 0) return [];
const placeholders = normalizedEmails.map(() => '?').join(',');
return getDb().prepare(`SELECT * FROM accounts WHERE email IN (${placeholders})`).all(...normalizedEmails);
const database = getDb();
const foundAccounts = [];
// SQLite 的绑定参数数量有编译时上限;分块查询以支持上万个账号。
for (let i = 0; i < normalizedEmails.length; i += 200) {
const chunk = normalizedEmails.slice(i, i + 200);
const placeholders = chunk.map(() => '?').join(',');
foundAccounts.push(...database
.prepare(`SELECT * FROM accounts WHERE email IN (${placeholders})`)
.all(...chunk));
}
const accountByEmail = new Map(foundAccounts.map(account => [
String(account.email || '').trim().toLowerCase(),
account,
]));
return normalizedEmails
.map(email => accountByEmail.get(email.toLowerCase()))
.filter(Boolean);
}
/**
+28 -3
View File
@@ -148,11 +148,12 @@ router.get('/events', sseMiddleware);
/**
* POST /api/operations/run
* 启动后台任务,创建 Worker 线程执行指定任务类型
* @param {import('express').Request} req - Express 请求对象,body 包含 taskType、accounts、params
* @param {import('express').Request} req - Express 请求对象,body 包含 taskType、accounts/accountEmails、params
* @param {import('express').Response} res - Express 响应对象,返回 taskId 供客户端订阅 SSE
*/
router.post('/run', (req, res) => {
const { taskType, accounts, params } = req.body;
const { taskType, params } = req.body;
let { accounts } = req.body;
if (!taskType) return res.status(400).json({ success: false, msg: '缺少 taskType' });
// 充值和退款会使用服务端保存的敏感凭据,不能由局域网中的其他设备触发。
@@ -181,7 +182,31 @@ router.post('/run', (req, res) => {
&& !String(req.get('content-type') || '').toLowerCase().startsWith('application/json')) {
return res.status(415).json({ success: false, msg: '该任务仅接受 JSON 请求' });
}
if (!accounts || !Array.isArray(accounts)) {
if (taskType === 'aistudio_combo' && Array.isArray(req.body.accountEmails)) {
const requestedEmails = Array.from(new Set(req.body.accountEmails
.map(email => String(email || '').trim())
.filter(Boolean)));
if (requestedEmails.length === 0) {
return res.status(400).json({ success: false, msg: 'AI Studio 一键组合缺少有效账号' });
}
// 前端只传邮箱以支持上万账号一次启动;敏感字段和长文本由服务端重新读取。
const storedAccounts = db.getAccountsByEmails(requestedEmails);
const accountByEmail = new Map(storedAccounts.map(account => [
String(account.email || '').trim().toLowerCase(),
account,
]));
const missingCount = requestedEmails.reduce((count, email) => (
accountByEmail.has(email.toLowerCase()) ? count : count + 1
), 0);
if (missingCount > 0) {
return res.status(400).json({
success: false,
msg: `有 ${missingCount} 个账号已不存在,请刷新账号列表后重试`,
});
}
accounts = requestedEmails.map(email => accountByEmail.get(email.toLowerCase()));
} else if (!accounts || !Array.isArray(accounts)) {
return res.status(400).json({ success: false, msg: '缺少 accounts 数组' });
}
const normalizedParams = params && typeof params === 'object' && !Array.isArray(params)
@@ -0,0 +1,43 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const root = path.join(__dirname, '..');
const appSource = fs.readFileSync(path.join(root, 'public', 'app.js'), 'utf8');
const operationsSource = fs.readFileSync(path.join(root, 'src', 'routes', 'operations.js'), 'utf8');
const databaseSource = fs.readFileSync(path.join(root, 'src', 'core', 'database.js'), 'utf8');
assert.match(
appSource,
/taskType === 'aistudio_combo'[\s\S]{0,300}accountEmails:\s*taskAccounts\.map/,
'AI Studio 一键组合必须只提交邮箱引用,不能上传完整账号对象',
);
assert.match(
operationsSource,
/taskType === 'aistudio_combo'[\s\S]{0,120}Array\.isArray\(req\.body\.accountEmails\)/,
'服务端必须接受 AI Studio 邮箱引用列表',
);
assert.match(
operationsSource,
/db\.getAccountsByEmails\(requestedEmails\)/,
'服务端必须从数据库重新加载完整账号',
);
assert.match(
databaseSource,
/for \(let i = 0; i < normalizedEmails\.length; i \+= 200\)/,
'批量账号查询必须分块,避免 SQLite 绑定参数上限',
);
const emailOnlyPayload = {
taskType: 'aistudio_combo',
accountEmails: Array.from({ length: 10513 }, (_, index) => `account-${index}@example.com`),
params: { threadCount: 1 },
};
assert(
Buffer.byteLength(JSON.stringify(emailOnlyPayload), 'utf8') < 10 * 1024 * 1024,
'10513 个邮箱的 AI Studio 请求必须低于 10MB',
);
console.log('aistudio large batch request tests passed');