fix 一键组合只上传邮箱列表,服务端再从数据库加载完整账号
This commit is contained in:
+14
-5
@@ -3025,14 +3025,23 @@ function geminiApp() {
|
|||||||
this.addLog(`🚀 启动任务: ${taskType}, 账号数: ${requestedCount}, 并发: ${taskThreadCount}${monitorText}`, 'info');
|
this.addLog(`🚀 启动任务: ${taskType}, 账号数: ${requestedCount}, 并发: ${taskThreadCount}${monitorText}`, 'info');
|
||||||
|
|
||||||
try {
|
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', {
|
const res = await fetch('/api/operations/run', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(taskRequestBody),
|
||||||
taskType,
|
|
||||||
accounts: taskAccounts,
|
|
||||||
params: taskParams,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
|
|||||||
@@ -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
@@ -954,10 +954,27 @@ function getAccount(email) {
|
|||||||
* @returns {object[]} 账号记录数组
|
* @returns {object[]} 账号记录数组
|
||||||
*/
|
*/
|
||||||
function getAccountsByEmails(emails) {
|
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 [];
|
if (normalizedEmails.length === 0) return [];
|
||||||
const placeholders = normalizedEmails.map(() => '?').join(',');
|
const database = getDb();
|
||||||
return getDb().prepare(`SELECT * FROM accounts WHERE email IN (${placeholders})`).all(...normalizedEmails);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -148,11 +148,12 @@ router.get('/events', sseMiddleware);
|
|||||||
/**
|
/**
|
||||||
* POST /api/operations/run
|
* POST /api/operations/run
|
||||||
* 启动后台任务,创建 Worker 线程执行指定任务类型
|
* 启动后台任务,创建 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
|
* @param {import('express').Response} res - Express 响应对象,返回 taskId 供客户端订阅 SSE
|
||||||
*/
|
*/
|
||||||
router.post('/run', (req, res) => {
|
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' });
|
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')) {
|
&& !String(req.get('content-type') || '').toLowerCase().startsWith('application/json')) {
|
||||||
return res.status(415).json({ success: false, msg: '该任务仅接受 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 数组' });
|
return res.status(400).json({ success: false, msg: '缺少 accounts 数组' });
|
||||||
}
|
}
|
||||||
const normalizedParams = params && typeof params === 'object' && !Array.isArray(params)
|
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');
|
||||||
Reference in New Issue
Block a user