初始化提交:go比特版本精简
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 (_) {}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 } };
|
||||
Reference in New Issue
Block a user