初始化提交:go比特版本精简

This commit is contained in:
2026-09-20 17:02:11 +08:00
commit f41a979462
213 changed files with 58288 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
'use strict';
const path = require('path');
const Database = require('better-sqlite3');
const backupNames = process.argv.slice(2);
if (backupNames.length === 0) throw new Error('请提供至少一个备份文件名');
for (const name of backupNames) {
const filePath = path.join(process.cwd(), 'data', 'backups', path.basename(name));
const db = new Database(filePath, { readonly: true, fileMustExist: true });
try {
const total = db.prepare('SELECT COUNT(*) AS count FROM accounts').get().count;
const active = db.prepare('SELECT COUNT(*) AS count FROM accounts WHERE COALESCE(is_staged, 0) = 0').get().count;
const restricted = db.prepare("SELECT COUNT(*) AS count FROM accounts WHERE restriction_status = 'restricted'").get().count;
const activeRestricted = db.prepare(`
SELECT COUNT(*) AS count FROM accounts
WHERE restriction_status = 'restricted' AND COALESCE(is_staged, 0) = 0
`).get().count;
const recentRaw = db.prepare("SELECT value FROM settings WHERE key = 'recent_task_batch'").get();
let recentCount = 0;
try { recentCount = JSON.parse(recentRaw && recentRaw.value || '{}').emails.length || 0; } catch (_) {}
console.log(JSON.stringify({ name, total, active, restricted, activeRestricted, recentCount }));
} finally {
db.close();
}
}
+105
View File
@@ -0,0 +1,105 @@
'use strict';
const path = require('path');
const { chromium } = require('playwright');
const db = require('../src/core/database');
const { openBrowserWithRetry, closeBrowser } = require('../src/core/bit_api');
const { createGmailAppPassword } = require('../src/services/gmail_app_password_service');
async function inspectPage(page, email) {
return page.evaluate(targetEmail => {
const visible = element => {
const style = window.getComputedStyle(element);
return style.display !== 'none' && style.visibility !== 'hidden';
};
const clean = value => String(value || '')
.replace(new RegExp(targetEmail.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig'), '[email]')
.replace(/\s+/g, ' ')
.trim();
return {
title: document.title,
bodyText: clean(document.body && document.body.innerText).slice(0, 1200),
inputs: [...document.querySelectorAll('input')].filter(visible).map(input => ({
type: input.type,
name: input.name,
id: input.id,
ariaLabel: input.getAttribute('aria-label'),
placeholder: input.getAttribute('placeholder'),
hasValue: !!input.value,
})),
buttons: [...document.querySelectorAll('button, [role="button"]')].filter(visible)
.map(button => clean(button.innerText || button.getAttribute('aria-label')))
.filter(Boolean)
.slice(0, 30),
};
}, email).catch(() => ({ title: '', bodyText: '', inputs: [], buttons: [] }));
}
async function main() {
const email = String(process.argv[2] || '').trim();
if (!email) throw new Error('用法: node scripts/obtain_gmail_app_password.js <email>');
db.initDB();
const account = db.getAllAccounts().find(item => String(item.email || '').toLowerCase() === email.toLowerCase());
if (!account) throw new Error('数据库中不存在该邮箱');
if (!account.browser_id) throw new Error('该邮箱没有浏览器编号');
if (db.hasGmailAppPassword(email)) {
console.log(JSON.stringify({ ok: true, alreadySaved: true, email }));
return;
}
const opened = await openBrowserWithRetry(account.browser_id, {
email,
logCallback: message => console.log(message),
});
if (!opened || !opened.success || !opened.data || !opened.data.ws) {
throw new Error(`打开比特浏览器失败:${opened && opened.msg ? opened.msg : '无 ws 端点'}`);
}
let browser;
let page;
let succeeded = false;
try {
browser = await chromium.connectOverCDP(opened.data.ws);
const context = browser.contexts()[0];
page = await context.newPage();
await page.bringToFront().catch(() => {});
const result = await createGmailAppPassword(page, account, message => console.log(message), {
appName: 'zhaoqiaoqing2001',
timeoutMs: 60000,
captchaWaitMs: 5 * 60 * 1000,
});
if (result.ok && result.appPassword) {
db.upsertGmailAppPassword(email, result.appPassword);
succeeded = true;
console.log(JSON.stringify({ ok: true, saved: true, email, passwordLength: result.appPassword.length }));
return;
}
const inspection = await inspectPage(page, email);
const screenshotPath = path.join(
db.getDb().name ? path.dirname(db.getDb().name) : path.join(__dirname, '..', 'data'),
'screenshots',
`gmail_app_password_${email.replace(/[^a-z0-9]+/gi, '_')}.png`,
);
await page.screenshot({ path: screenshotPath, fullPage: true }).catch(() => {});
console.log(JSON.stringify({
ok: false,
email,
message: result.msg,
url: page.url(),
inspection,
screenshotPath,
}));
process.exitCode = 2;
} finally {
if (browser) await browser.close().catch(() => {});
if (succeeded) await closeBrowser(account.browser_id).catch(() => {});
}
}
main().catch(error => {
console.error(JSON.stringify({ ok: false, message: error.message }));
process.exitCode = 1;
});
@@ -0,0 +1,65 @@
'use strict';
const database = require('../src/core/database');
const backup = require('../src/core/backup');
function parseRecentBatch(raw) {
try {
const parsed = JSON.parse(String(raw || ''));
if (!parsed || !Array.isArray(parsed.emails)) return null;
return {
taskId: parsed.taskId || null,
taskType: parsed.taskType || null,
emails: [...new Set(parsed.emails.map(email => String(email || '').trim()).filter(Boolean))],
};
} catch (_) {
return null;
}
}
function main() {
database.initDB();
const batch = parseRecentBatch(database.getSetting('recent_task_batch', ''));
if (!batch || batch.taskType !== 'custom_pipeline' || batch.emails.length === 0) {
throw new Error('最近任务不是可恢复的组合任务批次');
}
const db = database.getDb();
const placeholders = batch.emails.map(() => '?').join(',');
const restrictedRows = db.prepare(`
SELECT email FROM accounts
WHERE restriction_status = 'restricted' AND email IN (${placeholders})
`).all(...batch.emails);
const backupName = backup.createBackupSync('pre_gcp_recovery');
if (!backupName) throw new Error('恢复前数据库备份失败');
const update = db.prepare(`
UPDATE accounts
SET ag_status = 'GCP被限制',
status_reason = 'GCP_RESTRICTED',
message = '🚫 GCP被限制(已从最近组合任务结果恢复)',
updated_at = CURRENT_TIMESTAMP
WHERE email = ? AND restriction_status = 'restricted'
`);
const recover = db.transaction((rows) => {
let changed = 0;
for (const row of rows) changed += update.run(row.email).changes;
return changed;
});
const changed = recover(restrictedRows);
console.log(JSON.stringify({
taskId: batch.taskId,
batchAccounts: batch.emails.length,
restrictedFound: restrictedRows.length,
changed,
backupName,
}, null, 2));
}
try {
main();
} finally {
try { database.getDb().close(); } catch (_) {}
}
+118
View File
@@ -0,0 +1,118 @@
'use strict';
const fs = require('fs');
const path = require('path');
const Database = require('better-sqlite3');
const Config = require('../src/core/config');
const backup = require('../src/core/backup');
function readArg(name) {
const index = process.argv.indexOf(name);
return index >= 0 ? process.argv[index + 1] : null;
}
const backupName = readArg('--backup');
const batchIds = String(readArg('--batch-ids') || '')
.split(',')
.map(value => Number(value.trim()))
.filter(Number.isInteger);
const shouldApply = process.argv.includes('--apply');
if (!backupName || batchIds.length === 0) {
throw new Error('用法: node scripts/recover_staging_batches.js --backup BACKUP.db --batch-ids 1,2 [--apply]');
}
const backupPath = path.join(Config.DATA_DIR, 'backups', path.basename(backupName));
if (!fs.existsSync(backupPath)) throw new Error(`备份不存在: ${backupPath}`);
const source = new Database(backupPath, { readonly: true, timeout: 10000 });
const target = new Database(Config.DB_PATH, { timeout: 10000 });
target.pragma('busy_timeout = 10000');
target.pragma('journal_mode = WAL');
try {
const placeholders = batchIds.map(() => '?').join(',');
const batches = source.prepare(`
SELECT id, name, selected_fields, account_count, created_at
FROM staging_batches
WHERE id IN (${placeholders})
ORDER BY id
`).all(...batchIds);
if (batches.length !== batchIds.length) throw new Error('备份中未找到全部指定暂存批次');
const recovery = batches.map(batch => {
const accounts = source.prepare(`
SELECT email, staged_note, staged_at
FROM accounts
WHERE staged_batch_id = ? AND COALESCE(is_staged, 0) = 1
ORDER BY rowid
`).all(batch.id);
if (accounts.length !== batch.account_count) {
throw new Error(`批次 ${batch.id} 账号数量不一致: 元数据 ${batch.account_count},实际 ${accounts.length}`);
}
const targetAccount = target.prepare('SELECT email, is_staged, staged_batch_id FROM accounts WHERE email = ?');
for (const account of accounts) {
const current = targetAccount.get(account.email);
if (!current) throw new Error(`当前数据库缺少待恢复账号: ${account.email}`);
if (Number(current.is_staged) === 1 || current.staged_batch_id != null) {
throw new Error(`账号已在其他暂存文件夹中: ${account.email}`);
}
}
return { batch, accounts };
});
const summary = recovery.map(item => ({
id: item.batch.id,
name: item.batch.name,
accountCount: item.accounts.length,
selectedFields: JSON.parse(item.batch.selected_fields),
}));
console.log(JSON.stringify({ mode: shouldApply ? 'apply' : 'dry-run', batches: summary }, null, 2));
if (!shouldApply) process.exit(0);
const existingBatch = target.prepare('SELECT id FROM staging_batches WHERE id = ?');
for (const item of recovery) {
if (existingBatch.get(item.batch.id)) throw new Error(`当前数据库已存在批次 ID: ${item.batch.id}`);
}
const restore = target.transaction(() => {
const insertBatch = target.prepare(`
INSERT INTO staging_batches (id, name, selected_fields, account_count, created_at)
VALUES (?, ?, ?, ?, ?)
`);
const stageAccount = target.prepare(`
UPDATE accounts
SET is_staged = 1,
staged_batch_id = ?,
staged_note = ?,
staged_at = ?
WHERE email = ? AND COALESCE(is_staged, 0) = 0
`);
for (const item of recovery) {
const batch = item.batch;
insertBatch.run(batch.id, batch.name, batch.selected_fields, item.accounts.length, batch.created_at);
let updated = 0;
for (const account of item.accounts) {
updated += stageAccount.run(
batch.id,
account.staged_note || batch.name,
account.staged_at || batch.created_at,
account.email,
).changes;
}
if (updated !== item.accounts.length) {
throw new Error(`批次 ${batch.id} 恢复数量不完整: ${updated}/${item.accounts.length}`);
}
}
});
const safetyBackup = backup.createBackupSync('pre_staging_recovery');
if (!safetyBackup) throw new Error('创建恢复前安全备份失败,已取消写入');
restore.immediate();
console.log(JSON.stringify({ success: true, safetyBackup, restoredAccounts: recovery.reduce((sum, item) => sum + item.accounts.length, 0) }));
} finally {
source.close();
target.close();
}
+148
View File
@@ -0,0 +1,148 @@
'use strict';
const tls = require('tls');
const db = require('../src/core/database');
const IMAP_HOST = 'imap.gmail.com';
const IMAP_PORT = 993;
function quoteImap(value) {
return `"${String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
function decodeMimeWords(value) {
return String(value || '').replace(/=\?([^?]+)\?([bq])\?([^?]*)\?=/gi, (_, charset, encoding, data) => {
try {
if (encoding.toLowerCase() === 'b') return Buffer.from(data, 'base64').toString('utf8');
const bytes = data.replace(/_/g, ' ').replace(/=([0-9a-f]{2})/gi, (_m, hex) => String.fromCharCode(parseInt(hex, 16)));
return Buffer.from(bytes, 'binary').toString(/^utf-?8$/i.test(charset) ? 'utf8' : 'latin1');
} catch (_) {
return data;
}
});
}
function headerValue(block, name) {
const unfolded = String(block || '').replace(/\r?\n[\t ]+/g, ' ');
const match = unfolded.match(new RegExp(`(?:^|\\r?\\n)${name}:\\s*([^\\r\\n]*)`, 'i'));
return match ? decodeMimeWords(match[1].trim()) : '';
}
function recentHeaders(fetchResponse) {
const blocks = String(fetchResponse || '').split(/(?=\* \d+ FETCH \()/i);
return blocks.map(block => {
const uid = (block.match(/\bUID\s+(\d+)/i) || [])[1];
if (!uid) return null;
return {
uid: Number(uid),
from: headerValue(block, 'From'),
subject: headerValue(block, 'Subject'),
date: headerValue(block, 'Date'),
};
}).filter(Boolean);
}
async function connectImap(email, appPassword) {
return new Promise((resolve, reject) => {
const socket = tls.connect({
host: IMAP_HOST,
port: IMAP_PORT,
servername: IMAP_HOST,
rejectUnauthorized: true,
});
socket.setTimeout(30000);
let buffer = '';
let greetingSeen = false;
let settled = false;
const waiters = new Map();
const finishError = error => {
if (settled) return;
settled = true;
try { socket.destroy(); } catch (_) {}
reject(error);
};
socket.on('timeout', () => finishError(new Error('IMAP连接或命令响应超时')));
socket.on('error', finishError);
socket.on('data', chunk => {
buffer += chunk.toString('utf8');
if (!greetingSeen && /^\* (?:OK|PREAUTH)/m.test(buffer)) greetingSeen = true;
for (const [tag, waiter] of waiters) {
const match = buffer.match(new RegExp(`(?:^|\\r\\n)${tag} (OK|NO|BAD)(?: |\\r\\n)`, 'i'));
if (!match) continue;
const end = match.index + match[0].length;
const response = buffer.slice(waiter.start, end);
waiters.delete(tag);
if (match[1].toUpperCase() === 'OK') waiter.resolve(response);
else waiter.reject(new Error(`IMAP命令失败:${match[1].toUpperCase()}`));
}
});
socket.once('secureConnect', async () => {
try {
const greetingDeadline = Date.now() + 10000;
while (!greetingSeen && Date.now() < greetingDeadline) {
await new Promise(r => setTimeout(r, 50));
}
if (!greetingSeen) throw new Error('未收到 Gmail IMAP 欢迎响应');
let sequence = 0;
const command = text => new Promise((commandResolve, commandReject) => {
const tag = `A${String(++sequence).padStart(3, '0')}`;
waiters.set(tag, { start: buffer.length, resolve: commandResolve, reject: commandReject });
socket.write(`${tag} ${text}\r\n`);
});
await command(`LOGIN ${quoteImap(email)} ${quoteImap(appPassword)}`);
const statusResponse = await command('STATUS INBOX (MESSAGES UNSEEN)');
await command('EXAMINE INBOX');
const searchResponse = await command('UID SEARCH ALL');
const searchLine = (searchResponse.match(/\* SEARCH([^\r\n]*)/i) || [])[1] || '';
const uids = searchLine.trim().split(/\s+/).filter(value => /^\d+$/.test(value));
const latestUids = uids.slice(-3);
let headers = [];
if (latestUids.length) {
const fetchResponse = await command(`UID FETCH ${latestUids.join(',')} (UID BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])`);
headers = recentHeaders(fetchResponse).sort((a, b) => b.uid - a.uid);
}
await command('LOGOUT').catch(() => {});
const statusItems = (statusResponse.match(/\* STATUS\s+"?INBOX"?\s+\(([^)]*)\)/i) || [])[1] || '';
const messageCount = (statusItems.match(/\bMESSAGES\s+(\d+)/i) || [])[1];
const unseenCount = (statusItems.match(/\bUNSEEN\s+(\d+)/i) || [])[1];
settled = true;
socket.end();
resolve({
ok: true,
host: IMAP_HOST,
port: IMAP_PORT,
tlsAuthorized: socket.authorized,
messages: messageCount ? Number(messageCount) : uids.length,
unseen: unseenCount ? Number(unseenCount) : null,
recentHeaders: headers,
});
} catch (error) {
finishError(error);
}
});
});
}
async function main() {
const email = String(process.argv[2] || '').trim();
if (!email) throw new Error('用法: node scripts/test_gmail_imap.js <email>');
db.initDB();
const saved = db.getGmailAppPassword(email);
if (!saved || !saved.app_password) throw new Error('数据库中没有该邮箱的16位应用专用密码');
const result = await connectImap(email, saved.app_password);
console.log(JSON.stringify({ email, ...result }, null, 2));
}
main().catch(error => {
console.error(JSON.stringify({ ok: false, message: error.message }));
process.exitCode = 1;
});
module.exports = { connectImap, _private: { decodeMimeWords, recentHeaders } };