Files
go_bt/public/app.js
T

5822 lines
245 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// public/app.js - Alpine.js 应用逻辑
function geminiApp() {
return {
// ==================== 状态 ====================
activeTab: 'accounts',
accounts: [],
browsers: [],
accountPage: 1,
accountPageSize: 100,
_accountByEmail: new Map(),
_browserById: new Map(),
selectedEmails: new Set(),
isDeletingAccounts: false,
settings: {},
logs: [],
currentTaskId: null,
eventSource: null,
isRunning: false,
isPaused: false,
taskProgress: { current: 0, total: 0, startedAt: null },
taskClockNow: Date.now(),
bitStatus: 'checking',
// 操作参数
threadCount: 1,
monitorEnabled: false,
inviteEmails: '',
// TOTP
totpCodes: {},
totpRemaining: 30,
// 顶部仪表盘快照:每批账号更新后只重算一次,所有卡片共用结果。
dashboardStats: {
statusCounts: { '任务中': 0 },
tierCounts: {
level1: { '全部': 0, '未登录': 0, '已登录': 0, '登录失败': 0 },
level2: { '未违反规则': 0, '违反规则': 0, '有凭证': 0, 'GCP被限制': 0, 'OAuth等待7天': 0, '未检测凭证': 0 },
level3: { '未改密': 0, '未改FA': 0, '改密<7天': 0, '改密≥7天': 0, '可重新提取': 0, 'ChatGPT成功': 0, 'ChatGPT失败': 0 },
},
},
// Cliproxy
cliproxySid: '',
cliproxyFullStr: '',
nodemavenFullStr: '',
// 代理预设
proxyPresets: [],
presetName: '',
// 静态代理池
staticProxies: [],
staticProxyImportText: '',
staticProxyMaxUsage: 1,
// 半价Ultra
skipHalfPriceCheck: false,
// 模态框
showImportAccountModal: false,
showBatchEmailSearchModal: false,
showSettingsModal: false,
showCustomErrorsModal: false,
showEditAccountModal: false,
showExportModal: false,
showProblemExportModal: false,
showBatchStatusModal: false,
showBatchCustomerNameModal: false,
showRefundDeadlineModal: false,
showStagingModal: false,
showChatgptSessionModal: false,
showChatgptRunModal: false,
showSecurityRunModal: false,
showOpenAIPlanRunModal: false,
showOpenAIAuthModal: false,
showAgCredentialModal: false,
showCliAcCredentialModal: false,
showAistudioKeysModal: false,
aistudioKeyRecord: null,
aistudioKeyEmail: '',
showRechargeModal: false,
showRechargeCardModal: false,
showGoogleAccountAgModal: false,
chatgptAuthMode: 'keep',
securityRunMode: 'overwrite',
openAIPlanConnectionMode: 'browser_proxy',
chatgptSessionEmail: '',
chatgptSessionText: '',
chatgptSessionLoading: false,
chatgptSessionError: '',
openAIAuthEmail: '',
openAIAuthText: '',
openAIAuthLoading: false,
openAIAuthError: '',
agCredentialEmail: '',
agCredentialText: '',
agCredentialLoading: false,
agCredentialError: '',
cliAcCredentialEmail: '',
cliAcCredentialText: '',
cliAcCredentialLoading: false,
cliAcCredentialError: '',
rechargeCardEmail: '',
rechargeCardText: '',
rechargeCardCompletedAt: '',
rechargeCardLoading: false,
rechargeCardError: '',
rechargeAccounts: [],
rechargeCardKeys: '',
rechargeConcurrency: 2,
rechargeSubmitting: false,
rechargeCredentialMeta: { configured: false, keyIdMasked: '', updatedAt: null },
rechargeCredentialForm: { keyId: '', secret: '' },
rechargeCredentialSaving: false,
rechargeCredentialDeleting: false,
workspaceRefundCredentialMeta: {
configured: false,
ownerMasked: '',
proxyConfigured: false,
proxyCount: 0,
proxyMasked: '',
updatedAt: null,
},
workspaceRefundCredentialForm: { value: '', proxy: '' },
workspaceRefundCredentialSaving: false,
workspaceRefundCredentialDeleting: false,
googleAccountCredentialMeta: { configured: false, keyMasked: '', apiUrl: 'https://sv9.api999api.com/google/apiv2.php', timelive: 60, updatedAt: null },
googleAccountCredentialForm: { value: '', timelive: 60 },
googleAccountCredentialSaving: false,
googleAccountCredentialDeleting: false,
googleAccountAgQuantity: 1,
googleAccountAgConcurrency: 1,
feishuCredentialMeta: { configured: false, appIdMasked: '', updatedAt: null },
feishuCredentialForm: { appId: '', appSecret: '' },
feishuCredentialSaving: false,
feishuCredentialDeleting: false,
feishuSyncRunning: false,
feishuSyncTaskId: null,
feishuEventSource: null,
feishuSyncProgress: { current: 0, total: 0 },
feishuSyncResult: null,
feishuSyncStatusText: '',
feishuSyncStatusType: 'info',
quotaMonitorStatus: {
running: false,
activeTask: null,
nextRunAt: null,
lastRun: null,
deadlineMonitor: { enabled: false, running: false, intervalSeconds: 15, nextCheckAt: null, lastRun: null },
config: { enabled: false, intervalValue: 24, intervalUnit: 'hours', concurrency: 2, threshold: 100, autoRefundEnabled: false },
},
quotaMonitorEventSource: null,
quotaMonitorTaskId: null,
quotaMonitorProgress: {
phase: 'query', current: 0, total: 0, thresholdCount: 0, failureCount: 0,
refundCurrent: 0, refundTotal: 0, refundSuccessCount: 0, refundFailureCount: 0, refundSkippedCount: 0,
},
quotaMonitorStatusText: '',
quotaMonitorStatusType: 'info',
batchStatus: '未登录',
batchCustomerName: '',
batchCustomerNameSaving: false,
refundDeadlineEmails: [],
refundDeadlineValue: '',
refundDeadlineMode: 'absolute',
refundDeadlineOffsetValue: 1,
refundDeadlineOffsetUnit: 'days',
refundDeadlineSaving: false,
exportStagingNote: '',
deleteBrowserWindowsOnStage: false,
exportInProgress: false,
stagedBatches: [],
stagedBatchDetail: null,
inlineEditStatusEmail: null,
inlineEditRechargeStatusEmail: null,
inlineEditAccountRefundEmail: null,
inlineEditRefundProcessEmail: null,
exportFields: {
email: true, password: true, recovery_email: true, secret_key: true,
country: false, sheerid_link: false, status: false, message: false, customer_name: false,
account_refund_status: false, refund_deadline_at: false, sms_url: false, recharged_at: false, ag_refresh_token: false,
cli_ac_credential: false, chatgpt_session: false, openai_auth_json: false, aistudio_api_key: false
},
exportMode: { file: false, csv: false, sub2api_json: false, clipboard: true, staging: false },
exportFieldDefs: [
{ key: 'email', label: '邮箱' },
{ key: 'password', label: '密码' },
{ key: 'recovery_email', label: '辅助邮箱' },
{ key: 'secret_key', label: '2FA密钥' },
{ key: 'country', label: '国家' },
{ key: 'customer_name', label: '客户名' },
{ key: 'account_refund_status', label: '账号退款' },
{ key: 'refund_deadline_at', label: '退款截止时间' },
{ key: 'sheerid_link', label: 'SheerID链接' },
{ key: 'status', label: '状态' },
{ key: 'message', label: '消息' },
{ key: 'recharged_at', label: '充值时间' },
{ key: 'sms_url', label: '接码链接' },
{ key: 'ag_refresh_token', label: 'AG凭证' },
{ key: 'cli_ac_credential', label: 'Cli_AC凭证' },
{ key: 'chatgpt_session', label: 'ChatGPT Session' },
{ key: 'openai_auth_json', label: '授权JSON' },
{ key: 'aistudio_api_key', label: 'AI Studio Key' },
],
_exportList: [],
problemExportCategories: {
password: true,
twofa: true,
captcha: true,
},
importText: '',
importTemplates: [],
importSelectedIndex: 0,
importNewTemplateName: '',
importNewTemplatePattern: '',
showTemplateEditor: false,
importStatus: '未登录',
importSkipDuplicates: false,
// 拖拽选择状态
_dragState: null, // { type: 'account', mode: 'select'|'deselect' }
// 复制提示
copyToastVisible: false,
copyToastTimer: null,
customErrorRules: [],
customErrorNewTasks: ['login'],
customErrorNewText: '',
customErrorNewTip: '',
// 自定义执行
customMode: false,
customCheckedTasks: [],
customQueue: [],
customCurrentIndex: -1,
customRunning: false,
customStoppedIndex: null,
customJobList: [], // 自定义任务列表 [{email, tasks, currentTaskLabel, completedCount, status, message}]
customStatusSubFilter: '全部', // 自定义任务列表内的状态子筛选
// 自定义模板
savedTemplates: [],
templateName: '',
customStepParams: {}, // { bind_card: { halfPrice: true/false } }
selectedTemplateId: null,
showTemplateCreator: false,
// 截图弹窗
showScreenshotModal: false,
screenshotUrl: '',
screenshotEmail: '',
screenshotHover: null,
// 诊断日志页
diagAccounts: [],
diagEmail: '',
diagEmailSearch: '',
diagDates: [],
diagDate: '',
diagTotal: 0,
diagOffset: 0,
diagLimit: 200,
diagEvents: [],
diagLoading: false,
diagTypeFilter: '',
diagTaskFilter: '',
diagSearch: '',
diagQuickFilter: '',
diagSelected: null,
// SheerLink 结果弹窗
showSheerLinkResultModal: false,
sheerLinkResults: [],
// 任务追踪(供任务完成后使用)
_lastTaskType: null,
_lastTaskEmails: [],
_lastTaskStartedAt: null,
_lastTaskId: null,
// 当前活跃任务与“最近一次任务”分开,避免刷新/多标签页时互相覆盖。
_activeTaskType: null,
_activeTaskEmails: [],
_activeTaskEmailSet: new Set(),
_activeTaskStartedAt: null,
// 筛选 & 搜索
statusFilter: '全部',
tierFilter: { level1: '全部', level2: '全部', level3: '全部', errorCode: null },
accountFilterConditions: [],
_nextAccountFilterConditionId: 1,
accountFilterFields: [
{ value: 'email', label: '邮箱' },
{ value: 'status', label: '状态' },
{ value: 'message', label: '处理结论' },
{ value: 'customer_name', label: '客户名' },
{ value: 'account_refund_status', label: '账号退款' },
{ value: 'refund_process_status', label: '是否已发起退款流程' },
{ value: 'refund_deadline_at', label: '退款截止时间' },
{ value: 'proxy_ip', label: '静态IP' },
{ value: 'browser_seq', label: '浏览器编号' },
{ value: 'country', label: '国家' },
{ value: 'chatgpt_status', label: 'ChatGPT状态' },
{ value: 'recharge_status', label: '是否已充值' },
{ value: 'has_openai_auth', label: '授权JSON' },
{ value: 'has_ag_credential', label: 'AC凭证' },
{ value: 'has_cli_ac_credential', label: 'Cli_AC凭证' },
{ value: 'gmail_app_password', label: '16位应用密码' },
{ value: 'openai_plan_type', label: '账号套餐' },
{ value: 'is_pro', label: 'Pro' },
{ value: 'ag_status', label: 'AG状态' },
],
rechargeStatusOptions: ['未充值', '充值中', '等待已确认', '充值成功', '充值失败', '账号本身已有会员'],
accountRefundStatusOptions: [
'无状态', '已退款', '未退款', '退款失败', '已申请退款', 'google封号无法查询',
'gpt封号退款失败', 'gpt警告', '已被改密无法查询', '已被改2FA无法查询',
'需要手机号查询', '无订阅',
],
refundProcessStatusOptions: ['未发起', '已发起', '发起失败'],
searchText: '',
batchEmailSearchText: '',
batchEmailSearchEmails: [],
batchEmailSearchActive: false,
customerNameFilter: '',
importCustomerName: '',
// 编辑账号
editAccount: null,
inlineEditErrorEmail: null,
// UI
logDrawerOpen: false,
// 账号实时日志:{ email: { message, type } }
accountLogs: {},
_pendingLogEntries: [],
_logFlushTimer: null,
_logLimit: 500,
_pendingAccountUpdates: new Map(),
_accountUpdateTimer: null,
_pendingTaskProgress: null,
_taskProgressTimer: null,
_totpUpdateId: 0,
openUrlPresets: {
student: 'https://one.google.com/ai-student?g1_landing_page=75&utm_source=antigravity&utm_campaign=argon_limit_reached',
country: 'https://policies.google.com/terms?hl=zh_CN',
family: 'https://myaccount.google.com/family/details?continue=https%3A%2F%2Fmyaccount.google.com%2Fpeople-and-sharing%3Futm_source%3Dchrome-profile-chooser&utm_source=chrome-profile-chooser',
payment: 'https://payments.google.com/payments/home#paymentMethods',
},
// ==================== 初始化 ====================
async init() {
try {
const savedMonitor = localStorage.getItem('monitor_enabled');
if (savedMonitor !== null) this.monitorEnabled = savedMonitor === '1';
} catch (_) {}
try {
const savedJobs = localStorage.getItem('customJobList');
if (savedJobs) this.customJobList = JSON.parse(savedJobs);
} catch (_) {}
try {
const savedTasks = localStorage.getItem('customCheckedTasks');
if (savedTasks) this.customCheckedTasks = JSON.parse(savedTasks);
} catch (_) {}
this.cliproxySid = this._generateClipproxySid();
// 加载代理预设
try {
const savedPresets = localStorage.getItem('proxyPresets');
if (savedPresets) this.proxyPresets = JSON.parse(savedPresets);
} catch (_) {}
// 一次性迁移"自定义成功"状态并清除自定义消息
if (!localStorage.getItem('_customMigrated2')) {
try {
const mRes = await fetch('/api/accounts/migrate-custom', { method: 'POST' });
if (mRes.ok) {
localStorage.removeItem('customJobList');
this.customJobList = [];
localStorage.setItem('_customMigrated2', '1');
}
} catch (_) {}
}
await this.loadAccounts();
await this.loadSettings();
await this.loadRechargeCredentialMeta();
await this.loadWorkspaceRefundCredentialMeta();
await this.loadGoogleAccountCredentialMeta();
await this.loadFeishuCredentialMeta();
await this.loadQuotaMonitorStatus();
this._restoreRecentTaskBatch();
await this.loadStaticProxies();
await this.loadImportTemplates();
await this.checkBitStatus();
await this.loadBrowsers();
await this.loadDiagAccounts();
await this.loadTemplates();
await this.checkRunningTasks(); // 刷新页面后恢复运行中任务
// 每30秒检查一次比特浏览器状态
setInterval(() => this.checkBitStatus(), 30000);
setInterval(() => this.loadQuotaMonitorStatus(true), 15000);
// 拖拽选择:松开鼠标时结束
document.addEventListener('mouseup', () => {
this._dragState = null;
document.body.classList.remove('drag-selecting');
});
// TOTP:每秒更新剩余时间,窗口切换时重新计算验证码
await this.updateTotpCodes();
let lastWindow = Math.floor(Date.now() / 1000 / 30);
setInterval(async () => {
this.taskClockNow = Date.now();
const remaining = 30 - (Math.floor(Date.now() / 1000) % 30);
this.totpRemaining = remaining;
const w = Math.floor(Date.now() / 1000 / 30);
if (w !== lastWindow) { lastWindow = w; await this.updateTotpCodes(); }
}, 1000);
},
// ==================== 账号管理 ====================
async loadAccounts() {
try {
const res = await fetch('/api/accounts');
const data = await res.json();
if (data.success) {
this.accounts = Array.isArray(data.data) ? data.data : [];
this._accountByEmail = new Map(this.accounts.map(account => [account.email, account]));
this._rebuildDashboardStats();
this.accountPage = Math.min(this.accountCurrentPage, this.accountTotalPages);
await this.updateTotpCodes();
}
} catch (e) {
this.addLog(`加载账号失败: ${e.message}`, 'error');
}
},
get selectedAccounts() {
return this.accounts.filter(a => this.selectedEmails.has(a.email));
},
get accountTotalPages() {
const size = Math.max(1, Number(this.accountPageSize) || 100);
return Math.max(1, Math.ceil(this.filteredAccounts.length / size));
},
get accountCurrentPage() {
const page = Math.max(1, Number(this.accountPage) || 1);
return Math.min(page, this.accountTotalPages);
},
get pagedAccounts() {
const filtered = this.filteredAccounts;
const size = Math.max(1, Number(this.accountPageSize) || 100);
const totalPages = Math.max(1, Math.ceil(filtered.length / size));
const page = Math.min(Math.max(1, Number(this.accountPage) || 1), totalPages);
const start = (page - 1) * size;
return filtered.slice(start, start + size);
},
_scheduleVisibleTotpUpdate() {
const update = () => this.updateTotpCodes();
if (typeof this.$nextTick === 'function') this.$nextTick(update);
else if (typeof setTimeout === 'function') setTimeout(update, 0);
},
resetAccountPage() {
this.accountPage = 1;
this._scheduleVisibleTotpUpdate();
},
goToAccountPage(page) {
const next = Math.min(Math.max(1, Number(page) || 1), this.accountTotalPages);
if (next === this.accountCurrentPage) return;
this.accountPage = next;
this._scheduleVisibleTotpUpdate();
},
setAccountPageSize(size) {
const allowed = new Set([50, 100, 200]);
const next = Number(size);
this.accountPageSize = allowed.has(next) ? next : 100;
this.resetAccountPage();
},
_setActiveTaskEmails(emails) {
const normalized = [...new Set((Array.isArray(emails) ? emails : []).filter(Boolean))];
this._activeTaskEmails = normalized;
this._activeTaskEmailSet = new Set(normalized);
},
_parseRecentTaskBatch(stored) {
if (!stored) return null;
try {
const parsed = typeof stored === 'string' ? JSON.parse(stored) : stored;
if (!parsed || !Array.isArray(parsed.emails)) return null;
const emails = [...new Set(parsed.emails
.map(email => String(email || '').trim())
.filter(Boolean))];
return {
taskType: parsed.taskType || null,
emails,
startedAt: parsed.startedAt || null,
taskId: parsed.taskId || null,
};
} catch (_) {
return null;
}
},
_applyRecentTaskBatch(batch, persistLocal = true) {
if (!batch) return;
this._lastTaskType = batch.taskType || null;
this._lastTaskEmails = [...new Set((batch.emails || []).filter(Boolean))];
this._lastTaskStartedAt = batch.startedAt || null;
this._lastTaskId = batch.taskId || null;
if (persistLocal) {
try { localStorage.setItem('recentTaskBatch', JSON.stringify(batch)); } catch (_) {}
}
},
_restoreRecentTaskBatch() {
const serverBatch = this._parseRecentTaskBatch(this.settings && this.settings.recent_task_batch);
let localBatch = null;
try { localBatch = this._parseRecentTaskBatch(localStorage.getItem('recentTaskBatch')); } catch (_) {}
const candidates = [serverBatch, localBatch].filter(Boolean);
if (candidates.length === 0) return;
candidates.sort((a, b) => {
const aTime = Date.parse(a.startedAt || '') || 0;
const bTime = Date.parse(b.startedAt || '') || 0;
return bTime - aTime;
});
this._applyRecentTaskBatch(candidates[0], true);
},
_rememberTaskBatch(taskType, taskAccounts, taskId = null) {
const emails = [...new Set((taskAccounts || [])
.map(account => String((account && account.email) || '').trim())
.filter(Boolean))];
const batch = {
taskId: taskId || null,
taskType: taskType || null,
emails,
startedAt: new Date().toISOString(),
};
this._applyRecentTaskBatch(batch, true);
},
get recentTaskCount() {
if (!this._lastTaskEmails || this._lastTaskEmails.length === 0) return 0;
const emailSet = new Set(this._lastTaskEmails);
return this.customerFilteredAccounts.filter(account => emailSet.has(account.email)).length;
},
get recentTaskTooltip() {
if (!this._lastTaskEmails || this._lastTaskEmails.length === 0) return '暂无最近运行的账号';
const labels = {
custom_pipeline: '自定义任务',
login: '登录',
change_password: '改密',
change_2fa: '改2FA',
chatgpt_register: 'ChatGPT注册/登录',
openai_get_auth: '提取授权JSON',
openai_query_plan: '查询套餐',
refund_query: '查询退款',
card_pay_recharge: '充值',
workspace_refund: '退款',
security_chatgpt: '改密+2FA',
ag_revoke_authorization: '取消授权反重力',
ag_get_credential: '获取凭证',
gemini_cli_get_credential: '获取Cli凭证',
google_account_ag_credential: '链接提取+获取凭证',
};
const taskLabel = labels[this._lastTaskType] || this._lastTaskType || '任务';
let timeLabel = '';
if (this._lastTaskStartedAt) {
const d = new Date(this._lastTaskStartedAt);
if (!Number.isNaN(d.getTime())) timeLabel = `,启动于 ${d.toLocaleString()}`;
}
return `最近任务:${taskLabel},${this.recentTaskCount} 个账号${timeLabel};点击筛选并全选`;
},
_taskTerminalStatuses(taskType) {
switch (taskType) {
case 'login':
return new Set(['登录成功', '登录失败', '订阅成功']);
case 'change_password':
return new Set(['改密成功', '改密失败', '未验证已改密和2FA', '未注册成功已改密和2FA']);
case 'change_2fa':
return new Set(['2FA修改成功', '2FA修改失败', '未验证已改密和2FA', '未注册成功已改密和2FA']);
case 'chatgpt_register':
return new Set(['ChatGPT成功', 'ChatGPT失败', '已验证改密和2FA', '未注册成功已改密和2FA']);
case 'security_chatgpt':
return new Set(['改密失败', '2FA修改失败', '未验证已改密和2FA', '未注册成功已改密和2FA']);
case 'bind_card':
return new Set(['订阅成功', '绑卡失败', '无资格', '有资格']);
default:
return new Set();
}
},
_isTaskAccountInProgress(acc) {
if (!this.isRunning) return false;
const email = acc && acc.email ? acc.email : '';
if (!email) return false;
if (!this._activeTaskEmailSet || this._activeTaskEmailSet.size === 0) return false;
if (!this._activeTaskEmailSet.has(email)) return false;
const taskType = this._activeTaskType || null;
const st = acc.status || '未登录';
const terminals = this._taskTerminalStatuses(taskType);
if (terminals.size > 0 && terminals.has(st)) return false;
if (taskType === 'change_2fa') {
const msg = (acc.message || '').trim();
if (/^(✅|❌|⚠️)/.test(msg)) return false;
if (msg && msg !== '处理中...') return false;
}
if (taskType === 'card_pay_recharge'
&& ['充值成功', '充值失败', '账号本身已有会员'].includes(String(acc.recharge_status || ''))) {
return false;
}
if (taskType === 'workspace_refund'
&& /^(✅|退款失败|任务已停止|账号已经退款|退款已经提交)/.test(String(acc.message || '').trim())) {
return false;
}
return true;
},
_accountFilterFieldValue(acc, field) {
if (!acc) return '';
if (field === 'browser_seq') return this.getBrowserSeq(acc.browser_id, acc.browser_seq);
if (field === 'status') return String(acc.status || '未登录').trim() || '未登录';
if (field === 'recharge_status') return String(acc.recharge_status || '未充值').trim() || '未充值';
if (field === 'account_refund_status') return String(acc.account_refund_status || '无状态').trim() || '无状态';
if (field === 'refund_process_status') return String(acc.refund_process_status || '未发起').trim() || '未发起';
if (field === 'refund_deadline_at') {
if (!acc.refund_deadline_at) return '未设置';
const instant = new Date(acc.refund_deadline_at);
return Number.isFinite(instant.getTime()) && instant.getTime() <= Date.now() ? '已到期' : '未到期';
}
if (field === 'message') return this.getLiveMessage(acc) || acc.message || '';
// 账号列表只返回授权 JSON 是否存在,筛选过程不读取或暴露完整令牌内容。
if (field === 'has_openai_auth') return acc.has_openai_auth ? '有授权JSON' : '';
// AC 凭证同样只依据列表中的存在标记筛选,不在前端读取完整凭证。
if (field === 'has_ag_credential') return (acc.has_ag_credential || acc.ag_refresh_token) ? '有AC凭证' : '';
// Gemini CLI 凭证只依据列表中的存在标记筛选,不在前端读取完整凭证。
if (field === 'has_cli_ac_credential') return acc.has_cli_ac_credential ? '有Cli_AC凭证' : '';
// 是否 Google AI Pro:三态 —— 1=Pro / 0=非Pro / null=未检测。
if (field === 'is_pro') {
if (acc.is_pro == null) return '未检测';
return (acc.is_pro === true || Number(acc.is_pro) === 1) ? 'Pro' : '非Pro';
}
return acc[field] == null ? '' : acc[field];
},
accountFilterNeedsValue(operator) {
return operator !== 'empty' && operator !== 'filled';
},
_isActiveAccountFilterCondition(condition) {
if (!condition || !condition.field || !condition.operator) return false;
if (!this.accountFilterNeedsValue(condition.operator)) return true;
return String(condition.value == null ? '' : condition.value).trim().length > 0;
},
_matchesAccountFilterCondition(acc, condition) {
if (!this._isActiveAccountFilterCondition(condition)) return true;
const actual = String(this._accountFilterFieldValue(acc, condition.field) || '').trim();
if (condition.operator === 'empty') return actual.length === 0;
if (condition.operator === 'filled') return actual.length > 0;
const expected = String(condition.value == null ? '' : condition.value).trim();
const actualLower = actual.toLowerCase();
const expectedLower = expected.toLowerCase();
if (condition.operator === 'equals') return actualLower === expectedLower;
if (condition.operator === 'not_equals') return actualLower !== expectedLower;
if (condition.operator === 'not_contains') return !actualLower.includes(expectedLower);
return actualLower.includes(expectedLower);
},
_matchesAccountFieldFilters(acc) {
return (this.accountFilterConditions || [])
.every(condition => this._matchesAccountFilterCondition(acc, condition));
},
_applyAccountQuery(accounts, keyword) {
return accounts.filter(acc => (
this._matchesAccountFieldFilters(acc) && this._matchSearch(acc, keyword)
));
},
get accountStatusOptions() {
const preferred = [
'未登录', '登录成功', '登录失败', '改密成功', '改密失败', '2FA修改成功', '2FA修改失败', '未验证已改密和2FA', '已验证改密和2FA', '未注册成功已改密和2FA',
'ChatGPT成功', 'ChatGPT失败', '订阅成功', '订阅失败', '已充值提取',
];
const available = new Set(this.accounts.map(acc => (
String((acc && acc.status) || '未登录').trim() || '未登录'
)));
const ordered = preferred.filter(status => available.has(status));
const remaining = [...available]
.filter(status => !preferred.includes(status))
.sort((a, b) => a.localeCompare(b, 'zh-CN'));
return [...ordered, ...remaining];
},
get hasAccountFieldFilters() {
return (this.accountFilterConditions || [])
.some(condition => this._isActiveAccountFilterCondition(condition));
},
addAccountFilterCondition() {
this.accountFilterConditions.push({
id: this._nextAccountFilterConditionId++,
field: 'email',
operator: 'contains',
value: '',
});
},
removeAccountFilterCondition(id) {
this.accountFilterConditions = this.accountFilterConditions
.filter(condition => condition.id !== id);
this.selectedEmails = new Set();
this.resetAccountPage();
},
onAccountFilterConditionChange(condition) {
if (condition && !this.accountFilterNeedsValue(condition.operator)) condition.value = '';
this.selectedEmails = new Set();
this.resetAccountPage();
},
clearAccountFieldFilters() {
this.accountFilterConditions = [];
this.selectedEmails = new Set();
this.resetAccountPage();
},
_normalizeBatchEmailSearchKey(value) {
const email = String(value || '').trim().toLowerCase();
const atIndex = email.lastIndexOf('@');
if (atIndex <= 0) return email;
let localPart = email.slice(0, atIndex);
let domain = email.slice(atIndex + 1);
// Gmail 用户名不区分大小写和点号;googlemail.com 与 gmail.com 也按同一域处理。
if (domain === 'gmail.com' || domain === 'googlemail.com') {
localPart = localPart.replace(/\./g, '');
domain = 'gmail.com';
}
return `${localPart}@${domain}`;
},
_parseBatchEmailSearchText(value) {
const seen = new Set();
return String(value || '')
.split(/\r?\n/)
.map(line => line.replace(/^\uFEFF/, '').trim())
.filter((email) => {
if (!email) return false;
const key = this._normalizeBatchEmailSearchKey(email);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
},
get batchEmailSearchAccounts() {
if (!this.batchEmailSearchActive) return [];
const accountsByKey = new Map();
this.accounts.forEach((account) => {
const key = this._normalizeBatchEmailSearchKey(account && account.email);
if (!key) return;
if (!accountsByKey.has(key)) accountsByKey.set(key, []);
accountsByKey.get(key).push(account);
});
return this.batchEmailSearchEmails.flatMap(email => (
accountsByKey.get(this._normalizeBatchEmailSearchKey(email)) || []
));
},
get batchEmailSearchMissingEmails() {
if (!this.batchEmailSearchActive) return [];
const existingEmails = new Set(this.accounts.map(account => (
this._normalizeBatchEmailSearchKey(account && account.email)
)));
return this.batchEmailSearchEmails.filter(email => (
!existingEmails.has(this._normalizeBatchEmailSearchKey(email))
));
},
openBatchEmailSearch() {
this.showBatchEmailSearchModal = true;
},
applyBatchEmailSearch() {
const emails = this._parseBatchEmailSearchText(this.batchEmailSearchText);
if (emails.length === 0) return this.addLog('⚠️ 请粘贴至少一个邮箱,每行一个', 'warn');
this.batchEmailSearchEmails = emails;
this.batchEmailSearchActive = true;
this.showBatchEmailSearchModal = false;
this.selectedEmails = new Set();
this.resetAccountPage();
this.addLog(
`批量邮箱查询:输入 ${emails.length} 个,找到 ${this.batchEmailSearchAccounts.length} 个,未找到 ${this.batchEmailSearchMissingEmails.length} 个`,
'success',
);
},
clearBatchEmailSearch() {
this.batchEmailSearchActive = false;
this.batchEmailSearchEmails = [];
this.batchEmailSearchText = '';
this.showBatchEmailSearchModal = false;
this.selectedEmails = new Set();
this.resetAccountPage();
},
async copyBatchEmailSearchMissing() {
const missing = this.batchEmailSearchMissingEmails;
if (missing.length === 0) return this.addLog('没有未找到的邮箱', 'warn');
await this.copyCell(missing.join('\n'));
this.addLog(`已复制 ${missing.length} 个未找到邮箱`, 'success');
},
get filteredAccounts() {
const kw = this.searchText.trim().toLowerCase();
const customJobEmails = this.customJobList.length > 0 ? new Set(this.customJobList.map(j => j.email)) : null;
const batchAccounts = this.batchEmailSearchActive ? this.batchEmailSearchAccounts : null;
// 批量邮箱只负责限定账号范围;其结果仍可叠加普通搜索、组合筛选和三级筛选。
// 使用对象集合做交集,并按批量邮箱的输入顺序返回结果。
const restrictToBatchEmails = (accounts) => {
if (!batchAccounts) return accounts;
const allowedAccounts = new Set(accounts);
return batchAccounts.filter(account => allowedAccounts.has(account));
};
// 特殊筛选路径:任务中 / 自定义任务(保留原逻辑)
if (this.statusFilter === '任务中') {
const pool = restrictToBatchEmails(
this.customerFilteredAccounts.filter(acc => this._isTaskAccountInProgress(acc)),
);
return this._applyAccountQuery(pool, kw);
}
if (this.statusFilter === '刚刚运行') {
const emailSet = new Set(this._lastTaskEmails || []);
const pool = restrictToBatchEmails(
this.customerFilteredAccounts.filter(acc => emailSet.has(acc.email)),
);
return this._applyAccountQuery(pool, kw);
}
if (this.statusFilter === '自定义任务') {
const pool = restrictToBatchEmails(this.customerFilteredAccounts.filter(acc => {
if (!customJobEmails || !customJobEmails.has(acc.email)) return false;
if (this.customStatusSubFilter && this.customStatusSubFilter !== '全部') {
if ((acc.status || '未登录') !== this.customStatusSubFilter) return false;
}
return true;
}));
return this._applyAccountQuery(pool, kw);
}
if (this.statusFilter === '家庭组母号') {
const pool = restrictToBatchEmails(
this.customerFilteredAccounts.filter(acc => acc.tag === '家庭组母号'),
);
return this._applyAccountQuery(pool, kw);
}
// 三级联动筛选
let pool = restrictToBatchEmails(this._tier2Accounts);
const l3 = this.tierFilter.level3;
if (l3 !== '全部') {
if (l3 === '未改密') pool = pool.filter(a => !this._hasPasswordChanged(a));
else if (l3 === '未改FA') pool = pool.filter(a => !a.fa_changed);
else if (l3 === '未关资料') pool = pool.filter(a => !a.payment_closed);
else if (l3 === '未查订阅') pool = pool.filter(a => !a.subscription_checked);
else if (l3 === '改密<7天') pool = pool.filter(a => { const d = this._getPasswordDaysAgo(a); return d >= 0 && d < 7; });
else if (l3 === '改密≥7天') pool = pool.filter(a => { const d = this._getPasswordDaysAgo(a); return d >= 7; });
else if (l3 === '可重新提取') pool = pool.filter(a => a.ag_status === '可重新授权');
else if (l3 === 'ChatGPT成功') pool = pool.filter(a => ['已登录', '已注册'].includes(a.chatgpt_status));
else if (l3 === 'ChatGPT失败') pool = pool.filter(a => a.chatgpt_status === '失败');
}
// 错误码筛选
const ec = this.tierFilter.errorCode;
if (ec === '__all__') {
pool = pool.filter(a => a.card_error_code);
} else if (ec === '__none__') {
pool = pool.filter(a => !a.card_error_code);
} else if (ec) {
pool = pool.filter(a => a.card_error_code === ec);
}
return this._applyAccountQuery(pool, kw);
},
/**
* 按机器原因码返回统一的账号处理结论,兼容旧版本中文原因值。
* @param {string} code - status_reason 或 SSE 推送的原因码
* @param {string} message - 历史消息,用于旧数据兜底识别
* @returns {string} 短处理结论
*/
_failureLabelForCode(code, message = '') {
const c = String(code || '').trim().toUpperCase();
const text = String(message || '').toLowerCase();
if (c.startsWith('GEMINI_CLI_')) return 'Cli凭证失败';
if (c === 'GCP_RESTRICTED' || text.includes('gcp被限制')) return 'GCP被限制';
if (c === 'GCP_NO_PERMISSION' || text.includes('无权限')) return '无权限';
if (c === 'GOOGLE_UNDER_18' || text.includes('未满18岁')) return '未满18岁';
if (c === 'CAPTCHA' || c === '人机验证' || c === '人机认证'
|| /人机(?:验证|认证)|recaptcha|(?:confirm|verify)(?:\s+that)?\s+you(?:['’]re|\s+are)\s+not\s+a\s+robot|\bnot\s+a\s+robot\b/i.test(text)) return '人机验证';
if (text.includes('密码错误')) return '密码错误';
if (c === 'PASSWORD_CHANGED_RECENTLY' || text.includes('密码已被修改')
|| /your\s+password\s+was\s+changed|密码(?:近期)?已(?:被)?(?:更改|修改)/i.test(text)) return '密码已被修改';
if (c === 'CHATGPT_ACCOUNT_DEACTIVATED'
|| /(?:账号|account).{0,40}(?:已删除|已停用|被删除|被停用|deleted|deactivated)/i.test(text)) return '账号已删除/停用';
if (c === 'TOTP_KEY_INCORRECT' || c === 'TOTP_CODE_WRONG' || text.includes('2fa密钥不正确')) return '2fa密钥不正确';
if (text.includes('2fa错误')) return '2fa错误';
if (text.includes('无法登录')) return '无法登录';
if (text.includes('切换ip重试')) return '切换ip重试';
if (c === 'PHONE_TABLET_VERIFY' || text.includes('需手机/平板验证')) return '要等7天';
if (text.includes('要等7天')) return '要等7天';
if (text.includes('功能失败')) return '功能失败';
if (['密码错误', '2FA错误', '无法登录', '切换IP重试', '要等7天', '功能失败'].includes(c)) {
return c === '切换IP重试' ? '切换ip重试' : c === '2FA错误' ? '2fa错误' : c;
}
if (c === '密码错误' || c === 'PASSWORD_WRONG') return '密码错误';
if (c === '2FA错误' || c === '2FA密钥无效' || c === '2FA验证码错误'
|| ['TOTP_KEY_INVALID', 'TOTP_OPTION_UNAVAILABLE'].includes(c)) return '2fa错误';
if (['NETWORK_PROXY', 'NETWORK_TIMEOUT', 'DNS', 'SIGNIN_REJECTED', 'NAVIGATION_FAILED',
'GOOGLE_TEMPORARY_ERROR', 'DOM_NOT_FOUND', 'CLICK_BLOCKED'].includes(c)) return '切换ip重试';
if (['OAUTH_RESTART_FAILED', 'OAUTH_FAILED', 'TOKEN_EXCHANGE_FAILED'].includes(c)
&& /net::err_|proxy|timeout|network|failed\s+to\s+fetch|超时|网络|代理.*(?:失败|异常|不可用)|测速.*失败|出口(?:ip)?获取失败|连接失败/i.test(text)) return '切换ip重试';
if (c.startsWith('OAUTH_') || ['OAUTH_FAILED', 'NO_REFRESH_TOKEN', 'TOKEN_EXCHANGE_FAILED'].includes(c)) return '要等7天';
if (['ENV_NOT_SECURE', 'PHONE_VERIFY', 'REAUTH_REQUIRED', 'VERIFY_ITS_YOU',
'ACCOUNT_LOCKED', 'PASSWORD_CHANGED_RECENTLY', 'RECOVERY_EMAIL_REQUIRED',
'BACKUP_CODE_REQUIRED', 'UNSUPPORTED_CHALLENGE', 'PASSWORD_REVERIFY_STUCK',
'FORCED_PASSWORD_CHANGE_FAILED', 'RISK', 'GCP_RESTRICTED', 'LOGIN_FAILED', 'NO_BROWSER'].includes(c)) {
return '无法登录';
}
if (/密码(?:错误|不正确|不对|有误)|wrong\s+password|incorrect\s+password/i.test(text)) return '密码错误';
if (/(?:2fa|totp|authenticator).*(?:错误|失败|无效|密钥|验证码)|验证码(?:错误|失败|不正确)|wrong\s+(?:2fa\s+|totp\s+)?code/i.test(text)) return '2fa错误';
if (/\boauth\b|授权页面|获取凭证|refresh[_ -]?token/i.test(text)) return '要等7天';
if (/net::err_|econnreset|etimedout|enotfound|eai_again|proxy|tunnel|timed\s*out|network|failed\s+to\s+fetch|网络|代理.*(?:失败|异常|不可用)|测速.*失败|出口(?:ip)?获取失败|连接(?:失败|超时)|页面跳转.*(?:失败|超时)|请求超时/i.test(text)) return '切换ip重试';
if (c === 'UNKNOWN' || c === 'CUSTOM_ERROR') {
return /登录|登陆|signin|challenge|account/i.test(text) ? '无法登录' : '功能失败';
}
return c ? '功能失败' : '';
},
/**
* 从账号记录和实时消息中取得最终处理结论,供表格和搜索共用。
* @param {object} account - 账号记录
* @returns {string} 短处理结论或空字符串
*/
_getAccountFailureLabel(account) {
if (!account) return '';
const direct = String(account.failure_label || '').trim();
if (['密码错误', '密码已被修改', '2fa错误', '2fa密钥不正确', '人机验证', '无法登录', '切换ip重试', '要等7天', '账号已删除/停用', '功能失败', 'GCP被限制', '无权限', '未满18岁', 'Cli凭证失败'].includes(direct)) return direct;
if (account.ag_status === 'OAuth等待7天') return '要等7天';
const live = this.accountLogs && this.accountLogs[account.email];
const message = [account.message, live && live.message].filter(Boolean).join(' ');
const reason = String(account.status_reason || '').trim();
if (reason) {
const fromCode = this._failureLabelForCode(reason, message);
if (fromCode) return fromCode;
}
// 仅对明确的失败消息做旧数据兼容识别,成功/处理中消息保持原样。
if (!/(失败|错误|异常|无法|拒绝|受阻|超时|不可用|已被修改|近期已更改|已删除|已停用|被删除|被停用|deleted|deactivated|password was changed|人机验证|人机认证|recaptcha|not a robot|测速|出口|wrong|incorrect|invalid|oauth|proxy|network)/i.test(message)) return '';
return this._failureLabelForCode('', message);
},
_matchSearch(acc, kw) {
if (!kw) return true;
const failureLabel = this._getAccountFailureLabel(acc);
const fields = [
acc.email,
acc.password,
acc.recovery_email,
acc.secret_key,
acc.browser_id,
this.getBrowserSeq(acc.browser_id, acc.browser_seq),
acc.country,
acc.status,
acc.message,
acc.sheerid_link,
acc.customer_name,
acc.card_error_code,
acc.ag_status,
acc.chatgpt_status,
acc.status_reason,
acc.failure_label,
failureLabel,
].filter(Boolean).map(v => String(v).toLowerCase());
return fields.some(v => v.includes(kw));
},
get statusCounts() {
const statuses = ['未登录','登录成功','登录失败','改密成功','改密失败','2FA修改成功','2FA修改失败','未验证已改密和2FA','已验证改密和2FA','未注册成功已改密和2FA','ChatGPT成功','ChatGPT失败','半价Ultra','订阅成功','订阅失败','已充值提取','自定义成功'];
const counts = Object.fromEntries(statuses.map(s => [s, 0]));
counts['未改2FA'] = 0;
counts['任务中'] = 0;
counts['其他'] = 0;
const customJobEmails = this.customJobList.length > 0 ? new Set(this.customJobList.map(j => j.email)) : null;
this.customerFilteredAccounts.forEach(acc => {
// 自定义任务中的账号不计入其他状态统计
if (customJobEmails && customJobEmails.has(acc.email)) return;
const st = acc.status || '未登录';
if (counts[st] !== undefined) counts[st]++;
else counts['其他']++;
// 统计未改2FA:有2FA且是小写的
if (st !== '订阅成功' && st !== '登录失败' && acc.secret_key && acc.secret_key === acc.secret_key.toLowerCase() && /[a-z]/.test(acc.secret_key)) {
counts['未改2FA']++;
}
if (this._isTaskAccountInProgress(acc)) {
counts['任务中']++;
}
});
return counts;
},
_rebuildDashboardStats() {
// tierCounts 是这里最贵的计算。先完成一次统计,再一次性替换快照,
// 避免 17 个 Alpine 卡片各自重扫全部账号。
const tierCounts = this.tierCounts;
const statusCounts = this.statusCounts;
this.dashboardStats = { tierCounts, statusCounts };
return this.dashboardStats;
},
get familyMainCount() {
return this.customerFilteredAccounts.filter(a => a.tag === '家庭组母号').length;
},
// ==================== 三级仪表盘 ====================
_LOGIN_FAIL_STATUSES: new Set(['登录失败', '失败', '未注册成功已改密和2FA']),
_UNLOGIN_STATUSES: new Set(['未登录']),
_getLevel1Group(status) {
const st = String(status || '未登录').trim();
if (this._UNLOGIN_STATUSES.has(st)) return '未登录';
// 任务会把主状态更新成“改密失败”“ChatGPT失败”等;这些不能兜底算作已登录。
if (this._LOGIN_FAIL_STATUSES.has(st) || st.endsWith('失败')) return '登录失败';
return '已登录';
},
_hasPasswordChanged(acc) {
if (!acc) return false;
if (acc.password_changed === true || Number(acc.password_changed) === 1) return true;
if (String(acc.status || '').trim() === '改密成功') return true;
return /密码(?:更改|修改)成功|改密成功/.test(String(acc.message || ''));
},
/** 从 security_dates 或本项目记录的改密时间计算天数,-1 表示未知 */
_getPasswordDaysAgo(acc) {
if (!acc) return -1;
if (acc.security_dates) {
try {
const obj = typeof acc.security_dates === 'string' ? JSON.parse(acc.security_dates) : acc.security_dates;
if (obj.passwordMonth && obj.passwordDay) {
return this._calcDaysAgo(obj.passwordMonth, obj.passwordDay, obj.checkedYear);
}
} catch (_) {}
}
if (!acc.password_changed_at) return -1;
const raw = String(acc.password_changed_at).trim();
const normalized = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(raw)
? `${raw.replace(' ', 'T')}Z`
: raw;
const changedAt = Date.parse(normalized);
if (!Number.isFinite(changedAt)) return -1;
return Math.max(0, Math.floor((Date.now() - changedAt) / (1000 * 60 * 60 * 24)));
},
_isOAuthWait7Account(acc) {
if (!acc) return false;
if (acc.ag_refresh_token) return false;
if (this._isGcpRestricted(acc) || acc.ag_status === '凭证已获取') return false;
if (String(acc.status_reason || '').toUpperCase() === 'OAUTH_WAIT_7_DAYS') return true;
if (String(acc.status_reason || '').toUpperCase().startsWith('OAUTH_')) return true;
if (acc.ag_status === 'OAuth等待7天') return true;
if (acc.ag_status === '可重新授权') return false;
if (this._getAccountFailureLabel(acc) === '要等7天') return true;
const message = String(acc.message || '');
return /验证方式(?:选择|选项)页(?:面)?无.*(?:2FA|TOTP|Authenticator)/i.test(message);
},
_isGcpRestricted(acc) {
if (!acc) return false;
return acc.ag_status === 'GCP被限制'
|| acc.restriction_status === 'restricted'
|| String(acc.status_reason || '').toUpperCase() === 'GCP_RESTRICTED';
},
// 第一行过滤后的账号列表
get _tier1Accounts() {
const pool = this.customerFilteredAccounts;
const base = pool;
const l1 = this.tierFilter.level1;
if (l1 === '全部') return base;
return base.filter(acc => this._getLevel1Group(acc.status) === l1);
},
// 第二行过滤后的账号列表
get _tier2Accounts() {
const pool = this._tier1Accounts;
const l2 = this.tierFilter.level2;
if (l2 === '全部') return pool;
if (l2 === '半价邮箱') return pool.filter(a => a.is_half_price === 1);
if (l2 === '全价邮箱') return pool.filter(a => a.is_half_price === 0);
if (l2 === '未检测') return pool.filter(a => a.is_half_price == null);
if (l2 === '订阅成功') return pool.filter(a => (a.status || '') === '订阅成功');
if (l2 === '订阅失败') return pool.filter(a => a.card_last4 && (a.status || '') !== '订阅成功');
if (l2 === '未违反规则') return pool.filter(a => a.restriction_status === 'ok');
if (l2 === '违反规则') return pool.filter(a => a.restriction_status === 'restricted');
if (l2 === '有凭证') return pool.filter(a => a.ag_refresh_token);
if (l2 === 'GCP被限制') return pool.filter(a => this._isGcpRestricted(a));
if (l2 === 'OAuth等待7天') return pool.filter(a => this._isOAuthWait7Account(a));
if (l2 === '未检测凭证') return pool.filter(a => !a.ag_refresh_token && !this._isGcpRestricted(a) && !this._isOAuthWait7Account(a));
return pool;
},
get tierCounts() {
const pool = this.customerFilteredAccounts;
const base = pool;
// Level 1 counts
const l1 = { '全部': base.length, '未登录': 0, '已登录': 0, '登录失败': 0 };
base.forEach(acc => {
const g = this._getLevel1Group(acc.status);
l1[g] = (l1[g] || 0) + 1;
});
// Level 2 counts (based on tier1 filtered)
const t1 = this._tier1Accounts;
const l2 = {
'全部': t1.length,
'半价邮箱': t1.filter(a => a.is_half_price === 1).length,
'全价邮箱': t1.filter(a => a.is_half_price === 0).length,
'未检测': t1.filter(a => a.is_half_price == null).length,
'订阅成功': t1.filter(a => (a.status || '') === '订阅成功').length,
'订阅失败': t1.filter(a => a.card_last4 && (a.status || '') !== '订阅成功').length,
'未违反规则': t1.filter(a => a.restriction_status === 'ok').length,
'违反规则': t1.filter(a => a.restriction_status === 'restricted').length,
'有凭证': t1.filter(a => a.ag_refresh_token).length,
'GCP被限制': t1.filter(a => this._isGcpRestricted(a)).length,
'OAuth等待7天': t1.filter(a => this._isOAuthWait7Account(a)).length,
'未检测凭证': t1.filter(a => !a.ag_refresh_token && !this._isGcpRestricted(a) && !this._isOAuthWait7Account(a)).length,
};
// Level 3 counts (based on tier2 filtered)
const t2 = this._tier2Accounts;
const l3 = {
'全部': t2.length,
'未改密': t2.filter(a => !this._hasPasswordChanged(a)).length,
'未改FA': t2.filter(a => !a.fa_changed).length,
'改密<7天': t2.filter(a => { const d = this._getPasswordDaysAgo(a); return d >= 0 && d < 7; }).length,
'改密≥7天': t2.filter(a => { const d = this._getPasswordDaysAgo(a); return d >= 7; }).length,
'可重新提取': t2.filter(a => a.ag_status === '可重新授权').length,
'ChatGPT成功': t2.filter(a => ['已登录', '已注册'].includes(a.chatgpt_status)).length,
'ChatGPT失败': t2.filter(a => a.chatgpt_status === '失败').length,
};
return { level1: l1, level2: l2, level3: l3 };
},
get errorCodeList() {
const codes = new Set();
this.customerFilteredAccounts.forEach(a => {
if (a.card_error_code && a.card_error_code !== '已订阅') codes.add(a.card_error_code);
});
return [...codes].sort();
},
get errorCodeTotalCount() {
return this._tier2Accounts.filter(a => a.card_error_code && a.card_error_code !== '已订阅').length;
},
get noErrorCodeCount() {
return this._tier2Accounts.filter(a => !a.card_error_code || a.card_error_code === '已订阅').length;
},
// 按客户名预筛选的账号列表
get customerFilteredAccounts() {
if (!this.customerNameFilter) return this.accounts;
return this.accounts.filter(acc => {
if (this.customerNameFilter === '__empty__') return !acc.customer_name;
return (acc.customer_name || '') === this.customerNameFilter;
});
},
get customerNames() {
const names = new Set();
this.accounts.forEach(acc => {
if (acc.customer_name) names.add(acc.customer_name);
});
return [...names].sort();
},
_customerColorMap: {},
_customerColorIdx: 0,
_customerColors: [
'bg-blue-100 text-blue-700',
'bg-green-100 text-green-700',
'bg-purple-100 text-purple-700',
'bg-orange-100 text-orange-700',
'bg-pink-100 text-pink-700',
'bg-teal-100 text-teal-700',
'bg-yellow-100 text-yellow-800',
'bg-red-100 text-red-700',
'bg-indigo-100 text-indigo-700',
'bg-cyan-100 text-cyan-700',
],
customerTagClass(name) {
if (!name) return '';
if (!this._customerColorMap[name]) {
this._customerColorMap[name] = this._customerColors[this._customerColorIdx % this._customerColors.length];
this._customerColorIdx++;
}
return this._customerColorMap[name];
},
toggleSelectAll() {
const visible = this.filteredAccounts;
const allSelected = visible.every(a => this.selectedEmails.has(a.email)) && visible.length > 0;
if (allSelected) {
const newSet = new Set(this.selectedEmails);
visible.forEach(a => newSet.delete(a.email));
this.selectedEmails = newSet;
} else {
this.selectedEmails = new Set([...this.selectedEmails, ...visible.map(a => a.email)]);
}
},
selectCurrentPage() {
const pageEmails = this.pagedAccounts.map(account => account.email).filter(Boolean);
if (pageEmails.length === 0) return;
this.selectedEmails = new Set([...this.selectedEmails, ...pageEmails]);
},
deselectCurrentPage() {
const pageEmails = new Set(this.pagedAccounts.map(account => account.email).filter(Boolean));
if (pageEmails.size === 0) return;
this.selectedEmails = new Set(
[...this.selectedEmails].filter(email => !pageEmails.has(email)),
);
},
clearSelection() {
this.selectedEmails = new Set();
},
toggleSelect(email) {
const newSet = new Set(this.selectedEmails);
if (newSet.has(email)) newSet.delete(email);
else newSet.add(email);
this.selectedEmails = newSet;
},
isSelected(email) {
return this.selectedEmails.has(email);
},
// ---- 拖拽选择(账号) ----
startAccountDrag(event, account) {
if (event.target.closest('input, button, .copy-cell')) return;
event.preventDefault();
const mode = this.isSelected(account.email) ? 'deselect' : 'select';
this._dragState = { type: 'account', mode };
document.body.classList.add('drag-selecting');
const newSet = new Set(this.selectedEmails);
if (mode === 'select') newSet.add(account.email);
else newSet.delete(account.email);
this.selectedEmails = newSet;
},
onAccountDragOver(event, account) {
if (!this._dragState || this._dragState.type !== 'account') return;
const newSet = new Set(this.selectedEmails);
if (this._dragState.mode === 'select') newSet.add(account.email);
else newSet.delete(account.email);
this.selectedEmails = newSet;
},
async importAccounts() {
if (!this.importText.trim()) return;
try {
const res = await fetch('/api/accounts/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: this.importText,
template: this.getSelectedTemplatePattern(),
defaultStatus: this.importStatus,
customerName: this.importCustomerName.trim() || null,
skipDuplicates: this.importSkipDuplicates,
}),
});
const data = await res.json();
if (data.success) {
const skippedText = data.skippedCount > 0 ? `,跳过重复 ${data.skippedCount} 个` : '';
this.addLog(`导入成功 ${data.successCount} 个${skippedText},失败 ${data.errorCount} 个`, 'success');
if (data.errors && data.errors.length > 0) {
data.errors.slice(0, 5).forEach(e => this.addLog(` ⚠️ ${e}`, 'warn'));
}
this.showImportAccountModal = false;
const importedEmails = data.importedEmails || [];
this.importText = '';
this.importCustomerName = '';
this.accountPage = 1;
await this.loadAccounts();
if (importedEmails.length > 0) {
this.addLog('账号已入库;执行具体任务时会自动创建临时比特浏览器,任务结束后自动删除', 'info');
}
} else {
this.addLog(`导入失败: ${data.msg}`, 'error');
}
} catch (e) {
this.addLog(`导入失败: ${e.message}`, 'error');
}
},
async deleteSelectedAccounts() {
if (this.selectedEmails.size === 0) return;
if (this.isDeletingAccounts) {
this.addLog('账号删除任务仍在进行中,请等待当前批次完成', 'warn');
return;
}
if (!confirm(
`确认删除选中的 ${this.selectedEmails.size} 个账号?\n\n`
+ '账号的浏览器编号会一并删除,并尝试删除对应的比特浏览器窗口。\n'
+ '家庭组母号和正在使用静态代理的窗口会保留。',
)) return;
const emails = [...this.selectedEmails];
this.isDeletingAccounts = true;
// 中文注释:先给出明确进度提示,避免大批量删除时用户误以为没有触发。
this.addLog(`开始删除 ${emails.length} 个账号,请稍候...`, 'info');
try {
const res = await fetch('/api/accounts', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ emails }),
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '删除请求失败');
const browserCount = Number(data.browsersDeleted) || 0;
const browserFound = Number(data.browsersFound) || 0;
const browserFailed = Number(data.browserDeleteFailed) || 0;
if (browserFailed > 0) {
this.addLog(
`已删除 ${data.count} 个账号;比特浏览器窗口成功 ${browserCount} 个、失败 ${browserFailed} 个`,
'warn',
);
} else if (browserFound > 0) {
this.addLog(`已删除 ${data.count} 个账号,并删除 ${browserCount} 个比特浏览器窗口`, 'success');
} else {
this.addLog(`已删除 ${data.count} 个账号(没有需要删除的比特浏览器窗口)`, 'success');
}
this.selectedEmails = new Set();
await this.loadAccounts();
} catch (e) {
this.addLog(`删除失败: ${e.message}`, 'error');
} finally {
this.isDeletingAccounts = false;
}
},
async stageExportAccounts(accounts, selectedFields) {
const emails = accounts.map(account => account.email).filter(Boolean);
if (emails.length === 0) return false;
try {
const res = await fetch('/api/accounts/staging', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
emails,
note: this.exportStagingNote.trim(),
selectedFields,
deleteBrowserWindows: this.deleteBrowserWindowsOnStage,
}),
});
const data = await res.json();
if (!data.success) {
this.addLog(`放入暂存箱失败: ${data.msg || '未知错误'}`, 'error');
return false;
}
if (data.count === 0) {
this.addLog('⚠️ 没有账号被放入暂存箱,列表可能已经刷新', 'warn');
return false;
}
const stagedEmailSet = new Set(emails);
this.selectedEmails = new Set([...this.selectedEmails].filter(email => !stagedEmailSet.has(email)));
this.addLog(`已将 ${data.count} 个账号放入暂存箱`, 'success');
if (data.browserDeleteRequested) {
if (data.browsersFound === 0) {
this.addLog('暂存账号没有绑定比特浏览器窗口', 'info');
} else if (data.browserDeleteFailed > 0) {
this.addLog(`比特窗口删除完成: 成功 ${data.browsersDeleted} 个,失败 ${data.browserDeleteFailed} 个`, 'warn');
} else {
this.addLog(`已同时删除 ${data.browsersDeleted} 个比特浏览器窗口`, 'success');
}
}
await this.loadAccounts();
return true;
} catch (e) {
this.addLog(`放入暂存箱失败: ${e.message}`, 'error');
return false;
}
},
async openStagingModal() {
this.showStagingModal = true;
this.stagedBatchDetail = null;
await this.loadStagingBatches();
},
async loadStagingBatches() {
try {
const res = await fetch('/api/accounts/staging');
const data = await res.json();
if (data.success) {
this.stagedBatches = Array.isArray(data.data) ? data.data : [];
} else {
this.addLog(`加载暂存箱失败: ${data.msg || '未知错误'}`, 'error');
}
} catch (e) {
this.addLog(`加载暂存箱失败: ${e.message}`, 'error');
}
},
getStagingSelectedFields(batch) {
if (!batch) return [];
let fields = batch.selected_fields;
if (typeof fields === 'string') {
try { fields = JSON.parse(fields); } catch (_) { fields = []; }
}
return Array.isArray(fields) ? fields : [];
},
getStagingFieldLabels(batch) {
const selected = new Set(this.getStagingSelectedFields(batch));
return this.exportFieldDefs.filter(field => selected.has(field.key)).map(field => field.label).join(' · ');
},
async openStagedBatch(batch) {
try {
const res = await fetch(`/api/accounts/staging/${batch.id}`);
const data = await res.json();
if (!data.success) {
this.addLog(`打开暂存文件夹失败: ${data.msg || '未知错误'}`, 'error');
return;
}
this.stagedBatchDetail = data.data;
} catch (e) {
this.addLog(`打开暂存文件夹失败: ${e.message}`, 'error');
}
},
get stagedBatchText() {
if (!this.stagedBatchDetail) return '';
const fields = this.getStagingSelectedFields(this.stagedBatchDetail.batch);
return this.stagedBatchDetail.accounts
.map(account => fields.map(field => this.exportFieldValue(account, field)).join('----'))
.join('\n');
},
async copyStagedBatch() {
if (!this.stagedBatchText) return;
try {
await navigator.clipboard.writeText(this.stagedBatchText);
} catch (e) {
const ta = document.createElement('textarea');
ta.value = this.stagedBatchText;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
this.addLog(`已复制 ${this.stagedBatchDetail.accounts.length} 个暂存账号`, 'success');
},
downloadStagedBatch() {
if (!this.stagedBatchDetail || !this.stagedBatchText) return;
const blob = new Blob([this.stagedBatchText], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
const safeName = this.stagedBatchDetail.batch.name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').slice(0, 80) || '暂存账号';
link.href = url;
link.download = `${safeName}.txt`;
link.click();
URL.revokeObjectURL(url);
this.addLog(`已导出 ${this.stagedBatchDetail.accounts.length} 个暂存账号到 TXT`, 'success');
},
async restoreStagedBatch() {
if (!this.stagedBatchDetail) return;
if (this.isRunning) {
this.addLog('⚠️ 当前有任务正在运行,请等待完成后再恢复账号', 'warn');
return;
}
const batch = this.stagedBatchDetail.batch;
try {
const res = await fetch(`/api/accounts/staging/${batch.id}/restore`, { method: 'POST' });
const data = await res.json();
if (!data.success) {
this.addLog(`恢复失败: ${data.msg || '未知错误'}`, 'error');
return;
}
this.addLog(`已从“${batch.name}”恢复 ${data.count} 个账号`, 'success');
this.stagedBatchDetail = null;
await Promise.all([this.loadStagingBatches(), this.loadAccounts()]);
const restoredAccounts = Array.isArray(data.accounts) ? data.accounts : [];
if (restoredAccounts.length > 0) {
this.addLog('恢复完成;执行具体任务时会自动创建临时比特浏览器', 'info');
}
} catch (e) {
this.addLog(`恢复失败: ${e.message}`, 'error');
}
},
async permanentlyDeleteStagedBatch() {
if (!this.stagedBatchDetail) return;
const batch = this.stagedBatchDetail.batch;
if (!confirm(`确认彻底删除文件夹“${batch.name}”中的 ${batch.account_count} 个账号记录?此操作不会删除浏览器窗口。`)) return;
try {
const res = await fetch(`/api/accounts/staging/${batch.id}`, { method: 'DELETE' });
const data = await res.json();
if (!data.success) {
this.addLog(`彻底删除失败: ${data.msg || '未知错误'}`, 'error');
return;
}
this.addLog(`已彻底删除文件夹“${batch.name}”及 ${data.count} 个账号记录`, 'success');
this.stagedBatchDetail = null;
await this.loadStagingBatches();
} catch (e) {
this.addLog(`彻底删除失败: ${e.message}`, 'error');
}
},
async setAccountTag(account, tag) {
try {
await fetch(`/api/accounts/${encodeURIComponent(account.email)}/tag`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag }),
});
account.tag = tag || null;
} catch (e) {
this.addLog(`设置标签失败: ${e.message}`, 'error');
}
},
async batchSetFamilyTag(setOrClear) {
const emails = [...this.selectedEmails];
if (emails.length === 0) { this.addLog('⚠️ 未选择账号', 'warn'); return; }
const tag = setOrClear ? '家庭组母号' : null;
for (const email of emails) {
try {
await fetch(`/api/accounts/${encodeURIComponent(email)}/tag`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag }),
});
const acc = this._accountByEmail.get(email);
if (acc) acc.tag = tag;
} catch (_) {}
}
this.addLog(`${setOrClear ? '设置' : '清除'}家庭组母号标签: ${emails.length} 个账号`, 'info');
},
// ==================== 设置管理 ====================
async loadSettings() {
try {
const res = await fetch('/api/settings');
const data = await res.json();
if (data.success) {
this.settings = data.data;
if (!this.settings.feishu_notify_mode) this.settings.feishu_notify_mode = 'none';
if (!this.settings.feishu_notify_receive_id_type) this.settings.feishu_notify_receive_id_type = 'email';
if (!this.settings.quota_monitor_enabled) this.settings.quota_monitor_enabled = '0';
if (!this.settings.quota_monitor_interval_value) this.settings.quota_monitor_interval_value = '24';
if (!this.settings.quota_monitor_interval_unit) this.settings.quota_monitor_interval_unit = 'hours';
if (!this.settings.quota_monitor_concurrency) this.settings.quota_monitor_concurrency = '2';
if (!this.settings.quota_monitor_threshold) this.settings.quota_monitor_threshold = '100';
if (!this.settings.quota_monitor_skip_401_enabled) this.settings.quota_monitor_skip_401_enabled = '0';
if (!this.settings.quota_monitor_auto_refund_enabled) this.settings.quota_monitor_auto_refund_enabled = '0';
if (!this.settings.refund_deadline_check_interval_seconds) this.settings.refund_deadline_check_interval_seconds = '15';
if (this.settings.default_thread_count) {
this.threadCount = parseInt(this.settings.default_thread_count) || 1;
}
}
} catch (e) {}
},
async saveSetting(key, value) {
try {
await fetch(`/api/settings/${key}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ value }),
});
this.settings[key] = value;
this.addLog(`设置已保存: ${key}`, 'info');
} catch (e) {
this.addLog(`保存设置失败: ${e.message}`, 'error');
}
},
async loadRechargeCredentialMeta() {
try {
const res = await fetch('/api/recharge-credentials', { cache: 'no-store' });
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '读取失败');
this.rechargeCredentialMeta = {
configured: !!(data.data && data.data.configured),
keyIdMasked: String((data.data && data.data.keyIdMasked) || ''),
updatedAt: (data.data && data.data.updatedAt) || null,
};
} catch (_) {
this.rechargeCredentialMeta = { configured: false, keyIdMasked: '', updatedAt: null };
}
},
async saveRechargeCredentials() {
if (this.rechargeCredentialSaving) return;
const keyId = String(this.rechargeCredentialForm.keyId || '').trim();
const secret = String(this.rechargeCredentialForm.secret || '');
if (!keyId || !secret.trim()) {
this.addLog('⚠️ API Key ID 和 API Secret 都不能为空', 'warn');
return;
}
this.rechargeCredentialSaving = true;
try {
const res = await fetch('/api/recharge-credentials', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ keyId, secret }),
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '保存失败');
this.rechargeCredentialMeta = {
configured: !!(data.data && data.data.configured),
keyIdMasked: String((data.data && data.data.keyIdMasked) || ''),
updatedAt: (data.data && data.data.updatedAt) || null,
};
// Secret 从不保存在 Alpine 的全局 settings 中,保存后立即清空表单。
this.rechargeCredentialForm = { keyId: '', secret: '' };
this.addLog('✅ 充值 API 凭据已加密保存', 'success');
} catch (e) {
this.addLog(`❌ 保存充值 API 凭据失败: ${e.message}`, 'error');
} finally {
this.rechargeCredentialSaving = false;
}
},
async deleteRechargeCredentials() {
if (this.rechargeCredentialDeleting || !this.rechargeCredentialMeta.configured) return;
if (!confirm('确认清除已保存的充值 API Key ID 和 API Secret?')) return;
this.rechargeCredentialDeleting = true;
try {
const res = await fetch('/api/recharge-credentials', { method: 'DELETE' });
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '清除失败');
this.rechargeCredentialMeta = { configured: false, keyIdMasked: '', updatedAt: null };
this.rechargeCredentialForm = { keyId: '', secret: '' };
this.addLog('✅ 充值 API 凭据已清除', 'success');
} catch (e) {
this.addLog(`❌ 清除充值 API 凭据失败: ${e.message}`, 'error');
} finally {
this.rechargeCredentialDeleting = false;
}
},
async loadWorkspaceRefundCredentialMeta() {
try {
const res = await fetch('/api/workspace-refund-credentials', { cache: 'no-store' });
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '读取失败');
this.workspaceRefundCredentialMeta = {
configured: !!(data.data && data.data.configured),
ownerMasked: String((data.data && data.data.ownerMasked) || ''),
proxyConfigured: !!(data.data && data.data.proxyConfigured),
proxyCount: Number((data.data && data.data.proxyCount) || 0),
proxyMasked: String((data.data && data.data.proxyMasked) || ''),
updatedAt: (data.data && data.data.updatedAt) || null,
};
} catch (_) {
this.workspaceRefundCredentialMeta = {
configured: false,
ownerMasked: '',
proxyConfigured: false,
proxyCount: 0,
proxyMasked: '',
updatedAt: null,
};
}
},
async saveWorkspaceRefundCredentials() {
if (this.workspaceRefundCredentialSaving) return;
const value = String(this.workspaceRefundCredentialForm.value || '').trim();
const proxy = String(this.workspaceRefundCredentialForm.proxy || '').trim();
if (!value && !this.workspaceRefundCredentialMeta.configured) {
this.addLog('⚠️ 请填写母号 ChatGPT Session 或 Access Token', 'warn');
return;
}
if (!proxy && !this.workspaceRefundCredentialMeta.proxyConfigured) {
this.addLog('⚠️ 请填写退款专用代理', 'warn');
return;
}
if (!value && !proxy) {
this.addLog('⚠️ 请填写需要更新的母号登录内容或退款专用代理', 'warn');
return;
}
this.workspaceRefundCredentialSaving = true;
try {
const body = { value };
// 已保存代理时输入框留空代表保留原代理,不会意外清除。
if (proxy) body.proxy = proxy;
const res = await fetch('/api/workspace-refund-credentials', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '保存失败');
this.workspaceRefundCredentialMeta = {
configured: !!(data.data && data.data.configured),
ownerMasked: String((data.data && data.data.ownerMasked) || ''),
proxyConfigured: !!(data.data && data.data.proxyConfigured),
proxyCount: Number((data.data && data.data.proxyCount) || 0),
proxyMasked: String((data.data && data.data.proxyMasked) || ''),
updatedAt: (data.data && data.data.updatedAt) || null,
};
this.workspaceRefundCredentialForm = { value: '', proxy: '' };
this.addLog('✅ 退款母号和专用代理已加密保存', 'success');
} catch (e) {
this.addLog(`❌ 保存退款母号失败: ${e.message}`, 'error');
} finally {
this.workspaceRefundCredentialSaving = false;
}
},
async deleteWorkspaceRefundCredentials() {
if (this.workspaceRefundCredentialDeleting || !this.workspaceRefundCredentialMeta.configured) return;
if (!confirm('确认清除已保存的退款母号?')) return;
this.workspaceRefundCredentialDeleting = true;
try {
const res = await fetch('/api/workspace-refund-credentials', { method: 'DELETE' });
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '清除失败');
this.workspaceRefundCredentialMeta = {
configured: false,
ownerMasked: '',
proxyConfigured: false,
proxyCount: 0,
proxyMasked: '',
updatedAt: null,
};
this.workspaceRefundCredentialForm = { value: '', proxy: '' };
this.addLog('✅ 退款母号已清除', 'success');
} catch (e) {
this.addLog(`❌ 清除退款母号失败: ${e.message}`, 'error');
} finally {
this.workspaceRefundCredentialDeleting = false;
}
},
async loadGoogleAccountCredentialMeta() {
try {
const res = await fetch('/api/google-account-credentials', { cache: 'no-store' });
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '读取失败');
this.googleAccountCredentialMeta = {
configured: !!(data.data && data.data.configured),
keyMasked: String((data.data && data.data.keyMasked) || ''),
apiUrl: String((data.data && data.data.apiUrl) || 'https://sv9.api999api.com/google/apiv2.php'),
timelive: Number((data.data && data.data.timelive) || 60),
updatedAt: (data.data && data.data.updatedAt) || null,
};
this.googleAccountCredentialForm.timelive = this.googleAccountCredentialMeta.timelive;
} catch (_) {
this.googleAccountCredentialMeta = { configured: false, keyMasked: '', apiUrl: 'https://sv9.api999api.com/google/apiv2.php', timelive: 60, updatedAt: null };
this.googleAccountCredentialForm.timelive = 60;
}
},
async saveGoogleAccountCredentials() {
if (this.googleAccountCredentialSaving) return;
const value = String(this.googleAccountCredentialForm.value || '').trim();
const timelive = Math.max(1, Math.min(43200, Number.parseInt(this.googleAccountCredentialForm.timelive, 10) || 60));
if (!value && !this.googleAccountCredentialMeta.configured) {
this.addLog('⚠️ 请填写取号 API Key 或完整链接', 'warn');
return;
}
this.googleAccountCredentialSaving = true;
try {
const res = await fetch('/api/google-account-credentials', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ value, timelive }),
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '保存失败');
this.googleAccountCredentialMeta = {
configured: !!(data.data && data.data.configured),
keyMasked: String((data.data && data.data.keyMasked) || ''),
apiUrl: String((data.data && data.data.apiUrl) || 'https://sv9.api999api.com/google/apiv2.php'),
timelive: Number((data.data && data.data.timelive) || timelive),
updatedAt: (data.data && data.data.updatedAt) || null,
};
this.googleAccountCredentialForm = { value: '', timelive: this.googleAccountCredentialMeta.timelive };
this.addLog('✅ 取号 API 凭据已加密保存', 'success');
} catch (e) {
this.addLog(`❌ 保存取号 API 凭据失败: ${e.message}`, 'error');
} finally {
this.googleAccountCredentialSaving = false;
}
},
async deleteGoogleAccountCredentials() {
if (this.googleAccountCredentialDeleting || !this.googleAccountCredentialMeta.configured) return;
if (!confirm('确认清除已保存的取号 API 凭据?')) return;
this.googleAccountCredentialDeleting = true;
try {
const res = await fetch('/api/google-account-credentials', { method: 'DELETE' });
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '清除失败');
this.googleAccountCredentialMeta = { configured: false, keyMasked: '', apiUrl: 'https://sv9.api999api.com/google/apiv2.php', timelive: 60, updatedAt: null };
this.googleAccountCredentialForm = { value: '', timelive: 60 };
this.addLog('✅ 取号 API 凭据已清除', 'success');
} catch (e) {
this.addLog(`❌ 清除取号 API 凭据失败: ${e.message}`, 'error');
} finally {
this.googleAccountCredentialDeleting = false;
}
},
async loadFeishuCredentialMeta() {
try {
const res = await fetch('/api/feishu/credentials', { cache: 'no-store' });
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '读取失败');
this.feishuCredentialMeta = {
configured: !!(data.data && data.data.configured),
appIdMasked: String((data.data && data.data.appIdMasked) || ''),
updatedAt: (data.data && data.data.updatedAt) || null,
};
} catch (_) {
this.feishuCredentialMeta = { configured: false, appIdMasked: '', updatedAt: null };
}
},
async saveFeishuCredentials() {
if (this.feishuCredentialSaving) return;
const appId = String(this.feishuCredentialForm.appId || '').trim();
const appSecret = String(this.feishuCredentialForm.appSecret || '');
if (!appId || !appSecret.trim()) {
this.addLog('⚠️ 飞书 App ID 和 App Secret 都不能为空', 'warn');
return;
}
this.feishuCredentialSaving = true;
try {
const res = await fetch('/api/feishu/credentials', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ appId, appSecret }),
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '保存失败');
this.feishuCredentialMeta = {
configured: !!(data.data && data.data.configured),
appIdMasked: String((data.data && data.data.appIdMasked) || ''),
updatedAt: (data.data && data.data.updatedAt) || null,
};
this.feishuCredentialForm = { appId: '', appSecret: '' };
this.addLog('✅ 飞书应用凭据已在本机加密保存', 'success');
} catch (e) {
this.addLog(`❌ 保存飞书应用凭据失败:${e.message}`, 'error');
} finally {
this.feishuCredentialSaving = false;
}
},
async deleteFeishuCredentials() {
if (this.feishuCredentialDeleting || !this.feishuCredentialMeta.configured) return;
if (!confirm('确认清除已保存的飞书 App ID 和 App Secret?')) return;
this.feishuCredentialDeleting = true;
try {
const res = await fetch('/api/feishu/credentials', { method: 'DELETE' });
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '清除失败');
this.feishuCredentialMeta = { configured: false, appIdMasked: '', updatedAt: null };
this.feishuCredentialForm = { appId: '', appSecret: '' };
this.addLog('✅ 飞书应用凭据已清除', 'success');
} catch (e) {
this.addLog(`❌ 清除飞书应用凭据失败:${e.message}`, 'error');
} finally {
this.feishuCredentialDeleting = false;
}
},
async openCustomErrors() {
this.showCustomErrorsModal = true;
await this.loadCustomErrorRules();
},
async loadCustomErrorRules() {
try {
const res = await fetch('/api/settings/custom_error_rules');
const json = await res.json();
const raw = json && json.success ? (json.value || '[]') : '[]';
let arr = [];
try { arr = JSON.parse(raw || '[]'); } catch (e) { arr = []; }
if (!Array.isArray(arr)) arr = [];
this.customErrorRules = arr.map(r => {
if (!r) return r;
const taskTypes = Array.isArray(r.taskTypes) ? r.taskTypes : (r.taskType ? [r.taskType] : []);
return { ...r, taskTypes };
});
} catch (e) {
this.customErrorRules = [];
}
},
async saveCustomErrorRules() {
const v = JSON.stringify(this.customErrorRules || []);
await this.saveSetting('custom_error_rules', v);
},
async addCustomErrorRule() {
const taskTypes = (this.customErrorNewTasks || []).map(s => String(s || '').trim()).filter(Boolean);
const matchText = String(this.customErrorNewText || '').replace(/\s+/g, ' ').trim();
const tip = String(this.customErrorNewTip || '').replace(/\s+/g, ' ').trim();
if (taskTypes.length === 0 || !matchText || !tip) {
this.addLog('自定义错误添加失败:任务类型/匹配文本/提示不能为空', 'error');
return;
}
const id = `${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
this.customErrorRules = [...(this.customErrorRules || []), { id, taskTypes, matchText, tip }];
this.customErrorNewText = '';
this.customErrorNewTip = '';
await this.saveCustomErrorRules();
await this.loadCustomErrorRules();
},
async removeCustomErrorRule(id) {
const s = String(id || '');
this.customErrorRules = (this.customErrorRules || []).filter(r => String(r && r.id) !== s);
await this.saveCustomErrorRules();
await this.loadCustomErrorRules();
},
// ==================== 诊断日志页 ====================
async loadDiagAccounts() {
try {
const res = await fetch('/api/diagnostics/accounts');
const json = await res.json();
if (json.success) {
this.diagAccounts = json.data || [];
if (this.diagEmail && !this.diagAccounts.includes(this.diagEmail)) this.diagEmail = '';
}
} catch (e) {}
},
filteredDiagAccounts() {
const q = (this.diagEmailSearch || '').trim().toLowerCase();
if (!q) return this.diagAccounts;
return this.diagAccounts.filter(e => String(e || '').toLowerCase().includes(q));
},
async onDiagEmailChange() {
this.diagSelected = null;
this.diagOffset = 0;
await this.loadDiagDates();
await this.loadDiagEvents();
},
async loadDiagDates() {
if (!this.diagEmail) {
this.diagDates = [];
this.diagDate = '';
return;
}
try {
const res = await fetch(`/api/diagnostics/dates?email=${encodeURIComponent(this.diagEmail)}`);
const json = await res.json();
if (json.success) {
this.diagDates = json.data || [];
if (!this.diagDate || !this.diagDates.includes(this.diagDate)) {
this.diagDate = this.diagDates[0] || '';
}
}
} catch (e) {}
},
async loadDiagEvents() {
if (!this.diagEmail || !this.diagDate) return;
this.diagLoading = true;
this.diagSelected = null;
try {
const qs = new URLSearchParams({
email: this.diagEmail,
date: this.diagDate,
offset: String(this.diagOffset || 0),
limit: String(this.diagLimit || 200),
});
const res = await fetch(`/api/diagnostics/events?${qs.toString()}`);
const json = await res.json();
if (json.success) {
this.diagTotal = json.data.total || 0;
this.diagEvents = json.data.events || [];
} else {
this.addLog(`加载诊断日志失败: ${json.msg}`, 'error');
}
} catch (e) {
this.addLog(`加载诊断日志失败: ${e.message}`, 'error');
} finally {
this.diagLoading = false;
}
},
diagPrevPage() {
if (this.diagOffset <= 0) return;
this.diagOffset = Math.max(0, this.diagOffset - this.diagLimit);
this.loadDiagEvents();
},
diagNextPage() {
if ((this.diagOffset + this.diagLimit) >= this.diagTotal) return;
this.diagOffset = this.diagOffset + this.diagLimit;
this.loadDiagEvents();
},
filteredDiagEvents() {
let list = this.diagEvents || [];
const t = (this.diagTypeFilter || '').trim();
const task = (this.diagTaskFilter || '').trim();
const kw = (this.diagSearch || '').trim().toLowerCase();
const quick = (this.diagQuickFilter || '').trim();
if (quick === 'errors') {
const keep = new Set(['error', 'exception', 'requestfailed', 'response', 'console', 'pageerror']);
list = list.filter(e => keep.has(e.type));
} else if (quick === 'checkpoint') {
list = list.filter(e => e.type === 'checkpoint');
} else if (quick === 'network') {
const netRe = /ERR_PROXY|ERR_TUNNEL|ERR_TIMED_OUT|ERR_NAME_NOT_RESOLVED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|proxy|tunnel|timed/i;
list = list.filter(e => {
if (e.type === 'requestfailed' || e.type === 'response') return true;
const text = [e.message, e.url].filter(Boolean).join(' ');
return netRe.test(text);
});
}
if (t) list = list.filter(e => (e.type || '') === t);
if (task) list = list.filter(e => (e.taskType || '') === task);
if (kw) {
list = list.filter(e => {
const hay = [
e.message, e.url, e.step, e.phase, e.name, e.type, e.taskType,
].filter(Boolean).join(' ').toLowerCase();
return hay.includes(kw);
});
}
return list;
},
diagFinalReason() {
const list = this.filteredDiagEvents() || [];
for (let i = list.length - 1; i >= 0; i--) {
const t = list[i] && list[i].type;
if (t === 'exception' || t === 'error' || t === 'pageerror' || t === 'requestfailed' || t === 'response' || t === 'console') {
return list[i];
}
}
return null;
},
diagFinalHint() {
const ev = this.diagFinalReason();
if (!ev) return '';
const text = [ev.message, ev.url].filter(Boolean).join(' ');
const t = ev.type || '';
if (t === 'requestfailed' || /ERR_PROXY|ERR_TUNNEL|proxy/i.test(text)) return '优先检查VPN/上游代理端口与代理是否可用,必要时先换代理再重试。';
if (/recaptcha|人机验证|robot/i.test(text)) return '触发人机验证,建议手动完成验证后再重试。';
if (/手机号|phone|iap/i.test(text)) return '需要手机验证,自动流程无法完成,建议手动处理。';
if (/找不到|未找到|waiting for selector|timeout|超时/i.test(text)) return '页面结构变化或加载过慢,建议更换网络/代理后重试,并结合截图确认页面元素。';
if (/验证码|wrong code|invalid code|try again/i.test(text)) return '验证码校验失败,检查2FA密钥是否正确或验证码是否过期。';
if (/密码错误|wrong password|incorrect password/i.test(text)) return '密码可能不正确,请核对后重试。';
return '结合错误事件详情与截图进一步定位。';
},
diagTypeLabel(t) {
const map = {
error: '错误',
exception: '异常',
requestfailed: '请求失败',
response: '响应异常',
console: '控制台错误',
pageerror: '页面异常',
checkpoint: '关键节点',
navigate: '页面跳转',
};
return map[t] || (t || '-');
},
diagTypeClass(t) {
const map = {
error: 'bg-red-100 text-red-700',
exception: 'bg-red-100 text-red-700',
requestfailed: 'bg-orange-100 text-orange-700',
response: 'bg-yellow-100 text-yellow-700',
console: 'bg-rose-100 text-rose-700',
pageerror: 'bg-rose-100 text-rose-700',
checkpoint: 'bg-blue-100 text-blue-700',
navigate: 'bg-gray-100 text-gray-700',
};
return map[t] || 'bg-gray-100 text-gray-700';
},
diagTaskLabel(t) {
const map = {
login: '登录',
change_2fa: '更改2FA',
change_password: '更改密码',
chatgpt_register: 'ChatGPT注册/登录',
openai_get_auth: '提取授权JSON',
openai_query_plan: '查询套餐',
refund_query: '查询退款',
workspace_refund: '退款',
security_chatgpt: '改密+2FA',
change_country: '更改国家',
ag_get_credential: '获取凭证',
gemini_cli_get_credential: '获取Cli凭证',
google_account_ag_credential: '链接提取+获取凭证',
ag_revoke_authorization: '取消授权反重力',
};
return map[t] || (t || '-');
},
selectDiagEvent(ev) {
this.diagSelected = ev || null;
},
formatDiagTs(ts) {
try {
if (!ts) return '-';
const d = new Date(ts);
if (Number.isNaN(d.getTime())) return String(ts);
return d.toLocaleString();
} catch (_) {
return ts || '-';
}
},
diagSummary(ev) {
if (!ev) return '-';
if (ev.type === 'checkpoint') return ev.name || '关键节点';
if (ev.type === 'requestfailed') {
const m = ev.meta || {};
const left = [m.method, m.resourceType].filter(Boolean).join(' ');
const msg = ev.message || '请求失败';
return left ? `请求失败:${left} · ${msg}` : `请求失败:${msg}`;
}
if (ev.type === 'response') {
const m = ev.meta || {};
const s = m.status ? `HTTP ${m.status}` : (ev.message || '响应异常');
return `响应异常:${s}`;
}
if (ev.type === 'console') return `控制台错误:${ev.message || '-'}`;
if (ev.type === 'pageerror') return `页面异常:${ev.message || '-'}`;
if (ev.type === 'exception') return `异常:${ev.message || '-'}`;
if (ev.type === 'error') return `错误:${ev.message || '-'}`;
if (ev.type === 'navigate') return `跳转:${ev.url || '-'}`;
return ev.message || ev.url || '-';
},
viewScreenshotPath(screenshotPath, email) {
if (!screenshotPath) return;
this.screenshotUrl = '/' + screenshotPath + '?t=' + Date.now();
this.screenshotEmail = email || '';
this.showScreenshotModal = true;
},
// ==================== 导入模板管理 ====================
async loadImportTemplates() {
const fiveFieldTemplate = {
name: '默认5字段(----)',
pattern: '{{账号}}----{{密码}}----{{辅助邮箱}}----{{2fa}}----{{rt凭证}}',
};
const sixFieldTemplate = {
name: '默认6字段(----)',
pattern: '{{账号}}----{{密码}}----{{辅助邮箱}}----{{2fa}}----{{接码链接}}----{{rt凭证}}',
};
const raw = this.settings.import_templates;
if (raw) {
try {
this.importTemplates = JSON.parse(raw);
} catch (e) {
this.importTemplates = [];
}
}
if (!this.importTemplates || this.importTemplates.length === 0) {
this.importTemplates = [
fiveFieldTemplate,
sixFieldTemplate,
{ name: '默认4字段(----)', pattern: '{{账号}}----{{密码}}----{{辅助邮箱}}----{{2fa}}' },
{ name: '账号密码(----)', pattern: '{{账号}}----{{密码}}' },
];
} else {
// 已有用户配置也补入标准模板,确保升级后无需清理历史设置。
if (!this.importTemplates.some(tpl => tpl && tpl.pattern === sixFieldTemplate.pattern)) {
this.importTemplates.unshift(sixFieldTemplate);
}
if (!this.importTemplates.some(tpl => tpl && tpl.pattern === fiveFieldTemplate.pattern)) {
this.importTemplates.unshift(fiveFieldTemplate);
}
}
if (this.settings.import_templates !== JSON.stringify(this.importTemplates)) {
await this.saveImportTemplates();
}
this.importSelectedIndex = 0;
},
async saveImportTemplates() {
const json = JSON.stringify(this.importTemplates);
await this.saveSetting('import_templates', json);
},
async addImportTemplate() {
const name = this.importNewTemplateName.trim();
const pattern = this.importNewTemplatePattern.trim();
if (!name || !pattern) {
this.addLog('模板名称和格式不能为空', 'warn');
return;
}
if (!pattern.includes('{{账号}}')) {
this.addLog('模板必须包含 {{账号}} 字段', 'warn');
return;
}
this.importTemplates.push({ name, pattern });
await this.saveImportTemplates();
this.importSelectedIndex = this.importTemplates.length - 1;
this.importNewTemplateName = '';
this.importNewTemplatePattern = '';
this.showTemplateEditor = false;
this.addLog(`模板已保存: ${name}`, 'info');
},
async deleteImportTemplate(index) {
if (this.importTemplates.length <= 1) {
this.addLog('至少保留一个模板', 'warn');
return;
}
const removed = this.importTemplates.splice(index, 1);
await this.saveImportTemplates();
if (this.importSelectedIndex >= this.importTemplates.length) {
this.importSelectedIndex = this.importTemplates.length - 1;
}
this.addLog(`模板已删除: ${removed[0].name}`, 'info');
},
getSelectedTemplatePattern() {
// 新建模板编辑器展开时,优先用正在编辑的 pattern 做预览
if (this.showTemplateEditor && this.importNewTemplatePattern.trim()) {
return this.importNewTemplatePattern.trim();
}
if (this.importTemplates.length === 0) return null;
return this.importTemplates[this.importSelectedIndex]?.pattern || null;
},
getTemplateHint() {
const pattern = this.getSelectedTemplatePattern();
if (!pattern) return '';
return pattern
.replace(/\{\{账号\}\}/g, 'xxx@gmail.com')
.replace(/\{\{密码\}\}/g, 'password123')
.replace(/\{\{辅助邮箱\}\}/g, 'recovery@outlook.com')
.replace(/\{\{2fa\}\}/g, 'JBSWY3DPEHPK3PXP')
.replace(/\{\{接码(?:地址|链接)\}\}/g, 'https://sms.example/link')
.replace(/\{\{(?:rt|RT|AG)凭证\}\}/g, '1//refresh-token');
},
/**
* 用当前模板解析第1行输入,返回预览数据(跳过 {{无用}} 列)
* 返回: [ [{label, value, ok}, ...] ]
*/
getImportPreview() {
const pattern = this.getSelectedTemplatePattern();
if (!pattern || !this.importText.trim()) return [];
const fieldRegex = /\{\{(.+?)\}\}/g;
const fieldKeys = [];
let m;
while ((m = fieldRegex.exec(pattern)) !== null) fieldKeys.push(m[1]);
const separators = pattern.split(/\{\{.+?\}\}/);
const line = this.importText.trim().split('\n').find(l => l.trim() && !l.trim().startsWith('#'));
if (!line) return [];
let remaining = line.trim();
const allCols = [];
const findImportSeparator = (text, separator) => {
if (/^-{3,}$/.test(separator)) {
const match = /-{3,}/.exec(text);
return match ? { index: match.index, length: match[0].length } : null;
}
const index = text.indexOf(separator);
return index === -1 ? null : { index, length: separator.length };
};
if (separators[0] && remaining.startsWith(separators[0])) {
remaining = remaining.slice(separators[0].length);
}
for (let i = 0; i < fieldKeys.length; i++) {
let value = '';
if (i < fieldKeys.length - 1) {
const sep = separators[i + 1];
if (!sep) {
value = remaining.trim();
allCols.push({ label: fieldKeys[i], value, ok: !!value });
remaining = '';
break;
}
const separatorMatch = findImportSeparator(remaining, sep);
if (!separatorMatch) {
value = remaining.trim();
allCols.push({ label: fieldKeys[i], value, ok: !!value });
remaining = '';
break;
}
value = remaining.slice(0, separatorMatch.index).trim();
allCols.push({ label: fieldKeys[i], value, ok: !!value });
remaining = remaining.slice(separatorMatch.index + separatorMatch.length);
} else {
value = remaining.trim();
allCols.push({ label: fieldKeys[i], value, ok: !!value });
}
}
for (let i = allCols.length; i < fieldKeys.length; i++) {
allCols.push({ label: fieldKeys[i], value: '(缺失)', ok: false });
}
const credentialLabels = new Set(['辅助邮箱', '2fa', '接码地址', '接码链接', 'rt凭证', 'RT凭证', 'AG凭证']);
const credentialCols = allCols.filter(col => credentialLabels.has(col.label));
const credentialValues = credentialCols
.flatMap(col => String(col.value === '(缺失)' ? '' : col.value).split(/-{3,}/))
.map(value => value.trim())
.filter(Boolean);
const looksLikeEmail = value => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value);
const looksLikeTotp = value => {
if (/^otpauth:\/\/totp\//i.test(value)) return true;
const compact = value.replace(/[\s-]/g, '').replace(/=+$/g, '');
return compact.length >= 16 && /^[a-z2-7]+$/i.test(compact);
};
const looksLikeRt = value => /^1\/\/[a-z0-9_-]+$/i.test(value);
const looksLikeUrl = value => /^https?:\/\//i.test(value);
const detected = {
recovery: credentialValues.find(looksLikeEmail) || '',
twofa: credentialValues.find(looksLikeTotp) || '',
sms: credentialValues.find(looksLikeUrl) || '',
rt: credentialValues.find(looksLikeRt) || '',
};
const originalValue = label => credentialCols.find(col => col.label === label)?.value || '';
const recoveryFallback = originalValue('辅助邮箱');
const twofaFallback = originalValue('2fa');
const hasPackedValue = credentialCols.some(col => /-{3,}/.test(String(col.value)));
const hasSwappedValue = looksLikeTotp(recoveryFallback) || looksLikeEmail(twofaFallback);
let previewCols = allCols;
if (hasPackedValue || hasSwappedValue) {
previewCols = allCols.filter(col => !credentialLabels.has(col.label));
const fallbackRecovery = recoveryFallback && !looksLikeTotp(recoveryFallback) && !looksLikeRt(recoveryFallback)
? recoveryFallback
: '';
const fallbackTwofa = twofaFallback && !looksLikeEmail(twofaFallback) && !looksLikeRt(twofaFallback)
? twofaFallback
: '';
previewCols.push({ label: '辅助邮箱', value: detected.recovery || fallbackRecovery, ok: !!(detected.recovery || fallbackRecovery) });
previewCols.push({ label: '2fa', value: detected.twofa || fallbackTwofa, ok: !!(detected.twofa || fallbackTwofa) });
if (detected.sms) previewCols.push({ label: '接码链接', value: detected.sms, ok: true });
if (detected.rt) previewCols.push({ label: 'rt凭证', value: detected.rt, ok: true });
}
// 过滤掉 {{无用}} 列
const cols = previewCols.filter(c => c.label !== '无用');
return [cols];
},
async checkBitStatus() {
this.bitStatus = 'checking';
try {
const res = await fetch('/api/settings/bit_status');
const data = await res.json();
this.bitStatus = data.success ? 'online' : 'offline';
} catch (e) {
this.bitStatus = 'offline';
}
},
async loadBrowsers(force = false) {
try {
const res = await fetch(force ? '/api/settings/browsers?force=1' : '/api/settings/browsers');
const data = await res.json();
if (data.success) {
this.browsers = data.data || [];
this._browserById = new Map(this.browsers
.filter(browser => browser && browser.id)
.map(browser => [String(browser.id), browser]));
// 接口已先把编号同步到数据库;同时更新当前页面对象,避免再等一次账号列表刷新。
for (const account of this.accounts) {
if (!account || !account.browser_id) continue;
const browser = this._browserById.get(String(account.browser_id));
if (browser && browser.seq !== undefined && browser.seq !== null) {
account.browser_seq = String(browser.seq);
}
}
}
} catch (e) {
// 比特接口瞬时失败时保留上一次映射,并继续使用数据库保存的编号。
}
},
getBrowserName(browserId) {
if (!browserId) return '-';
const b = this._browserById.get(String(browserId));
return b ? b.name : '-';
},
getBrowserSeq(browserId, savedSeq = '') {
if (!browserId) return savedSeq == null ? '' : String(savedSeq);
const b = this._browserById.get(String(browserId));
return b && b.seq !== undefined && b.seq !== null
? String(b.seq)
: (savedSeq == null ? '' : String(savedSeq));
},
/** 计算某个月/日距离今天过了几天 */
_calcDaysAgo(month, day, year) {
if (!month || !day) return -1;
const now = new Date();
const target = new Date(year || now.getFullYear(), month - 1, day);
// 如果目标日期在未来(跨年情况),用去年
if (target > now) target.setFullYear(target.getFullYear() - 1);
const diff = Math.floor((now - target) / (1000 * 60 * 60 * 24));
return diff;
},
/** 解析安全日期 JSON 中的某个字段,附带天数 */
parseSecurityDate(jsonStr, field) {
if (!jsonStr) return '-';
try {
const obj = typeof jsonStr === 'string' ? JSON.parse(jsonStr) : jsonStr;
const label = obj[field] || '';
if (!label) return '-';
const month = obj[field + 'Month'];
const day = obj[field + 'Day'];
const year = obj.checkedYear;
const days = this._calcDaysAgo(month, day, year);
if (days === 0) return label.startsWith('今天') ? label : `${label}(今天)`;
if (days > 0) return `${label}(${days}天)`;
return label;
} catch (_) { return '-'; }
},
/** 格式化安全日期用于 title 悬停提示 */
formatSecurityDatesTitle(jsonStr) {
if (!jsonStr) return '未检查';
try {
const obj = typeof jsonStr === 'string' ? JSON.parse(jsonStr) : jsonStr;
const parts = [];
if (obj.twoStep) {
const d = this._calcDaysAgo(obj.twoStepMonth, obj.twoStepDay, obj.checkedYear);
parts.push(`两步验证: ${obj.twoStep}${d >= 0 ? ` (${d}天前)` : ''}`);
}
if (obj.password) {
const d = this._calcDaysAgo(obj.passwordMonth, obj.passwordDay, obj.checkedYear);
parts.push(`密码修改: ${obj.password}${d >= 0 ? ` (${d}天前)` : ''}`);
}
if (obj.authenticator) {
const d = this._calcDaysAgo(obj.authenticatorMonth, obj.authenticatorDay, obj.checkedYear);
parts.push(`验证器: ${obj.authenticator}${d >= 0 ? ` (${d}天前)` : ''}`);
}
return parts.join('\n') || '未检查';
} catch (_) { return '解析错误'; }
},
// AI Studio 一键组合流程日志:鼠标悬停显示完整信息
formatAistudioFlowTitle(account) {
if (!account || !account.aistudio_flow_status) return '';
const lines = [];
lines.push(account.aistudio_flow_status === 'success' ? '结果:成功(全部步骤通过)' : '结果:失败');
if (account.aistudio_flow_status !== 'success' && account.aistudio_flow_failed_step) {
lines.push(`失败步骤:${account.aistudio_flow_failed_step}`);
}
if (account.aistudio_flow_detail) lines.push(`详情:${account.aistudio_flow_detail}`);
if (account.aistudio_flow_at) lines.push(`时间:${this.formatDate(account.aistudio_flow_at)}`);
return lines.join('\n');
},
// ==================== 静态代理池 ====================
async loadStaticProxies() {
try {
const res = await fetch('/api/static-proxies');
const data = await res.json();
if (data.success) this.staticProxies = data.data || [];
} catch (_) {}
},
async importStaticProxies() {
if (!this.staticProxyImportText.trim()) return this.addLog('⚠️ 请粘贴代理列表', 'warn');
try {
const res = await fetch('/api/static-proxies/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: this.staticProxyImportText, maxUsage: this.staticProxyMaxUsage }),
});
const data = await res.json();
if (data.success) {
this.addLog(`✅ 导入静态代理: 新增 ${data.added}/${data.total}`, 'success');
this.staticProxyImportText = '';
await this.loadStaticProxies();
} else {
this.addLog(`导入失败: ${data.msg}`, 'error');
}
} catch (e) {
this.addLog(`导入失败: ${e.message}`, 'error');
}
},
async deleteStaticProxy(id) {
try {
await fetch(`/api/static-proxies/${id}`, { method: 'DELETE' });
await this.loadStaticProxies();
} catch (_) {}
},
async clearAllStaticProxies() {
if (!confirm('确定清空所有静态代理?')) return;
try {
await fetch('/api/static-proxies', { method: 'DELETE' });
await this.loadStaticProxies();
this.addLog('✅ 已清空静态代理池', 'success');
} catch (_) {}
},
async releaseStaticProxy(id) {
try {
await fetch(`/api/static-proxies/${id}/release`, { method: 'PUT' });
await this.loadStaticProxies();
await this.loadAccounts();
} catch (_) {}
},
async toggleStaticProxy(id) {
try {
await fetch(`/api/static-proxies/${id}/toggle`, { method: 'PUT' });
await this.loadStaticProxies();
} catch (_) {}
},
async editStaticProxyMaxUsage(p) {
const val = prompt('设置可用次数:', p.max_usage);
if (val === null) return;
const num = parseInt(val, 10);
if (!num || num < 1) return this.addLog('⚠️ 次数必须大于0', 'warn');
try {
await fetch(`/api/static-proxies/${p.id}/max-usage`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ maxUsage: num }),
});
await this.loadStaticProxies();
} catch (_) {}
},
async setMotherAccount() {
const emails = [...this.selectedEmails];
if (emails.length === 0) return this.addLog('⚠️ 请先选中需要设置的账号', 'warn');
let ok = 0;
for (const email of emails) {
try {
await fetch(`/api/accounts/${encodeURIComponent(email)}/tag`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag: '家庭组母号' }),
});
const acc = this._accountByEmail.get(email);
if (acc) acc.tag = '家庭组母号';
ok++;
} catch (_) {}
}
this.addLog(`✅ 已设置母号: ${ok}/${emails.length}`, 'success');
},
// ==================== 模板代理(Cliproxy / Nexip) ====================
_generateClipproxySid() {
return Math.random().toString(36).substring(2, 10);
},
_generateSidFromSettings() {
const letters = 'abcdefghijklmnopqrstuvwxyz';
const numbers = '0123456789';
const useLetters = this.settings.sid_use_letters !== '0' && this.settings.sid_use_letters !== false;
const useNumbers = this.settings.sid_use_numbers !== '0' && this.settings.sid_use_numbers !== false;
let chars = '';
if (useLetters) chars += letters;
if (useNumbers) chars += numbers;
if (!chars) chars = letters + numbers;
const len = parseInt(this.settings.sid_length || '8', 10) || 8;
let result = '';
for (let i = 0; i < len; i++) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
},
get templateProxyPreview() {
const tpl = this.settings.proxy_template || '';
if (!tpl) return '';
const sid = this._generateSidFromSettings();
return tpl.replace(/\{sid\}/gi, sid);
},
refreshTemplatePreview() {
// 触发 Alpine 重新计算 getter
this.settings.proxy_template = this.settings.proxy_template;
},
get clipproxyPreview() {
const host = this.settings.cliproxy_host || '';
const username = this.settings.cliproxy_username || '';
const region = this.settings.cliproxy_region || '';
const duration = this.settings.cliproxy_duration || '120';
const password = this.settings.cliproxy_password || '';
if (!host || !username) return '';
const middle = region ? `${username}-${region}` : username;
return `${host}:${middle}-sid-${this.cliproxySid}-t-${duration}:${password}`;
},
get dataimpulsePreview() {
const username = this.settings.dataimpulse_username || '';
const password = this.settings.dataimpulse_password || '';
if (!username || !password) return '';
const port = 10000 + Math.floor(Math.random() * 10000);
return `gw.dataimpulse.com:${port}:${username}:${password}`;
},
get cliApiPreview() {
const url = this.settings.cliapi_url || '';
if (!url) return '';
const region = this.settings.cliapi_region || 'US';
const num = this.settings.cliapi_num || '1';
const time = this.settings.cliapi_time || '30';
return `${url}?region=${region}&num=${num}&time=${time}&format=n&type=txt`;
},
async parseClipproxyFull() {
const text = this.cliproxyFullStr.trim();
if (!text) return;
try {
// Format: host:port:username-region-sid-XXXX-t-duration:password
const c1 = text.indexOf(':');
const c2 = text.indexOf(':', c1 + 1);
if (c1 === -1 || c2 === -1) throw new Error('格式错误,需要至少3个冒号');
const hostPort = text.substring(0, c2);
const rest = text.substring(c2 + 1); // username-region-sid-XXXX-t-duration:password
const dash1 = rest.indexOf('-');
if (dash1 === -1) throw new Error('格式错误');
const username = rest.substring(0, dash1);
const rest2 = rest.substring(dash1 + 1); // region-sid-XXXX-t-duration:password
const sidIdx = rest2.indexOf('-sid-');
if (sidIdx === -1) throw new Error('格式错误,找不到 -sid-');
const region = rest2.substring(0, sidIdx);
const tail = rest2.substring(sidIdx + 5); // XXXX-t-duration:password
const colonInTail = tail.indexOf(':');
if (colonInTail === -1) throw new Error('格式错误,找不到密码分隔符');
const sidAndDuration = tail.substring(0, colonInTail); // XXXX-t-duration
const password = tail.substring(colonInTail + 1);
const tIdx = sidAndDuration.indexOf('-t-');
if (tIdx === -1) throw new Error('格式错误,找不到 -t-');
const sid = sidAndDuration.substring(0, tIdx);
const duration = sidAndDuration.substring(tIdx + 3);
// Update reactive state
this.settings.cliproxy_host = hostPort;
this.settings.cliproxy_username = username;
this.settings.cliproxy_region = region;
this.settings.cliproxy_duration = duration;
this.settings.cliproxy_password = password;
if (sid) this.cliproxySid = sid;
// Batch-save to backend
await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
settings: {
cliproxy_host: hostPort,
cliproxy_username: username,
cliproxy_region: region,
cliproxy_duration: duration,
cliproxy_password: password,
},
}),
});
this.cliproxyFullStr = '';
this.addLog('Cliproxy 配置解析并保存成功', 'success');
} catch (e) {
alert('解析失败: ' + e.message);
}
},
async parseNodemavenFull() {
const text = (this.nodemavenFullStr || '').trim();
if (!text) return;
try {
const parts = text.split(':');
if (parts.length < 4) throw new Error('格式错误,需要至少3个冒号');
const host = (parts[0] || '').trim();
const port = (parts[1] || '').trim();
const username = (parts[2] || '').trim();
const password = parts.slice(3).join(':').trim();
if (!host || !port || !username || !password) throw new Error('格式错误:host/port/username/password 不能为空');
const proxy = `${host}:${port}:${username}:${password}`;
this.settings.proxy_provider = 'nodemaven';
this.settings.nodemaven_proxy = proxy;
await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
settings: {
proxy_provider: 'nodemaven',
nodemaven_proxy: proxy,
},
}),
});
this.nodemavenFullStr = '';
this.addLog('nodemaven 代理解析并保存成功', 'success');
} catch (e) {
alert('解析失败: ' + e.message);
}
},
// ==================== 代理预设管理 ====================
_proxySettingKeys: [
'proxy_provider',
'proxy_template', 'sid_use_letters', 'sid_use_numbers', 'sid_length',
'nexip_sid_reuse_enabled', 'nexip_sid_reuse_count',
'cliproxy_host', 'cliproxy_username', 'cliproxy_region', 'cliproxy_duration', 'cliproxy_password',
'nodemaven_proxy',
'dataimpulse_username', 'dataimpulse_password',
'cliapi_url', 'cliapi_region', 'cliapi_num', 'cliapi_time',
],
async saveProxyPreset() {
const name = (this.presetName || '').trim();
if (!name) { alert('请输入预设名称'); return; }
// 收集当前代理配置
const config = {};
for (const key of this._proxySettingKeys) {
config[key] = this.settings[key] || '';
}
// 检查是否同名覆盖
const idx = this.proxyPresets.findIndex(p => p.name === name);
const preset = { name, config, updatedAt: new Date().toLocaleString() };
if (idx >= 0) {
this.proxyPresets[idx] = preset;
} else {
this.proxyPresets.push(preset);
}
localStorage.setItem('proxyPresets', JSON.stringify(this.proxyPresets));
this.presetName = '';
this.addLog(`✅ 代理预设「${name}」已保存`, 'success');
},
async loadProxyPreset(preset) {
if (!preset || !preset.config) return;
// 将预设配置写入 settings
for (const [key, value] of Object.entries(preset.config)) {
this.settings[key] = value;
}
// 批量保存到后端
try {
await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ settings: preset.config }),
});
} catch (_) {}
this.cliproxySid = this._generateClipproxySid();
this.addLog(`✅ 已加载代理预设「${preset.name}」`, 'success');
},
deleteProxyPreset(idx) {
if (!confirm(`确定删除预设「${this.proxyPresets[idx].name}」?`)) return;
const name = this.proxyPresets[idx].name;
this.proxyPresets.splice(idx, 1);
localStorage.setItem('proxyPresets', JSON.stringify(this.proxyPresets));
this.addLog(`🗑️ 已删除代理预设「${name}」`, 'info');
},
_getPresetProviderLabel(config) {
const p = config.proxy_provider || 'cliproxy';
const labels = { cliproxy: 'Cliproxy', nexip: 'Nexip', cliapi: 'Cliapi', nodemaven: 'Nodemaven', dataimpulse: 'DataImpulse' };
return labels[p] || p;
},
_getPresetSummary(config) {
const p = config.proxy_provider || 'cliproxy';
if (p === 'cliproxy') {
if (config.proxy_template) {
const t = config.proxy_template;
return t.length > 35 ? t.substring(0, 35) + '...' : t;
}
return config.cliproxy_region || config.cliproxy_host || '';
}
if (p === 'nexip') return (config.proxy_template || '').substring(0, 35);
if (p === 'nodemaven') return (config.nodemaven_proxy || '').substring(0, 30);
if (p === 'dataimpulse') return config.dataimpulse_username || '';
if (p === 'cliapi') return config.cliapi_region || '';
return '';
},
// ==================== 任务执行 ====================
getTaskAccounts() {
const selected = this.selectedAccounts;
if (selected.length > 0) return selected;
// 最近批次为空时必须返回空,避免“未选中”被误解释为运行整个号池。
if (this.statusFilter === '刚刚运行') return this.filteredAccounts;
return this.accounts;
},
onMonitorToggle() {
try {
localStorage.setItem('monitor_enabled', this.monitorEnabled ? '1' : '0');
} catch (_) {}
this.addLog(`旁监听${this.monitorEnabled ? '已启用' : '已关闭'}(适用于业务任务)`, 'info');
},
buildTaskParams(taskType, extraParams = {}) {
const params = { threadCount: parseInt(this.threadCount) || 1, ...extraParams };
if (this.monitorEnabled) {
params.monitor = { enabled: true, mode: 'full' };
}
return params;
},
_taskPendingMessage(taskType) {
const messages = {
create_accounts: '等待创建浏览器...',
change_proxy: '等待切换 IP...',
login: '等待登录...',
change_password: '等待改密码...',
change_2fa: '等待更改 2FA...',
chatgpt_register: '等待 ChatGPT 注册/登录...',
openai_get_auth: '等待提取授权 JSON...',
openai_query_plan: '等待查询套餐...',
refund_query: '等待查询退款...',
card_pay_recharge: '等待充值...',
workspace_refund: '等待提交退款...',
security_chatgpt: '等待改密和2FA...',
change_language: '等待更改语言...',
check_restrictions: '等待检查账号限制...',
check_security_dates: '等待检查安全日期...',
ag_get_credential: '等待获取凭证...',
aistudio_combo: '等待处理 AI Studio...',
open_manual_aistudio: '浏览器已打开,等待人工创建 AI Studio Key...',
gemini_cli_get_credential: '等待获取 Cli 凭证...',
google_account_ag_credential: '等待链接取号并获取凭证...',
ag_revoke_authorization: '等待取消授权...',
open: '等待打开浏览器...',
open_url: '等待打开网页...',
delete: '等待删除浏览器...',
custom_pipeline: '等待组合任务...',
};
return messages[taskType] || '等待处理...';
},
_taskDisplayLabel(taskType) {
if (taskType === 'custom_pipeline') {
const steps = (this.customQueue || []).map(key => this.getCustomTaskLabel(key)).filter(Boolean);
return steps.length > 0 ? `组合任务:${steps.join(' → ')}` : '组合任务';
}
const labels = {
create_accounts: '创建浏览器', change_proxy: '换代理', login: '登录',
change_password: '改密码', change_2fa: '更改 2FA', change_language: '更改语言',
chatgpt_register: 'ChatGPT注册/登录',
openai_get_auth: '提取授权JSON',
openai_query_plan: '查询套餐',
refund_query: '查询退款',
card_pay_recharge: '充值',
workspace_refund: '退款',
security_chatgpt: '改密+2FA',
check_restrictions: '检查账号限制', check_security_dates: '检查安全日期',
ag_get_credential: '获取凭证', aistudio_combo: 'AI Studio 一键组合', aistudio_sync_key: '同步人工AI Key', gemini_cli_get_credential: '获取Cli凭证', ag_revoke_authorization: '取消授权',
google_account_ag_credential: '链接提取+获取凭证',
open: '打开浏览器', open_manual_aistudio: '打开浏览器(自动保存人工 Key)', open_url: '打开网页', delete: '删除浏览器',
};
return labels[taskType] || taskType || '任务';
},
get activeTaskDisplayLabel() {
return this._taskDisplayLabel(this._activeTaskType || this._lastTaskType);
},
get taskProgressPercent() {
const total = Number(this.taskProgress.total) || 0;
const current = Number(this.taskProgress.current) || 0;
if (total <= 0) return 0;
return Math.max(0, Math.min(100, Math.round((current / total) * 100)));
},
_formatEtaDuration(ms) {
const totalSeconds = Math.max(0, Math.ceil(ms / 1000));
if (totalSeconds < 60) return `${totalSeconds} 秒`;
const minutes = Math.ceil(totalSeconds / 60);
if (minutes < 60) return `${minutes} 分钟`;
const hours = Math.floor(minutes / 60);
const restMinutes = minutes % 60;
return restMinutes > 0 ? `${hours} 小时 ${restMinutes} 分钟` : `${hours} 小时`;
},
get taskEtaText() {
const current = Number(this.taskProgress.current) || 0;
const total = Number(this.taskProgress.total) || 0;
const startedAt = Number(this.taskProgress.startedAt) || 0;
if (!startedAt || total <= 0 || current <= 0) return '预计结束时间:完成首个账号后计算';
if (current >= total) return '预计结束时间:即将完成';
const sampleTarget = Math.min(5, total);
if (current < sampleTarget) return `预计结束时间:采样中 (${current}/${sampleTarget})`;
const elapsed = Math.max(1000, this.taskClockNow - startedAt);
const remainingMs = (elapsed / current) * (total - current);
const eta = new Date(this.taskClockNow + remainingMs);
const time = eta.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
return `预计结束 ${time} · 剩余约 ${this._formatEtaDuration(remainingMs)}`;
},
_setLiveAccountMessage(email, message) {
if (!email || message === undefined) return;
const text = String(message || '处理中...');
let type = 'info';
if (/⚠️|警告/.test(text)) type = 'warn';
else if (/❌|🚫|✗|失败|错误|异常/.test(text)) type = 'error';
else if (/✅|✓|成功|已登录/.test(text)) type = 'success';
this.accountLogs[email] = { message: text, type };
},
_showTaskStartFeedback(taskType, accounts) {
const parsedStart = Date.parse(this._activeTaskStartedAt || '');
this.taskProgress = {
current: 0,
total: accounts.length,
startedAt: Number.isFinite(parsedStart) ? parsedStart : Date.now(),
};
this.taskClockNow = Date.now();
this._rebuildDashboardStats();
},
_isTaskMessagePending(account) {
return !!(
this.isRunning
&& account
&& this._activeTaskEmailSet
&& this._activeTaskEmailSet.has(account.email)
&& !this.accountLogs[account.email]
);
},
_queueTaskProgress(progress) {
this._pendingTaskProgress = progress;
if (this._taskProgressTimer) return;
this._taskProgressTimer = setTimeout(() => this._flushTaskProgress(), 200);
},
_flushTaskProgress() {
if (this._taskProgressTimer) clearTimeout(this._taskProgressTimer);
this._taskProgressTimer = null;
const next = this._pendingTaskProgress;
this._pendingTaskProgress = null;
if (!next) return;
this.taskProgress = {
...next,
startedAt: this.taskProgress.startedAt || Date.now(),
};
},
async runTask(taskType, extraParams = {}, explicitAccounts = null) {
if (this.isRunning) {
this.addLog('⚠️ 有任务正在运行,请等待完成或停止', 'warn');
return false;
}
const taskAccounts = Array.isArray(explicitAccounts) ? explicitAccounts : this.getTaskAccounts();
const createsAccounts = taskType === 'google_account_ag_credential';
if (taskAccounts.length === 0 && !createsAccounts) {
this.addLog('⚠️ 没有可用的账号', 'warn');
return false;
}
const taskParams = this.buildTaskParams(taskType, extraParams);
const taskThreadCount = Math.max(1, parseInt(taskParams.threadCount, 10) || 1);
this.clearLogs();
const monitorText = this.monitorEnabled ? ', 旁监听: 开(full)' : '';
const requestedCount = createsAccounts
? Math.max(1, Number.parseInt(taskParams.quantity, 10) || 1)
: taskAccounts.length;
this.addLog(`🚀 启动任务: ${taskType}, 账号数: ${requestedCount}, 并发: ${taskThreadCount}${monitorText}`, 'info');
try {
const res = await fetch('/api/operations/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
taskType,
accounts: taskAccounts,
params: taskParams,
}),
});
const data = await res.json();
if (!data.success) {
this.addLog(`❌ 启动失败: ${data.msg}`, 'error');
return false;
}
this.currentTaskId = data.taskId;
this._rememberTaskBatch(taskType, taskAccounts, data.taskId);
this._activeTaskType = taskType;
this._setActiveTaskEmails(this._lastTaskEmails);
this._activeTaskStartedAt = this._lastTaskStartedAt;
this.isRunning = true;
if (taskType === 'card_pay_recharge') {
for (const taskAccount of taskAccounts) {
const liveAccount = this._accountByEmail.get(taskAccount.email);
if (liveAccount) {
liveAccount.recharge_status = '充值中';
liveAccount.recharge_message = '等待充值';
liveAccount.message = '等待充值';
liveAccount.has_recharge_card = true;
}
}
}
this._showTaskStartFeedback(taskType, taskAccounts);
if (createsAccounts) {
this.taskProgress.total = requestedCount * 2;
this.activeTab = 'accounts';
this.selectedEmails = new Set();
} else if (taskType !== 'custom_pipeline') {
this.activeTab = 'accounts';
this.statusFilter = '任务中';
this.selectedEmails = new Set(this._lastTaskEmails);
} else if (taskType === 'custom_pipeline') {
this.activeTab = 'accounts';
this.statusFilter = '自定义任务';
this.selectedEmails = new Set(this._lastTaskEmails);
}
this.resetAccountPage();
this.startSSEListener(data.taskId);
return true;
} catch (e) {
this.addLog(`❌ 网络错误: ${e.message}`, 'error');
return false;
}
},
// 页面刷新后恢复运行中的任务
async checkRunningTasks() {
try {
const res = await fetch('/api/operations/tasks');
const data = await res.json();
if (data.success && data.data && data.data.length > 0) {
const task = data.data[0]; // 取第一个运行中的任务
this._activeTaskType = task.taskType || null;
this._setActiveTaskEmails(task.emails);
this._activeTaskStartedAt = task.startedAt || null;
this.currentTaskId = task.taskId;
this.isRunning = true;
this.isPaused = !!task.paused;
if (task.taskType === 'custom_pipeline' && Array.isArray(task.taskSteps)) {
this.customQueue = task.taskSteps.slice();
}
this._showTaskStartFeedback(task.taskType, task.emails.map(email => ({ email })));
if (task.taskType === 'custom_pipeline') {
this.customRunning = true;
this.statusFilter = '自定义任务';
} else if (this._activeTaskEmails.length > 0) {
this.statusFilter = '任务中';
}
if (this._activeTaskEmails.length > 0) this.selectedEmails = new Set(this._activeTaskEmails);
this.resetAccountPage();
this.addLog('🔄 检测到运行中的任务,已自动恢复连接', 'info');
this.startSSEListener(task.taskId);
}
} catch (_) {}
},
_queueAccountUpdate(data) {
if (!data || !data.email) return;
const previous = this._pendingAccountUpdates.get(data.email) || {};
this._pendingAccountUpdates.set(data.email, { ...previous, ...data });
if (this._accountUpdateTimer) return;
this._accountUpdateTimer = setTimeout(() => this._flushAccountUpdates(), 200);
},
_flushAccountUpdates() {
if (this._accountUpdateTimer) {
clearTimeout(this._accountUpdateTimer);
this._accountUpdateTimer = null;
}
const pending = this._pendingAccountUpdates;
this._pendingAccountUpdates = new Map();
if (!pending || pending.size === 0) return;
const jobsByEmail = this.customJobList.length > 0
? new Map(this.customJobList.map(job => [job.email, job]))
: null;
let customJobsChanged = false;
for (const [email, data] of pending) {
const account = this._accountByEmail.get(email);
if (account) {
if (data.status !== undefined) account.status = data.status;
if (data.message !== undefined) account.message = data.message;
if (data.status_reason !== undefined) account.status_reason = data.status_reason;
if (data.failure_label !== undefined) account.failure_label = data.failure_label;
if (data.ag_status !== undefined) account.ag_status = data.ag_status;
if (data.has_cli_ac_credential !== undefined) account.has_cli_ac_credential = data.has_cli_ac_credential;
if (data.chatgpt_status !== undefined) account.chatgpt_status = data.chatgpt_status;
if (data.openai_auth_status !== undefined) account.openai_auth_status = data.openai_auth_status;
if (data.has_openai_auth !== undefined) account.has_openai_auth = data.has_openai_auth;
if (data.openai_plan_status !== undefined) account.openai_plan_status = data.openai_plan_status;
if (data.openai_plan_type !== undefined) account.openai_plan_type = data.openai_plan_type;
if (data.openai_usage_percent !== undefined) account.openai_usage_percent = data.openai_usage_percent;
if (data.openai_subscription_expires_at !== undefined) {
account.openai_subscription_expires_at = data.openai_subscription_expires_at;
}
if (data.openai_plan_checked_at !== undefined) account.openai_plan_checked_at = data.openai_plan_checked_at;
if (data.account_refund_status !== undefined) account.account_refund_status = data.account_refund_status;
if (data.refund_process_status !== undefined) account.refund_process_status = data.refund_process_status;
if (data.refund_deadline_at !== undefined) account.refund_deadline_at = data.refund_deadline_at;
if (data.recharge_status !== undefined) account.recharge_status = data.recharge_status;
if (data.recharge_task_id !== undefined) account.recharge_task_id = data.recharge_task_id;
if (data.recharge_message !== undefined) account.recharge_message = data.recharge_message;
if (data.recharged_at !== undefined) account.recharged_at = data.recharged_at;
if (data.has_recharge_card !== undefined) account.has_recharge_card = data.has_recharge_card;
if (data.password_changed !== undefined) account.password_changed = data.password_changed;
if (data.password_changed_at !== undefined) account.password_changed_at = data.password_changed_at;
if (data.security_dates !== undefined) account.security_dates = data.security_dates;
if (data.gmail_app_password !== undefined) account.gmail_app_password = data.gmail_app_password;
if (data.aistudio_api_key !== undefined) account.aistudio_api_key = data.aistudio_api_key;
if (data.aistudio_api_key_json !== undefined) account.aistudio_api_key_json = data.aistudio_api_key_json;
if (data.aistudio_flow_status !== undefined) account.aistudio_flow_status = data.aistudio_flow_status;
if (data.aistudio_flow_failed_step !== undefined) account.aistudio_flow_failed_step = data.aistudio_flow_failed_step;
if (data.aistudio_flow_detail !== undefined) account.aistudio_flow_detail = data.aistudio_flow_detail;
if (data.aistudio_flow_at !== undefined) account.aistudio_flow_at = data.aistudio_flow_at;
if (data.browser_id !== undefined) account.browser_id = data.browser_id;
if (data.browser_seq !== undefined) account.browser_seq = data.browser_seq;
if (data.screenshot_path !== undefined) account.screenshot_path = data.screenshot_path;
}
if (data.message !== undefined) this._setLiveAccountMessage(email, data.message);
const job = jobsByEmail && jobsByEmail.get(email);
if (job && data.message !== undefined) {
job.message = data.message;
customJobsChanged = true;
}
}
this._rebuildDashboardStats();
if (customJobsChanged) this._saveCustomJobList();
},
startSSEListener(taskId) {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
const es = new EventSource(`/api/operations/events?taskId=${taskId}`);
this.eventSource = es;
// 诊断日志:前端 SSE 生命周期
es.onopen = () => {
console.log('[SSE] open', { taskId });
};
es.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'log') {
this.addLog(data.message);
} else if (data.type === 'done') {
const doneResult = data.result && typeof data.result === 'object' ? data.result : {};
const doneFailed = doneResult.success === false || !!doneResult.error;
this._flushAccountUpdates();
this._flushTaskProgress();
if (this.taskProgress.total > 0) this.taskProgress.current = this.taskProgress.total;
this.isRunning = false;
this.isPaused = false;
this._rebuildDashboardStats();
es.close();
this.eventSource = null;
this.currentTaskId = null;
this.addLog(
doneFailed ? '⚠️ 任务已停止或异常结束' : '✅ 任务完成',
doneFailed ? 'warn' : 'success',
);
// 充值不修改比特浏览器,完成时只刷新账号数据,避免大批量
// 浏览器映射同步让页面产生不必要的卡顿。
const finishedTaskType = this._activeTaskType || this._lastTaskType;
const taskEmails = this._activeTaskEmails.length > 0
? this._activeTaskEmails.slice()
: this._lastTaskEmails.slice();
const wasSheerLink = finishedTaskType === 'sheerlink' || finishedTaskType === 'half_auto';
const refreshFinishedTask = ['card_pay_recharge', 'workspace_refund'].includes(finishedTaskType)
? this.loadAccounts()
: this.loadBrowsers(true).then(() => this.loadAccounts());
refreshFinishedTask
.then(() => {
if (wasSheerLink) this._showSheerLinkResults(taskEmails);
});
// 自定义流水线:全部完成
if (this.customRunning) {
this.customJobList.forEach(job => {
job.completedCount = job.tasks.length;
job.status = 'done';
job.currentTaskLabel = '全部完成';
});
this.customRunning = false;
this._saveCustomJobList();
this.addLog(`🏁 自定义流水线全部完成`, 'success');
}
this._activeTaskType = null;
this._setActiveTaskEmails([]);
this._activeTaskStartedAt = null;
} else if (data.type === 'account_update') {
this._queueAccountUpdate(data);
} else if (data.type === 'task_accounts') {
const emails = [...new Set((Array.isArray(data.emails) ? data.emails : []).filter(Boolean))];
this._setActiveTaskEmails(emails);
this.selectedEmails = new Set(emails);
this.statusFilter = '任务中';
this._rememberTaskBatch(this._activeTaskType || 'google_account_ag_credential', emails.map(email => ({ email })), taskId);
this.loadAccounts().then(() => this.resetAccountPage());
} else if (data.type === 'progress') {
this._queueTaskProgress({ current: Number(data.current) || 0, total: Number(data.total) || 0 });
} else if (data.type === 'task_state') {
this.isPaused = !!data.paused;
} else if (data.type === 'connected') {
// 每次连接/重连都检查任务是否还在运行
this._checkTaskStillRunning(taskId);
}
} catch (e) {}
};
es.onerror = (err) => {
console.warn('[SSE] error', err);
if (this.isRunning) {
this.addLog('[SSE] 连接中断,3秒后自动重连...', 'warn');
}
// 不调用 es.close(),浏览器会根据 retry 指令自动重连
};
},
// 重连后检查任务是否已结束(断线期间可能已完成)
async _checkTaskStillRunning(taskId) {
if (!this.isRunning) return;
try {
const res = await fetch('/api/operations/tasks');
const data = await res.json();
if (data.success) {
const activeTask = data.data.find(t => t.taskId === taskId);
const isActive = !!activeTask;
if (activeTask) this.isPaused = !!activeTask.paused;
if (!isActive) {
// 任务在断线期间已结束
const finishedTaskType = this._activeTaskType || this._lastTaskType;
this.isRunning = false;
this.isPaused = false;
this._rebuildDashboardStats();
this.currentTaskId = null;
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
this.addLog('✅ 任务已完成', 'success');
if (this.customRunning) {
this.customJobList.forEach(job => {
job.completedCount = job.tasks.length;
job.status = 'done';
job.currentTaskLabel = '全部完成';
});
this.customRunning = false;
this._saveCustomJobList();
}
this._activeTaskType = null;
this._setActiveTaskEmails([]);
this._activeTaskStartedAt = null;
if (['card_pay_recharge', 'workspace_refund'].includes(finishedTaskType)) {
this.loadAccounts();
} else {
this.loadBrowsers(true).then(() => this.loadAccounts());
}
}
}
} catch (e) {}
},
async togglePauseTask() {
if (!this.currentTaskId || !this.isRunning) return;
const nextPaused = !this.isPaused;
try {
const action = nextPaused ? 'pause' : 'resume';
const res = await fetch(`/api/operations/${action}/${this.currentTaskId}`, { method: 'POST' });
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.success) throw new Error(data.msg || `${nextPaused ? '暂停' : '继续'}请求失败`);
this.isPaused = nextPaused;
this.addLog(nextPaused
? '⏸️ 已暂停领取新账号,当前正在处理的账号会安全收尾'
: '▶️ 任务已继续,开始处理剩余账号', nextPaused ? 'warn' : 'info');
} catch (e) {
this.addLog(`${nextPaused ? '暂停' : '继续'}失败: ${e.message}`, 'error');
}
},
async stopTask() {
if (!this.currentTaskId) return;
try {
const stoppingSensitiveTask = ['card_pay_recharge', 'workspace_refund'].includes(this._activeTaskType);
const res = await fetch(`/api/operations/stop/${this.currentTaskId}`, { method: 'POST' });
if (!res.ok) throw new Error('停止请求失败');
if (stoppingSensitiveTask) {
// 充值/退款只能软停止:请求可能已经被外部系统受理。保留 SSE 和
// 当前任务状态,等待 Worker 安全收尾并返回最终/待确认结果。
this.addLog(`⏳ 正在安全停止${this._activeTaskType === 'workspace_refund' ? '退款' : '充值'}任务,请等待当前请求完成...`, 'warn');
return;
}
this.isRunning = false;
this.isPaused = false;
this._rebuildDashboardStats();
this.currentTaskId = null;
this._activeTaskType = null;
this._setActiveTaskEmails([]);
this._activeTaskStartedAt = null;
this._pendingTaskProgress = null;
if (this._taskProgressTimer) clearTimeout(this._taskProgressTimer);
this._taskProgressTimer = null;
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
this.addLog('⏹️ 任务已停止', 'warn');
} catch (e) {
this.addLog(`停止失败: ${e.message}`, 'error');
}
},
// 各操作快捷方法
runChangeProxy() { this.runTask('change_proxy'); },
runOpenBrowsers() {
this.runTask('open_manual_aistudio', { watchTimeoutMs: 30 * 60 * 1000 });
},
runDeleteBrowsers() {
if (this.selectedCount === 0) {
this.addLog('⚠️ 请先勾选要删除浏览器的账号', 'warn');
return;
}
const accounts = this.selectedAccounts.filter(account => account.browser_id);
if (accounts.length === 0) {
this.addLog('⚠️ 所选账号没有绑定浏览器编号', 'warn');
return;
}
const browserCount = new Set(accounts.map(account => account.browser_id)).size;
if (!confirm(
`确认删除所选账号中的 ${browserCount} 个比特浏览器窗口?\n\n`
+ '删除成功后会同时清空账号列表里的浏览器编号,账号本身会保留。',
)) return;
this.runTask('delete', {}, accounts);
},
runLogin() { this.runTask('login'); },
runChatgptRegister() {
if (this.isRunning) return;
this.chatgptAuthMode = 'keep';
this.showChatgptRunModal = true;
},
confirmChatgptRegister() {
const clearAuthState = this.chatgptAuthMode === 'clear';
this.showChatgptRunModal = false;
this.runTask('chatgpt_register', {
age: parseInt(this.settings.chatgpt_age || '23', 10) || 23,
clearAuthState,
});
},
runOpenAIGetAuth() {
this.runTask('openai_get_auth', {
age: parseInt(this.settings.chatgpt_age || '23', 10) || 23,
});
},
runOpenAIQueryPlan() {
if (this.isRunning) return;
this.openAIPlanConnectionMode = 'browser_proxy';
this.showOpenAIPlanRunModal = true;
},
confirmOpenAIQueryPlan() {
const connectionMode = this.openAIPlanConnectionMode === 'direct'
? 'direct'
: 'browser_proxy';
this.showOpenAIPlanRunModal = false;
this.runTask('openai_query_plan', { connectionMode });
},
runRefundQuery() {
this.runTask('refund_query');
},
async runWorkspaceRefund() {
if (this.isRunning) return;
if (this.selectedCount === 0) {
this.addLog('⚠️ 请先勾选要退款的账号', 'warn');
return;
}
await this.loadWorkspaceRefundCredentialMeta();
if (!this.workspaceRefundCredentialMeta.configured) {
this.activeTab = 'settings';
this.addLog('⚠️ 请先在设置中填写退款母号 ChatGPT Session 或 Access Token', 'warn');
return;
}
if (!this.workspaceRefundCredentialMeta.proxyConfigured) {
this.activeTab = 'settings';
this.addLog('⚠️ 请先在设置中填写退款专用代理', 'warn');
return;
}
const selected = this.selectedAccounts;
const pending = selected.filter(account => (
(String(account.refund_process_status || '').trim() || '未发起') !== '已发起'
));
if (pending.length === 0) {
this.addLog('⚠️ 所选账号均已发起退款流程', 'warn');
return;
}
const skipped = selected.length - pending.length;
const confirmed = confirm(
`确认提交 ${pending.length} 个账号的退款流程?\n\n`
+ '流程会清理母号团队中的全部非 Owner 成员,并依次执行邀请、接受、迁移个人空间、移出团队。\n'
+ '任务固定单路串行;成功后“是否已发起退款流程”改为“已发起”,现有“账号退款”结果保持不变,继续由查询退款功能根据邮件确认。'
+ (skipped > 0 ? `\n\n另有 ${skipped} 个退款流程状态为“已发起”的账号会跳过。` : ''),
);
if (!confirmed) return;
const taskAccounts = selected.map(account => ({ email: account.email }));
await this.runTask('workspace_refund', { threadCount: 1 }, taskAccounts);
},
_normalizedRechargeCardLines() {
const lines = String(this.rechargeCardKeys || '').replace(/\r/g, '').split('\n');
while (lines.length > 0 && !String(lines[lines.length - 1] || '').trim()) lines.pop();
return lines.map(line => String(line || '').trim());
},
get rechargeCardLineCount() {
return this._normalizedRechargeCardLines().filter(Boolean).length;
},
get normalizedRechargeConcurrency() {
const parsed = parseInt(this.rechargeConcurrency, 10);
return Math.min(5, Math.max(1, Number.isFinite(parsed) ? parsed : 2));
},
get rechargeInputError() {
const expected = Array.isArray(this.rechargeAccounts) ? this.rechargeAccounts.length : 0;
const lines = this._normalizedRechargeCardLines();
if (expected === 0) return '没有待充值账号';
if (lines.some(line => !line)) return '卡密中间不能有空行';
if (lines.length !== expected) return `需要 ${expected} 行卡密,当前 ${lines.length} 行`;
if (new Set(lines).size !== lines.length) return '卡密不能重复';
return '';
},
async openRechargeModal() {
if (this.isRunning) return;
if (this.selectedCount === 0) {
this.addLog('⚠️ 请先勾选要充值的账号', 'warn');
return;
}
await this.loadRechargeCredentialMeta();
if (!this.rechargeCredentialMeta.configured) {
this.activeTab = 'settings';
this.addLog('⚠️ 请先在设置中填写充值 API Key ID 和 API Secret', 'warn');
return;
}
this.rechargeAccounts = this.selectedAccounts.map(account => ({ email: account.email }));
this.rechargeCardKeys = '';
this.rechargeSubmitting = false;
this.showRechargeModal = true;
},
closeRechargeModal() {
if (this.rechargeSubmitting) return;
this.showRechargeModal = false;
this.rechargeAccounts = [];
this.rechargeCardKeys = '';
},
async confirmRecharge() {
if (this.rechargeSubmitting) return;
const validationError = this.rechargeInputError;
if (validationError) {
this.addLog(`⚠️ ${validationError}`, 'warn');
return;
}
const emails = this.rechargeAccounts.map(row => row.email);
const allAccountsStillExist = emails.every(email => this._accountByEmail.has(email));
if (!allAccountsStillExist) {
this.addLog('❌ 所选账号已变化,请关闭弹框后重新选择', 'error');
return;
}
// 充值 Worker 会从数据库读取最新 Session;前端只提交邮箱和卡密,
// 不重复发送密码、2FA、代理或浏览器编号等无关字段。
const taskAccounts = emails.map(email => ({ email }));
const lines = this._normalizedRechargeCardLines();
const assignments = emails.map((email, index) => ({ email, cardKey: lines[index] }));
const rechargeConcurrency = this.normalizedRechargeConcurrency;
this.rechargeConcurrency = rechargeConcurrency;
this.rechargeSubmitting = true;
try {
const started = await this.runTask(
'card_pay_recharge',
{ assignments, confirmDuplicate: false, threadCount: rechargeConcurrency },
taskAccounts,
);
if (started) {
this.showRechargeModal = false;
this.rechargeAccounts = [];
this.rechargeCardKeys = '';
}
} finally {
this.rechargeSubmitting = false;
}
},
runChange2fa() { this.runTask('change_2fa'); },
runChangePassword() { this.runTask('change_password'); },
runSecurityChatgpt() {
if (this.isRunning) return;
this.securityRunMode = 'overwrite';
this.showSecurityRunModal = true;
},
confirmSecurityTask() {
const skipCompletedSecuritySteps = this.securityRunMode === 'skip';
this.showSecurityRunModal = false;
this.runTask('security_chatgpt', { skipCompletedSecuritySteps });
},
runChangeCountry() { this.runTask('open_url', { url: 'https://policies.google.com/country-association-form?hl=en' }); },
runChangeLanguage() { this.runTask('change_language'); },
runAgGetCredential() { this.runTask('ag_get_credential'); },
runAistudioCombo() { this.runTask('aistudio_combo'); },
async openAistudioKeyModal(account) {
if (!account || !account.email) return;
this.aistudioKeyEmail = account.email;
this.aistudioKeyRecord = null;
try {
const url = `/api/operations/aistudio-api-keys?email=${encodeURIComponent(account.email)}`;
const r = await fetch(url).then(x => x.json());
this.aistudioKeyRecord = (r && r.success && r.data) || null;
if (!this.aistudioKeyRecord) this.addLog(`⚠️ ${account.email} 暂无已保存的 AI Studio API Key`, 'warn');
} catch (e) {
this.aistudioKeyRecord = null;
this.addLog(`⚠️ 读取 ${account.email} 的 AI Studio API Key 失败`, 'warn');
}
this.showAistudioKeysModal = true;
},
copyAistudioKeys() {
const text = JSON.stringify(this.aistudioKeyRecord || {}, null, 2);
const done = () => { this.copyToastVisible = true; setTimeout(() => { this.copyToastVisible = false; }, 1500); };
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done).catch(() => {});
}
},
runGeminiCliGetCredential() { this.runTask('gemini_cli_get_credential'); },
async openGoogleAccountAgModal() {
if (this.isRunning) return;
await this.loadGoogleAccountCredentialMeta();
if (!this.googleAccountCredentialMeta.configured) {
this.addLog('⚠️ 请先在设置中保存取号 API Key 或完整链接', 'warn');
this.activeTab = 'settings';
return;
}
this.googleAccountAgQuantity = Math.max(1, Number.parseInt(this.googleAccountAgQuantity, 10) || 1);
this.googleAccountAgConcurrency = Math.max(1, Math.min(30, Number.parseInt(this.googleAccountAgConcurrency, 10) || 1));
this.showGoogleAccountAgModal = true;
},
async confirmGoogleAccountAgTask() {
const quantity = Number.parseInt(this.googleAccountAgQuantity, 10);
const concurrency = Number.parseInt(this.googleAccountAgConcurrency, 10);
if (!Number.isFinite(quantity) || quantity < 1 || quantity > 5000) {
this.addLog('⚠️ 取号数量必须为 1~5000', 'warn');
return;
}
if (!Number.isFinite(concurrency) || concurrency < 1 || concurrency > 30) {
this.addLog('⚠️ 并发必须为 1~30', 'warn');
return;
}
this.showGoogleAccountAgModal = false;
await this.runTask('google_account_ag_credential', { quantity, threadCount: concurrency }, []);
},
runAgRevokeAuthorization() {
if (this.selectedCount === 0) {
this.addLog('⚠️ 请先勾选要取消授权的账号', 'warn');
return;
}
if (!confirm(`确认取消已选 ${this.selectedCount} 个账号的 Google Antigravity 授权?`)) return;
this.runTask('ag_revoke_authorization');
},
runCheckRestrictions() { this.runTask('check_restrictions'); },
runCheckSecurityDates() { this.runTask('check_security_dates'); },
// ==================== 自定义执行 ====================
/** 所有可勾选的任务定义 */
_customTaskDefs: [
{ key: 'change_password', label: '改密码' },
{ key: 'change_2fa', label: '改2FA' },
{ key: 'chatgpt_register', label: 'ChatGPT注册/登录' },
{ key: 'openai_get_auth', label: '提取授权JSON' },
{ key: 'ag_revoke_authorization', label: '取消授权反重力' },
{ key: 'ag_get_credential', label: '获取凭证' },
{ key: 'change_language', label: '改语言' },
{ key: 'detect_country', label: '查看国家' },
{ key: 'check_restrictions', label: '检查账号限制' },
{ key: 'check_security_dates', label: '查安全日期' },
],
toggleCustomMode() {
this.customMode = !this.customMode;
if (!this.customMode) {
// 退出自定义模式时只清空运行状态,保留勾选记忆
this.customQueue = [];
this.customCurrentIndex = -1;
this.customRunning = false;
this.customStoppedIndex = null;
}
},
isCustomTaskChecked(key) {
return this.customCheckedTasks.includes(key);
},
toggleCustomTask(key) {
const idx = this.customCheckedTasks.indexOf(key);
if (idx >= 0) {
this.customCheckedTasks.splice(idx, 1);
} else {
this.customCheckedTasks.push(key);
}
try { localStorage.setItem('customCheckedTasks', JSON.stringify(this.customCheckedTasks)); } catch (_) {}
},
getCustomTaskLabel(key) {
const def = this._customTaskDefs.find(d => d.key === key);
if (def) return def.label;
// 旧key兼容映射
const legacy = {
login: '登录', open_payment: '删除支付资料', open_one_ai: '半价Ultra', check_eligibility: '查资格',
remove_recovery_email: '删除辅助邮箱', detect_country: '查看国家',
};
return legacy[key] || key;
},
async runCustomPipeline() {
if (this.customCheckedTasks.length === 0) {
this.addLog('⚠️ 请先勾选要执行的任务', 'warn');
return;
}
if (this.isRunning || this.customRunning) {
this.addLog('⚠️ 有任务正在运行', 'warn');
return;
}
// 构建队列(按勾选顺序)
this.customQueue = [...this.customCheckedTasks];
this.customRunning = true;
this.customStoppedIndex = null;
// 构建任务列表 UI
const taskAccounts = this.getTaskAccounts();
this.customJobList = taskAccounts.map(a => ({
email: a.email,
tasks: [...this.customCheckedTasks],
currentTaskLabel: '等待中',
completedCount: 0,
status: 'pending',
message: ''
}));
this._rebuildDashboardStats();
const taskLabels = this.customQueue.map(k => this.getCustomTaskLabel(k)).join(' → ');
this.addLog(`🔗 自定义流水线启动,共 ${this.customQueue.length} 个任务, ${taskAccounts.length} 个账号: ${taskLabels}`, 'info');
// 标记所有 job 为 running
this.customJobList.forEach(job => {
job.status = 'running';
job.currentTaskLabel = this.getCustomTaskLabel(this.customQueue[0]);
});
// 发送单个 custom_pipeline 任务到后端,后端一次打开浏览器执行全部
// 传递步骤对象(带参数)
const stepsWithParams = this.customCheckedTasks.map(key => {
const stepObj = { key };
const def = this._customTaskDefs.find(d => d.key === key);
if (def && def.params && this.customStepParams[key]) {
stepObj.params = { ...this.customStepParams[key] };
}
return stepObj;
});
this._saveCustomJobList();
// 若通过模板启动,带上模板名;否则尝试匹配当前选中的模板
let templateName = this._currentTemplateName || '';
if (!templateName && this.selectedTemplateId) {
const tpl = this.savedTemplates.find(t => t.id === this.selectedTemplateId);
if (tpl) templateName = tpl.name || '';
}
const age = parseInt(this.settings.chatgpt_age || '23', 10) || 23;
const started = await this.runTask('custom_pipeline', { tasks: stepsWithParams, templateName, age });
if (!started) {
this.customRunning = false;
this.customJobList.forEach(job => {
job.status = 'error';
job.currentTaskLabel = '启动失败';
job.message = '任务未能启动';
});
this._saveCustomJobList();
}
// 用完清掉,避免下次误带
this._currentTemplateName = '';
},
stopCustomPipeline() {
this.customStoppedIndex = this.customCurrentIndex;
this.customRunning = false;
this.customCurrentIndex = -1;
this.addLog('⏹️ 自定义流水线已中断', 'warn');
// 更新任务列表状态
this.customJobList.forEach(job => {
if (job.status === 'running' || job.status === 'pending') {
job.status = 'error';
job.message = '已中断';
}
});
this._saveCustomJobList();
this.stopTask();
},
clearCustomJobList() {
this.customJobList = [];
this._rebuildDashboardStats();
this._saveCustomJobList();
},
// ==================== 自定义模板管理 ====================
async loadTemplates() {
try {
const res = await fetch('/api/templates');
const data = await res.json();
if (data.success) this.savedTemplates = data.data;
} catch (_) {}
},
async saveAsTemplate() {
if (!this.templateName.trim()) {
this.addLog('⚠️ 请输入模板名称', 'warn');
return;
}
if (this.customCheckedTasks.length === 0) {
this.addLog('⚠️ 请先勾选要保存的步骤', 'warn');
return;
}
// 构建步骤列表(带参数)
const steps = this.customCheckedTasks.map(key => {
const stepObj = { key };
const def = this._customTaskDefs.find(d => d.key === key);
if (def && def.params && this.customStepParams[key]) {
stepObj.params = { ...this.customStepParams[key] };
}
return stepObj;
});
try {
const res = await fetch('/api/templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: this.templateName.trim(), steps }),
});
const data = await res.json();
if (data.success) {
this.addLog(`✅ 模板「${this.templateName}」已保存`, 'success');
this.templateName = '';
await this.loadTemplates();
} else {
this.addLog(`❌ 保存失败: ${data.msg}`, 'error');
}
} catch (e) {
this.addLog(`❌ 保存失败: ${e.message}`, 'error');
}
},
loadTemplateSteps(template) {
// 用模板的步骤填充勾选
this.customCheckedTasks = template.steps.map(s => s.key || s);
// 填充参数
this.customStepParams = {};
for (const s of template.steps) {
if (s.params) this.customStepParams[s.key] = { ...s.params };
}
try { localStorage.setItem('customCheckedTasks', JSON.stringify(this.customCheckedTasks)); } catch (_) {}
this.addLog(`📋 已加载模板「${template.name}」: ${this.customCheckedTasks.map(k => this.getCustomTaskLabel(k)).join(' → ')}`, 'info');
},
async runTemplate(template) {
// 先加载步骤再运行
this.loadTemplateSteps(template);
// 记录当前模板名,流水线完成消息会带上
this._currentTemplateName = template && template.name ? template.name : '';
// 确保在自定义模式
if (!this.customMode) this.customMode = true;
await this.$nextTick();
this.runCustomPipeline();
},
async deleteTemplateById(id) {
if (!confirm('确认删除此模板?')) return;
try {
const res = await fetch(`/api/templates/${id}`, { method: 'DELETE' });
const data = await res.json();
if (data.success) {
this.addLog('🗑️ 模板已删除', 'info');
await this.loadTemplates();
} else {
this.addLog(`❌ 删除失败: ${data.msg}`, 'error');
}
} catch (e) {
this.addLog(`❌ 删除失败: ${e.message}`, 'error');
}
},
getStepParamValue(stepKey, paramKey) {
return this.customStepParams[stepKey] && this.customStepParams[stepKey][paramKey];
},
setStepParamValue(stepKey, paramKey, value) {
if (!this.customStepParams[stepKey]) this.customStepParams[stepKey] = {};
this.customStepParams[stepKey][paramKey] = value;
},
selectTemplate(tpl) {
this.selectedTemplateId = tpl.id;
this.loadTemplateSteps(tpl);
},
runSelectedTemplate() {
const tpl = this.savedTemplates.find(t => t.id === this.selectedTemplateId);
if (!tpl) {
this.addLog('⚠️ 请先选择一个模板', 'warn');
return;
}
this.runTemplate(tpl);
},
_saveCustomJobList() {
try { localStorage.setItem('customJobList', JSON.stringify(this.customJobList)); } catch (_) {}
},
// ---- 自定义运行列表 辅助方法 ----
getCustomStepClass(ti) {
if (!this.customRunning && this.customStoppedIndex == null) return 'step-pending';
const cur = this.customRunning ? this.customCurrentIndex : (this.customStoppedIndex ?? 0);
const total = this.customCheckedTasks.length;
const allDone = !this.customRunning && cur > total;
if (ti < cur - 1 || allDone) return 'step-done';
if (ti === cur - 1) return this.customRunning ? 'step-running' : 'step-stopped';
return 'step-pending';
},
getCustomStepIcon(ti) {
if (!this.customRunning && this.customStoppedIndex == null) return '<span style="opacity:.4">⏳</span>';
const cur = this.customRunning ? this.customCurrentIndex : (this.customStoppedIndex ?? 0);
const total = this.customCheckedTasks.length;
const allDone = !this.customRunning && cur > total;
if (ti < cur - 1 || allDone) return '✅';
if (ti === cur - 1) return this.customRunning ? '<span class="custom-run-spinner"></span>' : '⏹️';
return '<span style="opacity:.4">⏳</span>';
},
getCustomStepStatusText(ti) {
if (!this.customRunning && this.customStoppedIndex == null) return '等待';
const cur = this.customRunning ? this.customCurrentIndex : (this.customStoppedIndex ?? 0);
const total = this.customCheckedTasks.length;
const allDone = !this.customRunning && cur > total;
if (ti < cur - 1 || allDone) return '已完成';
if (ti === cur - 1) return this.customRunning ? '执行中...' : '已中断';
return '等待';
},
/** 获取自定义流水线涉及的账号列表 */
getCustomRunAccounts() {
// 运行中优先使用活跃批次;运行后使用最近一次批次。
const trackedEmails = this.customRunning && this._activeTaskEmails.length > 0
? this._activeTaskEmails
: this._lastTaskEmails;
if ((this.customRunning || this.customStoppedIndex != null) && trackedEmails && trackedEmails.length > 0) {
const emailSet = new Set(trackedEmails);
return this.accounts.filter(a => emailSet.has(a.email));
}
// 运行前:用当前选中的账号,没选就用全部
const selected = this.selectedAccounts;
return selected.length > 0 ? selected : this.accounts;
},
getCustomRunAccountCount() {
return this.getCustomRunAccounts().length;
},
// ==================== SheerLink 结果 ====================
_showSheerLinkResults(taskEmails) {
const emailSet = new Set(taskEmails);
const results = this.accounts
.filter(a => emailSet.size === 0 || emailSet.has(a.email))
.filter(a => a.sheerid_link)
.map(a => ({ email: a.email, link: a.sheerid_link }));
if (results.length > 0) {
this.sheerLinkResults = results;
this.showSheerLinkResultModal = true;
}
},
copyOnlySheerLinks() {
const text = this.sheerLinkResults.map(r => r.link).join('\n');
navigator.clipboard.writeText(text).then(() => this.showCopyToast());
},
// ==================== 日志 ====================
addLog(message, type = 'default') {
const ts = new Date().toLocaleTimeString('zh-CN');
this._pendingLogEntries.push({ message: `[${ts}] ${message}`, type });
if (this._pendingLogEntries.length > this._logLimit * 2) {
this._pendingLogEntries.splice(0, this._pendingLogEntries.length - this._logLimit);
}
if (!this._logFlushTimer) {
this._logFlushTimer = setTimeout(() => this._flushLogs(), 80);
}
// 提取 [email] 格式的账号日志,更新对应账号的实时消息列
const bracketEmailRe = /^\[([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})\]\s*/;
const bem = message.match(bracketEmailRe);
if (bem) {
const email = bem[1];
const display = message.slice(bem[0].length).trim() || '处理中...';
this._setLiveAccountMessage(email, display);
}
},
_flushLogs() {
if (this._logFlushTimer) {
clearTimeout(this._logFlushTimer);
this._logFlushTimer = null;
}
if (this._pendingLogEntries.length === 0) return;
const pending = this._pendingLogEntries.slice(-this._logLimit);
this._pendingLogEntries = [];
const keepExisting = Math.max(0, this._logLimit - pending.length);
if (this.logs.length > keepExisting) {
this.logs.splice(0, this.logs.length - keepExisting);
}
this.logs.push(...pending);
if (!this.logDrawerOpen || typeof this.$nextTick !== 'function') return;
this.$nextTick(() => {
const container = document.getElementById('log-container');
if (container) container.scrollTop = container.scrollHeight;
});
},
clearLogs() {
if (this._logFlushTimer) clearTimeout(this._logFlushTimer);
this._logFlushTimer = null;
this._pendingLogEntries = [];
this.logs = [];
this.accountLogs = {};
},
toggleLogDrawer() {
this.logDrawerOpen = !this.logDrawerOpen;
if (this.logDrawerOpen) this._flushLogs();
console.log(`[UI] log drawer ${this.logDrawerOpen ? 'open' : 'close'}`);
},
logClass(type) {
const map = { success: 'log-success', error: 'log-error', warn: 'log-warn', info: 'log-info' };
return map[type] || '';
},
// ==================== 辅助 ====================
statusClass(status) {
let s = status == null ? '未登录' : String(status);
s = s.replace(/[\r\n\t]/g, ' ').replace(/\s+/g, ' ').trim();
if (!s) s = '未登录';
if (s === '已登录' || s.startsWith('已登录') || s.startsWith('登录成功')) return 'status-登录成功';
if (s.startsWith('登录失败')) return 'status-登录失败';
if (s.startsWith('订阅成功')) return 'status-订阅成功';
if (s.startsWith('改密成功')) return 'status-改密成功';
if (s.startsWith('改密失败')) return 'status-改密失败';
if (s.startsWith('2FA修改成功')) return 'status-2FA修改成功';
if (s.startsWith('2FA修改失败')) return 'status-2FA修改失败';
if (s.startsWith('未验证已改密和2FA')) return 'status-未验证已改密和2FA';
if (s.startsWith('已验证改密和2FA')) return 'status-已验证改密和2FA';
if (s.startsWith('未注册成功已改密和2FA')) return 'status-ChatGPT失败';
if (s.startsWith('ChatGPT成功') || s === '已注册') return 'status-ChatGPT成功';
if (s.startsWith('ChatGPT失败') || s === '失败') return 'status-ChatGPT失败';
if (s.startsWith('已提取')) return 'status-登录成功';
if (s.startsWith('提取失败')) return 'status-登录失败';
if (s === '查询成功' || s === '部分成功') return 'status-登录成功';
if (s === '查询失败'
|| s === '授权JSON无效'
|| s === '保存失败'
|| s.startsWith('Token失效')
|| s.startsWith('Token刷新失败')
|| s.startsWith('缺少Access Token')
|| s.startsWith('缺少Refresh Token')
|| s.startsWith('代理认证失败')
|| s.startsWith('代理连接失败')
|| s.startsWith('直连失败')
|| s.startsWith('浏览器代理格式错误')
|| s.startsWith('套餐查询异常')
|| s.startsWith('套餐任务异常')) return 'status-登录失败';
if (s === '查询中' || s === '无授权JSON' || s === '无浏览器') return 'status-未登录';
if (s.startsWith('充值成功')) return 'status-登录成功';
if (s.startsWith('充值失败')) return 'status-登录失败';
if (s.startsWith('充值中')) return 'status-未登录';
if (s.startsWith('等待已确认')) return 'status-等待已确认';
if (s.startsWith('账号本身已有会员')) return 'status-账号本身已有会员';
if (s.startsWith('认证成功')) return 'status-认证成功';
if (s.startsWith('认证失败')) return 'status-认证失败';
if (s.startsWith('绑卡成功')) return 'status-绑卡成功';
if (s.startsWith('绑卡失败')) return 'status-绑卡失败';
if (s.startsWith('有资格')) return 'status-有资格';
if (s.startsWith('无资格')) return 'status-无资格';
return `status-${s}`;
},
formatDate(ts) {
if (!ts) return '';
try {
const raw = String(ts).trim();
// SQLite CURRENT_TIMESTAMP 保存 UTC,但格式不带时区。先补成标准
// UTC ISO,再强制转换到北京时间,避免被浏览器误当成本地时间。
const normalized = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(raw)
? `${raw.replace(' ', 'T')}Z`
: raw;
const date = new Date(normalized);
if (Number.isNaN(date.getTime())) return raw;
return date.toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
hour12: false,
});
} catch (e) {
return ts;
}
},
formatOpenAIPlanType(value) {
const raw = String(value || '').trim();
if (!raw) return '';
const labels = {
free: 'Free', plus: 'Plus', pro: 'Pro', team: 'Team',
business: 'Business', enterprise: 'Enterprise', edu: 'Edu',
};
return labels[raw.toLowerCase()] || raw;
},
formatOpenAIPlanCell(account) {
if (!account) return '-';
return this.formatOpenAIPlanType(account.openai_plan_type)
|| String(account.openai_plan_status || '').trim()
|| '-';
},
formatOpenAIUsagePercent(value) {
if (value === null || value === undefined || value === '') return '';
const numeric = Number(value);
if (!Number.isFinite(numeric)) return '';
return `${Math.round(numeric * 100) / 100}%`;
},
formatOpenAIPlanTitle(account) {
if (!account) return '';
return [
account.openai_plan_status ? `状态:${account.openai_plan_status}` : '',
account.openai_plan_type ? `套餐:${this.formatOpenAIPlanType(account.openai_plan_type)}` : '',
this.formatOpenAIUsagePercent(account.openai_usage_percent)
? `主窗口用量:${this.formatOpenAIUsagePercent(account.openai_usage_percent)}`
: '',
account.openai_subscription_expires_at
? `订阅到期:${this.formatDate(account.openai_subscription_expires_at)}`
: '',
account.openai_plan_checked_at
? `查询时间:${this.formatDate(account.openai_plan_checked_at)}`
: '',
].filter(Boolean).join('\n');
},
formatCardNumber(num) {
if (!num) return '';
return num.slice(0, 4) + ' **** **** ' + num.slice(-4);
},
async copyCell(text) {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
} catch (e) {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
// 显示 toast
this.copyToastVisible = true;
clearTimeout(this.copyToastTimer);
this.copyToastTimer = setTimeout(() => { this.copyToastVisible = false; }, 1500);
},
get allSelected() {
const visible = this.filteredAccounts;
return visible.length > 0 && visible.every(account => this.selectedEmails.has(account.email));
},
get selectedCount() {
return this.selectedEmails.size;
},
async copySelectedEmails() {
const list = this.selectedAccounts.map(a => a.email).filter(Boolean);
if (list.length === 0) return this.addLog('⚠️ 没有勾选任何账号', 'warn');
const text = list.join('\n');
try {
await navigator.clipboard.writeText(text);
} catch (e) {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
this.addLog(`✅ 已复制 ${list.length} 个邮箱到剪贴板`, 'success');
},
setStatusFilter(status) {
if (this.isRunning && status !== '任务中') {
const t = this._activeTaskType || null;
if (t === 'login' && (status === '登录成功' || status === '登录失败' || status === '订阅成功')) {
status = '任务中';
} else if (t === 'change_password' && (status === '改密成功' || status === '改密失败' || status === '未验证已改密和2FA' || status === '未注册成功已改密和2FA')) {
status = '任务中';
} else if (t === 'chatgpt_register' && (status === 'ChatGPT成功' || status === 'ChatGPT失败' || status === '已验证改密和2FA' || status === '未注册成功已改密和2FA')) {
status = '任务中';
} else if (t === 'change_2fa' && (status === '未改2FA' || status === '2FA修改成功' || status === '2FA修改失败' || status === '未验证已改密和2FA' || status === '未注册成功已改密和2FA')) {
status = '任务中';
} else if (t === 'security_chatgpt' && ['改密失败', '2FA修改失败', '未验证已改密和2FA', '未注册成功已改密和2FA'].includes(status)) {
status = '任务中';
} else if (t === 'bind_card' && (status === '订阅成功' || status === '绑卡失败' || status === '无资格' || status === '有资格')) {
status = '任务中';
}
}
this.resetAccountPage();
// 如果点击的是当前已选中的状态,则回到"全部"并清空选择
if (this.statusFilter === status) {
this.statusFilter = '全部';
this.selectedEmails = new Set();
return;
}
// 切换到新状态
this.statusFilter = status;
this.selectedEmails = new Set();
// 自动勾选符合筛选条件的账号(基于客户筛选后的列表)
const pool = this.customerFilteredAccounts;
let matchedAccounts = [];
if (status === '未改2FA') {
// 特殊处理:2FA是小写的账号
matchedAccounts = pool.filter(acc => {
const st = acc.status || '未登录';
if (st === '订阅成功' || st === '登录失败') return false;
return acc.secret_key && acc.secret_key === acc.secret_key.toLowerCase() && /[a-z]/.test(acc.secret_key);
});
} else if (status === '任务中') {
matchedAccounts = pool.filter(acc => this._isTaskAccountInProgress(acc));
} else if (status === '刚刚运行') {
const emailSet = new Set(this._lastTaskEmails || []);
matchedAccounts = pool.filter(acc => emailSet.has(acc.email));
} else if (status === '家庭组母号') {
matchedAccounts = pool.filter(acc => acc.tag === '家庭组母号');
} else if (status !== '全部') {
// 普通状态筛选
matchedAccounts = pool.filter(acc => (acc.status || '未登录') === status);
}
if (matchedAccounts.length > 0) {
this.selectedEmails = new Set(matchedAccounts.map(a => a.email));
}
},
setTierFilter(level, value) {
// 切换特殊筛选回正常模式
this.statusFilter = '全部';
if (level === 1) {
// 点击已选中的 → 回到全部
if (this.tierFilter.level1 === value && value !== '全部') {
this.tierFilter = { level1: '全部', level2: '全部', level3: '全部', errorCode: null };
} else {
this.tierFilter = { level1: value, level2: '全部', level3: '全部', errorCode: null };
}
} else if (level === 2) {
if (this.tierFilter.level2 === value && value !== '全部') {
this.tierFilter = { ...this.tierFilter, level2: '全部', level3: '全部', errorCode: null };
} else {
this.tierFilter = { ...this.tierFilter, level2: value, level3: '全部', errorCode: null };
}
} else if (level === 3) {
if (this.tierFilter.level3 === value && value !== '全部') {
this.tierFilter = { ...this.tierFilter, level3: '全部' };
} else {
this.tierFilter = { ...this.tierFilter, level3: value };
}
} else if (level === 'error') {
if (this.tierFilter.errorCode === value) {
this.tierFilter = { ...this.tierFilter, errorCode: null };
} else {
this.tierFilter = { ...this.tierFilter, errorCode: value };
}
}
// 自动勾选当前筛选结果
this.selectedEmails = new Set(this.filteredAccounts.map(a => a.email));
this._rebuildDashboardStats();
this.resetAccountPage();
},
onCustomerFilterChange() {
this.selectedEmails = new Set();
this.statusFilter = '全部';
this._rebuildDashboardStats();
this.resetAccountPage();
},
// ==================== TOTP ====================
_base32Decode(input) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const s = input.toUpperCase().replace(/[\s=]+/g, '');
let bits = 0, value = 0;
const bytes = [];
for (const char of s) {
const idx = alphabet.indexOf(char);
if (idx === -1) continue;
value = (value << 5) | idx;
bits += 5;
if (bits >= 8) { bytes.push((value >>> (bits - 8)) & 0xff); bits -= 8; }
}
return new Uint8Array(bytes);
},
async _computeTotp(secret) {
try {
const keyBytes = this._base32Decode(secret);
const counter = Math.floor(Date.now() / 1000 / 30);
const buf = new Uint8Array(8);
let c = counter;
for (let i = 7; i >= 0; i--) { buf[i] = c & 0xff; c = Math.floor(c / 256); }
const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
const sig = new Uint8Array(await crypto.subtle.sign('HMAC', key, buf));
const offset = sig[19] & 0xf;
const code = ((sig[offset] & 0x7f) << 24 | sig[offset+1] << 16 | sig[offset+2] << 8 | sig[offset+3]) % 1000000;
return code.toString().padStart(6, '0');
} catch { return null; }
},
async updateTotpCodes() {
const updateId = ++this._totpUpdateId;
const visibleAccounts = this.pagedAccounts.filter(account => account.secret_key);
const entries = await Promise.all(visibleAccounts.map(async account => [
account.email,
await this._computeTotp(account.secret_key),
]));
// 翻页或筛选后,丢弃上一页尚未完成的异步计算。
if (updateId !== this._totpUpdateId) return;
this.totpCodes = Object.fromEntries(entries);
},
openEditAccount(account) {
this.editAccount = { ...account };
this.showEditAccountModal = true;
},
async saveEditAccount() {
if (!this.editAccount || !this.editAccount.email) return;
const payload = {
password: this.editAccount.password || '',
recovery_email: this.editAccount.recovery_email || '',
secret_key: this.editAccount.secret_key || '',
sheerid_link: this.editAccount.sheerid_link || '',
proxy_ip: this.editAccount.proxy_ip || '',
status: this.editAccount.status || '未登录',
message: this.editAccount.message || '',
country: this.editAccount.country || '',
browser_id: this.editAccount.browser_id || '',
customer_name: this.editAccount.customer_name || '',
};
try {
const res = await fetch(`/api/accounts/${encodeURIComponent(this.editAccount.email)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json();
if (data.success) {
this.addLog(`账号已更新: ${this.editAccount.email}`, 'info');
this.showEditAccountModal = false;
await this.loadAccounts();
} else {
this.addLog(`更新失败: ${data.msg}`, 'error');
}
} catch (e) {
this.addLog(`更新失败: ${e.message}`, 'error');
}
},
startInlineEditAccountRefund(email) {
if (!email) return;
this.inlineEditStatusEmail = null;
this.inlineEditRechargeStatusEmail = null;
this.inlineEditRefundProcessEmail = null;
this.inlineEditAccountRefundEmail = email;
},
async quickSetAccountRefundStatus(email, newStatus) {
this.inlineEditAccountRefundEmail = null;
const rawStatus = String(newStatus || '').trim();
const status = !rawStatus || rawStatus === '未操作' ? '无状态' : rawStatus;
if (!this.accountRefundStatusOptions.includes(status)) {
this.addLog('账号退款状态无效', 'warn');
return;
}
try {
const res = await fetch(`/api/accounts/${encodeURIComponent(email)}/account-refund-status`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '账号退款状态修改失败');
const account = this._accountByEmail.get(email);
if (account && data.data) Object.assign(account, data.data);
this.addLog(`已修改账号退款状态: ${email} → ${status}`, 'success');
} catch (e) {
this.addLog(`账号退款状态修改失败: ${e.message}`, 'error');
}
},
startInlineEditRefundProcess(email) {
if (!email) return;
this.inlineEditStatusEmail = null;
this.inlineEditRechargeStatusEmail = null;
this.inlineEditAccountRefundEmail = null;
this.inlineEditRefundProcessEmail = email;
},
async quickSetRefundProcessStatus(email, newStatus) {
this.inlineEditRefundProcessEmail = null;
const status = String(newStatus || '').trim() || '未发起';
if (!this.refundProcessStatusOptions.includes(status)) {
this.addLog('退款流程状态无效', 'warn');
return;
}
try {
const res = await fetch(`/api/accounts/${encodeURIComponent(email)}/refund-process-status`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '退款流程状态修改失败');
const account = this._accountByEmail.get(email);
if (account && data.data) Object.assign(account, data.data);
this.addLog(`已修改退款流程状态: ${email} → ${status}`, 'success');
} catch (e) {
this.addLog(`退款流程状态修改失败: ${e.message}`, 'error');
}
},
startInlineEditRechargeStatus(email) {
if (!email) return;
this.inlineEditStatusEmail = null;
this.inlineEditAccountRefundEmail = null;
this.inlineEditRefundProcessEmail = null;
this.inlineEditRechargeStatusEmail = email;
},
// 保留旧入口,统一切换为与“状态”列相同的原地下拉编辑。
editRechargeStatus(account) {
if (!account || !account.email) return;
this.startInlineEditRechargeStatus(account.email);
},
async quickSetRechargeStatus(email, newStatus) {
this.inlineEditRechargeStatusEmail = null;
const status = String(newStatus || '').trim() || '未充值';
const allowed = new Set(['未充值', '充值中', '等待已确认', '充值成功', '充值失败', '账号本身已有会员']);
if (!allowed.has(status)) {
this.addLog('充值状态无效,请使用提示中的状态', 'warn');
return;
}
try {
const res = await fetch(`/api/accounts/${encodeURIComponent(email)}/recharge-status`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '充值状态修改失败');
const account = this._accountByEmail.get(email);
if (account && data.data) Object.assign(account, data.data);
this._rebuildDashboardStats();
this.addLog(`已人工修改充值状态: ${email} → ${status}`, 'success');
} catch (e) {
this.addLog(`充值状态修改失败: ${e.message}`, 'error');
}
},
// ==================== 批量改写客户名 ====================
openBatchCustomerNameModal() {
if (this.selectedEmails.size === 0) {
this.addLog('⚠️ 请先选择要改写客户名的账号', 'warn');
return;
}
this.batchCustomerName = '';
this.showBatchCustomerNameModal = true;
},
async confirmBatchCustomerName() {
if (this.batchCustomerNameSaving) return;
const emails = [...this.selectedEmails];
if (emails.length === 0) return;
const customerName = String(this.batchCustomerName || '').trim();
this.batchCustomerNameSaving = true;
try {
const res = await fetch('/api/accounts/batch/customer-name', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ emails, customerName }),
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '批量改写失败');
const selectedSet = new Set(emails);
for (const account of this.accounts) {
if (selectedSet.has(account.email)) account.customer_name = customerName;
}
this._rebuildDashboardStats();
this.showBatchCustomerNameModal = false;
this.addLog(
customerName
? `已将 ${data.count} 个账号的客户名改写为“${customerName}”`
: `已清空 ${data.count} 个账号的客户名`,
'success',
);
} catch (e) {
this.addLog(`批量改写客户名失败: ${e.message}`, 'error');
} finally {
this.batchCustomerNameSaving = false;
}
},
// ==================== 退款截止时间 ====================
refundDeadlineInputValue(value) {
if (!value) return '';
const date = new Date(value);
if (!Number.isFinite(date.getTime())) return '';
const beijing = new Date(date.getTime() + 8 * 60 * 60 * 1000);
const pad = number => String(number).padStart(2, '0');
return `${beijing.getUTCFullYear()}-${pad(beijing.getUTCMonth() + 1)}-${pad(beijing.getUTCDate())}`
+ `T${pad(beijing.getUTCHours())}:${pad(beijing.getUTCMinutes())}:${pad(beijing.getUTCSeconds())}`;
},
openRefundDeadlineModal(account = null) {
const emails = account && account.email ? [account.email] : [...this.selectedEmails];
if (emails.length === 0) {
this.addLog('⚠️ 请先选择要设置退款截止时间的账号', 'warn');
return;
}
this.refundDeadlineEmails = emails;
this.refundDeadlineValue = account ? this.refundDeadlineInputValue(account.refund_deadline_at) : '';
this.refundDeadlineMode = 'absolute';
this.refundDeadlineOffsetValue = 1;
this.refundDeadlineOffsetUnit = 'days';
this.showRefundDeadlineModal = true;
},
setRefundDeadlineQuick(hours) {
const date = new Date(Date.now() + Number(hours || 0) * 60 * 60 * 1000);
this.refundDeadlineValue = this.refundDeadlineInputValue(date.toISOString());
this.refundDeadlineMode = 'absolute';
},
setRefundDeadlineRechargeOffset(value, unit) {
this.refundDeadlineOffsetValue = Number(value) || 1;
this.refundDeadlineOffsetUnit = ['minutes', 'hours', 'days'].includes(unit) ? unit : 'hours';
this.refundDeadlineMode = 'recharge_offset';
},
refundDeadlineOffsetLabel(value = this.refundDeadlineOffsetValue, unit = this.refundDeadlineOffsetUnit) {
const labels = { minutes: '分钟', hours: '小时', days: '天' };
return `${Number(value) || 0}${labels[unit] || '小时'}`;
},
async saveRefundDeadline(clear = false) {
if (this.refundDeadlineSaving) return;
const emails = [...this.refundDeadlineEmails];
if (emails.length === 0) return;
let deadlineAt = null;
const useRechargeOffset = !clear && this.refundDeadlineMode === 'recharge_offset';
let offsetValue = null;
let offsetUnit = null;
if (useRechargeOffset) {
offsetValue = Number(this.refundDeadlineOffsetValue);
offsetUnit = String(this.refundDeadlineOffsetUnit || '');
if (!Number.isInteger(offsetValue) || offsetValue <= 0
|| !['minutes', 'hours', 'days'].includes(offsetUnit)) {
this.addLog('⚠️ 请输入有效的充值时间偏移量', 'warn');
return;
}
} else if (!clear) {
const localText = String(this.refundDeadlineValue || '').trim();
if (!localText) {
this.addLog('⚠️ 请选择退款截止时间,或点击“清除时间”', 'warn');
return;
}
const parsed = new Date(`${localText}+08:00`);
if (!Number.isFinite(parsed.getTime())) {
this.addLog('⚠️ 退款截止时间格式无效', 'warn');
return;
}
deadlineAt = parsed.toISOString();
}
this.refundDeadlineSaving = true;
try {
const res = await fetch('/api/accounts/batch/refund-deadline', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(useRechargeOffset
? { emails, mode: 'recharge_offset', offsetValue, offsetUnit }
: { emails, mode: 'absolute', deadlineAt }),
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '退款截止时间保存失败');
if (useRechargeOffset) {
const deadlineByEmail = new Map((data.items || []).map(item => [item.email, item.deadlineAt]));
for (const account of this.accounts) {
if (deadlineByEmail.has(account.email)) account.refund_deadline_at = deadlineByEmail.get(account.email);
}
} else {
const emailSet = new Set(emails);
for (const account of this.accounts) {
if (emailSet.has(account.email)) account.refund_deadline_at = data.deadlineAt || null;
}
}
this.showRefundDeadlineModal = false;
if (useRechargeOffset) {
const skippedText = data.skippedCount > 0 ? `;另有 ${data.skippedCount} 个没有充值时间,已跳过` : '';
this.addLog(
`已按每个账号充值时间 + ${this.refundDeadlineOffsetLabel(offsetValue, offsetUnit)},设置 ${data.count} 个退款截止时间${skippedText}`,
data.skippedCount > 0 ? 'warn' : 'success',
);
} else {
this.addLog(
data.deadlineAt
? `已为 ${data.count} 个账号设置退款截止时间:${this.formatDate(data.deadlineAt)}`
: `已清除 ${data.count} 个账号的退款截止时间`,
'success',
);
}
} catch (e) {
this.addLog(`退款截止时间保存失败:${e.message}`, 'error');
} finally {
this.refundDeadlineSaving = false;
}
},
// ==================== 批量更改状态 ====================
openBatchStatusModal() {
if (this.selectedEmails.size === 0) {
this.addLog('⚠️ 请先选择要更改状态的账号', 'warn');
return;
}
this.batchStatus = '未登录';
this.showBatchStatusModal = true;
},
async confirmBatchStatus() {
const emails = [...this.selectedEmails];
if (emails.length === 0) return;
let successCount = 0;
let failCount = 0;
for (const email of emails) {
try {
const res = await fetch(`/api/accounts/${encodeURIComponent(email)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: this.batchStatus }),
});
const data = await res.json();
if (data.success) successCount++;
else failCount++;
} catch (e) {
failCount++;
}
}
this.addLog(`批量更改状态完成: 成功 ${successCount} 个,失败 ${failCount} 个`, successCount > 0 ? 'success' : 'error');
this.showBatchStatusModal = false;
await this.loadAccounts();
},
// ==================== 快速设定状态(双击) ====================
startInlineEditStatus(email) {
this.inlineEditRechargeStatusEmail = null;
this.inlineEditAccountRefundEmail = null;
this.inlineEditRefundProcessEmail = null;
this.inlineEditStatusEmail = email;
},
async quickSetStatus(email, newStatus) {
this.inlineEditStatusEmail = null;
try {
const res = await fetch(`/api/accounts/${encodeURIComponent(email)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus }),
});
const data = await res.json();
if (data.success) {
const acc = this._accountByEmail.get(email);
if (acc) acc.status = newStatus;
this._rebuildDashboardStats();
} else {
this.addLog(`状态更新失败: ${data.msg}`, 'error');
}
} catch (e) {
this.addLog(`状态更新失败: ${e.message}`, 'error');
}
},
// ==================== 截图弹窗 ====================
viewScreenshot(account) {
if (!account.screenshot_path) return;
this.screenshotUrl = '/' + account.screenshot_path + '?t=' + Date.now();
this.screenshotEmail = account.email;
this.showScreenshotModal = true;
},
closeScreenshot() {
this.showScreenshotModal = false;
this.screenshotUrl = '';
this.screenshotEmail = '';
},
// ==================== ChatGPT Session 弹窗 ====================
async openChatgptSession(account) {
if (!account || !account.email) return;
this.chatgptSessionEmail = account.email;
this.chatgptSessionText = '';
this.chatgptSessionError = '';
this.chatgptSessionLoading = true;
this.showChatgptSessionModal = true;
try {
const res = await fetch(`/api/accounts/${encodeURIComponent(account.email)}/chatgpt-session`, {
cache: 'no-store',
});
const data = await res.json().catch(() => null);
if (!res.ok || !data || !data.success) {
throw new Error((data && data.msg) || `读取失败 (${res.status})`);
}
this.chatgptSessionText = JSON.stringify(data.data, null, 2);
} catch (e) {
this.chatgptSessionError = e.message || '读取 ChatGPT Session 失败';
} finally {
this.chatgptSessionLoading = false;
}
},
closeChatgptSession() {
this.showChatgptSessionModal = false;
this.chatgptSessionEmail = '';
this.chatgptSessionText = '';
this.chatgptSessionError = '';
this.chatgptSessionLoading = false;
},
async copyChatgptSession() {
if (!this.chatgptSessionText) return;
await this.copyCell(this.chatgptSessionText);
},
// ==================== 最近充值卡密弹窗 ====================
async openRechargeCard(account) {
if (!account || !account.email) return;
this.rechargeCardEmail = account.email;
this.rechargeCardText = '';
this.rechargeCardCompletedAt = '';
this.rechargeCardError = '';
this.rechargeCardLoading = true;
this.showRechargeCardModal = true;
try {
const res = await fetch(`/api/accounts/${encodeURIComponent(account.email)}/recharge-card`, {
cache: 'no-store',
});
const data = await res.json().catch(() => null);
if (!res.ok || !data || !data.success) {
throw new Error((data && data.msg) || `读取失败 (${res.status})`);
}
this.rechargeCardText = String((data.data && data.data.cardKey) || '');
this.rechargeCardCompletedAt = String((data.data && data.data.rechargedAt) || '');
if (!this.rechargeCardText) throw new Error('该账号尚未保存充值卡密');
} catch (e) {
this.rechargeCardError = e.message || '读取充值卡密失败';
} finally {
this.rechargeCardLoading = false;
}
},
closeRechargeCard() {
this.showRechargeCardModal = false;
this.rechargeCardEmail = '';
this.rechargeCardText = '';
this.rechargeCardCompletedAt = '';
this.rechargeCardError = '';
this.rechargeCardLoading = false;
},
async copyRechargeCard() {
if (!this.rechargeCardText) return;
await this.copyCell(this.rechargeCardText);
},
// ==================== 授权 JSON 弹窗 ====================
async openOpenAIAuth(account) {
if (!account || !account.email) return;
this.openAIAuthEmail = account.email;
this.openAIAuthText = '';
this.openAIAuthError = '';
this.openAIAuthLoading = true;
this.showOpenAIAuthModal = true;
try {
const res = await fetch(`/api/accounts/${encodeURIComponent(account.email)}/openai-auth`, {
cache: 'no-store',
});
const data = await res.json().catch(() => null);
if (!res.ok || !data || !data.success) {
throw new Error((data && data.msg) || `读取失败 (${res.status})`);
}
this.openAIAuthText = JSON.stringify(data.data, null, 2);
} catch (e) {
this.openAIAuthError = e.message || '读取授权 JSON 失败';
} finally {
this.openAIAuthLoading = false;
}
},
closeOpenAIAuth() {
this.showOpenAIAuthModal = false;
this.openAIAuthEmail = '';
this.openAIAuthText = '';
this.openAIAuthError = '';
this.openAIAuthLoading = false;
},
async copyOpenAIAuth() {
if (!this.openAIAuthText) return;
await this.copyCell(this.openAIAuthText);
},
// ==================== 反重力 AC 凭证弹窗 ====================
async openAgCredential(account) {
if (!account || !account.email) return;
this.agCredentialEmail = account.email;
this.agCredentialText = '';
this.agCredentialError = '';
this.agCredentialLoading = true;
this.showAgCredentialModal = true;
try {
const res = await fetch(`/api/accounts/${encodeURIComponent(account.email)}/ag-credential`, {
cache: 'no-store',
});
const data = await res.json().catch(() => null);
if (!res.ok || !data || !data.success) {
throw new Error((data && data.msg) || `读取失败 (${res.status})`);
}
this.agCredentialText = JSON.stringify(data.data, null, 2);
} catch (e) {
this.agCredentialError = e.message || '读取 AC 凭证失败';
} finally {
this.agCredentialLoading = false;
}
},
closeAgCredential() {
this.showAgCredentialModal = false;
this.agCredentialEmail = '';
this.agCredentialText = '';
this.agCredentialError = '';
this.agCredentialLoading = false;
},
async copyAgCredential() {
if (!this.agCredentialText) return;
await this.copyCell(this.agCredentialText);
},
// ==================== Gemini CLI AC 凭证弹窗 ====================
async openCliAcCredential(account) {
if (!account || !account.email) return;
this.cliAcCredentialEmail = account.email;
this.cliAcCredentialText = '';
this.cliAcCredentialError = '';
this.cliAcCredentialLoading = true;
this.showCliAcCredentialModal = true;
try {
const res = await fetch(`/api/accounts/${encodeURIComponent(account.email)}/cli-ac-credential`, {
cache: 'no-store',
});
const data = await res.json().catch(() => null);
if (!res.ok || !data || !data.success) {
throw new Error((data && data.msg) || `读取失败 (${res.status})`);
}
this.cliAcCredentialText = JSON.stringify(data.data, null, 2);
} catch (e) {
this.cliAcCredentialError = e.message || '读取 Cli_AC 凭证失败';
} finally {
this.cliAcCredentialLoading = false;
}
},
closeCliAcCredential() {
this.showCliAcCredentialModal = false;
this.cliAcCredentialEmail = '';
this.cliAcCredentialText = '';
this.cliAcCredentialError = '';
this.cliAcCredentialLoading = false;
},
async copyCliAcCredential() {
if (!this.cliAcCredentialText) return;
await this.copyCell(this.cliAcCredentialText);
},
async loadChatgptSessionsForExport(accounts) {
const source = Array.isArray(accounts) ? accounts : [];
const hydrated = source.map(account => ({ ...account, chatgpt_session: '' }));
let cursor = 0;
let missingCount = 0;
const workerCount = Math.min(8, hydrated.length);
await Promise.all(Array.from({ length: workerCount }, async () => {
while (true) {
const index = cursor++;
if (index >= hydrated.length) break;
const account = hydrated[index];
const res = await fetch(`/api/accounts/${encodeURIComponent(account.email)}/chatgpt-session`, {
cache: 'no-store',
});
const data = await res.json().catch(() => null);
if (res.ok && data && data.success) {
account.chatgpt_session = JSON.stringify(data.data) || '';
} else if (res.status === 404) {
missingCount++;
} else {
throw new Error((data && data.msg) || `读取 ChatGPT Session 失败 (${res.status})`);
}
}
}));
return { accounts: hydrated, missingCount };
},
async loadOpenAIAuthForExport(accounts) {
const source = Array.isArray(accounts) ? accounts : [];
const hydrated = source.map(account => ({ ...account, openai_auth_json: '' }));
let cursor = 0;
let missingCount = 0;
const workerCount = Math.min(8, hydrated.length);
await Promise.all(Array.from({ length: workerCount }, async () => {
while (true) {
const index = cursor++;
if (index >= hydrated.length) break;
const account = hydrated[index];
const res = await fetch(`/api/accounts/${encodeURIComponent(account.email)}/openai-auth`, {
cache: 'no-store',
});
const data = await res.json().catch(() => null);
if (res.ok && data && data.success) {
account.openai_auth_json = JSON.stringify(data.data) || '';
} else if (res.status === 404) {
missingCount++;
} else {
throw new Error((data && data.msg) || `读取授权 JSON 失败 (${res.status})`);
}
}
}));
return { accounts: hydrated, missingCount };
},
async loadCliAcCredentialsForExport(accounts) {
const source = Array.isArray(accounts) ? accounts : [];
const hydrated = source.map(account => ({ ...account, cli_ac_credential: '' }));
let cursor = 0;
let missingCount = 0;
const workerCount = Math.min(8, hydrated.length);
await Promise.all(Array.from({ length: workerCount }, async () => {
while (true) {
const index = cursor++;
if (index >= hydrated.length) break;
const account = hydrated[index];
const res = await fetch(`/api/accounts/${encodeURIComponent(account.email)}/cli-ac-credential`, {
cache: 'no-store',
});
const data = await res.json().catch(() => null);
if (res.ok && data && data.success) {
account.cli_ac_credential = JSON.stringify(data.data) || '';
} else if (res.status === 404) {
missingCount++;
} else {
throw new Error((data && data.msg) || `读取 Cli_AC 凭证失败 (${res.status})`);
}
}
}));
return { accounts: hydrated, missingCount };
},
// 消息列显示处理结论;详细步骤仍可在诊断日志中查看。
getLiveMessage(account) {
const live = this.accountLogs[account.email];
const label = this._getAccountFailureLabel(account);
const rechargeMessage = String(account.recharge_message || '').trim();
const persistedMessage = String(account.message || '').trim();
// 充值流程会把两个字段同步保存;任务完成后优先展示完整充值
// 结论,避免“卡密未找到”等具体原因被压缩成“功能失败”。
if (account.recharge_status && rechargeMessage && rechargeMessage === persistedMessage
&& (!this.isRunning || !live)) {
return rechargeMessage;
}
// 任务结束后以落库结论为准,避免最后一条详细日志重新占据列表。
if (label && (!this.isRunning || !live)) return label;
if (live && live.message) return live.message;
if (this._isTaskMessagePending(account)) return this._taskPendingMessage(this._activeTaskType);
return label || account.message || '-';
},
getLiveMessageClass(account) {
const live = this.accountLogs[account.email];
if (!live) {
if (this._isTaskMessagePending(account)) return 'text-blue-500';
const m = (account && account.message != null) ? String(account.message) : '';
const s = m.replace(/[\r\n\t]/g, ' ').replace(/\s+/g, ' ').trim();
const label = this._getAccountFailureLabel(account);
if (label === '切换ip重试' || label === '要等7天') return 'text-yellow-600';
if (label === '密码错误' || label === '密码已被修改' || label === '2fa错误' || label === '2fa密钥不正确' || label === '人机验证' || label === '无法登录' || label === '功能失败' || label === 'GCP被限制' || label === '无权限' || label === '未满18岁' || label === 'Cli凭证失败') return 'text-red-500';
if (/^⚠️/.test(s) || /警告/.test(s)) return 'text-yellow-600';
if (/^❌/.test(s) || /错误|异常/.test(s)) return 'text-red-500';
if (/^✅/.test(s) || /成功|已复制|已登录/.test(s)) return 'text-green-600';
if (!s || s === '-') return '';
return 'text-blue-500';
}
const liveLabel = this._failureLabelForCode('', live.message || '');
if (liveLabel === '切换ip重试' || liveLabel === '要等7天') return 'text-yellow-600';
if (liveLabel === '密码错误' || liveLabel === '密码已被修改' || liveLabel === '2fa错误' || liveLabel === '2fa密钥不正确' || liveLabel === '人机验证' || liveLabel === '无法登录' || liveLabel === '功能失败' || liveLabel === 'GCP被限制' || liveLabel === '无权限' || liveLabel === '未满18岁' || liveLabel === 'Cli凭证失败') return 'text-red-500';
const map = { error: 'text-red-500', success: 'text-green-600', warn: 'text-yellow-600', info: 'text-blue-500' };
return map[live.type] || 'text-blue-500';
},
exportSelectedAccounts() {
const list = this.selectedAccounts.length > 0 ? this.selectedAccounts : this.filteredAccounts;
if (list.length === 0) return this.addLog('⚠️ 没有可导出的账号', 'warn');
this._exportList = list;
this.exportMode = { file: false, csv: false, sub2api_json: false, clipboard: true, staging: false };
this.exportStagingNote = '';
this.deleteBrowserWindowsOnStage = false;
this.showExportModal = true;
},
escapeCsvCell(value) {
let text = '';
if (value !== null && value !== undefined) {
text = typeof value === 'object' ? JSON.stringify(value) : String(value);
}
return `"${text.replace(/"/g, '""')}"`;
},
exportFieldValue(account, key) {
if (!account) return '';
if (key === 'recharged_at') return this.formatDate(account.recharged_at);
if (key === 'refund_deadline_at') return this.formatDate(account.refund_deadline_at);
return account[key] ?? '';
},
buildExportCsv(accounts, selectedKeys) {
const source = Array.isArray(accounts) ? accounts : [];
const keys = Array.isArray(selectedKeys) ? selectedKeys : [];
const labels = keys.map((key) => {
const field = this.exportFieldDefs.find(item => item.key === key);
return field ? field.label : key;
});
const rows = [labels, ...source.map(account => keys.map(key => this.exportFieldValue(account, key)))];
return rows.map(row => row.map(value => this.escapeCsvCell(value)).join(',')).join('\r\n');
},
decodeJwtPayload(token) {
try {
const part = String(token || '').split('.')[1];
if (!part) return {};
const base64 = part.replace(/-/g, '+').replace(/_/g, '/')
+ '='.repeat((4 - (part.length % 4)) % 4);
const binary = atob(base64);
const bytes = Uint8Array.from(binary, char => char.charCodeAt(0));
let jsonText = '';
if (typeof TextDecoder === 'function') {
jsonText = new TextDecoder('utf-8').decode(bytes);
} else {
const encoded = Array.from(bytes, byte => `%${byte.toString(16).padStart(2, '0')}`).join('');
jsonText = decodeURIComponent(encoded);
}
const payload = JSON.parse(jsonText);
return payload && typeof payload === 'object' ? payload : {};
} catch (_) {
return {};
}
},
buildSub2ApiExport(accounts, exportedAt = new Date()) {
const source = Array.isArray(accounts) ? accounts : [];
const exportedDate = exportedAt instanceof Date && Number.isFinite(exportedAt.getTime())
? exportedAt
: new Date();
const result = {
exported_at: exportedDate.toISOString().replace(/\.\d{3}Z$/, 'Z'),
proxies: [],
accounts: [],
};
let skippedCount = 0;
for (const account of source) {
let auth = account && account.openai_auth_json;
if (typeof auth === 'string') {
try { auth = JSON.parse(auth); } catch (_) { auth = null; }
}
if (!auth || typeof auth !== 'object') {
skippedCount++;
continue;
}
const tokenBag = auth.tokens && typeof auth.tokens === 'object' ? auth.tokens : {};
const accessToken = String(auth.access_token || tokenBag.access_token || '').trim();
const refreshToken = String(auth.refresh_token || tokenBag.refresh_token || '').trim();
const idToken = String(auth.id_token || tokenBag.id_token || '').trim();
if (!accessToken || !refreshToken) {
skippedCount++;
continue;
}
const accessClaims = this.decodeJwtPayload(accessToken);
const idClaims = this.decodeJwtPayload(idToken);
const accessAuth = accessClaims['https://api.openai.com/auth'] || {};
const idAuth = idClaims['https://api.openai.com/auth'] || {};
const profile = accessClaims['https://api.openai.com/profile'] || {};
const organizations = Array.isArray(idAuth.organizations) ? idAuth.organizations : [];
const email = String(idClaims.email || profile.email || auth.email || account.email || '').trim();
if (!email) {
skippedCount++;
continue;
}
const credentials = {
access_token: accessToken,
};
const chatgptAccountId = String(
accessAuth.chatgpt_account_id
|| idAuth.chatgpt_account_id
|| tokenBag.account_id
|| auth.account_id
|| '',
).trim();
const chatgptUserId = String(
accessAuth.chatgpt_user_id
|| idAuth.chatgpt_user_id
|| auth.chatgpt_user_id
|| auth.user_id
|| '',
).trim();
const organizationId = String(
accessAuth.poid
|| (organizations[0] && organizations[0].id)
|| auth.organization_id
|| '',
).trim();
const planType = String(accessAuth.chatgpt_plan_type || idAuth.chatgpt_plan_type || auth.plan_type || '').trim();
const clientId = String(accessClaims.client_id || auth.client_id || '').trim();
let expiresAt = Number(accessClaims.exp) || 0;
if (!expiresAt && auth.expires_at) {
const numericExpiry = Number(auth.expires_at);
if (Number.isFinite(numericExpiry) && numericExpiry > 0) {
expiresAt = numericExpiry > 1e12
? Math.floor(numericExpiry / 1000)
: Math.floor(numericExpiry);
} else {
const parsedExpiry = Date.parse(auth.expires_at);
if (Number.isFinite(parsedExpiry)) expiresAt = Math.floor(parsedExpiry / 1000);
}
}
if (chatgptAccountId) credentials.chatgpt_account_id = chatgptAccountId;
if (chatgptUserId) credentials.chatgpt_user_id = chatgptUserId;
if (clientId) credentials.client_id = clientId;
credentials.email = email;
if (expiresAt > 0) credentials.expires_at = Math.floor(expiresAt);
if (idToken) credentials.id_token = idToken;
if (organizationId) credentials.organization_id = organizationId;
if (planType) credentials.plan_type = planType;
credentials.refresh_token = refreshToken;
result.accounts.push({
name: email,
platform: 'openai',
type: 'oauth',
credentials,
extra: { email },
concurrency: 100,
priority: 1,
rate_multiplier: 1,
auto_pause_on_expired: true,
});
}
return { data: result, skippedCount };
},
formatSub2ApiExportTimestamp(value = new Date()) {
const date = value instanceof Date && Number.isFinite(value.getTime()) ? value : new Date();
const pad = number => String(number).padStart(2, '0');
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`
+ `${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
},
async confirmExport() {
if (this.exportInProgress) return;
const list = this._exportList;
if (!list || list.length === 0) return;
const selectedKeys = this.exportFieldDefs.filter(f => this.exportFields[f.key]).map(f => f.key);
const exportsText = this.exportMode.file || this.exportMode.clipboard;
const exportsCsv = Boolean(this.exportMode.csv);
const exportsSub2Api = Boolean(this.exportMode.sub2api_json);
const exportsTabular = exportsText || exportsCsv;
const exportsContent = exportsTabular || exportsSub2Api;
const needsSelectedFields = exportsTabular || Boolean(this.exportMode.staging);
if (needsSelectedFields && selectedKeys.length === 0) {
return this.addLog('⚠️ 请至少选择一个导出字段', 'warn');
}
if (!exportsContent && !this.exportMode.staging) return this.addLog('⚠️ 请至少选择一种处理方式', 'warn');
if (this.exportMode.staging && !this.exportStagingNote.trim()) {
return this.addLog('⚠️ 请填写暂存文件夹名称', 'warn');
}
this.exportInProgress = true;
try {
let exportList = list;
if (exportsTabular && selectedKeys.includes('chatgpt_session')) {
const loaded = await this.loadChatgptSessionsForExport(list);
exportList = loaded.accounts;
if (loaded.missingCount > 0) {
this.addLog(`${loaded.missingCount} 个账号没有可导出的 ChatGPT Session,已保留为空字段`, 'warn');
}
}
if (exportsTabular && selectedKeys.includes('cli_ac_credential')) {
const loaded = await this.loadCliAcCredentialsForExport(exportList);
exportList = loaded.accounts;
if (loaded.missingCount > 0) {
this.addLog(`${loaded.missingCount} 个账号没有可导出的 Cli_AC 凭证,已保留为空字段`, 'warn');
}
}
if (exportsSub2Api || (exportsTabular && selectedKeys.includes('openai_auth_json'))) {
const loaded = await this.loadOpenAIAuthForExport(exportList);
exportList = loaded.accounts;
if (loaded.missingCount > 0 && exportsTabular && selectedKeys.includes('openai_auth_json')) {
this.addLog(`${loaded.missingCount} 个账号没有可导出的授权 JSON,已保留为空字段`, 'warn');
}
}
const text = exportsText
? exportList.map(a => selectedKeys.map(k => this.exportFieldValue(a, k)).join('----')).join('\n')
: '';
const csv = exportsCsv ? this.buildExportCsv(exportList, selectedKeys) : '';
const sub2ApiExport = exportsSub2Api ? this.buildSub2ApiExport(exportList) : null;
if (sub2ApiExport && sub2ApiExport.skippedCount > 0) {
this.addLog(
`${sub2ApiExport.skippedCount} 个账号缺少有效授权 JSON,已从 sub2api 导出中跳过`,
'warn',
);
}
if (sub2ApiExport && sub2ApiExport.data.accounts.length === 0
&& !exportsTabular && !this.exportMode.staging) {
this.addLog('没有包含有效授权 JSON 的账号,未生成 sub2api 文件', 'error');
return;
}
if (this.exportMode.file) {
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `accounts_${new Date().toISOString().slice(0, 10)}.txt`;
a.click();
URL.revokeObjectURL(url);
this.addLog(`已导出 ${list.length} 个账号到文件`, 'success');
}
if (exportsCsv) {
// UTF-8 BOM 让 Excel 直接打开时能正确识别中文;单元格内容由
// buildExportCsv 统一处理逗号、双引号和换行。
const blob = new Blob(['\uFEFF', csv], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `accounts_${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
this.addLog(`已导出 ${list.length} 个账号到 CSV`, 'success');
}
if (sub2ApiExport && sub2ApiExport.data.accounts.length > 0) {
const json = JSON.stringify(sub2ApiExport.data, null, 2);
const blob = new Blob([json], { type: 'application/json;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `sub2api-account-${this.formatSub2ApiExportTimestamp()}.json`;
a.click();
URL.revokeObjectURL(url);
this.addLog(`已导出 ${sub2ApiExport.data.accounts.length} 个账号到 sub2api JSON`, 'success');
}
if (this.exportMode.clipboard) {
try {
await navigator.clipboard.writeText(text);
} catch (e) {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
this.addLog(`已复制 ${list.length} 个账号到剪贴板`, 'success');
}
if (this.exportMode.staging) {
const staged = await this.stageExportAccounts(list, selectedKeys);
if (!staged) return;
}
this.showExportModal = false;
} catch (e) {
this.addLog(`导出失败: ${e.message}`, 'error');
} finally {
this.exportInProgress = false;
}
},
// ==================== 一次性导出问题号 ====================
openProblemExportModal() {
// 每次打开都恢复为三类全选,避免上次的选择影响本次导出。
this.problemExportCategories = { password: true, twofa: true, captcha: true };
this.showProblemExportModal = true;
},
_problemCategoryForAccount(account) {
if (!account) return null;
// 新任务优先使用持久化的标准原因码;旧数据再从最后消息兼容识别。
const reason = String(account.status_reason || '').trim().toUpperCase();
if (reason === 'PASSWORD_WRONG' || reason === 'PASSWORD_CHANGED_RECENTLY'
|| reason === '密码错误' || reason === '密码已被修改') return 'password';
if (reason === 'TOTP_KEY_INVALID' || reason === 'TOTP_KEY_INCORRECT' || reason === 'TOTP_CODE_WRONG'
|| reason === '2FA密钥无效' || reason === '2FA验证码错误') return 'twofa';
if (reason === 'CAPTCHA' || reason === '人机验证' || reason === '人机认证') return 'captcha';
const live = this.accountLogs && this.accountLogs[account.email];
const text = [account.message, live && live.message].filter(Boolean).join(' ');
if (!text) return null;
const compact = this._getAccountFailureLabel(account);
if (compact === '密码错误' || compact === '密码已被修改') return 'password';
if (compact === '2fa错误' || compact === '2fa密钥不正确') return 'twofa';
if (/人机认证|人机验证|recaptcha|verify you are not a robot|not a robot/i.test(text)) {
return 'captcha';
}
if (/密码错误|密码不正确|密码不对|wrong password|incorrect password|password is incorrect/i.test(text)) {
return 'password';
}
// OAuth 验证方式页没有 Authenticator 属于“等待7天”,不是 2FA 密钥/验证码错误。
if (this._isOAuthWait7Account(account)) return null;
if (/2fa密钥(?:格式不正确|过短|解析失败|无效)|需要2fa验证但未提供密钥|2fa验证码错误|连续两次明确拒绝2fa验证码|验证码校验失败|密钥不匹配|(?:wrong|incorrect|invalid)\s+(?:2fa\s+|totp\s+|authenticator\s+)?code/i.test(text)) {
return 'twofa';
}
return null;
},
problemExportAccounts(category) {
return this.accounts.filter(account => this._problemCategoryForAccount(account) === category);
},
problemExportCount(category) {
return this.problemExportAccounts(category).length;
},
_problemExportAccountLine(account) {
// 新导入的账号保留整行原文,导出时不重新拼接、不转换大小写。
if (account && account.raw_account_line !== undefined && account.raw_account_line !== null
&& String(account.raw_account_line).length > 0) {
return String(account.raw_account_line);
}
// 升级前导入的旧账号没有原文快照,只能按现有默认四字段顺序兼容导出。
return [account && account.email, account && account.password,
account && account.recovery_email, account && account.secret_key]
.map(value => value === undefined || value === null ? '' : String(value))
.join('----');
},
buildProblemExportText() {
const definitions = [
{ key: 'password', heading: '密码错误:' },
{ key: 'twofa', heading: '2fa错误:' },
{ key: 'captcha', heading: '人机认证:' },
];
return definitions
.filter(def => this.problemExportCategories[def.key])
.map(def => [
def.heading,
...this.problemExportAccounts(def.key).map(account => this._problemExportAccountLine(account)),
].join('\n'))
.join('\n\n');
},
confirmProblemExport() {
const selectedKeys = Object.keys(this.problemExportCategories)
.filter(key => this.problemExportCategories[key]);
if (selectedKeys.length === 0) {
return this.addLog('⚠️ 请至少勾选一类问题号', 'warn');
}
const counts = selectedKeys.map(key => this.problemExportCount(key));
const total = counts.reduce((sum, count) => sum + count, 0);
if (total === 0) {
return this.addLog('⚠️ 所选类别中没有可导出的问题号', 'warn');
}
const text = this.buildProblemExportText();
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
const stamp = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 19);
link.href = url;
link.download = `问题号_${stamp}.txt`;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 0);
this.showProblemExportModal = false;
const countLabels = {
password: '密码错误',
twofa: '2FA错误',
captcha: '人机认证',
};
const exportedSummary = selectedKeys
.map(key => `${countLabels[key]} ${this.problemExportCount(key)}`)
.join(' / ');
this.addLog(`已导出 ${total} 个问题号(${exportedSummary})`, 'success');
},
async runOpenUrl(urlKey) {
const target = this.openUrlPresets[urlKey];
if (!target) return;
if (this.isRunning) {
this.addLog('⚠️ 有任务正在运行,请等待完成或停止', 'warn');
return;
}
const taskAccounts = this.getTaskAccounts();
if (taskAccounts.length === 0) {
this.addLog('⚠️ 没有可用的账号', 'warn');
return;
}
this.clearLogs();
this.addLog(`🚀 打开窗口并跳转: ${target},账号数: ${taskAccounts.length}`, 'info');
try {
const res = await fetch('/api/operations/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
taskType: 'open_url',
accounts: taskAccounts,
params: this.buildTaskParams('open_url', { url: target }),
}),
});
const data = await res.json();
if (!data.success) {
this.addLog(`❌ 启动失败: ${data.msg}`, 'error');
return;
}
this.currentTaskId = data.taskId;
this._rememberTaskBatch('open_url', taskAccounts, data.taskId);
this._activeTaskType = 'open_url';
this._setActiveTaskEmails(this._lastTaskEmails);
this._activeTaskStartedAt = this._lastTaskStartedAt;
this.isRunning = true;
this._rebuildDashboardStats();
this.startSSEListener(data.taskId);
} catch (e) {
this.addLog(`❌ 网络错误: ${e.message}`, 'error');
}
},
async runOpenCustomUrl() {
const target = prompt('请输入要打开的网址(http/https)');
if (!target) return;
const url = String(target).trim();
if (!/^https?:\/\//i.test(url)) {
this.addLog('❌ 网址格式错误:必须以 http:// 或 https:// 开头', 'error');
return;
}
if (this.isRunning) {
this.addLog('⚠️ 有任务正在运行,请等待完成或停止', 'warn');
return;
}
const taskAccounts = this.getTaskAccounts();
if (taskAccounts.length === 0) {
this.addLog('⚠️ 没有可用的账号', 'warn');
return;
}
this.clearLogs();
this.addLog(`🚀 打开窗口并跳转: ${url},账号数: ${taskAccounts.length}`, 'info');
try {
const res = await fetch('/api/operations/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
taskType: 'open_url',
accounts: taskAccounts,
params: this.buildTaskParams('open_url', { url }),
}),
});
const data = await res.json();
if (!data.success) {
this.addLog(`❌ 启动失败: ${data.msg}`, 'error');
return;
}
this.currentTaskId = data.taskId;
this._rememberTaskBatch('open_url', taskAccounts, data.taskId);
this._activeTaskType = 'open_url';
this._setActiveTaskEmails(this._lastTaskEmails);
this._activeTaskStartedAt = this._lastTaskStartedAt;
this.isRunning = true;
this._rebuildDashboardStats();
this.startSSEListener(data.taskId);
} catch (e) {
this.addLog(`❌ 网络错误: ${e.message}`, 'error');
}
},
async saveQuotaMonitorSetting(key, value) {
const normalized = String(value);
this.settings[key] = normalized;
await this.saveSetting(key, normalized);
await this.loadQuotaMonitorStatus(true);
},
async loadQuotaMonitorStatus(silent = false) {
try {
const res = await fetch('/api/quota-monitor/status', { cache: 'no-store' });
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '读取额度监控状态失败');
const status = data.data && typeof data.data === 'object' ? data.data : {};
this.quotaMonitorStatus = {
running: !!status.running,
activeTask: status.activeTask || null,
nextRunAt: status.nextRunAt || null,
lastRun: status.lastRun || null,
deadlineMonitor: status.deadlineMonitor || this.quotaMonitorStatus.deadlineMonitor,
config: status.config || this.quotaMonitorStatus.config,
};
const config = this.quotaMonitorStatus.config || {};
this.settings.quota_monitor_enabled = config.enabled ? '1' : '0';
this.settings.quota_monitor_interval_value = String(config.intervalValue || 24);
this.settings.quota_monitor_interval_unit = String(config.intervalUnit || 'hours');
this.settings.quota_monitor_concurrency = String(config.concurrency || 2);
this.settings.quota_monitor_threshold = String(config.threshold ?? 100);
this.settings.quota_monitor_skip_401_enabled = config.skip401Enabled ? '1' : '0';
this.settings.quota_monitor_auto_refund_enabled = config.autoRefundEnabled ? '1' : '0';
if (this.quotaMonitorStatus.deadlineMonitor) {
this.settings.refund_deadline_check_interval_seconds = String(
this.quotaMonitorStatus.deadlineMonitor.intervalSeconds || 15,
);
}
const active = this.quotaMonitorStatus.activeTask;
if (active && active.progress) this.quotaMonitorProgress = { ...this.quotaMonitorProgress, ...active.progress };
if (status.running && active && active.taskId && this.quotaMonitorTaskId !== active.taskId) {
this.startQuotaMonitorListener(active.taskId, false);
}
if (!status.running && !this.quotaMonitorEventSource) this.quotaMonitorTaskId = null;
} catch (error) {
if (!silent) {
this.quotaMonitorStatusText = `状态读取失败:${error.message}`;
this.quotaMonitorStatusType = 'error';
}
}
},
async runQuotaMonitorNow() {
if (this.quotaMonitorStatus.running) {
const active = this.quotaMonitorStatus.activeTask;
const message = '额度监控任务正在执行,请等待本轮完成';
this.quotaMonitorStatusText = message;
this.quotaMonitorStatusType = 'warn';
if (active && active.taskId) this.startQuotaMonitorListener(active.taskId, false);
return;
}
if (!this.feishuCredentialMeta.configured) {
const message = '请先保存飞书 App ID 和 App Secret';
this.quotaMonitorStatusText = message;
this.quotaMonitorStatusType = 'warn';
alert(message);
return;
}
if (!['private', 'group'].includes(String(this.settings.feishu_notify_mode || 'none'))) {
const message = '请先在飞书设置中选择机器人私发或发送到群';
this.quotaMonitorStatusText = message;
this.quotaMonitorStatusType = 'warn';
alert(message);
return;
}
if (!String(this.settings.feishu_notify_receive_id || '').trim()) {
const message = '请先填写飞书私发目标或群 Chat ID';
this.quotaMonitorStatusText = message;
this.quotaMonitorStatusType = 'warn';
alert(message);
return;
}
const autoRefundEnabled = this.settings.quota_monitor_auto_refund_enabled === '1';
if (autoRefundEnabled) {
const threshold = this.settings.quota_monitor_threshold || '100';
const credentialWarning = (!this.workspaceRefundCredentialMeta.configured || !this.workspaceRefundCredentialMeta.proxyConfigured)
? '\n\n注意:退款母号或退款专用代理尚未完整配置,本轮将只查询额度并跳过自动退款。'
: '';
const confirmed = confirm(
`本轮将在额度查询完成后,对用量 ≥ ${threshold}% 且退款流程为“未发起”的账号自动发起退款。\n退款固定单路执行,账号退款字段不会被修改。${credentialWarning}\n\n确认继续吗?`,
);
if (!confirmed) return;
}
this.logDrawerOpen = true;
this.quotaMonitorProgress = {
phase: 'query', current: 0, total: 0, thresholdCount: 0, failureCount: 0,
refundCurrent: 0, refundTotal: 0, refundSuccessCount: 0, refundFailureCount: 0, refundSkippedCount: 0,
};
this.quotaMonitorStatusText = autoRefundEnabled ? '正在启动额度查询与自动退款…' : '正在启动额度查询…';
this.quotaMonitorStatusType = 'info';
this.addLog(autoRefundEnabled ? '🚀 手动启动额度监控与自动退款' : '🚀 手动启动额度监控', 'info');
try {
const res = await fetch('/api/quota-monitor/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}',
});
const data = await res.json();
if (!res.ok || !data.success) {
if (data.taskId) this.startQuotaMonitorListener(data.taskId, false);
throw new Error(data.msg || '启动额度监控失败');
}
this.quotaMonitorStatus.running = true;
this.quotaMonitorTaskId = data.taskId;
this.quotaMonitorStatusText = autoRefundEnabled ? '额度查询进行中,达到阈值后将自动退款' : '额度查询进行中';
this.startQuotaMonitorListener(data.taskId, true);
} catch (error) {
this.quotaMonitorStatusText = `启动失败:${error.message}`;
this.quotaMonitorStatusType = 'error';
this.addLog(`❌ 额度监控启动失败:${error.message}`, 'error');
alert(`额度监控启动失败:${error.message}`);
}
},
startQuotaMonitorListener(taskId, showCompletionAlert = false) {
if (!taskId) return;
if (this.quotaMonitorEventSource && this.quotaMonitorTaskId === taskId) return;
if (this.quotaMonitorEventSource) this.quotaMonitorEventSource.close();
const es = new EventSource(`/api/operations/events?taskId=${encodeURIComponent(taskId)}`);
this.quotaMonitorEventSource = es;
this.quotaMonitorTaskId = taskId;
let disconnectLogged = false;
es.onmessage = async event => {
try {
const data = JSON.parse(event.data);
if (data.type === 'log') {
this.addLog(String(data.message || ''));
} else if (data.type === 'progress') {
this.quotaMonitorProgress = { ...this.quotaMonitorProgress, ...data };
this.quotaMonitorProgress.current = Number(data.current) || 0;
this.quotaMonitorProgress.total = Number(data.total) || 0;
this.quotaMonitorProgress.refundCurrent = Number(data.refundCurrent) || 0;
this.quotaMonitorProgress.refundTotal = Number(data.refundTotal) || 0;
this.quotaMonitorProgress.refundSuccessCount = Number(data.refundSuccessCount) || 0;
this.quotaMonitorProgress.refundFailureCount = Number(data.refundFailureCount) || 0;
this.quotaMonitorProgress.refundSkippedCount = Number(data.refundSkippedCount) || 0;
this.quotaMonitorStatusText = data.phase === 'refund'
? `自动退款中:${this.quotaMonitorProgress.refundCurrent}/${this.quotaMonitorProgress.refundTotal},成功 ${this.quotaMonitorProgress.refundSuccessCount},失败 ${this.quotaMonitorProgress.refundFailureCount}`
: `额度查询中:${this.quotaMonitorProgress.current}/${this.quotaMonitorProgress.total}`;
this.quotaMonitorStatusType = 'info';
} else if (data.type === 'done') {
const result = data.result && typeof data.result === 'object' ? data.result : {};
es.close();
this.quotaMonitorEventSource = null;
this.quotaMonitorTaskId = null;
this.quotaMonitorStatus.running = false;
if (result.queryCompleted) {
const refundSuffix = result.autoRefundEnabled
? `,退款成功 ${result.refundSuccessCount || 0},失败 ${result.refundFailureCount || 0},跳过 ${result.refundSkippedCount || 0}`
: '';
this.quotaMonitorStatusText = result.deliveryStatus === 'sent'
? `完成:查询成功 ${result.successCount || 0},≥${result.threshold}% ${result.thresholdCount},查询失败 ${result.failureCount}${refundSuffix}`
: `查询完成,但飞书发送失败:${result.deliveryError || '未知错误'}`;
this.quotaMonitorStatusType = result.deliveryStatus === 'sent' ? 'success' : 'warn';
this.addLog(
`🏁 额度监控完成:检查 ${result.eligibleCount || 0},查询成功 ${result.successCount || 0},达到阈值 ${result.thresholdCount || 0},查询失败 ${result.failureCount || 0}${refundSuffix}`,
result.deliveryStatus === 'sent' ? 'success' : 'warn',
);
if (showCompletionAlert) {
alert(`额度监控完成\n检查:${result.eligibleCount || 0}\n查询成功:${result.successCount || 0}\n达到阈值:${result.thresholdCount || 0}\n查询失败:${result.failureCount || 0}${result.autoRefundEnabled ? `\n退款候选:${result.refundCandidateCount || 0}\n退款成功:${result.refundSuccessCount || 0}\n退款失败:${result.refundFailureCount || 0}\n退款跳过:${result.refundSkippedCount || 0}` : ''}\n飞书发送:${result.deliveryStatus === 'sent' ? '成功' : '失败'}`);
}
} else {
this.quotaMonitorStatusText = `执行失败:${result.error || '未知错误'}`;
this.quotaMonitorStatusType = 'error';
if (showCompletionAlert) alert(this.quotaMonitorStatusText);
}
await this.loadQuotaMonitorStatus(true);
await this.loadAccounts();
}
} catch (_) {}
};
es.onerror = () => {
if (!disconnectLogged) {
disconnectLogged = true;
this.quotaMonitorStatusText = '进度连接暂时中断,正在自动重连…';
this.quotaMonitorStatusType = 'warn';
}
};
},
async runFeishuSync() {
this.logDrawerOpen = true;
if (this.feishuSyncRunning) {
this.feishuSyncStatusText = '同步正在运行,请等待完成';
this.feishuSyncStatusType = 'warn';
this.addLog('⚠️ 飞书同步正在运行', 'warn');
return;
}
if (this.isRunning) {
const message = '当前有账号任务正在运行,请等待完成后再同步飞书';
this.feishuSyncStatusText = message;
this.feishuSyncStatusType = 'warn';
this.addLog(`⚠️ ${message}`, 'warn');
alert(message);
return;
}
const emails = [...this.selectedEmails];
if (emails.length === 0) {
const message = '请先勾选需要同步到飞书的账号';
this.feishuSyncStatusText = message;
this.feishuSyncStatusType = 'warn';
this.addLog(`⚠️ ${message}`, 'warn');
alert(message);
return;
}
if (!this.feishuCredentialMeta.configured) {
const message = '请先在设置中保存飞书 App ID 和 App Secret';
this.feishuSyncStatusText = message;
this.feishuSyncStatusType = 'warn';
this.addLog(`⚠️ ${message}`, 'warn');
this.showSettingsModal = true;
alert(message);
return;
}
if (!String(this.settings.feishu_document_url || '').trim()) {
const message = '请先在设置中填写飞书电子表格地址';
this.feishuSyncStatusText = message;
this.feishuSyncStatusType = 'warn';
this.addLog(`⚠️ ${message}`, 'warn');
this.showSettingsModal = true;
alert(message);
return;
}
this.clearLogs();
this.logDrawerOpen = true;
this.feishuSyncProgress = { current: 0, total: emails.length };
this.feishuSyncResult = null;
this.feishuSyncStatusText = `正在启动:0/${emails.length}`;
this.feishuSyncStatusType = 'info';
this.addLog(`🚀 开始同步飞书:${emails.length} 个已选账号`, 'info');
try {
// 浏览器只提交邮箱。密码、2FA 和 Session 始终由服务端直接读取,
// 不进入账号列表接口、前端状态或浏览器日志。
const res = await fetch('/api/feishu/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ emails }),
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(data.msg || '启动同步失败');
this.feishuSyncRunning = true;
this.feishuSyncTaskId = data.taskId;
this.feishuSyncStatusText = `同步进行中:0/${emails.length}`;
this.startFeishuSyncListener(data.taskId);
} catch (e) {
this.feishuSyncRunning = false;
this.feishuSyncTaskId = null;
this.feishuSyncStatusText = `启动失败:${e.message}`;
this.feishuSyncStatusType = 'error';
this.addLog(`❌ 飞书同步启动失败:${e.message}`, 'error');
alert(`飞书同步启动失败:${e.message}`);
}
},
startFeishuSyncListener(taskId) {
if (this.feishuEventSource) this.feishuEventSource.close();
const es = new EventSource(`/api/operations/events?taskId=${encodeURIComponent(taskId)}`);
this.feishuEventSource = es;
let disconnectLogged = false;
es.onopen = () => {
disconnectLogged = false;
this.feishuSyncStatusType = 'info';
};
es.onmessage = event => {
try {
const data = JSON.parse(event.data);
if (data.type === 'log') {
this.addLog(String(data.message || ''));
} else if (data.type === 'progress') {
this.feishuSyncProgress = {
current: Number(data.current) || 0,
total: Number(data.total) || 0,
};
this.feishuSyncStatusText = `同步进行中:${this.feishuSyncProgress.current}/${this.feishuSyncProgress.total}`;
} else if (data.type === 'done') {
const result = data.result && typeof data.result === 'object' ? data.result : {};
this.feishuSyncResult = result;
if (this.feishuSyncProgress.total > 0) {
this.feishuSyncProgress.current = this.feishuSyncProgress.total;
}
this.feishuSyncRunning = false;
this.feishuSyncTaskId = null;
es.close();
this.feishuEventSource = null;
const successCount = Number(result.successCount) || 0;
const failedCount = Number(result.failedCount) || 0;
if (result.error && successCount === 0) {
this.feishuSyncStatusText = `同步失败:${result.error}`;
this.feishuSyncStatusType = 'error';
this.addLog(`❌ 飞书同步失败:${result.error}`, 'error');
alert(`飞书同步失败:${result.error}`);
} else {
this.feishuSyncStatusText = `同步完成:成功 ${successCount},失败 ${failedCount}`;
this.feishuSyncStatusType = failedCount > 0 ? 'warn' : 'success';
this.addLog(
`🏁 飞书同步完成:成功 ${successCount} 个,失败 ${failedCount} 个`,
failedCount > 0 ? 'warn' : 'success',
);
alert(`飞书同步完成\n成功:${successCount} 个\n失败:${failedCount} 个`);
}
}
} catch (_) {}
};
es.onerror = () => {
if (this.feishuSyncRunning && !disconnectLogged) {
disconnectLogged = true;
this.feishuSyncStatusText = '进度连接暂时中断,正在自动重连…';
this.feishuSyncStatusType = 'warn';
this.addLog('⚠️ 飞书同步进度连接暂时中断,正在自动重连…', 'warn');
}
};
},
};
}