Files

2680 lines
168 KiB
HTML
Raw Permalink 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.
<!DOCTYPE html>
<html lang="zh-CN" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemini Go - Google账号管理工具</title>
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<link rel="stylesheet" href="/style.css">
</head>
<body class="bg-gray-50 h-full overflow-hidden flex flex-col" x-data="geminiApp()" x-init="init()">
<!-- 顶部栏 -->
<header class="app-header bg-white shadow-sm border-b border-gray-200 sticky top-0 z-50">
<div class="app-header__inner app-page-wide flex items-stretch justify-between">
<!-- 左侧:账号管理 + 客户筛选 -->
<div class="app-header__left flex items-stretch gap-1">
<button @click="activeTab='accounts'" :class="activeTab==='accounts' ? 'active' : ''" class="tab-btn">
账号管理 <span class="text-xs text-gray-400" x-text="'(' + accounts.length + ')'"></span>
</button>
<div class="flex items-center" x-show="activeTab==='accounts'">
<select x-model="customerNameFilter" @change="onCustomerFilterChange()"
style="height:28px;padding:2px 8px;font-size:12px;min-width:90px;cursor:pointer;border:1px solid #d1d5db;border-radius:4px;background:#fff;color:#374151">
<option value="">全部客户</option>
<option value="__empty__">未分配</option>
<template x-for="cn in customerNames" :key="cn">
<option :value="cn" x-text="cn"></option>
</template>
</select>
</div>
</div>
<!-- 右侧:其他选项卡 + 状态信息 -->
<div class="app-header__right flex items-stretch">
<button @click="activeTab='settings'" :class="activeTab==='settings' ? 'active' : ''" class="tab-btn">
设置
</button>
<button @click="activeTab='logs'" :class="activeTab==='logs' ? 'active' : ''" class="tab-btn">
诊断日志
</button>
<div class="header-top-controls flex items-center gap-4 ml-4">
<!-- 比特浏览器连接状态 -->
<div class="flex items-center text-sm">
<span class="status-dot" :class="bitStatus"></span>
<span x-text="bitStatus === 'online' ? '比特浏览器已连接' : (bitStatus === 'checking' ? '检测中...' : '比特浏览器未连接')"
:class="bitStatus === 'online' ? 'text-green-600' : 'text-red-500'"></span>
<button @click="checkBitStatus()"
class="ml-2 text-gray-400 hover:text-gray-600 text-xs cursor-pointer">↻</button>
</div>
<!-- 并发设置 -->
<div class="flex items-center gap-2 text-sm">
<label class="text-gray-500">并发:</label>
<input type="number" x-model="threadCount" @change="saveSetting('default_thread_count', threadCount)"
min="1" max="100" class="w-20 text-center" style="width:70px">
<button @click="toggleLogDrawer()" class="btn btn-gray text-xs" style="padding:4px 10px">日志</button>
</div>
</div>
</div>
</div>
</header>
<main class="app-page-wide pt-3 pb-3 flex-1 overflow-hidden flex flex-col relative">
<!-- ===== 账号管理 ===== -->
<div x-show="activeTab === 'accounts'" class="flex-1 min-h-0 flex flex-col gap-3 overflow-hidden relative">
<!-- 第一行:顶级状态 -->
<div class="stats-grid stats-grid--six" style="margin-bottom:4px">
<div class="stat-card" :class="tierFilter.level1==='全部' ? 'active' : ''" @click="setTierFilter(1,'全部')">
<div class="stat-card__title">全部</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level1['全部'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level1==='未登录' ? 'active' : ''" @click="setTierFilter(1,'未登录')">
<div class="stat-card__title">未登录</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level1['未登录'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level1==='已登录' ? 'active' : ''" @click="setTierFilter(1,'已登录')">
<div class="stat-card__title">已登录</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level1['已登录'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level1==='登录失败' ? 'active' : ''" @click="setTierFilter(1,'登录失败')">
<div class="stat-card__title">登录失败</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level1['登录失败'] || 0"></div>
</div>
<div class="stat-card" :class="statusFilter==='任务中' ? 'active' : ''" @click="setStatusFilter('任务中')">
<div class="stat-card__title">任务中</div>
<div class="stat-card__value" x-text="dashboardStats.statusCounts['任务中'] || 0"></div>
</div>
<div class="stat-card" :class="statusFilter==='刚刚运行' ? 'active' : ''" @click="setStatusFilter('刚刚运行')"
style="border-left:2px solid #3b82f6" :title="recentTaskTooltip">
<div class="stat-card__title" style="color:#2563eb">🕘 刚刚运行</div>
<div class="stat-card__value" x-text="recentTaskCount"></div>
</div>
</div>
<!-- 第二行:业务状态(受第一行过滤) -->
<div class="stats-grid stats-grid--six" style="margin-bottom:4px">
<div class="stat-card" :class="tierFilter.level2==='未违反规则' ? 'active' : ''" @click="setTierFilter(2,'未违反规则')"
style="border-left:2px solid #10b981">
<div class="stat-card__title" style="color:#059669">✅ 未违反规则</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level2['未违反规则'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level2==='违反规则' ? 'active' : ''" @click="setTierFilter(2,'违反规则')"
style="border-left:2px solid #ef4444">
<div class="stat-card__title" style="color:#dc2626">❌ 违反规则</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level2['违反规则'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level2==='有凭证' ? 'active' : ''" @click="setTierFilter(2,'极速凭证') || setTierFilter(2,'有凭证')"
style="border-left:2px solid #7c3aed">
<div class="stat-card__title" style="color:#7c3aed">🔑 有凭证</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level2['有凭证'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level2==='GCP被限制' ? 'active' : ''" @click="setTierFilter(2,'GCP被限制')"
style="border-left:2px solid #ef4444">
<div class="stat-card__title" style="color:#dc2626">🚫 GCP被限制</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level2['GCP被限制'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level2==='OAuth等待7天' ? 'active' : ''" @click="setTierFilter(2,'OAuth等待7天')"
style="border-left:2px solid #f59e0b" title="点击筛选并全选要等7天的账号">
<div class="stat-card__title" style="color:#d97706">⏳ 要等7天</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level2['OAuth等待7天'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level2==='未检测凭证' ? 'active' : ''" @click="setTierFilter(2,'未检测凭证')"
style="border-left:2px solid #9ca3af">
<div class="stat-card__title" style="color:#6b7280">未检测凭证</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level2['未检测凭证'] || 0"></div>
</div>
</div>
<!-- 第三行:操作状态(受第二行过滤) -->
<div class="stats-grid stats-grid--seven" style="margin-bottom:4px">
<div class="stat-card" :class="tierFilter.level3==='未改密' ? 'active' : ''" @click="setTierFilter(3,'未改密')">
<div class="stat-card__title">未改密</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['未改密'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level3==='未改FA' ? 'active' : ''" @click="setTierFilter(3,'未改FA')">
<div class="stat-card__title">未改FA</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['未改FA'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level3==='改密<7天' ? 'active' : ''" @click="setTierFilter(3,'改密<7天')"
style="border-left:2px solid #10b981">
<div class="stat-card__title" style="color:#059669">改密&lt;7天</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['改密<7天'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level3==='改密≥7天' ? 'active' : ''" @click="setTierFilter(3,'改密≥7天')"
style="border-left:2px solid #f59e0b">
<div class="stat-card__title" style="color:#d97706">改密≥7天</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['改密≥7天'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level3==='可重新提取' ? 'active' : ''" @click="setTierFilter(3,'可重新提取')"
style="border-left:2px solid #0ea5e9">
<div class="stat-card__title" style="color:#0284c7">可重新提取</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['可重新提取'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level3==='ChatGPT成功' ? 'active' : ''" @click="setTierFilter(3,'ChatGPT成功')"
style="border-left:2px solid #10b981">
<div class="stat-card__title" style="color:#047857">ChatGPT成功</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['ChatGPT成功'] || 0"></div>
</div>
<div class="stat-card" :class="tierFilter.level3==='ChatGPT失败' ? 'active' : ''" @click="setTierFilter(3,'ChatGPT失败')"
style="border-left:2px solid #ef4444">
<div class="stat-card__title" style="color:#b91c1c">ChatGPT失败</div>
<div class="stat-card__value" x-text="dashboardStats.tierCounts.level3['ChatGPT失败'] || 0"></div>
</div>
</div>
<!-- 工具栏:账号操作 + 搜索 合并一行 -->
<div class="toolbar-row">
<div class="toolbar-stack">
<div class="toolbar-line toolbar-line--accounts">
<div class="btn-group">
<span class="btn-group-label">账号</span>
<button @click="showImportAccountModal=true" class="btn btn-primary">导入</button>
<button @click="exportSelectedAccounts()" class="btn btn-gray">导出</button>
<button @click="openProblemExportModal()" class="btn btn-orange">导出问题号</button>
<button @click="openStagingModal()" class="btn btn-gray">暂存箱</button>
<button @click="loadAccounts()" class="btn btn-gray">刷新</button>
<button @click="deleteSelectedAccounts()" :disabled="isDeletingAccounts || selectedCount===0"
class="btn btn-red" title="删除所选账号、浏览器编号和对应的比特浏览器窗口"
x-text="isDeletingAccounts ? '删账号中...' : '删除账号'"></button>
<button @click="copySelectedEmails()" :disabled="selectedCount===0" class="btn btn-gray">复制邮箱</button>
</div>
<div class="btn-group">
<span class="btn-group-label">搜索</span>
<div class="search-box">
<input type="text" x-model="searchText" @input.debounce.150ms="resetAccountPage()"
placeholder="搜索邮箱/处理结论/状态/客户名..." class="search-input">
</div>
<button @click="openBatchEmailSearch()" class="btn btn-gray">批量邮箱查询</button>
<div style="display:flex;align-items:center;gap:6px;flex-shrink:0;flex-wrap:nowrap">
<div class="toolbar-info">
<span>共 <strong x-text="accounts.length"></strong> 个</span>
<span x-show="selectedCount > 0"> · 已选 <strong x-text="selectedCount"></strong></span>
<span x-show="isRunning && taskProgress.total > 0"> · 进度 <strong x-text="taskProgress.current"></strong>/<strong x-text="taskProgress.total"></strong></span>
</div>
</div>
</div>
</div>
<div class="toolbar-line toolbar-line--operations">
<div class="btn-group">
<span class="btn-group-label">操作</span>
<button @click="openBatchStatusModal()" :disabled="selectedCount===0" class="btn btn-orange">更改状态</button>
<button @click="runDeleteBrowsers()" :disabled="isRunning || selectedCount===0"
class="btn btn-red" title="删除所选账号的比特浏览器窗口并清空浏览器编号,账号保留">删除浏览器</button>
<button @click="runChangeProxy()" :disabled="isRunning" class="btn btn-primary">换代理</button>
<button @click="openBatchCustomerNameModal()"
:disabled="selectedCount===0 || batchCustomerNameSaving" class="btn btn-gray">改写客户名</button>
<button @click="openRefundDeadlineModal()" :disabled="selectedCount===0 || refundDeadlineSaving"
class="btn btn-purple">退款截止时间</button>
</div>
</div>
</div>
</div>
<div x-show="batchEmailSearchActive" x-cloak
class="flex items-center gap-2 flex-wrap rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 mb-2 text-xs text-blue-800">
<strong>批量邮箱查询中</strong>
<span>输入 <b x-text="batchEmailSearchEmails.length"></b> 个</span>
<span>· 找到 <b class="text-green-700" x-text="batchEmailSearchAccounts.length"></b> 个</span>
<span>· 未找到 <b class="text-red-600" x-text="batchEmailSearchMissingEmails.length"></b> 个</span>
<span class="text-blue-500">(可继续叠加普通搜索和组合筛选)</span>
<button @click="openBatchEmailSearch()" class="btn btn-sm btn-outline ml-auto">查看/修改</button>
<button x-show="batchEmailSearchMissingEmails.length > 0" @click="copyBatchEmailSearchMissing()"
class="btn btn-sm btn-outline">复制未找到</button>
<button @click="clearBatchEmailSearch()" class="btn btn-sm btn-red">退出查询</button>
</div>
<div class="account-filter-bar">
<div class="account-filter-bar__header">
<span class="account-filter-bar__title">组合筛选</span>
<button @click="addAccountFilterCondition()" class="btn btn-sm btn-outline">添加条件</button>
<button x-show="accountFilterConditions.length > 0" x-cloak @click="clearAccountFieldFilters()"
class="btn btn-sm btn-outline account-filter-clear">清空</button>
<span class="account-filter-bar__result">结果 <strong x-text="filteredAccounts.length"></strong></span>
</div>
<div class="account-filter-list" x-show="accountFilterConditions.length > 0" x-cloak>
<template x-for="(condition, index) in accountFilterConditions" :key="condition.id">
<div class="account-filter-condition">
<span class="account-filter-condition__index" x-text="index + 1"></span>
<select x-model="condition.field" @change="onAccountFilterConditionChange(condition)" aria-label="筛选字段">
<template x-for="field in accountFilterFields" :key="field.value">
<option :value="field.value" x-text="field.label"></option>
</template>
</select>
<select x-model="condition.operator" @change="onAccountFilterConditionChange(condition)" aria-label="匹配方式">
<option value="contains">包含</option>
<option value="not_contains">不包含</option>
<option value="equals">等于</option>
<option value="not_equals">不等于</option>
<option value="empty">为空</option>
<option value="filled">不为空</option>
</select>
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field === 'status'">
<select x-model="condition.value" @change="onAccountFilterConditionChange(condition)" aria-label="状态值">
<option value="">选择状态</option>
<template x-for="status in accountStatusOptions" :key="status">
<option :value="status" x-text="status"></option>
</template>
</select>
</template>
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field === 'recharge_status'">
<select x-model="condition.value" @change="onAccountFilterConditionChange(condition)" aria-label="是否已充值状态值">
<option value="">选择充值状态</option>
<template x-for="status in rechargeStatusOptions" :key="status">
<option :value="status" x-text="status"></option>
</template>
</select>
</template>
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field === 'account_refund_status'">
<select x-model="condition.value" @change="onAccountFilterConditionChange(condition)" aria-label="账号退款状态值">
<option value="">选择账号退款状态</option>
<template x-for="status in accountRefundStatusOptions" :key="status">
<option :value="status" x-text="status"></option>
</template>
</select>
</template>
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field === 'refund_process_status'">
<select x-model="condition.value" @change="onAccountFilterConditionChange(condition)" aria-label="是否已发起退款流程状态值">
<option value="">选择退款流程状态</option>
<template x-for="status in refundProcessStatusOptions" :key="status">
<option :value="status" x-text="status"></option>
</template>
</select>
</template>
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field === 'refund_deadline_at'">
<select x-model="condition.value" @change="onAccountFilterConditionChange(condition)" aria-label="退款截止时间状态值">
<option value="">选择时间状态</option>
<option value="未设置">未设置</option>
<option value="未到期">未到期</option>
<option value="已到期">已到期</option>
</select>
</template>
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field === 'is_pro'">
<select x-model="condition.value" @change="onAccountFilterConditionChange(condition)" aria-label="Pro 状态值">
<option value="">选择 Pro 状态</option>
<option value="Pro">Pro</option>
<option value="非Pro">非Pro</option>
<option value="未检测">未检测</option>
</select>
</template>
<template x-if="accountFilterNeedsValue(condition.operator) && condition.field !== 'status' && condition.field !== 'recharge_status' && condition.field !== 'account_refund_status' && condition.field !== 'refund_process_status' && condition.field !== 'refund_deadline_at' && condition.field !== 'is_pro'">
<input type="text" x-model="condition.value" @input.debounce.150ms="onAccountFilterConditionChange(condition)"
placeholder="输入筛选值" aria-label="筛选值">
</template>
<span x-show="!accountFilterNeedsValue(condition.operator)" class="account-filter-condition__no-value">无需填写</span>
<button @click="removeAccountFilterCondition(condition.id)" class="account-filter-remove"
title="删除此条件" aria-label="删除此条件">×</button>
</div>
</template>
</div>
</div>
<!-- 批量操作:横向按钮组 -->
<div class="action-row">
<div class="action-line action-line--tasks">
<!-- 账号任务:普通模式 -->
<template x-if="!customMode">
<div class="btn-group">
<span class="btn-group-label">任务</span>
<button @click="runLogin()" :disabled="isRunning" class="btn btn-primary">登录</button>
<button @click="runChatgptRegister()" :disabled="isRunning" class="btn btn-green">ChatGPT登录/注册</button>
<button @click="runOpenAIGetAuth()" :disabled="isRunning" class="btn btn-green">提取授权JSON</button>
<button @click="runOpenAIQueryPlan()" :disabled="isRunning" class="btn btn-green">查询套餐</button>
<button @click="runRefundQuery()" :disabled="isRunning" class="btn btn-purple">查询退款</button>
<button @click="openRechargeModal()" :disabled="isRunning || selectedCount===0" class="btn btn-orange">充值</button>
<button @click="runWorkspaceRefund()" :disabled="isRunning || selectedCount===0" class="btn btn-red">退款</button>
<button @click="runChange2fa()" :disabled="isRunning" class="btn btn-primary">更改2FA</button>
<button @click="runChangePassword()" :disabled="isRunning" class="btn btn-primary">改密码</button>
<button @click="runSecurityChatgpt()" :disabled="isRunning" class="btn btn-purple">改密+2FA</button>
<button @click="runAgGetCredential()" :disabled="isRunning" class="btn btn-purple">获取凭证</button>
<button @click="runAistudioCombo()" :disabled="isRunning" class="btn btn-purple">AI Studio 一键组合</button>
<button @click="runGeminiCliGetCredential()" :disabled="isRunning" class="btn btn-purple">获取Cli凭证</button>
<button @click="openGoogleAccountAgModal()" :disabled="isRunning" class="btn btn-blue">链接提取+获取凭证</button>
<button @click="runAgRevokeAuthorization()" :disabled="isRunning || selectedCount===0" class="btn btn-red">取消授权反重力</button>
<button @click="runCheckRestrictions()" :disabled="isRunning" class="btn btn-green">检查限制</button>
<button @click="runCheckSecurityDates()" :disabled="isRunning" class="btn btn-green">查安全日期</button>
</div>
</template>
<!-- 自定义模式:模板列表 -->
<template x-if="customMode">
<div style="flex:1;min-width:0;display:flex;flex-direction:column;gap:4px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:8px 10px;min-height:60px;max-height:50vh;overflow-y:auto;resize:vertical">
<!-- 已保存的模板列表(每个一行) -->
<template x-for="tpl in savedTemplates" :key="tpl.id">
<div style="display:flex;align-items:center;gap:6px;padding:4px 8px;border-radius:6px;cursor:pointer;transition:all .15s"
:style="selectedTemplateId === tpl.id ? 'background:#dbeafe;border:1px solid #93c5fd' : 'background:#fff;border:1px solid #e5e7eb'"
@click="selectTemplate(tpl)">
<!-- 单选 -->
<input type="radio" name="tpl_select" :checked="selectedTemplateId === tpl.id"
@click.stop="selectTemplate(tpl)"
style="accent-color:#3b82f6;width:14px;height:14px;flex-shrink:0">
<!-- 步骤流 -->
<div style="display:flex;align-items:center;gap:3px;flex:1;min-width:0;overflow-x:auto">
<template x-for="(s, si) in tpl.steps" :key="si">
<span style="display:inline-flex;align-items:center;gap:2px;white-space:nowrap">
<span style="padding:1px 6px;border-radius:4px;font-size:11px;background:#e2e8f0;color:#475569"
x-text="getCustomTaskLabel(s.key || s)"></span>
<span x-show="si < tpl.steps.length - 1" style="color:#cbd5e1;font-size:10px">→</span>
</span>
</template>
</div>
<!-- 模板名 -->
<span style="font-size:12px;font-weight:600;color:#6d28d9;white-space:nowrap;flex-shrink:0;padding:0 4px"
x-text="tpl.name"></span>
<!-- 删除 -->
<button @click.stop="deleteTemplateById(tpl.id)"
style="color:#f87171;font-size:11px;background:none;border:none;cursor:pointer;padding:0 2px;flex-shrink:0"
title="删除模板">✕</button>
</div>
</template>
<!-- 无模板提示 -->
<template x-if="savedTemplates.length === 0">
<div style="text-align:center;padding:8px;color:#94a3b8;font-size:12px">暂无模板,点击下方「新建」创建</div>
</template>
<!-- 新建模板区(折叠) -->
<div style="border-top:1px solid #e2e8f0;margin-top:2px;padding-top:6px">
<div style="display:flex;align-items:center;gap:4px;cursor:pointer" @click="showTemplateCreator = !showTemplateCreator">
<span style="font-size:11px;color:#3b82f6;font-weight:600">+ 新建模板</span>
<span style="font-size:10px;color:#94a3b8" x-text="showTemplateCreator ? '▲' : '▼'"></span>
</div>
<template x-if="showTemplateCreator">
<div style="margin-top:6px;display:flex;flex-direction:column;gap:4px">
<!-- 步骤勾选 -->
<div style="display:flex;align-items:center;gap:3px;flex-wrap:wrap">
<template x-for="td in _customTaskDefs" :key="td.key">
<div style="display:inline-flex;align-items:center;gap:2px">
<label style="display:inline-flex;align-items:center;gap:2px;cursor:pointer;padding:2px 7px;border-radius:5px;font-size:11px;user-select:none;white-space:nowrap"
:style="isCustomTaskChecked(td.key) ? 'background:#dbeafe;color:#1d4ed8;font-weight:600' : 'background:#f1f5f9;color:#64748b'">
<input type="checkbox" :checked="isCustomTaskChecked(td.key)" @change="toggleCustomTask(td.key)"
style="accent-color:#3b82f6;width:12px;height:12px">
<span x-text="td.label"></span>
</label>
<template x-if="td.params && isCustomTaskChecked(td.key)">
<template x-for="p in td.params" :key="p.key">
<label style="display:inline-flex;align-items:center;gap:2px;cursor:pointer;font-size:10px;padding:2px 5px;border-radius:3px;background:#fff7ed;color:#ea580c;user-select:none;white-space:nowrap">
<input type="checkbox" :checked="getStepParamValue(td.key, p.key)"
@change="setStepParamValue(td.key, p.key, $event.target.checked)"
style="accent-color:#f97316;width:10px;height:10px">
<span x-text="p.label"></span>
</label>
</template>
</template>
</div>
</template>
</div>
<!-- 保存行:名称+按钮 -->
<div style="display:flex;align-items:center;gap:4px" x-show="customCheckedTasks.length > 0">
<div style="display:flex;align-items:center;gap:2px;flex:1;min-width:0;overflow-x:auto">
<template x-for="(tk, ti) in customCheckedTasks" :key="tk">
<span style="display:inline-flex;align-items:center;gap:2px;white-space:nowrap">
<span style="padding:1px 5px;border-radius:3px;font-size:10px;background:#dbeafe;color:#1d4ed8"
x-text="getCustomTaskLabel(tk)"></span>
<span x-show="ti < customCheckedTasks.length - 1" style="color:#93c5fd;font-size:9px">→</span>
</span>
</template>
</div>
<input type="text" x-model="templateName" placeholder="模板名称"
style="width:80px;padding:3px 6px;font-size:11px;border-radius:4px;border:1px solid #d1d5db;outline:none;flex-shrink:0">
<button @click="saveAsTemplate()" class="btn btn-orange" style="padding:2px 10px;font-size:11px;flex-shrink:0">保存</button>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
<div class="action-line action-line--open">
<!-- 打开网页 -->
<template x-if="!customMode">
<div class="btn-group">
<span class="btn-group-label">打开</span>
<button @click="runOpenBrowsers()" :disabled="isRunning" class="btn btn-green"
title="打开比特浏览器;人工创建 AI Studio Key 成功后会自动保存到当前邮箱">浏览器</button>
<button class="btn btn-primary" @click="runOpenCustomUrl()">打开网址</button>
<button class="btn btn-green" @click="runFeishuSync()"
:disabled="feishuSyncRunning"
x-text="feishuSyncRunning ? ('同步中 ' + feishuSyncProgress.current + '/' + feishuSyncProgress.total) : '同步飞书'"
title="只同步当前勾选的账号"></button>
<span x-show="feishuSyncStatusText" x-cloak class="text-xs max-w-64 truncate"
:class="feishuSyncStatusType === 'error' ? 'text-red-600' : (feishuSyncStatusType === 'success' ? 'text-green-600' : (feishuSyncStatusType === 'warn' ? 'text-orange-600' : 'text-blue-600'))"
:title="feishuSyncStatusText" x-text="feishuSyncStatusText"></span>
</div>
</template>
<!-- 控制 -->
<div class="btn-group" style="margin-left:auto;flex-shrink:0">
<button @click="toggleCustomMode()" class="btn" :class="customMode ? 'btn-blue' : 'btn-gray'"
:disabled="isRunning || customRunning">
<span x-text="customMode ? '退出' : '自定义'"></span>
</button>
<template x-if="customMode">
<button @click="runSelectedTemplate()"
:disabled="isRunning || customRunning || !selectedTemplateId"
class="btn btn-green">▶ 运行</button>
</template>
<button @click="togglePauseTask()" :disabled="!isRunning || !currentTaskId"
class="btn" :class="isPaused ? 'btn-blue' : 'btn-orange'"
x-text="isPaused ? '继续' : '暂停'"></button>
<button @click="customRunning ? stopCustomPipeline() : stopTask()" :disabled="!isRunning && !customRunning"
class="btn btn-red">停止</button>
<div class="text-xs"
:class="isPaused ? 'text-orange-600' : (isRunning ? 'text-blue-500' : (customRunning ? 'text-purple-500' : 'text-gray-400'))">
<span x-text="isPaused ? '已暂停' : (customRunning ? '运行中...' : (isRunning ? '运行中...' : '空闲'))"></span>
</div>
</div>
</div>
</div>
<div class="task-progress-strip" x-show="isRunning && taskProgress.total > 0" x-cloak>
<div class="task-progress-strip__meta">
<span class="task-progress-strip__task" x-text="activeTaskDisplayLabel"></span>
<span class="task-progress-strip__count">
<strong x-text="taskProgress.current"></strong>/<span x-text="taskProgress.total"></span>
· <span x-text="taskProgressPercent + '%'"></span>
</span>
<span class="task-progress-strip__eta" x-text="taskEtaText"></span>
</div>
<div class="task-progress-strip__track" role="progressbar"
:aria-valuenow="taskProgress.current" :aria-valuemax="taskProgress.total">
<span class="task-progress-strip__fill" :style="'width:' + taskProgressPercent + '%'"></span>
</div>
</div>
<div class="task-progress-strip" x-show="feishuSyncRunning && feishuSyncProgress.total > 0" x-cloak>
<div class="task-progress-strip__meta">
<span class="task-progress-strip__task">同步飞书</span>
<span class="task-progress-strip__count">
<span x-text="feishuSyncProgress.current"></span>/<span x-text="feishuSyncProgress.total"></span>
· <span x-text="Math.round((feishuSyncProgress.current / Math.max(1, feishuSyncProgress.total)) * 100) + '%' "></span>
</span>
<span class="task-progress-strip__eta">实时同步结果请查看处理日志</span>
</div>
<div class="task-progress-strip__track" role="progressbar"
:aria-valuenow="feishuSyncProgress.current" aria-valuemin="0" :aria-valuemax="feishuSyncProgress.total">
<span class="task-progress-strip__fill"
:style="'width:' + Math.round((feishuSyncProgress.current / Math.max(1, feishuSyncProgress.total)) * 100) + '%' "></span>
</div>
</div>
<!-- 账号表格 -->
<div class="table-container bg-white flex-1 min-h-0">
<table>
<thead>
<tr>
<th style="width:32px">
<input type="checkbox" :checked="allSelected" @change="toggleSelectAll()">
</th>
<th>邮箱</th>
<th>客户名</th>
<th>账号退款</th>
<th>是否已发起退款流程</th>
<th>退款截止时间</th>
<th>状态</th>
<th>ChatGPT</th>
<th>是否已充值</th>
<th>授权JSON</th>
<th>AC凭证</th>
<th>Cli_AC凭证</th>
<th>账号套餐</th>
<th>Pro</th>
<th class="hide-mobile">密码</th>
<th class="hide-mobile">2FA验证码</th>
<th class="hide-mobile">16位应用密码</th>
<th class="hide-mobile">AI Studio Key</th>
<th class="hide-mobile">AI Studio 日志</th>
<th class="hide-mobile">辅助邮箱</th>
<th class="hide-mobile">安全日期</th>
<th class="hide-mobile">国家</th>
<th class="hide-mobile">浏览器编号</th>
<th class="hide-mobile">静态IP</th>
<th class="hide-mobile">处理结论</th>
<th class="hide-mobile">截图</th>
<th class="hide-mobile">更新时间</th>
</tr>
</thead>
<tbody>
<template x-for="account in pagedAccounts" :key="account.email">
<tr :class="isSelected(account.email) ? 'selected' : ''" @mousedown="startAccountDrag($event, account)"
@mouseenter="onAccountDragOver($event, account)">
<td @click.stop>
<input type="checkbox" :checked="isSelected(account.email)" @change="toggleSelect(account.email)">
</td>
<td class="copy-cell" :title="account.email" style="max-width:180px"
@click.stop="copyCell(account.email)" @dblclick.stop="openEditAccount(account)">
<span x-show="account.tag==='家庭组母号'" style="color:#f59e0b;margin-right:2px" title="家庭组母号">🏠</span>
<span x-show="account.restriction_status==='ok'" style="color:#059669;margin-right:2px;font-size:11px" title="未违反规则">✅</span>
<span x-show="account.restriction_status==='restricted'" style="color:#dc2626;margin-right:2px;font-size:11px" title="违反规则">⛔</span>
<span x-show="account.ag_refresh_token" style="color:#7c3aed;margin-right:2px;font-size:11px" title="AG凭证已获取">🔑</span>
<span x-show="_isGcpRestricted(account)" style="color:#dc2626;margin-right:2px;font-size:11px" title="GCP被限制">🚫</span>
<span x-show="_isOAuthWait7Account(account)" style="color:#d97706;margin-right:2px;font-size:11px" title="OAuth等待7天后重试">⏳</span>
<span x-text="account.email"></span>
</td>
<td>
<span x-show="account.customer_name" class="inline-block px-2 py-0.5 rounded-full text-xs font-medium"
:class="customerTagClass(account.customer_name)" x-text="account.customer_name"></span>
<span x-show="!account.customer_name" class="text-gray-300 text-xs">-</span>
</td>
<td class="cursor-pointer" @mousedown.stop @click.stop
title="双击可人工修改账号退款状态"
@dblclick.stop="startInlineEditAccountRefund(account.email)">
<template x-if="inlineEditAccountRefundEmail !== account.email">
<span class="status-badge" :class="statusClass(account.account_refund_status || '无状态')"
x-text="account.account_refund_status || '无状态'"></span>
</template>
<template x-if="inlineEditAccountRefundEmail === account.email">
<select class="status-select" style="min-width:132px;height:26px;padding:2px 4px;font-size:12px"
:value="account.account_refund_status || '无状态'"
@change="quickSetAccountRefundStatus(account.email, $event.target.value)"
@keydown.escape.stop="inlineEditAccountRefundEmail=null"
x-init="$nextTick(() => { $el.focus(); try { $el.showPicker() } catch(e) {} })">
<option value="无状态">无状态</option>
<option value="已退款">已退款</option>
<option value="未退款">未退款</option>
<option value="退款失败">退款失败</option>
<option value="已申请退款">已申请退款</option>
<option value="google封号无法查询">google封号无法查询</option>
<option value="gpt封号退款失败">gpt封号退款失败</option>
<option value="gpt警告">gpt警告</option>
<option value="已被改密无法查询">已被改密无法查询</option>
<option value="已被改2FA无法查询">已被改2FA无法查询</option>
<option value="需要手机号查询">需要手机号查询</option>
<option value="无订阅">无订阅</option>
</select>
</template>
</td>
<td class="cursor-pointer" @mousedown.stop @click.stop
title="双击可人工修改退款流程状态"
@dblclick.stop="startInlineEditRefundProcess(account.email)">
<template x-if="inlineEditRefundProcessEmail !== account.email">
<span class="status-badge" :class="statusClass(account.refund_process_status || '未发起')"
x-text="account.refund_process_status || '未发起'"></span>
</template>
<template x-if="inlineEditRefundProcessEmail === account.email">
<select class="status-select" style="min-width:92px;height:26px;padding:2px 4px;font-size:12px"
:value="account.refund_process_status || '未发起'"
@change="quickSetRefundProcessStatus(account.email, $event.target.value)"
@keydown.escape.stop="inlineEditRefundProcessEmail=null"
x-init="$nextTick(() => { $el.focus(); try { $el.showPicker() } catch(e) {} })">
<option value="未发起">未发起</option>
<option value="已发起">已发起</option>
<option value="发起失败">发起失败</option>
</select>
</template>
</td>
<td @mousedown.stop @click.stop>
<button type="button" class="chatgpt-session-trigger"
:class="account.refund_deadline_at && new Date(account.refund_deadline_at).getTime() <= Date.now() ? 'text-red-600' : ''"
:title="account.refund_deadline_at ? '点击修改退款截止时间' : '点击设置退款截止时间'"
@click.stop="openRefundDeadlineModal(account)"
x-text="account.refund_deadline_at ? formatDate(account.refund_deadline_at) : '未设置'"></button>
</td>
<td @dblclick.stop="startInlineEditStatus(account.email)" @mousedown.stop @click.stop>
<template x-if="inlineEditStatusEmail !== account.email">
<span class="status-badge" :class="statusClass(account.status)"
x-text="account.status || '未登录'"></span>
</template>
<template x-if="inlineEditStatusEmail === account.email">
<select class="status-select" style="width:100%;height:26px;padding:2px 4px;font-size:12px"
:value="account.status || '未登录'" @change="quickSetStatus(account.email, $event.target.value)"
@blur="inlineEditStatusEmail=null"
x-init="$nextTick(() => { $el.focus(); try { $el.showPicker() } catch(e) {} })">
<option value="未登录">未登录</option>
<option value="登录成功">登录成功</option>
<option value="登录失败">登录失败</option>
<option value="改密成功">改密成功</option>
<option value="改密失败">改密失败</option>
<option value="2FA修改成功">2FA修改成功</option>
<option value="2FA修改失败">2FA修改失败</option>
<option value="未验证已改密和2FA">未验证已改密和2FA</option>
<option value="已验证改密和2FA">已验证改密和2FA</option>
<option value="未注册成功已改密和2FA">未注册成功已改密和2FA</option>
<option value="ChatGPT成功">ChatGPT成功</option>
<option value="ChatGPT失败">ChatGPT失败</option>
<option value="有资格">有资格</option>
<option value="无资格">无资格</option>
<option value="认证成功">认证成功</option>
<option value="认证失败">认证失败</option>
<option value="绑卡成功">绑卡成功</option>
<option value="绑卡失败">绑卡失败</option>
<option value="订阅成功">订阅成功</option>
<option value="已充值提取">已充值提取</option>
<option value="超时">超时</option>
</select>
</template>
</td>
<td>
<div class="chatgpt-status-cell">
<span x-show="account.chatgpt_status" class="status-badge"
:class="statusClass(account.chatgpt_status)" x-text="account.chatgpt_status"></span>
<button x-show="account.chatgpt_status === '已登录' || account.chatgpt_status === '已注册'"
type="button" class="chatgpt-session-trigger" title="查看 ChatGPT Session"
@click.stop="openChatgptSession(account)">查看</button>
</div>
<span x-show="!account.chatgpt_status" class="text-gray-300 text-xs">-</span>
</td>
<td class="cursor-pointer" @mousedown.stop @click.stop
:title="(account.recharge_message ? account.recharge_message + ';' : '') + '双击可人工修改充值状态'"
@dblclick.stop="startInlineEditRechargeStatus(account.email)">
<template x-if="inlineEditRechargeStatusEmail !== account.email">
<div class="chatgpt-status-cell">
<span class="status-badge" :class="statusClass(account.recharge_status || '未充值')"
x-text="account.recharge_status || '未充值'"></span>
<button x-show="account.has_recharge_card" type="button"
class="chatgpt-session-trigger" title="查看本次充值使用的卡密"
@click.stop="openRechargeCard(account)" @dblclick.stop>查看卡密</button>
</div>
</template>
<template x-if="inlineEditRechargeStatusEmail === account.email">
<div class="chatgpt-status-cell">
<select class="status-select" style="min-width:104px;height:26px;padding:2px 4px;font-size:12px"
:value="account.recharge_status || '未充值'"
@change="quickSetRechargeStatus(account.email, $event.target.value)"
@blur="inlineEditRechargeStatusEmail=null"
x-init="$nextTick(() => { $el.focus(); try { $el.showPicker() } catch(e) {} })">
<option value="未充值">未充值</option>
<option value="充值中">充值中</option>
<option value="等待已确认">等待已确认</option>
<option value="充值成功">充值成功</option>
<option value="充值失败">充值失败</option>
<option value="账号本身已有会员">账号本身已有会员</option>
</select>
<button x-show="account.has_recharge_card" type="button"
class="chatgpt-session-trigger" title="查看本次充值使用的卡密"
@click.stop="openRechargeCard(account)" @dblclick.stop>查看卡密</button>
</div>
</template>
</td>
<td>
<div class="chatgpt-status-cell">
<span x-show="account.openai_auth_status || account.has_openai_auth" class="status-badge"
:class="statusClass(account.openai_auth_status || '已提取 JSON')"
x-text="account.openai_auth_status || '已提取 JSON'"></span>
<button x-show="account.has_openai_auth" type="button" class="chatgpt-session-trigger"
title="查看授权 JSON" @click.stop="openOpenAIAuth(account)">查看</button>
</div>
<span x-show="!account.openai_auth_status && !account.has_openai_auth"
class="text-gray-300 text-xs">-</span>
</td>
<td>
<div class="chatgpt-status-cell">
<span x-show="account.has_ag_credential || account.ag_refresh_token"
class="status-badge" :class="statusClass('凭证已获取')">已获取</span>
<button x-show="account.has_ag_credential || account.ag_refresh_token" type="button"
class="chatgpt-session-trigger" title="查看 AC 凭证"
@click.stop="openAgCredential(account)">查看</button>
</div>
<span x-show="!account.has_ag_credential && !account.ag_refresh_token"
class="text-gray-300 text-xs">-</span>
</td>
<td>
<div class="chatgpt-status-cell">
<span x-show="account.has_cli_ac_credential"
class="status-badge" :class="statusClass('凭证已获取')">已获取</span>
<button x-show="account.has_cli_ac_credential" type="button"
class="chatgpt-session-trigger" title="查看 Gemini CLI AC 凭证"
@click.stop="openCliAcCredential(account)">查看</button>
</div>
<span x-show="!account.has_cli_ac_credential" class="text-gray-300 text-xs">-</span>
</td>
<td style="min-width:132px;white-space:nowrap" :title="formatOpenAIPlanTitle(account)">
<div x-show="account.openai_plan_type || account.openai_plan_status" style="font-size:10px;line-height:1.45">
<span class="status-badge"
:class="statusClass(account.openai_plan_status || '查询成功')"
x-text="formatOpenAIPlanCell(account)"></span>
<div x-show="formatOpenAIUsagePercent(account.openai_usage_percent)">
用量:<span x-text="formatOpenAIUsagePercent(account.openai_usage_percent)"></span>
</div>
<div x-show="account.openai_subscription_expires_at">
到期:<span x-text="formatDate(account.openai_subscription_expires_at)"></span>
</div>
<div x-show="account.openai_plan_type
&& account.openai_plan_status
&& !['查询成功', '部分成功'].includes(account.openai_plan_status)"
:class="account.openai_plan_status === '查询失败' ? 'text-red-500' : 'text-gray-500'"
x-text="account.openai_plan_status"></div>
</div>
<span x-show="!account.openai_plan_type && !account.openai_plan_status"
class="text-gray-300 text-xs">-</span>
</td>
<td style="white-space:nowrap">
<template x-if="account.is_pro === true || Number(account.is_pro) === 1">
<span class="status-badge" style="background:#dcfce7;color:#166534">Pro</span>
</template>
<template x-if="account.is_pro === 0 || account.is_pro === false">
<span class="text-gray-400 text-xs">非Pro</span>
</template>
<template x-if="account.is_pro === null || account.is_pro === undefined">
<span class="text-gray-300 text-xs">-</span>
</template>
</td>
<td class="hide-mobile copy-cell" :title="account.password"
x-text="account.password ? account.password.slice(0,5) + '...' : '-'"
@click.stop="copyCell(account.password)" @dblclick.stop="openEditAccount(account)"></td>
<td class="hide-mobile" style="white-space:nowrap" @dblclick.stop="openEditAccount(account)">
<template x-if="account.secret_key">
<span>
<span class="copy-cell" :title="'剩余 ' + totpRemaining + 's'"
x-text="totpCodes[account.email] || '------'"
@click.stop="totpCodes[account.email] && copyCell(totpCodes[account.email])"></span>
<span>&nbsp;&nbsp;</span>
<span class="copy-cell" :title="account.secret_key" x-text="account.secret_key.slice(0,6) + '...'"
@click.stop="copyCell(account.secret_key)"></span>
</span>
</template>
<template x-if="!account.secret_key"><span>-</span></template>
</td>
<td class="hide-mobile copy-cell font-mono" style="white-space:nowrap"
:title="account.gmail_app_password ? '单击复制16位应用专用密码' : ''"
x-text="account.gmail_app_password || '-'"
@click.stop="account.gmail_app_password && copyCell(account.gmail_app_password)"></td>
<td class="hide-mobile" style="white-space:nowrap" @mousedown.stop @click.stop>
<div class="chatgpt-status-cell">
<span x-show="account.aistudio_api_key" class="status-badge"
:class="statusClass('成功')">已保存</span>
<span x-show="!account.aistudio_api_key" class="text-gray-300 text-xs">未保存</span>
<button x-show="account.aistudio_api_key" type="button"
class="chatgpt-session-trigger" title="查看该邮箱的 AI Studio API Key JSON"
@click.stop="openAistudioKeyModal(account)">查看</button>
</div>
</td>
<td class="hide-mobile" style="font-size:11px;line-height:1.4;white-space:nowrap;max-width:180px"
:title="formatAistudioFlowTitle(account)" @dblclick.stop="openEditAccount(account)">
<template x-if="account.aistudio_flow_status">
<div>
<div :style="account.aistudio_flow_status === 'success' ? 'color:#16a34a;font-weight:600' : 'color:#dc2626;font-weight:600'"
x-text="account.aistudio_flow_status === 'success' ? '✅ 成功' : ('❌ 失败:' + (account.aistudio_flow_failed_step || '未知步骤'))"></div>
<div class="text-gray-400" style="overflow:hidden;text-overflow:ellipsis"
x-text="account.aistudio_flow_status === 'success' ? formatDate(account.aistudio_flow_at) : (account.aistudio_flow_detail || '')"></div>
</div>
</template>
<template x-if="!account.aistudio_flow_status"><span class="text-gray-300">-</span></template>
</td>
<td class="hide-mobile copy-cell" :title="account.recovery_email"
x-text="account.recovery_email ? account.recovery_email.slice(0,5) + '...' : '-'"
@click.stop="copyCell(account.recovery_email)" @dblclick.stop="openEditAccount(account)"></td>
<td class="hide-mobile" style="font-size:11px;line-height:1.5;white-space:nowrap;min-width:110px"
:title="formatSecurityDatesTitle(account.security_dates)">
<template x-if="account.security_dates">
<div>
<div>2步:<span x-text="parseSecurityDate(account.security_dates, 'twoStep')"></span></div>
<div>改密:<span x-text="parseSecurityDate(account.security_dates, 'password')"></span></div>
<div>验证器:<span x-text="parseSecurityDate(account.security_dates, 'authenticator')"></span></div>
</div>
</template>
<template x-if="!account.security_dates"><span>-</span></template>
</td>
<td class="hide-mobile" x-text="account.country || '-'" @dblclick.stop="openEditAccount(account)"></td>
<td class="hide-mobile" :class="getBrowserSeq(account.browser_id, account.browser_seq) ? 'copy-cell' : ''"
:title="getBrowserSeq(account.browser_id, account.browser_seq) ? 'ID: ' + account.browser_id : ''"
x-text="getBrowserSeq(account.browser_id, account.browser_seq) || '-'"
@click.stop="getBrowserSeq(account.browser_id, account.browser_seq) && copyCell(getBrowserSeq(account.browser_id, account.browser_seq))"
@dblclick.stop="openEditAccount(account)"></td>
<td class="hide-mobile font-mono" style="font-size:10px;max-width:120px;overflow:hidden;text-overflow:ellipsis"
:class="account.proxy_ip ? 'copy-cell' : ''"
:title="account.proxy_ip || ''"
x-text="account.proxy_ip ? account.proxy_ip.split(':').slice(0,2).join(':') : '-'"
@click.stop="account.proxy_ip && copyCell(account.proxy_ip)"></td>
<td class="hide-mobile" :title="getLiveMessage(account)" :class="getLiveMessageClass(account)"
x-text="getLiveMessage(account).slice(0,35)" @dblclick.stop="openEditAccount(account)"></td>
<td class="hide-mobile" style="position:relative">
<span x-show="account.screenshot_path"
class="text-blue-500 hover:text-blue-700 text-xs whitespace-nowrap cursor-pointer"
@click.stop="viewScreenshot(account)"
@mouseenter="screenshotHover = '/' + account.screenshot_path + '?t=' + Date.now(); $nextTick(() => { const r=$event.target.getBoundingClientRect(); const el=document.getElementById('screenshotPreview'); if(el){el.style.top=(r.top-320)+'px'; el.style.left=Math.min(r.left, window.innerWidth-420)+'px';} })"
@mouseleave="screenshotHover = null"
>查看截图</span>
<span x-show="!account.screenshot_path" class="text-gray-300 text-xs">-</span>
</td>
<td class="hide-mobile" x-text="formatDate(account.updated_at)"></td>
</tr>
</template>
<tr x-show="filteredAccounts.length === 0">
<td colspan="27" class="text-center text-gray-400 py-8"
x-text="accounts.length === 0 ? '暂无账号,点击导入账号开始' : '没有符合当前筛选条件的账号'"></td>
</tr>
</tbody>
</table>
</div>
<div class="account-pagination" x-show="filteredAccounts.length > 0">
<div style="display:flex;align-items:center;gap:6px;min-width:0">
<span class="account-pagination__summary"
x-text="'本页 ' + pagedAccounts.length + ' 个 · 筛选结果 ' + filteredAccounts.length + ' 个'"></span>
<button class="btn btn-gray text-xs" style="padding:3px 9px" @click="selectCurrentPage()"
:disabled="pagedAccounts.length===0">选中当前页</button>
<button class="btn btn-gray text-xs" style="padding:3px 9px" @click="deselectCurrentPage()"
:disabled="pagedAccounts.length===0 || !pagedAccounts.some(account => isSelected(account.email))">取消当前页选择</button>
</div>
<div class="account-pagination__controls">
<button class="page-icon-btn" title="第一页" @click="goToAccountPage(1)"
:disabled="accountCurrentPage <= 1">«</button>
<button class="page-icon-btn" title="上一页" @click="goToAccountPage(accountCurrentPage - 1)"
:disabled="accountCurrentPage <= 1">‹</button>
<span class="account-pagination__page" x-text="accountCurrentPage + ' / ' + accountTotalPages"></span>
<button class="page-icon-btn" title="下一页" @click="goToAccountPage(accountCurrentPage + 1)"
:disabled="accountCurrentPage >= accountTotalPages">›</button>
<button class="page-icon-btn" title="最后一页" @click="goToAccountPage(accountTotalPages)"
:disabled="accountCurrentPage >= accountTotalPages">»</button>
<select class="account-page-size" :value="accountPageSize" @change="setAccountPageSize($event.target.value)"
title="每页账号数">
<option value="50">50 / 页</option>
<option value="100">100 / 页</option>
<option value="200">200 / 页</option>
</select>
</div>
</div>
</div>
<!-- 右侧日志抽屉 -->
<div class="log-drawer" :class="{ 'open': logDrawerOpen }">
<div class="log-drawer-header">
<div class="flex items-center gap-2">
<span class="font-semibold text-gray-700 text-sm">实时日志</span>
<span class="text-xs text-gray-400" x-text="logs.length + ' 条'"></span>
</div>
<div class="flex gap-2">
<button @click="clearLogs()" class="btn btn-gray btn-sm">清空</button>
<button @click="toggleLogDrawer()" class="btn btn-gray btn-sm">收起</button>
</div>
</div>
<div class="log-drawer-body">
<template x-if="logDrawerOpen">
<div id="log-container">
<template x-for="(line, index) in logs" :key="index">
<div class="log-line" :class="logClass(line.type)" x-text="line.message"></div>
</template>
<div x-show="logs.length === 0" class="text-gray-500 text-center py-4"
style="font-family:inherit;font-size:12px">日志将在这里显示...</div>
</div>
</template>
</div>
</div>
<!-- ===== 设置 ===== -->
<div x-show="activeTab === 'settings'" class="flex-1 overflow-y-auto">
<!-- 基础设置 + Cliproxy 并排 -->
<div class="settings-2col">
<!-- 基础设置 -->
<div class="bg-white rounded-lg border border-gray-200 p-6">
<h3 class="font-semibold text-gray-700 mb-4">基础设置</h3>
<div class="space-y-4">
<!-- 比特浏览器设置 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">比特浏览器端口</label>
<div class="flex gap-2">
<input type="number" :value="settings.bit_browser_port || '54345'"
@change="saveSetting('bit_browser_port', $event.target.value)" placeholder="54345" class="flex-1">
<button @click="checkBitStatus()" class="btn btn-gray">测试连接</button>
</div>
<div class="mt-1 flex items-center gap-2 text-xs">
<span class="status-dot" :class="bitStatus"></span>
<span x-text="bitStatus === 'online' ? '已连接' : (bitStatus === 'checking' ? '检测中...' : '未连接')"></span>
</div>
</div>
<!-- 任务完成后关闭浏览器 -->
<div>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" :checked="settings.close_browser_after_task !== '0'"
@change="saveSetting('close_browser_after_task', $event.target.checked ? '1' : '0')">
<span class="text-sm text-gray-700">任务完成后关闭浏览器窗口</span>
</label>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">ChatGPT 注册年龄</label>
<input type="number" min="18" max="99" :value="settings.chatgpt_age || '23'"
@change="saveSetting('chatgpt_age', Math.max(18, Math.min(99, parseInt($event.target.value || '23', 10) || 23)))">
<p class="text-xs text-gray-400 mt-1">Age 页面默认填写 23;Birthday 页面固定将年份改为 2000。</p>
</div>
<!-- 业务旁监听(开发调试) -->
<div>
<label class="flex items-center gap-2 cursor-pointer" title="勾选后,登录/绑卡等业务任务会自动写入旁监听日志">
<input type="checkbox" x-model="monitorEnabled" @change="onMonitorToggle()">
<span class="text-sm text-gray-700">业务旁监听</span>
</label>
<p class="text-xs text-gray-400 mt-1">开发调试用,勾选后业务任务会自动写入旁监听日志</p>
</div>
<!-- 本地 VPN 代理地址(测速中转) -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">本地VPN代理地址(测速中转,可选)</label>
<input type="text" :value="settings.upstream_proxy || ''"
@change="saveSetting('upstream_proxy', $event.target.value)" placeholder="http://127.0.0.1:7890">
<p class="text-xs text-gray-400 mt-1">填写后测速通过本地 VPN 全局代理中转,无需开启 TUN 模式。留空则直连。</p>
</div>
<!-- IP 欺诈度检查 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">IP 欺诈度检查</label>
<div class="flex items-center gap-4">
<label class="inline-flex items-center gap-2">
<input type="checkbox" :checked="settings.fraud_check_enabled === '1'"
@change="saveSetting('fraud_check_enabled', $event.target.checked ? '1' : '0')">
<span class="text-sm">启用欺诈度检查</span>
</label>
<div class="flex items-center gap-1">
<span class="text-sm text-gray-500">阈值:</span>
<input type="number" :value="settings.fraud_score_threshold || '15'"
@change="saveSetting('fraud_score_threshold', $event.target.value)" min="1" max="100" class="w-20">
</div>
</div>
<p class="text-xs text-gray-400 mt-1">启用后初始化时通过 scamalytics.com 查询出口 IP 欺诈度分数,低于阈值才合格。需要 Python + cloudscraper。</p>
</div>
<!-- 英国资料卡片池 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">英国资料卡片池</label>
<textarea :value="settings.uk_profile_cards || ''"
@change="saveSetting('uk_profile_cards', $event.target.value)"
placeholder="每行一张卡,格式: 卡号 月/年 CVV&#10;5481087158441166 02/32 973&#10;5481087188681666 02/32 435"
rows="4" class="w-full text-xs font-mono"></textarea>
<p class="text-xs text-gray-400 mt-1">「卡英国资料」专用卡片池,每行一张,格式: 卡号 月/年 CVV。运行时随机取一张。</p>
</div>
<!-- 打开限速 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">打开限速(每秒最大打开/创建窗口数)</label>
<input type="number" :value="settings.ramp_up_rate || '9'"
@change="saveSetting('ramp_up_rate', $event.target.value)" min="1" max="50" class="w-32">
<p class="text-xs text-gray-400 mt-1">控制创建窗口、打开窗口、换代理的 API 调用频率。设100线程时建议9/s,12秒可安全启动全部。</p>
</div>
<!-- 人脸验证视频路径 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">人脸验证视频路径</label>
<div class="flex gap-2">
<input type="text" :value="settings.face_video_path || ''"
@change="saveSetting('face_video_path', $event.target.value)"
placeholder="E:\videos\face.mp4" class="flex-1">
<label class="btn btn-gray cursor-pointer" style="display:inline-flex;align-items:center;gap:4px;">
选择
<input type="file" accept="video/*" style="display:none"
@change="if($event.target.files[0]){const p=$event.target.files[0].name; saveSetting('face_video_path', $event.target.files[0].path || p); $event.target.value=''}">
</label>
</div>
<p class="text-xs text-gray-400 mt-1">人脸验证使用的预录制视频文件绝对路径(.mp4/.webm/.mov),留空则不启用。</p>
</div>
</div>
<div class="mt-6 pt-4 border-t border-gray-100">
<h3 class="font-semibold text-gray-700 mb-2">数据库</h3>
<p class="text-sm text-gray-500">
数据库路径: <code class="bg-gray-100 px-1 rounded text-xs">data/accounts.db</code>
</p>
</div>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-6">
<h3 class="font-semibold text-gray-700 mb-4">代理配置</h3>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">代理系统</label>
<select x-model="settings.proxy_provider" @change="saveSetting('proxy_provider', settings.proxy_provider)"
class="w-full">
<option value="cliproxy">Cliproxy</option>
<option value="nexip">Nexip</option>
<option value="cliapi">Cliapi (API取IP)</option>
<option value="nodemaven">nodemaven</option>
<option value="dataimpulse">dataimpulse</option>
</select>
</div>
<!-- 代理预设 -->
<div class="border border-dashed border-gray-300 rounded-lg p-3 bg-gray-50">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-600">⚡ 代理预设</span>
</div>
<!-- 保存当前配置 -->
<div class="flex gap-2 mb-2">
<input type="text" x-model="presetName" placeholder="输入预设名称..."
class="flex-1 text-sm" @keydown.enter="saveProxyPreset()">
<button @click="saveProxyPreset()" class="btn btn-gray flex-shrink-0 text-sm px-3 py-1">
💾 保存当前
</button>
</div>
<!-- 预设列表 -->
<template x-if="proxyPresets.length > 0">
<div class="space-y-1 max-h-40 overflow-y-auto">
<template x-for="(preset, idx) in proxyPresets" :key="idx">
<div class="flex items-center gap-2 bg-white rounded px-2 py-1.5 border border-gray-200 text-sm">
<span class="flex-1 truncate cursor-pointer hover:text-blue-600" @click="loadProxyPreset(preset)"
:title="'点击加载: ' + preset.name">
<span class="font-medium" x-text="preset.name"></span>
<span class="text-gray-400 text-xs ml-1" x-text="'(' + _getPresetProviderLabel(preset.config) + ' · ' + _getPresetSummary(preset.config) + ')'"></span>
</span>
<button @click="loadProxyPreset(preset)" class="text-blue-500 hover:text-blue-700 text-xs flex-shrink-0">加载</button>
<button @click="deleteProxyPreset(idx)" class="text-red-400 hover:text-red-600 text-xs flex-shrink-0">删除</button>
</div>
</template>
</div>
</template>
<template x-if="proxyPresets.length === 0">
<p class="text-xs text-gray-400 text-center py-1">暂无预设,填好配置后点「保存当前」</p>
</template>
</div>
<div x-show="settings.proxy_provider === 'nodemaven'">
<label class="block text-sm font-medium text-gray-700 mb-1">一键解析</label>
<div class="flex gap-2">
<input type="text" x-model="nodemavenFullStr" placeholder="粘贴完整代理字符串..." class="flex-1">
<button @click="parseNodemavenFull()" class="btn btn-gray flex-shrink-0">解析</button>
</div>
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">nodemaven 代理</label>
<input type="text" x-model="settings.nodemaven_proxy"
@change="saveSetting('nodemaven_proxy', settings.nodemaven_proxy)"
placeholder="host:port:username:password">
<p class="text-xs text-gray-400 mt-1">格式:host:port:username:password</p>
</div>
<div x-show="settings.proxy_provider === 'cliapi'">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">API 地址</label>
<input type="text" x-model="settings.cliapi_url"
@change="saveSetting('cliapi_url', settings.cliapi_url)"
placeholder="https://api.cliproxy.io/white/api">
<p class="text-xs text-gray-400 mt-1">请求后返回 IP:PORT 格式</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">地区</label>
<input type="text" x-model="settings.cliapi_region"
@change="saveSetting('cliapi_region', settings.cliapi_region)" placeholder="US">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">数量 (num)</label>
<input type="number" x-model="settings.cliapi_num"
@change="saveSetting('cliapi_num', settings.cliapi_num)" min="1" max="100"
class="w-32" placeholder="1">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">时长 (分钟)</label>
<input type="number" x-model="settings.cliapi_time"
@change="saveSetting('cliapi_time', settings.cliapi_time)" min="1" max="1440"
class="w-32" placeholder="30">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">请求预览</label>
<div
class="bg-gray-50 border border-gray-200 rounded p-2 text-xs font-mono break-all text-gray-700 select-all"
x-text="cliApiPreview || '(填写上方字段后自动生成)'">
</div>
</div>
</div>
<div x-show="settings.proxy_provider === 'dataimpulse'">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">主机</label>
<input type="text" value="gw.dataimpulse.com" disabled
class="w-full bg-gray-100 text-gray-500 cursor-not-allowed">
<p class="text-xs text-gray-400 mt-1">端口 10000-19999 随机分配</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">用户名</label>
<input type="text" x-model="settings.dataimpulse_username"
@change="saveSetting('dataimpulse_username', settings.dataimpulse_username)" placeholder="dataimpulse 用户名">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">密码</label>
<input type="text" x-model="settings.dataimpulse_password"
@change="saveSetting('dataimpulse_password', settings.dataimpulse_password)" placeholder="dataimpulse 密码">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1 mt-3">代理预览</label>
<div
class="bg-gray-50 border border-gray-200 rounded p-2 text-xs font-mono break-all text-gray-700 select-all"
x-text="dataimpulsePreview || '(填写上方字段后自动生成)'">
</div>
</div>
</div>
<div x-show="['cliproxy', 'nexip'].includes(settings.proxy_provider || 'cliproxy')">
<div>
<div class="flex items-center justify-between gap-3 mb-1">
<label class="block text-sm font-medium text-gray-700"
x-text="settings.proxy_provider === 'nexip' ? 'Nexip 代理模板' : 'Cliproxy 代理模板'"></label>
<span x-show="settings.proxy_provider === 'nexip'" class="text-xs text-gray-500">每个账号最多测速 10 次</span>
</div>
<textarea x-model="settings.proxy_template"
@change="saveSetting('proxy_template', settings.proxy_template)"
:placeholder="settings.proxy_provider === 'nexip' ? 'us.nexip.cc:3010@USER-us-sid-{sid}-ttl-60:password' : 'hk.rotgb.711proxy.com:10000:USER-session-{sid}-sessTime-5:password'"
rows="2" class="w-full text-sm font-mono"></textarea>
<p class="text-xs text-gray-400 mt-1">
用 <code class="bg-gray-100 px-1 rounded">{sid}</code> 标记随机会话ID的位置,每次生成代理时自动替换;Nexip 支持 <code class="bg-gray-100 px-1 rounded">host:port@username:password</code>。
</p>
</div>
<div class="mt-3">
<label class="block text-sm font-medium text-gray-700 mb-2">SID 随机格式</label>
<div class="flex items-center gap-4 flex-wrap">
<label class="flex items-center gap-1.5 text-sm cursor-pointer">
<input type="checkbox"
:checked="settings.sid_use_letters !== '0' && settings.sid_use_letters !== false"
@change="settings.sid_use_letters = $event.target.checked ? '1' : '0'; saveSetting('sid_use_letters', settings.sid_use_letters)"
x-init="if (settings.sid_use_letters === undefined || settings.sid_use_letters === '') settings.sid_use_letters = true">
<span>字母 (a-z)</span>
</label>
<label class="flex items-center gap-1.5 text-sm cursor-pointer">
<input type="checkbox"
:checked="settings.sid_use_numbers !== '0' && settings.sid_use_numbers !== false"
@change="settings.sid_use_numbers = $event.target.checked ? '1' : '0'; saveSetting('sid_use_numbers', settings.sid_use_numbers)"
x-init="if (settings.sid_use_numbers === undefined || settings.sid_use_numbers === '') settings.sid_use_numbers = true">
<span>数字 (0-9)</span>
</label>
<div class="flex items-center gap-1.5">
<span class="text-sm">位数</span>
<input type="number" x-model="settings.sid_length"
@change="saveSetting('sid_length', settings.sid_length)"
min="4" max="32" class="w-16 text-sm text-center"
x-init="if (!settings.sid_length) settings.sid_length = '8'">
</div>
</div>
</div>
<div x-show="settings.proxy_provider === 'nexip'"
class="mt-3 border-l-2 border-blue-400 bg-blue-50 px-3 py-2.5">
<div class="flex items-center gap-4 flex-wrap">
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" :checked="settings.nexip_sid_reuse_enabled === '1'"
@change="settings.nexip_sid_reuse_enabled = $event.target.checked ? '1' : '0'; saveSetting('nexip_sid_reuse_enabled', settings.nexip_sid_reuse_enabled)">
<span class="font-medium text-gray-700">复用已验证 SID</span>
</label>
<label class="flex items-center gap-2 text-sm text-gray-600">
<span class="whitespace-nowrap">分配账号数</span>
<input type="number" :value="settings.nexip_sid_reuse_count || '10'"
:disabled="settings.nexip_sid_reuse_enabled !== '1'"
@change="settings.nexip_sid_reuse_count = String(Math.max(1, Math.min(100, parseInt($event.target.value || '10', 10) || 10))); $event.target.value = settings.nexip_sid_reuse_count; saveSetting('nexip_sid_reuse_count', settings.nexip_sid_reuse_count)"
min="1" max="100" class="w-20 text-sm text-center disabled:bg-gray-100 disabled:text-gray-400">
</label>
</div>
<p class="text-xs text-blue-700 mt-1.5">一个 SID 测速合格后,在本次批量任务内最多分配给指定数量的账号;首次测速账号包含在内。</p>
</div>
<div class="mt-3">
<label class="block text-sm font-medium text-gray-700 mb-1">代理预览</label>
<div
class="bg-gray-50 border border-gray-200 rounded p-2 text-xs font-mono break-all text-gray-700 select-all"
x-text="templateProxyPreview || '(填写模板后自动生成预览)'">
</div>
<button @click="refreshTemplatePreview()" class="text-xs text-blue-500 hover:text-blue-700 mt-1">🔄 刷新预览</button>
</div>
</div>
</div>
</div>
</div>
<!-- 外部充值 API(独立加密存储,不进入通用 settings) -->
<div class="bg-white rounded-lg border border-gray-200 p-6 mt-4">
<div class="flex items-center justify-between gap-3 mb-4">
<div>
<h3 class="font-semibold text-gray-700">外部充值 API</h3>
<p class="text-xs text-gray-400 mt-1">用于卡付充值;凭据只保存在本机并由 Windows 当前用户加密保护。</p>
</div>
<span class="status-badge"
:class="rechargeCredentialMeta.configured ? 'status-登录成功' : 'status-未登录'"
x-text="rechargeCredentialMeta.configured ? '已配置' : '未配置'"></span>
</div>
<div x-show="rechargeCredentialMeta.configured" class="text-xs text-gray-500 mb-3">
当前 Key ID:<code class="bg-gray-100 px-1.5 py-0.5 rounded" x-text="rechargeCredentialMeta.keyIdMasked"></code>
<span x-show="rechargeCredentialMeta.updatedAt" class="ml-2"
x-text="'更新于 ' + formatDate(rechargeCredentialMeta.updatedAt)"></span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">API Key ID</label>
<input type="text" x-model="rechargeCredentialForm.keyId" autocomplete="off"
placeholder="填写 API Key ID" class="w-full">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">API Secret</label>
<input type="password" x-model="rechargeCredentialForm.secret" autocomplete="new-password"
placeholder="填写 API Secret" class="w-full" @keydown.enter.prevent="saveRechargeCredentials()">
</div>
</div>
<p class="text-xs text-gray-400 mt-2">保存或更新时需要同时填写两项;Secret 保存后不会再次显示或返回浏览器。</p>
<div class="flex gap-2 mt-3">
<button @click="saveRechargeCredentials()" :disabled="rechargeCredentialSaving"
class="btn btn-primary"
x-text="rechargeCredentialSaving ? '保存中...' : (rechargeCredentialMeta.configured ? '更新凭据' : '保存凭据')"></button>
<button x-show="rechargeCredentialMeta.configured" @click="deleteRechargeCredentials()"
:disabled="rechargeCredentialDeleting" class="btn btn-red"
x-text="rechargeCredentialDeleting ? '清除中...' : '清除凭据'"></button>
</div>
</div>
<!-- Google 账号取号 API(独立加密存储,不进入通用 settings) -->
<div class="bg-white rounded-lg border border-gray-200 p-6 mt-4">
<div class="flex items-center justify-between gap-3 mb-4">
<div>
<h3 class="font-semibold text-gray-700">Google 账号取号 API</h3>
<p class="text-xs text-gray-400 mt-1">用于“链接提取+获取凭证”;API Key 只保存在本机,并由 Windows 当前用户加密保护。</p>
</div>
<span class="status-badge"
:class="googleAccountCredentialMeta.configured ? 'status-登录成功' : 'status-未登录'"
x-text="googleAccountCredentialMeta.configured ? '已配置' : '未配置'"></span>
</div>
<div x-show="googleAccountCredentialMeta.configured" class="text-xs text-gray-500 mb-3">
当前 Key:<code class="bg-gray-100 px-1.5 py-0.5 rounded" x-text="googleAccountCredentialMeta.keyMasked"></code>
<span class="ml-2" x-text="'默认存活 ' + googleAccountCredentialMeta.timelive + ' 分钟'"></span>
<span class="block mt-1">当前接口:<code class="bg-gray-100 px-1.5 py-0.5 rounded" x-text="googleAccountCredentialMeta.apiUrl"></code></span>
<span x-show="googleAccountCredentialMeta.updatedAt" class="ml-2"
x-text="'更新于 ' + formatDate(googleAccountCredentialMeta.updatedAt)"></span>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">API Key 或完整取号链接</label>
<input type="password" x-model="googleAccountCredentialForm.value" autocomplete="new-password"
placeholder="填写 key_value,或粘贴 api.php / apiv2.php / l0z/api.php 链接;已保存 Key 时可只填新接口地址"
class="w-full" @keydown.enter.prevent="saveGoogleAccountCredentials()">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">默认账号存活时间(分钟)</label>
<input type="number" min="1" max="43200" step="1"
x-model.number="googleAccountCredentialForm.timelive" class="w-full">
</div>
</div>
<p class="text-xs text-gray-400 mt-2">支持供应商 V1、V2 和 l0z 接口;系统会加密保存 Key 和所选接口。已保存 Key 时可只粘贴接口地址进行切换;留空可单独修改存活分钟。</p>
<div class="flex gap-2 mt-3">
<button @click="saveGoogleAccountCredentials()" :disabled="googleAccountCredentialSaving"
class="btn btn-primary"
x-text="googleAccountCredentialSaving ? '保存中...' : (googleAccountCredentialMeta.configured ? '更新配置' : '保存配置')"></button>
<button x-show="googleAccountCredentialMeta.configured" @click="deleteGoogleAccountCredentials()"
:disabled="googleAccountCredentialDeleting" class="btn btn-red"
x-text="googleAccountCredentialDeleting ? '清除中...' : '清除配置'"></button>
</div>
</div>
<!-- 退款母号(独立加密存储,不进入通用 settings) -->
<div class="bg-white rounded-lg border border-gray-200 p-6 mt-4">
<div class="flex items-center justify-between gap-3 mb-4">
<div>
<h3 class="font-semibold text-gray-700">退款母号</h3>
<p class="text-xs text-gray-400 mt-1">填写母号登录内容和退款专用代理;两项都只保存在本机,并由 Windows 当前用户加密保护。</p>
</div>
<span class="status-badge"
:class="workspaceRefundCredentialMeta.configured ? 'status-登录成功' : 'status-未登录'"
x-text="workspaceRefundCredentialMeta.configured ? '已配置' : '未配置'"></span>
</div>
<div x-show="workspaceRefundCredentialMeta.configured" class="text-xs text-gray-500 mb-3">
当前母号:<code class="bg-gray-100 px-1.5 py-0.5 rounded" x-text="workspaceRefundCredentialMeta.ownerMasked"></code>
<span class="ml-2">退款代理:</span>
<code class="bg-gray-100 px-1.5 py-0.5 rounded"
x-text="workspaceRefundCredentialMeta.proxyConfigured ? workspaceRefundCredentialMeta.proxyMasked : '未配置'"></code>
<span x-show="workspaceRefundCredentialMeta.updatedAt" class="ml-2"
x-text="'更新于 ' + formatDate(workspaceRefundCredentialMeta.updatedAt)"></span>
</div>
<div class="grid grid-cols-1 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">母号 ChatGPT Session / Access Token</label>
<input type="password" x-model="workspaceRefundCredentialForm.value" autocomplete="new-password"
placeholder="粘贴完整 Session JSON 或 eyJ... Access Token;已保存时可留空" class="w-full"
@keydown.enter.prevent="saveWorkspaceRefundCredentials()">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">退款专用代理池(每行一个)</label>
<textarea x-model="workspaceRefundCredentialForm.proxy" autocomplete="new-password" rows="5"
placeholder="socks5://用户名:密码@主机:端口&#10;socks5://用户名:密码@主机:端口"
class="w-full font-mono text-xs"></textarea>
</div>
</div>
<p class="text-xs text-gray-400 mt-2">支持 SOCKS5、HTTP 及 host:port:user:pass,最多20条;连接失败、认证失败或 Cloudflare 403时自动切换。保存后令牌、代理用户名和密码都不会再次返回浏览器。</p>
<p class="text-xs text-amber-600 mt-1">执行退款会先移除母号团队中的全部非 Owner 成员,随后逐个邀请、迁移个人空间并移出团队;任务固定串行。</p>
<div class="flex gap-2 mt-3">
<button @click="saveWorkspaceRefundCredentials()" :disabled="workspaceRefundCredentialSaving"
class="btn btn-primary"
x-text="workspaceRefundCredentialSaving ? '保存中...' : (workspaceRefundCredentialMeta.configured ? '更新退款配置' : '保存退款配置')"></button>
<button x-show="workspaceRefundCredentialMeta.configured" @click="deleteWorkspaceRefundCredentials()"
:disabled="workspaceRefundCredentialDeleting" class="btn btn-red"
x-text="workspaceRefundCredentialDeleting ? '清除中...' : '清除母号'"></button>
</div>
</div>
<!-- 飞书电子表格同步 -->
<div class="bg-white rounded-lg border border-gray-200 p-6 mt-4">
<div class="flex items-center justify-between gap-3 mb-4">
<div>
<h3 class="font-semibold text-gray-700">飞书电子表格同步</h3>
<p class="text-xs text-gray-400 mt-1">同步当前勾选账号;App Secret 只在本机加密保存,不会返回浏览器。</p>
</div>
<span class="status-badge"
:class="feishuCredentialMeta.configured ? 'status-登录成功' : 'status-未登录'"
x-text="feishuCredentialMeta.configured ? '凭据已配置' : '凭据未配置'"></span>
</div>
<div x-show="feishuCredentialMeta.configured" class="text-xs text-gray-500 mb-3">
当前 App ID:<code class="bg-gray-100 px-1.5 py-0.5 rounded" x-text="feishuCredentialMeta.appIdMasked"></code>
<span x-show="feishuCredentialMeta.updatedAt" class="ml-2"
x-text="'更新于 ' + formatDate(feishuCredentialMeta.updatedAt)"></span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">FEISHU_APP_ID</label>
<input type="text" x-model="feishuCredentialForm.appId" autocomplete="off"
placeholder="cli_xxxxxxxxx" class="w-full">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">FEISHU_APP_SECRET</label>
<input type="password" x-model="feishuCredentialForm.appSecret" autocomplete="new-password"
placeholder="填写 App Secret" class="w-full" @keydown.enter.prevent="saveFeishuCredentials()">
</div>
</div>
<div class="flex gap-2 mt-3">
<button @click="saveFeishuCredentials()" :disabled="feishuCredentialSaving"
class="btn btn-primary"
x-text="feishuCredentialSaving ? '保存中...' : (feishuCredentialMeta.configured ? '更新凭据' : '保存凭据')"></button>
<button x-show="feishuCredentialMeta.configured" @click="deleteFeishuCredentials()"
:disabled="feishuCredentialDeleting" class="btn btn-red"
x-text="feishuCredentialDeleting ? '清除中...' : '清除凭据'"></button>
</div>
<p class="text-xs text-gray-400 mt-2">更新凭据时需要同时重新填写 App ID 和 App Secret。</p>
<div class="border-t border-gray-100 mt-5 pt-5 space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">飞书电子表格地址(必填)</label>
<input type="url" x-model="settings.feishu_document_url"
@change="saveSetting('feishu_document_url', settings.feishu_document_url)"
placeholder="支持 /sheets/ 直链或知识库 /wiki/ 链接" class="w-full">
<p class="text-xs text-gray-400 mt-1">支持电子表格直链和知识库里的电子表格。已有邮箱按 Gmail 忽略点和大小写匹配;未找到会新增并继承上一数据行样式。</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">FEISHU_FOLDER_TOKEN(可选)</label>
<input type="text" x-model="settings.feishu_folder_token"
@change="saveSetting('feishu_folder_token', settings.feishu_folder_token)"
placeholder="fldcnxxxxxxxx" class="w-full">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">FEISHU_TENANT_DOMAIN(可选)</label>
<input type="url" x-model="settings.feishu_tenant_domain"
@change="saveSetting('feishu_tenant_domain', settings.feishu_tenant_domain)"
placeholder="https://你的企业域名.feishu.cn" class="w-full">
</div>
</div>
<p class="text-xs text-gray-400">同步已有电子表格时使用上面的表格地址;文件夹 Token 和企业域名先保留为可选配置,不参与本次写入。</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">同步完成通知</label>
<select x-model="settings.feishu_notify_mode"
@change="saveSetting('feishu_notify_mode', settings.feishu_notify_mode)" class="w-full settings-select">
<option value="none">不发送通知</option>
<option value="private">机器人私发</option>
<option value="group">发送到群</option>
</select>
</div>
<div x-show="settings.feishu_notify_mode === 'private'">
<label class="block text-sm font-medium text-gray-700 mb-1">私发目标类型</label>
<select x-model="settings.feishu_notify_receive_id_type"
@change="saveSetting('feishu_notify_receive_id_type', settings.feishu_notify_receive_id_type)" class="w-full">
<option value="email">飞书账号邮箱</option>
<option value="open_id">Open ID</option>
</select>
</div>
</div>
<div x-show="settings.feishu_notify_mode === 'private'">
<label class="block text-sm font-medium text-gray-700 mb-1">私发目标</label>
<input type="text" x-model="settings.feishu_notify_receive_id"
@change="saveSetting('feishu_notify_receive_id', settings.feishu_notify_receive_id)"
placeholder="填写飞书账号邮箱或 Open ID" class="w-full">
</div>
<div x-show="settings.feishu_notify_mode === 'group'">
<label class="block text-sm font-medium text-gray-700 mb-1">FEISHU_CHAT_ID</label>
<input type="text" x-model="settings.feishu_notify_receive_id"
@change="saveSetting('feishu_notify_receive_id', settings.feishu_notify_receive_id)"
placeholder="oc_xxxxxxxxx" class="w-full">
</div>
<div class="rounded border border-blue-100 bg-blue-50 p-3 text-xs text-blue-700 leading-5">
字段固定映射:邮件、密码、辅助邮件、2FA密钥、是否已申请退款(是否已发起退款流程)、是否已退款(账号退款)、AC(完整 ChatGPT Session)。
本地没有 ChatGPT Session 也可以同步:本地 AC 为空时保留飞书原值,不会用空值覆盖;本地 AC 有值时会覆盖飞书原值。账号退款为空或“无状态”时,在飞书文档中写为“未操作”;飞书“是否已退款”已有“已退款”时会保留,不被本地状态覆盖。充值时间仅在本地有值且飞书对应单元格为空时写入;已有值绝不覆盖。“最后同步”不会读取或写入,去处、套餐、限流等其他列也全部保持不变。
</div>
</div>
</div>
<!-- 已充值提取账号额度监控与自动退款 -->
<div class="bg-white rounded-lg border border-gray-200 p-6 mt-4">
<div class="flex items-center justify-between gap-3 mb-4">
<div>
<h3 class="font-semibold text-gray-700">额度监控与自动退款</h3>
<p class="text-xs text-gray-400 mt-1">额度任务按设置间隔查询;退款截止时间由独立任务每 15 秒本地检查。额度达到阈值或截止时间到达,任一条件满足都会进入同一个单路退款队列。</p>
</div>
<span class="status-badge"
:class="quotaMonitorStatus.running ? 'status-充值中' : (settings.quota_monitor_enabled === '1' ? 'status-登录成功' : 'status-未登录')"
x-text="quotaMonitorStatus.running ? '执行中' : (settings.quota_monitor_enabled === '1' ? '已开启' : '未开启')"></span>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-8 gap-3 items-end">
<label class="flex items-center gap-2 h-10 text-sm text-gray-700">
<input type="checkbox" :checked="settings.quota_monitor_enabled === '1'"
@change="saveQuotaMonitorSetting('quota_monitor_enabled', $event.target.checked ? '1' : '0')">
开启定时任务
</label>
<label class="flex items-center gap-2 h-10 text-sm text-gray-700" title="跳过上次套餐状态已经明确为 Token失效(401) 的账号;关闭后可重新查询">
<input type="checkbox" :checked="settings.quota_monitor_skip_401_enabled === '1'"
@change="saveQuotaMonitorSetting('quota_monitor_skip_401_enabled', $event.target.checked ? '1' : '0')">
跳过明确的 401
</label>
<label class="flex items-center gap-2 h-10 text-sm text-gray-700">
<input type="checkbox" :checked="settings.quota_monitor_auto_refund_enabled === '1'"
@change="saveQuotaMonitorSetting('quota_monitor_auto_refund_enabled', $event.target.checked ? '1' : '0')">
开启自动退款(阈值或截止时间)
</label>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">执行间隔</label>
<input type="number" :min="settings.quota_monitor_interval_unit === 'minutes' ? 10 : 1" max="10000"
:value="settings.quota_monitor_interval_value || '24'"
@change="saveQuotaMonitorSetting('quota_monitor_interval_value', String(Math.max(settings.quota_monitor_interval_unit === 'minutes' ? 10 : 1, parseInt($event.target.value || '24', 10) || 24)))"
class="w-full">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">间隔单位</label>
<select x-model="settings.quota_monitor_interval_unit"
@change="saveQuotaMonitorSetting('quota_monitor_interval_unit', settings.quota_monitor_interval_unit)" class="w-full settings-select">
<option value="minutes">分钟</option>
<option value="hours">小时</option>
<option value="days">天</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">查询并发(1~10)</label>
<input type="number" min="1" max="10" :value="settings.quota_monitor_concurrency || '2'"
@change="saveQuotaMonitorSetting('quota_monitor_concurrency', String(Math.max(1, Math.min(10, parseInt($event.target.value || '2', 10) || 2))))"
class="w-full">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">用量阈值(%)</label>
<input type="number" min="0" max="100" step="1" :value="settings.quota_monitor_threshold || '100'"
@change="saveQuotaMonitorSetting('quota_monitor_threshold', String(Math.max(0, Math.min(100, Number($event.target.value || '100')))))"
class="w-full">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">截止检查间隔(秒)</label>
<input type="number" min="5" max="3600" step="1"
:value="settings.refund_deadline_check_interval_seconds || '15'"
@change="saveQuotaMonitorSetting('refund_deadline_check_interval_seconds', String(Math.max(5, Math.min(3600, parseInt($event.target.value || '15', 10) || 15))))"
class="w-full">
</div>
</div>
<div class="flex flex-wrap items-center gap-3 mt-4">
<button class="btn btn-green" @click="runQuotaMonitorNow()" :disabled="quotaMonitorStatus.running"
x-text="quotaMonitorStatus.running
? (quotaMonitorProgress.phase === 'refund'
? ('退款中 ' + quotaMonitorProgress.refundCurrent + '/' + quotaMonitorProgress.refundTotal)
: ('查询中 ' + quotaMonitorProgress.current + '/' + quotaMonitorProgress.total))
: (settings.quota_monitor_auto_refund_enabled === '1' ? '立即查询并退款' : '立即查询一次')"></button>
<span x-show="quotaMonitorStatusText" x-cloak class="text-xs"
:class="quotaMonitorStatusType === 'error' ? 'text-red-600' : (quotaMonitorStatusType === 'success' ? 'text-green-600' : (quotaMonitorStatusType === 'warn' ? 'text-orange-600' : 'text-blue-600'))"
x-text="quotaMonitorStatusText"></span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4 text-xs text-gray-600">
<div class="rounded border border-gray-100 bg-gray-50 p-3 leading-6">
<div>下次执行:<span x-text="quotaMonitorStatus.nextRunAt ? formatDate(quotaMonitorStatus.nextRunAt) : (settings.quota_monitor_enabled === '1' ? '计算中' : '未安排')"></span></div>
<div x-show="quotaMonitorStatus.running && quotaMonitorProgress.phase !== 'refund'">额度查询:<span x-text="quotaMonitorProgress.current + '/' + quotaMonitorProgress.total"></span></div>
<div x-show="quotaMonitorStatus.running && quotaMonitorProgress.phase === 'refund'">自动退款:<span x-text="quotaMonitorProgress.refundCurrent + '/' + quotaMonitorProgress.refundTotal"></span>,成功 <span x-text="quotaMonitorProgress.refundSuccessCount"></span>,失败 <span x-text="quotaMonitorProgress.refundFailureCount"></span></div>
<div>用量阈值:主窗口已用比例 ≥ <span x-text="settings.quota_monitor_threshold || '100'"></span>%</div>
<div>明确 401:<span x-text="settings.quota_monitor_skip_401_enabled === '1' ? '跳过查询' : '继续查询'"></span></div>
<div>退款并发:固定 1 路</div>
<div>截止时间检查:<span x-text="settings.quota_monitor_auto_refund_enabled === '1' ? ('每 ' + (settings.refund_deadline_check_interval_seconds || '15') + ' 秒检查') : '未开启'"></span></div>
</div>
<div class="rounded border border-gray-100 bg-gray-50 p-3 leading-6" x-show="quotaMonitorStatus.lastRun">
<div>上次完成:<span x-text="quotaMonitorStatus.lastRun && quotaMonitorStatus.lastRun.finished_at ? formatDate(quotaMonitorStatus.lastRun.finished_at) : '未完成'"></span></div>
<div>
检查 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.eligible_count : 0"></span>,
达到阈值 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.threshold_count : 0"></span>
(新增 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.threshold_new_count : 0"></span>),
失败 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.failure_count : 0"></span>
(新增 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.failure_new_count : 0"></span>)
</div>
<div>明确 401 跳过:<span x-text="quotaMonitorStatus.lastRun ? (quotaMonitorStatus.lastRun.skipped_401_count || 0) : 0"></span></div>
<div x-show="quotaMonitorStatus.lastRun && Number(quotaMonitorStatus.lastRun.auto_refund_enabled) === 1">
退款候选 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.refund_candidate_count : 0"></span>,
成功 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.refund_success_count : 0"></span>,
失败 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.refund_failure_count : 0"></span>,
跳过 <span x-text="quotaMonitorStatus.lastRun ? quotaMonitorStatus.lastRun.refund_skipped_count : 0"></span>
</div>
<div>飞书发送:<span x-text="quotaMonitorStatus.lastRun && quotaMonitorStatus.lastRun.delivery_status === 'sent' ? '成功' : (quotaMonitorStatus.lastRun && quotaMonitorStatus.lastRun.delivery_status === 'failed' ? '失败' : '未发送')"></span></div>
</div>
</div>
<div class="rounded border border-orange-100 bg-orange-50 p-3 text-xs text-orange-700 leading-5 mt-4">
“账号退款”为“已退款”的账号会直接跳过,不查询用量、不自动退款,也不会修改“账号退款”字段。自动退款只更新独立的“是否已发起退款流程”;成功改为“已发起”,失败改为“发起失败”。退款截止时间和额度阈值是“或”关系,未来截止时间不会阻止额度达标退款;共享退款通道忙时会排队等待,不再丢弃本轮任务。同一邮箱提交前会重新校验状态,避免重复退款。飞书通知已精简为结果总览,详细账号和异常原因统一放在 CSV。
</div>
</div>
<!-- 静态代理池 -->
<div class="bg-white rounded-lg border border-gray-200 p-6 mt-4">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-gray-700">静态代理池</h3>
<div class="flex items-center gap-2 text-xs text-gray-500">
<span>总数: <b x-text="staticProxies.length"></b></span>
<span>空闲: <b class="text-green-600" x-text="staticProxies.filter(p=>p.usage_count<p.max_usage).length"></b></span>
<span>已满: <b class="text-orange-500" x-text="staticProxies.filter(p=>p.usage_count>=p.max_usage).length"></b></span>
</div>
</div>
<div class="space-y-3">
<!-- 导入 -->
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">导入代理(每行一个 host:port:user:pass)</label>
<textarea x-model="staticProxyImportText" rows="3"
placeholder="host:port:user:pass&#10;host:port:user:pass"
class="w-full text-xs border rounded p-2 font-mono"></textarea>
<div class="flex items-center gap-2 mt-2">
<label class="text-xs text-gray-500">可用次数:</label>
<input type="number" x-model.number="staticProxyMaxUsage" min="1" max="100"
class="w-16 text-xs border rounded px-2 py-1">
<button @click="importStaticProxies()" class="btn btn-primary text-xs">导入</button>
<button @click="clearAllStaticProxies()" class="btn btn-red text-xs"
x-show="staticProxies.length > 0">清空</button>
</div>
</div>
<!-- 列表 -->
<div x-show="staticProxies.length > 0" style="max-height:240px;overflow:auto">
<table class="data-table" style="font-size:11px">
<thead><tr>
<th>ID</th><th>代理地址</th><th>状态</th><th>使用/上限</th><th>绑定账号</th><th>操作</th>
</tr></thead>
<tbody>
<template x-for="p in staticProxies" :key="p.id">
<tr :class="!p.is_active ? 'opacity-40' : ''">
<td x-text="p.id"></td>
<td class="font-mono" style="max-width:220px;overflow:hidden;text-overflow:ellipsis"
x-text="p.proxy_str" :title="p.proxy_str"></td>
<td>
<span :class="p.is_active ? 'text-green-600' : 'text-gray-400'"
x-text="p.is_active ? '启用' : '禁用'"></span>
</td>
<td>
<span class="cursor-pointer hover:underline"
:class="p.usage_count >= p.max_usage ? 'text-orange-500' : 'text-green-600'"
@click="editStaticProxyMaxUsage(p)"
:title="'点击修改上限'"
x-text="p.usage_count + '/' + p.max_usage"></span>
</td>
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;font-size:10px"
x-text="p.bound_emails || '-'" :title="p.bound_emails"></td>
<td class="whitespace-nowrap">
<button @click="toggleStaticProxy(p.id)"
class="text-xs ml-1"
:class="p.is_active ? 'text-orange-500 hover:text-orange-700' : 'text-green-500 hover:text-green-700'"
x-text="p.is_active ? '禁用' : '启用'"></button>
<button x-show="p.usage_count > 0" @click="releaseStaticProxy(p.id)"
class="text-blue-500 hover:text-blue-700 text-xs ml-1">释放</button>
<button @click="deleteStaticProxy(p.id)"
class="text-red-400 hover:text-red-600 text-xs ml-1">删除</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- ===== 日志 ===== -->
<div x-show="activeTab === 'logs'" class="flex-1 overflow-y-auto">
<div class="bg-white rounded-lg border border-gray-200 p-4">
<div class="flex items-center justify-between gap-3 flex-wrap">
<div>
<div class="text-base font-semibold text-gray-800">诊断日志</div>
<div class="text-xs text-gray-500 mt-0.5">用于排查登录/更改2FA等任务失败原因(网络、超时、DOM异常、页面报错等)</div>
</div>
<div class="text-xs text-gray-400">
<span x-text="diagEmail ? ('当前账号:' + diagEmail) : '请选择账号'"></span>
</div>
</div>
<div class="mt-3 flex flex-wrap items-center gap-3">
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">账号</span>
<input type="text" x-model="diagEmailSearch" placeholder="搜索账号..." class="text-sm" style="min-width:180px">
<select x-model="diagEmail" @change="onDiagEmailChange()" class="text-sm" style="min-width:260px">
<option value="">请选择账号</option>
<template x-for="e in filteredDiagAccounts()" :key="e">
<option :value="e" x-text="e"></option>
</template>
</select>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">日期</span>
<select x-model="diagDate" @change="diagOffset=0; loadDiagEvents()" class="text-sm" style="min-width:140px">
<template x-for="d in diagDates" :key="d">
<option :value="d" x-text="d"></option>
</template>
</select>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">类型</span>
<select x-model="diagTypeFilter" class="text-sm" style="min-width:160px">
<option value="">全部</option>
<option value="error">错误</option>
<option value="exception">异常</option>
<option value="requestfailed">请求失败</option>
<option value="response">响应异常</option>
<option value="console">控制台错误</option>
<option value="pageerror">页面异常</option>
<option value="checkpoint">关键节点</option>
<option value="navigate">页面跳转</option>
</select>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">任务</span>
<select x-model="diagTaskFilter" class="text-sm" style="min-width:140px">
<option value="">全部</option>
<option value="login">登录</option>
<option value="change_2fa">更改2FA</option>
<option value="change_password">更改密码</option>
<option value="chatgpt_register">ChatGPT注册/登录</option>
<option value="change_country">更改国家</option>
</select>
</div>
<div class="flex items-center gap-2 flex-1 min-w-[220px]">
<input type="text" x-model="diagSearch" placeholder="搜索 信息/URL/步骤..." class="text-sm flex-1"
style="min-width:220px">
<button class="btn btn-gray" @click="loadDiagEvents()" :disabled="diagLoading || !diagEmail">刷新</button>
</div>
</div>
<div class="mt-3 flex items-center gap-2 flex-wrap">
<span class="text-sm text-gray-600">快捷筛选</span>
<button class="btn btn-gray" style="padding:2px 10px;font-size:12px"
:class="diagQuickFilter==='errors' ? 'active' : ''"
@click="diagQuickFilter = (diagQuickFilter==='errors' ? '' : 'errors')">仅错误</button>
<button class="btn btn-gray" style="padding:2px 10px;font-size:12px"
:class="diagQuickFilter==='network' ? 'active' : ''"
@click="diagQuickFilter = (diagQuickFilter==='network' ? '' : 'network')">仅网络</button>
<button class="btn btn-gray" style="padding:2px 10px;font-size:12px"
:class="diagQuickFilter==='checkpoint' ? 'active' : ''"
@click="diagQuickFilter = (diagQuickFilter==='checkpoint' ? '' : 'checkpoint')">仅关键节点</button>
<button class="btn btn-gray" style="padding:2px 10px;font-size:12px" @click="diagQuickFilter=''">清除</button>
</div>
<div class="mt-3 bg-gray-50 border border-gray-200 rounded p-3" x-show="diagEmail && !diagLoading">
<div class="text-sm font-medium text-gray-700">本页汇总</div>
<div class="mt-1 text-xs text-gray-600 flex items-center gap-2 flex-wrap">
<template x-if="diagFinalReason()">
<div class="flex items-center gap-2">
<span class="px-2 py-0.5 rounded text-xs font-medium" :class="diagTypeClass(diagFinalReason().type)"
x-text="diagTypeLabel(diagFinalReason().type)"></span>
<span class="text-gray-800 break-all" x-text="diagSummary(diagFinalReason())"></span>
<button class="btn btn-gray" style="padding:2px 8px;font-size:12px"
x-show="!!(diagFinalReason().screenshotPath)"
@click="viewScreenshotPath(diagFinalReason().screenshotPath, diagEmail)">查看截图</button>
</div>
</template>
<template x-if="!diagFinalReason()">
<div class="text-gray-500">未发现明显错误事件(可切换日期或取消筛选查看更多)。</div>
</template>
</div>
<div class="mt-1 text-xs text-gray-500" x-show="diagFinalHint()" x-text="'建议:' + diagFinalHint()"></div>
</div>
<div class="mt-3 text-xs text-gray-500 flex items-center justify-between">
<div>
<span x-show="diagLoading">加载中...</span>
<span x-show="!diagLoading"
x-text="diagEmail ? ('共 ' + filteredDiagEvents().length + ' 条') : '请选择账号查看'"></span>
</div>
<div class="flex items-center gap-2">
<button class="btn btn-gray" @click="diagPrevPage()" :disabled="diagLoading || diagOffset===0">上一页</button>
<button class="btn btn-gray" @click="diagNextPage()"
:disabled="diagLoading || (diagOffset + diagLimit) >= diagTotal">下一页</button>
</div>
</div>
<div class="mt-3 overflow-auto border border-gray-100 rounded diag-table">
<table class="min-w-full text-sm">
<thead class="bg-gray-50 diag-thead">
<tr class="text-left text-xs text-gray-600">
<th class="px-3 py-2" style="min-width:170px">时间</th>
<th class="px-3 py-2" style="min-width:120px">类型</th>
<th class="px-3 py-2" style="min-width:120px">任务</th>
<th class="px-3 py-2" style="min-width:140px">步骤</th>
<th class="px-3 py-2">摘要</th>
<th class="px-3 py-2" style="min-width:90px">截图</th>
</tr>
</thead>
<tbody>
<template x-for="ev in filteredDiagEvents()"
:key="(ev.ts||'') + '|' + (ev.type||'') + '|' + (ev.url||'') + '|' + (ev.name||'') + '|' + (ev.message||'')">
<tr class="border-t border-gray-100 hover:bg-gray-50 cursor-pointer" @click="selectDiagEvent(ev)">
<td class="px-3 py-2 whitespace-nowrap" x-text="formatDiagTs(ev.ts)"></td>
<td class="px-3 py-2 whitespace-nowrap">
<span class="px-2 py-0.5 rounded text-xs font-medium" :class="diagTypeClass(ev.type)"
x-text="diagTypeLabel(ev.type)"></span>
</td>
<td class="px-3 py-2 whitespace-nowrap" x-text="diagTaskLabel(ev.taskType)"></td>
<td class="px-3 py-2 whitespace-nowrap" x-text="ev.step || '-'"></td>
<td class="px-3 py-2 truncate" x-text="diagSummary(ev)"></td>
<td class="px-3 py-2">
<button class="btn btn-gray" style="padding:2px 8px;font-size:12px" x-show="!!(ev.screenshotPath)"
@click.stop="viewScreenshotPath(ev.screenshotPath, diagEmail)">查看</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<template x-if="diagSelected">
<div class="mt-3">
<div class="bg-gray-50 border border-gray-200 rounded p-3">
<div class="text-sm font-medium text-gray-700 flex items-center justify-between">
<span>详情</span>
<button class="btn btn-gray" style="padding:2px 8px;font-size:12px" @click="diagSelected=null">关闭</button>
</div>
<div class="mt-2 text-xs text-gray-600 space-y-1">
<div><span class="font-medium">时间:</span> <span x-text="formatDiagTs(diagSelected.ts)"></span></div>
<div><span class="font-medium">类型:</span> <span x-text="diagTypeLabel(diagSelected.type)"></span></div>
<div><span class="font-medium">任务:</span> <span x-text="diagTaskLabel(diagSelected.taskType)"></span>
</div>
<div><span class="font-medium">步骤:</span> <span x-text="diagSelected.step || '-'"></span></div>
<div><span class="font-medium">URL:</span> <span class="break-all"
x-text="diagSelected.url || '-'"></span></div>
<div><span class="font-medium">信息:</span> <span class="break-all"
x-text="diagSelected.message || '-'"></span></div>
<div x-show="diagSelected.name"><span class="font-medium">节点:</span> <span
x-text="diagSelected.name"></span></div>
<div x-show="diagSelected.screenshotPath"><span class="font-medium">截图:</span> <span class="break-all"
x-text="diagSelected.screenshotPath"></span></div>
<div x-show="diagSelected.meta"><span class="font-medium">元数据:</span>
<pre class="bg-white border border-gray-200 rounded p-2 overflow-auto"
x-text="JSON.stringify(diagSelected.meta, null, 2)"></pre>
</div>
<div x-show="diagSelected.stack"><span class="font-medium">堆栈:</span>
<pre class="bg-white border border-gray-200 rounded p-2 overflow-auto"
x-text="diagSelected.stack"></pre>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<!-- ===== 一次性导出问题号模态框 ===== -->
<div x-show="showProblemExportModal" x-cloak class="modal-backdrop" @click.self="showProblemExportModal=false">
<div class="modal-box modal-compact">
<h3 class="text-base font-semibold text-gray-800 mb-3">导出问题号</h3>
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-600 mb-2">选择要导出的类别(默认全选)</label>
<div class="space-y-2">
<label class="flex items-center justify-between gap-3 border border-gray-200 rounded px-3 py-2 cursor-pointer">
<span class="flex items-center gap-2 text-sm">
<input type="checkbox" x-model="problemExportCategories.password">
<span>密码错误</span>
</span>
<span class="text-xs text-gray-500" x-text="problemExportCount('password') + ' 个'"></span>
</label>
<label class="flex items-center justify-between gap-3 border border-gray-200 rounded px-3 py-2 cursor-pointer">
<span class="flex items-center gap-2 text-sm">
<input type="checkbox" x-model="problemExportCategories.twofa">
<span>2FA错误</span>
</span>
<span class="text-xs text-gray-500" x-text="problemExportCount('twofa') + ' 个'"></span>
</label>
<label class="flex items-center justify-between gap-3 border border-gray-200 rounded px-3 py-2 cursor-pointer">
<span class="flex items-center gap-2 text-sm">
<input type="checkbox" x-model="problemExportCategories.captcha">
<span>人机认证</span>
</span>
<span class="text-xs text-gray-500" x-text="problemExportCount('captcha') + ' 个'"></span>
</label>
</div>
</div>
<div class="bg-gray-50 border border-gray-200 rounded p-2 text-xs text-gray-500 leading-5">
将生成一个 TXT 文件,账号一行一个,类别之间空一行。<br>
新导入账号使用导入时的原始整行;旧数据如未保留原始行,将按当前保存的账号四字段导出。
</div>
</div>
<div class="flex gap-2 mt-3">
<button @click="showProblemExportModal=false" class="btn btn-red">取消</button>
<button @click="confirmProblemExport()" class="btn btn-primary flex-1">导出 TXT</button>
</div>
</div>
</div>
<!-- ===== 导出账号模态框 ===== -->
<div x-show="showExportModal" x-cloak class="modal-backdrop">
<div class="modal-box modal-compact">
<h3 class="text-base font-semibold text-gray-800 mb-3">
导出账号
<span class="text-xs font-normal text-gray-400 ml-2" x-text="'共 ' + _exportList.length + ' 个'"></span>
</h3>
<div class="space-y-3">
<!-- 字段选择 -->
<div>
<label class="block text-xs font-medium text-gray-600 mb-2">选择导出字段</label>
<div class="grid grid-cols-2 gap-1">
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportFields.email"> 邮箱
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportFields.password"> 密码
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportFields.recovery_email"> 辅助邮箱
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportFields.secret_key"> 2FA密钥
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportFields.country"> 国家
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportFields.customer_name"> 客户名
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportFields.account_refund_status"> 账号退款
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportFields.refund_deadline_at"> 退款截止时间
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportFields.sheerid_link"> SheerID链接
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportFields.status"> 状态
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportFields.message"> 消息
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportFields.recharged_at"> 充值时间
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportFields.sms_url"> 接码链接
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#7c3aed;font-weight:500">
<input type="checkbox" x-model="exportFields.ag_refresh_token"> AG凭证
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#6d28d9;font-weight:500">
<input type="checkbox" x-model="exportFields.cli_ac_credential"> Cli_AC凭证
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#0f766e;font-weight:500">
<input type="checkbox" x-model="exportFields.chatgpt_session"> ChatGPT Session
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#2563eb;font-weight:500">
<input type="checkbox" x-model="exportFields.openai_auth_json"> 授权JSON
</label>
</div>
</div>
<!-- 导出方式 -->
<div>
<label class="block text-xs font-medium text-gray-600 mb-2">导出方式</label>
<div class="flex flex-wrap gap-x-4 gap-y-2">
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportMode.file"> 导出到文件
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#047857;font-weight:500">
<input type="checkbox" x-model="exportMode.csv"> 导出到 CSV
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#c2410c;font-weight:500">
<input type="checkbox" x-model="exportMode.sub2api_json"> 导出 sub2api JSON
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input type="checkbox" x-model="exportMode.clipboard"> 复制到剪贴板
</label>
<label class="flex items-center gap-2 text-sm cursor-pointer" style="color:#6d28d9;font-weight:500">
<input type="checkbox" x-model="exportMode.staging"> 放入暂存箱
</label>
</div>
</div>
<div x-show="exportMode.staging" x-cloak>
<label class="block text-xs font-medium text-gray-600 mb-1">暂存文件夹名称</label>
<textarea x-model="exportStagingNote" rows="2" maxlength="500" placeholder="例如:登录成功,留档给客户 A"
class="text-sm" style="resize:vertical"></textarea>
<label class="staging-browser-toggle mt-2">
<span>
<strong>同时删除比特浏览器窗口</strong>
<small>默认关闭;打开后仅删除本批账号绑定的窗口</small>
</span>
<input type="checkbox" x-model="deleteBrowserWindowsOnStage">
<span class="staging-browser-toggle__switch" aria-hidden="true"></span>
</label>
</div>
<!-- 格式说明 -->
<div class="bg-gray-50 border border-gray-200 rounded p-2 text-xs text-gray-500 leading-5">
文本文件和剪贴板使用 <code class="text-gray-700">----</code> 分隔;CSV 含字段名;sub2api JSON 可直接用于账号数据导入
</div>
</div>
<div class="flex gap-2 mt-3">
<button @click="showExportModal=false" :disabled="exportInProgress" class="btn btn-red">取消</button>
<button @click="confirmExport()" :disabled="exportInProgress" class="btn btn-primary flex-1"
x-text="exportInProgress ? '正在导出...' : '确认执行'"></button>
</div>
</div>
</div>
<!-- ===== 批量邮箱查询模态框 ===== -->
<div x-show="showBatchEmailSearchModal" x-cloak class="modal-backdrop"
@click.self="showBatchEmailSearchModal=false" @keydown.escape.window="showBatchEmailSearchModal=false">
<div class="modal-box modal-compact" style="max-width:640px" role="dialog" aria-modal="true"
aria-labelledby="batch-email-search-title">
<div class="flex items-start justify-between gap-3 mb-3">
<div>
<h3 id="batch-email-search-title" class="text-base font-semibold text-gray-800">批量邮箱查询</h3>
<p class="text-xs text-gray-400 mt-1">每行一个邮箱;Gmail 匹配会忽略用户名中的点和大小写,其他邮箱按不区分大小写精确匹配。</p>
</div>
<button type="button" class="text-gray-400 hover:text-gray-600 text-xl leading-none"
@click="showBatchEmailSearchModal=false" aria-label="关闭">&times;</button>
</div>
<textarea x-model="batchEmailSearchText" rows="12" spellcheck="false"
class="font-mono text-xs" placeholder="qwertyu0134q@gmail.com&#10;tachenymolchan@gmail.com&#10;watjack128@gmail.com"></textarea>
<div x-show="batchEmailSearchActive" x-cloak class="mt-3 space-y-2">
<div class="grid grid-cols-3 gap-2 text-center text-xs">
<div class="rounded border border-gray-200 bg-gray-50 p-2">输入<br><b x-text="batchEmailSearchEmails.length"></b></div>
<div class="rounded border border-green-200 bg-green-50 p-2 text-green-700">找到<br><b x-text="batchEmailSearchAccounts.length"></b></div>
<div class="rounded border border-red-200 bg-red-50 p-2 text-red-600">未找到<br><b x-text="batchEmailSearchMissingEmails.length"></b></div>
</div>
<div x-show="batchEmailSearchMissingEmails.length > 0">
<div class="flex items-center justify-between mb-1">
<label class="text-xs font-medium text-gray-600">未找到的邮箱</label>
<button type="button" @click="copyBatchEmailSearchMissing()" class="btn btn-sm btn-outline">复制未找到</button>
</div>
<textarea readonly rows="5" spellcheck="false" class="font-mono text-xs bg-gray-50"
:value="batchEmailSearchMissingEmails.join('\n')"></textarea>
</div>
</div>
<div class="flex gap-2 mt-3">
<button @click="showBatchEmailSearchModal=false" class="btn btn-gray">取消</button>
<button x-show="batchEmailSearchActive" @click="clearBatchEmailSearch()" class="btn btn-red">清除查询</button>
<button @click="applyBatchEmailSearch()" class="btn btn-primary flex-1"
x-text="batchEmailSearchActive ? '重新查询' : '开始查询'"></button>
</div>
</div>
</div>
<!-- ===== 导入账号模态框 ===== -->
<div x-show="showImportAccountModal" x-cloak class="modal-backdrop">
<div class="modal-box modal-compact">
<h3 class="text-base font-semibold text-gray-800 mb-3">导入账号</h3>
<div class="space-y-2">
<!-- 客户名 -->
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">客户名</label>
<input type="text" x-model="importCustomerName" placeholder="输入客户名(可选)" class="text-sm">
</div>
<!-- 模板选择 -->
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">导入模板</label>
<div class="flex items-center gap-2">
<select x-model.number="importSelectedIndex" class="flex-1" style="height:32px;padding:4px 10px;">
<template x-for="(tpl, idx) in importTemplates" :key="idx">
<option :value="idx" x-text="tpl.name"></option>
</template>
</select>
<button @click="deleteImportTemplate(importSelectedIndex)" class="btn btn-red" title="删除当前模板"
:disabled="importTemplates.length <= 1">删除</button>
<button @click="showTemplateEditor = !showTemplateEditor" class="btn btn-outline" title="新建模板"
x-text="showTemplateEditor ? '收起' : '新建'"></button>
</div>
</div>
<!-- 新建模板(可折叠) -->
<div x-show="showTemplateEditor" x-cloak class="bg-blue-50 border border-blue-200 rounded p-2 space-y-2">
<input type="text" x-model="importNewTemplateName" placeholder="模板名称" class="text-xs">
<input type="text" x-model="importNewTemplatePattern" placeholder="如:{{账号}}|{{密码}}|{{无用}}|{{2fa}}"
class="font-mono text-xs">
<div class="flex items-center gap-2">
<span class="text-xs text-gray-400">可用: {{账号}} {{密码}} {{辅助邮箱}} {{2fa}} {{接码链接}} {{rt凭证}} {{无用}}</span>
<button @click="addImportTemplate()" class="btn btn-green ml-auto">保存模板</button>
</div>
</div>
<!-- 账号列表 -->
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">账号列表</label>
<textarea x-model="importText" rows="8" placeholder="粘贴账号列表..." class="font-mono text-xs"></textarea>
</div>
<label class="flex items-center gap-2 text-xs text-gray-600 cursor-pointer select-none">
<input type="checkbox" x-model="importSkipDuplicates" class="h-4 w-4">
<span>跳过重复邮箱(已有账号不做任何修改)</span>
</label>
<div class="text-xs text-blue-600 bg-blue-50 border border-blue-100 rounded p-2">
导入只保存账号;执行具体任务时才创建临时比特浏览器窗口,单个账号处理结束后自动删除。
</div>
<!-- 解析预览(第1行) -->
<template x-if="importText.trim() && getImportPreview().length > 0">
<div class="bg-gray-50 border border-gray-200 rounded p-2">
<div class="text-xs font-medium text-gray-600 mb-1">解析预览</div>
<div class="space-y-0.5">
<template x-for="(col, ci) in getImportPreview()[0]" :key="ci">
<div class="flex items-center gap-2 text-xs">
<span class="px-1.5 py-0.5 rounded text-white shrink-0" :class="col.ok ? 'bg-blue-500' : 'bg-red-400'"
x-text="col.label"></span>
<span class="font-mono text-gray-700 truncate" x-text="col.value"></span>
</div>
</template>
</div>
</div>
</template>
</div>
<div class="flex gap-2 mt-3">
<button
@click="showImportAccountModal=false; importText=''; importCustomerName=''; showTemplateEditor=false; importNewTemplateName=''; importNewTemplatePattern=''"
class="btn btn-red">取消</button>
<button @click="importAccounts()" class="btn btn-primary flex-1">导入<span x-show="importText.trim()"
x-text="' ' + importText.trim().split('\n').filter(l => l.trim() && !l.trim().startsWith('#')).length + ' 个'"></span></button>
</div>
</div>
</div>
<!-- ===== 暂存箱 ===== -->
<div x-show="showStagingModal" x-cloak class="modal-backdrop" @click.self="showStagingModal=false">
<div class="modal-box modal-compact staging-modal">
<template x-if="!stagedBatchDetail">
<div>
<div class="flex items-center justify-between gap-3 mb-3">
<div>
<h3 class="text-base font-semibold text-gray-800">暂存箱</h3>
<p class="text-xs text-gray-400 mt-1"
x-text="'共 ' + stagedBatches.length + ' 个文件夹,' + stagedBatches.reduce((sum, item) => sum + Number(item.account_count || 0), 0) + ' 个账号'"></p>
</div>
<button @click="loadStagingBatches()" class="btn btn-gray btn-sm">刷新</button>
</div>
<div class="staging-folder-grid">
<template x-for="batch in stagedBatches" :key="batch.id">
<button type="button" class="staging-folder" @click="openStagedBatch(batch)">
<span class="staging-folder__icon" aria-hidden="true"></span>
<span class="staging-folder__body">
<strong class="staging-folder__name" x-text="batch.name"></strong>
<span class="staging-folder__meta" x-text="batch.account_count + ' 个账号 · ' + formatDate(batch.created_at)"></span>
<span class="staging-folder__fields" x-text="getStagingFieldLabels(batch)"></span>
</span>
<span class="staging-folder__arrow">›</span>
</button>
</template>
<div x-show="stagedBatches.length === 0" class="staging-empty">暂存箱为空</div>
</div>
<div class="flex gap-2 mt-3">
<button @click="showStagingModal=false" class="btn btn-gray">关闭</button>
</div>
</div>
</template>
<template x-if="stagedBatchDetail">
<div>
<div class="staging-detail-header">
<button @click="stagedBatchDetail=null" class="page-icon-btn" title="返回暂存箱">‹</button>
<div class="min-w-0 flex-1">
<h3 class="text-base font-semibold text-gray-800 truncate" x-text="stagedBatchDetail.batch.name"></h3>
<p class="text-xs text-gray-400 mt-1"
x-text="stagedBatchDetail.batch.account_count + ' 个账号 · ' + getStagingFieldLabels(stagedBatchDetail.batch)"></p>
</div>
</div>
<textarea readonly class="staging-output" :value="stagedBatchText" spellcheck="false"></textarea>
<div class="flex gap-2 mt-3 staging-detail-actions">
<button @click="copyStagedBatch()" class="btn btn-primary">复制全部</button>
<button @click="downloadStagedBatch()" class="btn btn-green">导出 TXT</button>
<button @click="restoreStagedBatch()" :disabled="isRunning" class="btn btn-gray"
title="恢复只回写账号,具体任务执行时再创建临时比特窗口">恢复到主列表</button>
<button @click="permanentlyDeleteStagedBatch()" class="btn btn-red">彻底删除文件夹</button>
</div>
</div>
</template>
</div>
</div>
<!-- ===== 批量卡付充值模态框 ===== -->
<div x-show="showRechargeModal" x-cloak class="modal-backdrop"
@click.self="closeRechargeModal()" @keydown.escape.window="closeRechargeModal()">
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="recharge-title">
<h3 id="recharge-title" class="text-base font-semibold text-gray-800 mb-2">批量充值</h3>
<p class="text-xs text-gray-500 mb-3">
已选择 <b x-text="rechargeAccounts.length"></b> 个账号。请按下方账号顺序逐行填写卡密,一行只分配给一个账号。
</p>
<div class="border border-gray-200 rounded bg-gray-50 mb-3" style="max-height:150px;overflow:auto">
<template x-for="(row, index) in rechargeAccounts" :key="row.email">
<div class="flex items-center gap-2 px-3 py-1.5 text-xs border-b border-gray-100 last:border-b-0">
<span class="text-gray-400 w-6 text-right" x-text="(index + 1) + '.'"></span>
<span class="font-mono text-gray-700 truncate" x-text="row.email" :title="row.email"></span>
</div>
</template>
</div>
<label class="block text-xs font-medium text-gray-600 mb-1">卡密(每行一个)</label>
<textarea x-model="rechargeCardKeys" rows="10" class="w-full text-xs font-mono"
autocomplete="off" spellcheck="false" placeholder="第1行卡密对应第1个账号&#10;第2行卡密对应第2个账号"></textarea>
<div class="flex items-center justify-between gap-2 mt-1 text-xs">
<span class="text-gray-400"
x-text="'已填写 ' + rechargeCardLineCount + ' / ' + rechargeAccounts.length + ' 行'"></span>
<span x-show="rechargeInputError" class="text-red-500" x-text="rechargeInputError"></span>
</div>
<div class="flex items-center justify-between gap-3 mt-3">
<div>
<label for="recharge-concurrency" class="block text-xs font-medium text-gray-600">充值并发</label>
<p class="text-xs text-gray-400 mt-1">默认 2,最大 5;相邻请求仍错峰 1 秒发起。</p>
</div>
<input id="recharge-concurrency" type="number" x-model.number="rechargeConcurrency"
@change="rechargeConcurrency = normalizedRechargeConcurrency"
min="1" max="5" step="1" class="w-20 text-center" style="width:70px">
</div>
<p class="text-xs text-orange-600 bg-orange-50 border border-orange-100 rounded p-2 mt-3">
确认后将对每个账号自动执行“卡付预创建 → 卡付确认”;使用该账号数据库中已保存的完整 ChatGPT Session(结构化 JSON)。
</p>
<div class="flex gap-2 mt-3">
<button @click="closeRechargeModal()" :disabled="rechargeSubmitting" class="btn btn-gray">取消</button>
<button @click="confirmRecharge()" :disabled="rechargeSubmitting || !!rechargeInputError"
class="btn btn-orange flex-1"
x-text="rechargeSubmitting ? '正在启动...' : '确认并开始充值'"></button>
</div>
</div>
</div>
<!-- ===== 批量改写客户名模态框 ===== -->
<div x-show="showBatchCustomerNameModal" x-cloak class="modal-backdrop"
@click.self="showBatchCustomerNameModal=false" @keydown.escape.window="showBatchCustomerNameModal=false">
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="batch-customer-name-title">
<h3 id="batch-customer-name-title" class="text-base font-semibold text-gray-800 mb-3">
批量改写客户名
<span class="text-xs font-normal text-gray-400 ml-2" x-text="'已选 ' + selectedCount + ' 个账号'"></span>
</h3>
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">新客户名</label>
<input type="text" x-model="batchCustomerName" maxlength="100" class="w-full"
placeholder="输入客户名,留空表示清空"
@keydown.enter.prevent="confirmBatchCustomerName()">
<p class="text-xs text-gray-400 mt-1">留空后确认,将清空所选账号的客户名。</p>
</div>
</div>
<div class="flex gap-2 mt-3">
<button @click="showBatchCustomerNameModal=false" :disabled="batchCustomerNameSaving"
class="btn btn-red">取消</button>
<button @click="confirmBatchCustomerName()" :disabled="batchCustomerNameSaving"
class="btn btn-primary flex-1"
x-text="batchCustomerNameSaving ? '保存中...' : '确认改写'"></button>
</div>
</div>
</div>
<!-- ===== 退款截止时间模态框 ===== -->
<div x-show="showRefundDeadlineModal" x-cloak class="modal-backdrop"
@click.self="showRefundDeadlineModal=false" @keydown.escape.window="showRefundDeadlineModal=false">
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="refund-deadline-title">
<h3 id="refund-deadline-title" class="text-base font-semibold text-gray-800 mb-2">
设置退款截止时间
<span class="text-xs font-normal text-gray-400 ml-2" x-text="'账号 ' + refundDeadlineEmails.length + ' 个'"></span>
</h3>
<p class="text-xs text-gray-500 mb-3">
可指定北京时间,也可按每个账号自己的充值时间增加分钟、小时或天数。截止时间到达或套餐用量达到阈值,任一条件满足都会触发。
</p>
<div class="rounded border border-gray-200 p-3">
<label class="flex items-center gap-2 text-xs font-medium text-gray-700 mb-2 cursor-pointer">
<input type="radio" x-model="refundDeadlineMode" value="absolute">
指定退款截止时间(北京时间)
</label>
<input type="datetime-local" step="1" x-model="refundDeadlineValue"
@focus="refundDeadlineMode='absolute'" @input="refundDeadlineMode='absolute'" class="w-full">
<div class="flex flex-wrap gap-2 mt-2">
<button @click="setRefundDeadlineQuick(1)" class="btn btn-gray">当前时间 + 1小时</button>
<button @click="setRefundDeadlineQuick(24)" class="btn btn-gray">当前时间 + 1天</button>
<button @click="setRefundDeadlineQuick(72)" class="btn btn-gray">当前时间 + 3天</button>
</div>
</div>
<div class="rounded border border-purple-200 bg-purple-50 p-3 mt-3">
<label class="flex items-center gap-2 text-xs font-medium text-purple-800 mb-2 cursor-pointer">
<input type="radio" x-model="refundDeadlineMode" value="recharge_offset">
按每个账号的充值时间计算
</label>
<div class="flex gap-2 items-center">
<input type="number" min="1" step="1" x-model.number="refundDeadlineOffsetValue"
@focus="refundDeadlineMode='recharge_offset'" @input="refundDeadlineMode='recharge_offset'"
class="flex-1" placeholder="数量">
<select x-model="refundDeadlineOffsetUnit" @change="refundDeadlineMode='recharge_offset'"
class="settings-select refund-deadline-unit-select">
<option value="minutes">分钟</option>
<option value="hours">小时</option>
<option value="days">天</option>
</select>
</div>
<div class="flex flex-wrap gap-2 mt-2">
<button @click="setRefundDeadlineRechargeOffset(30, 'minutes')" class="btn btn-purple">+30分钟</button>
<button @click="setRefundDeadlineRechargeOffset(1, 'hours')" class="btn btn-purple">+1小时</button>
<button @click="setRefundDeadlineRechargeOffset(6, 'hours')" class="btn btn-purple">+6小时</button>
<button @click="setRefundDeadlineRechargeOffset(12, 'hours')" class="btn btn-purple">+12小时</button>
<button @click="setRefundDeadlineRechargeOffset(1, 'days')" class="btn btn-purple">+1天</button>
<button @click="setRefundDeadlineRechargeOffset(3, 'days')" class="btn btn-purple">+3天</button>
</div>
<p class="text-xs text-purple-600 mt-2">批量设置时,每个账号分别使用自己的充值时间;没有充值时间的账号会跳过并提示。</p>
</div>
<div class="flex gap-2 mt-4">
<button @click="showRefundDeadlineModal=false" :disabled="refundDeadlineSaving" class="btn btn-gray">取消</button>
<button @click="saveRefundDeadline(true)" :disabled="refundDeadlineSaving" class="btn btn-red">清除时间</button>
<button @click="saveRefundDeadline(false)" :disabled="refundDeadlineSaving" class="btn btn-primary flex-1"
x-text="refundDeadlineSaving ? '保存中...' : '确认保存'"></button>
</div>
</div>
</div>
<!-- ===== 批量更改状态模态框 ===== -->
<div x-show="showBatchStatusModal" x-cloak class="modal-backdrop" @click.self="showBatchStatusModal=false">
<div class="modal-box modal-compact">
<h3 class="text-base font-semibold text-gray-800 mb-3">
批量更改状态
<span class="text-xs font-normal text-gray-400 ml-2" x-text="'已选 ' + selectedCount + ' 个账号'"></span>
</h3>
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">目标状态</label>
<select x-model="batchStatus" class="status-select w-full">
<option value="未登录">未登录</option>
<option value="登录成功">登录成功</option>
<option value="登录失败">登录失败</option>
<option value="改密成功">改密成功</option>
<option value="改密失败">改密失败</option>
<option value="2FA修改成功">2FA修改成功</option>
<option value="2FA修改失败">2FA修改失败</option>
<option value="未验证已改密和2FA">未验证已改密和2FA</option>
<option value="已验证改密和2FA">已验证改密和2FA</option>
<option value="未注册成功已改密和2FA">未注册成功已改密和2FA</option>
<option value="ChatGPT成功">ChatGPT成功</option>
<option value="ChatGPT失败">ChatGPT失败</option>
<option value="半价Ultra">半价Ultra</option>
<option value="订阅成功">订阅成功</option>
<option value="已充值提取">已充值提取</option>
<option value="超时">超时</option>
</select>
</div>
</div>
<div class="flex gap-2 mt-3">
<button @click="showBatchStatusModal=false" class="btn btn-red">取消</button>
<button @click="confirmBatchStatus()" class="btn btn-primary flex-1">确认更改</button>
</div>
</div>
</div>
<!-- ===== 链接取号 + 反重力凭证弹窗 ===== -->
<div x-show="showGoogleAccountAgModal" x-cloak class="modal-backdrop"
@click.self="showGoogleAccountAgModal=false" @keydown.escape.window="showGoogleAccountAgModal=false">
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="google-account-ag-title">
<h3 id="google-account-ag-title" class="text-base font-semibold text-gray-800 mb-2">链接提取 + 获取凭证</h3>
<p class="text-xs text-gray-500 mb-3">从已配置的供应接口批量取号,只保存邮箱和密码;随后无代理创建浏览器,并执行反重力“获取凭证”。</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">处理账号数量(1~5000)</label>
<input type="number" min="1" max="5000" step="1" x-model.number="googleAccountAgQuantity"
class="w-full" @keydown.enter.prevent="confirmGoogleAccountAgTask()">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">并发数量(1~30)</label>
<input type="number" min="1" max="30" step="1" x-model.number="googleAccountAgConcurrency"
class="w-full" @keydown.enter.prevent="confirmGoogleAccountAgTask()">
</div>
</div>
<div class="rounded border border-blue-100 bg-blue-50 p-3 text-xs text-blue-700 leading-5 mt-3">
供应数据中的辅助邮箱及后续字段会被忽略;数据库已有邮箱会跳过且不覆盖。临时比特浏览器固定使用直连(无代理);每个并发工位完成一个账号并删除对应浏览器后,再领取下一个账号。
</div>
<div class="flex gap-2 mt-3">
<button @click="showGoogleAccountAgModal=false" class="btn btn-red">取消</button>
<button @click="confirmGoogleAccountAgTask()" class="btn btn-blue flex-1">开始处理</button>
</div>
</div>
</div>
<!-- ===== 改密 + 2FA 执行方式弹窗 ===== -->
<div x-show="showSecurityRunModal" x-cloak class="modal-backdrop"
@click.self="showSecurityRunModal=false" @keydown.escape.window="showSecurityRunModal=false">
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="security-run-title">
<h3 id="security-run-title" class="text-base font-semibold text-gray-800 mb-2">改密 + 2FA</h3>
<p class="text-xs text-gray-500 mb-3"
x-text="'本次将处理 ' + (selectedCount > 0 ? selectedCount : accounts.length) + ' 个账号,请选择执行方式。'"></p>
<div class="space-y-2">
<label class="flex items-start gap-2 border border-orange-200 bg-orange-50 rounded p-3 cursor-pointer">
<input type="radio" name="security_run_mode" value="overwrite" x-model="securityRunMode" class="mt-0.5">
<span>
<span class="block text-sm font-medium text-orange-800">覆盖修改</span>
<span class="block text-xs text-orange-600 mt-0.5">保持当前逻辑,重新修改密码和2FA。</span>
</span>
</label>
<label class="flex items-start gap-2 border border-gray-200 rounded p-3 cursor-pointer">
<input type="radio" name="security_run_mode" value="skip" x-model="securityRunMode" class="mt-0.5">
<span>
<span class="block text-sm font-medium text-gray-700">跳过已完成</span>
<span class="block text-xs text-gray-400 mt-0.5">按数据库记录逐项判断;已改密则只处理2FA,已改2FA则只处理密码。</span>
</span>
</label>
</div>
<div class="flex gap-2 mt-3">
<button @click="showSecurityRunModal=false" class="btn btn-red">取消</button>
<button @click="confirmSecurityTask()" class="btn btn-purple flex-1">开始执行</button>
</div>
</div>
</div>
<!-- ===== ChatGPT 登录/注册选项弹窗 ===== -->
<div x-show="showChatgptRunModal" x-cloak class="modal-backdrop"
@click.self="showChatgptRunModal=false" @keydown.escape.window="showChatgptRunModal=false">
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="chatgpt-run-title">
<h3 id="chatgpt-run-title" class="text-base font-semibold text-gray-800 mb-2">ChatGPT登录/注册</h3>
<p class="text-xs text-gray-500 mb-3"
x-text="'本次将处理 ' + (selectedCount > 0 ? selectedCount : accounts.length) + ' 个账号,请选择登录方式。'"></p>
<div class="space-y-2">
<label class="flex items-start gap-2 border border-gray-200 rounded p-3 cursor-pointer">
<input type="radio" name="chatgpt_auth_mode" value="keep" x-model="chatgptAuthMode" class="mt-0.5">
<span>
<span class="block text-sm font-medium text-gray-700">保留现有登录数据</span>
<span class="block text-xs text-gray-400 mt-0.5">沿用当前缓存、Cookie和Token,已登录账号可直接复用。</span>
</span>
</label>
<label class="flex items-start gap-2 border border-orange-200 bg-orange-50 rounded p-3 cursor-pointer">
<input type="radio" name="chatgpt_auth_mode" value="clear" x-model="chatgptAuthMode" class="mt-0.5">
<span>
<span class="block text-sm font-medium text-orange-800">清除缓存、Cookie和Token</span>
<span class="block text-xs text-orange-600 mt-0.5">清除每个账号浏览器窗口的登录数据,并重新走Google授权流程。</span>
</span>
</label>
</div>
<div class="flex gap-2 mt-3">
<button @click="showChatgptRunModal=false" class="btn btn-red">取消</button>
<button @click="confirmChatgptRegister()" class="btn btn-green flex-1">开始执行</button>
</div>
</div>
</div>
<!-- ===== 查询套餐连接方式弹窗 ===== -->
<div x-show="showOpenAIPlanRunModal" x-cloak class="modal-backdrop"
@click.self="showOpenAIPlanRunModal=false" @keydown.escape.window="showOpenAIPlanRunModal=false">
<div class="modal-box modal-compact" role="dialog" aria-modal="true" aria-labelledby="openai-plan-run-title">
<h3 id="openai-plan-run-title" class="text-base font-semibold text-gray-800 mb-2">查询套餐</h3>
<p class="text-xs text-gray-500 mb-3"
x-text="'本次将查询 ' + (selectedCount > 0 ? selectedCount : accounts.length) + ' 个账号,请选择连接方式。'"></p>
<div class="space-y-2">
<label class="flex items-start gap-2 border border-gray-200 rounded p-3 cursor-pointer">
<input type="radio" name="openai_plan_connection_mode" value="browser_proxy"
x-model="openAIPlanConnectionMode" class="mt-0.5">
<span>
<span class="block text-sm font-medium text-gray-700">使用浏览器代理</span>
<span class="block text-xs text-gray-400 mt-0.5">使用每个账号绑定的代理;代理失败时不会回退直连。</span>
</span>
</label>
<label class="flex items-start gap-2 border border-blue-200 bg-blue-50 rounded p-3 cursor-pointer">
<input type="radio" name="openai_plan_connection_mode" value="direct"
x-model="openAIPlanConnectionMode" class="mt-0.5">
<span>
<span class="block text-sm font-medium text-blue-800">直连</span>
<span class="block text-xs text-blue-600 mt-0.5">本次查询明确绕过账号代理,直接请求套餐和用量接口。</span>
</span>
</label>
</div>
<div class="flex gap-2 mt-3">
<button @click="showOpenAIPlanRunModal=false" class="btn btn-red">取消</button>
<button @click="confirmOpenAIQueryPlan()" class="btn btn-green flex-1">开始查询</button>
</div>
</div>
</div>
<!-- ===== ChatGPT Session 弹窗 ===== -->
<div x-show="showChatgptSessionModal" x-cloak class="modal-backdrop"
@click.self="closeChatgptSession()" @keydown.escape.window="closeChatgptSession()">
<div class="modal-box modal-compact chatgpt-session-modal" role="dialog" aria-modal="true"
aria-labelledby="chatgpt-session-title">
<div class="chatgpt-session-header">
<div class="min-w-0">
<h3 id="chatgpt-session-title" class="text-base font-semibold text-gray-800">ChatGPT Session</h3>
<p class="text-xs text-gray-400 truncate mt-1" x-text="chatgptSessionEmail"></p>
</div>
<button type="button" class="chatgpt-session-close" title="关闭"
@click="closeChatgptSession()" aria-label="关闭">&times;</button>
</div>
<div x-show="chatgptSessionLoading" class="chatgpt-session-state">正在读取 Session...</div>
<div x-show="chatgptSessionError" x-text="chatgptSessionError"
class="chatgpt-session-state chatgpt-session-state--error"></div>
<textarea x-show="!chatgptSessionLoading && !chatgptSessionError" readonly spellcheck="false"
class="chatgpt-session-output" :value="chatgptSessionText"
aria-label="ChatGPT Session JSON"></textarea>
<div class="chatgpt-session-actions">
<button type="button" @click="closeChatgptSession()" class="btn btn-gray">关闭</button>
<button type="button" @click="copyChatgptSession()" class="btn btn-primary"
:disabled="chatgptSessionLoading || !!chatgptSessionError || !chatgptSessionText">复制全部</button>
</div>
</div>
</div>
<!-- ===== 最近充值卡密弹窗 ===== -->
<div x-show="showRechargeCardModal" x-cloak class="modal-backdrop"
@click.self="closeRechargeCard()" @keydown.escape.window="closeRechargeCard()">
<div class="modal-box modal-compact recharge-card-modal" role="dialog" aria-modal="true"
aria-labelledby="recharge-card-title">
<div class="chatgpt-session-header">
<div class="min-w-0">
<h3 id="recharge-card-title" class="text-base font-semibold text-gray-800">本次充值卡密</h3>
<p class="text-xs text-gray-400 truncate mt-1" x-text="rechargeCardEmail"></p>
</div>
<button type="button" class="chatgpt-session-close" title="关闭"
@click="closeRechargeCard()" aria-label="关闭">&times;</button>
</div>
<div x-show="rechargeCardLoading" class="chatgpt-session-state recharge-card-state">正在读取卡密...</div>
<div x-show="rechargeCardError" x-text="rechargeCardError"
class="chatgpt-session-state recharge-card-state chatgpt-session-state--error"></div>
<div x-show="!rechargeCardLoading && !rechargeCardError"
class="text-xs text-gray-500 mt-3 mb-2">
<span>充值完成时间:</span>
<span x-text="rechargeCardCompletedAt ? formatDate(rechargeCardCompletedAt) : '暂无完成时间'"></span>
</div>
<textarea x-show="!rechargeCardLoading && !rechargeCardError" readonly spellcheck="false"
class="chatgpt-session-output recharge-card-output" :value="rechargeCardText"
aria-label="本次充值卡密"></textarea>
<div class="chatgpt-session-actions">
<button type="button" @click="closeRechargeCard()" class="btn btn-gray">关闭</button>
<button type="button" @click="copyRechargeCard()" class="btn btn-primary"
:disabled="rechargeCardLoading || !!rechargeCardError || !rechargeCardText">复制卡密</button>
</div>
</div>
</div>
<!-- ===== OpenAI 授权 JSON 弹窗 ===== -->
<div x-show="showOpenAIAuthModal" x-cloak class="modal-backdrop"
@click.self="closeOpenAIAuth()" @keydown.escape.window="closeOpenAIAuth()">
<div class="modal-box modal-compact chatgpt-session-modal" role="dialog" aria-modal="true"
aria-labelledby="openai-auth-title">
<div class="chatgpt-session-header">
<div class="min-w-0">
<h3 id="openai-auth-title" class="text-base font-semibold text-gray-800">授权 JSON</h3>
<p class="text-xs text-gray-400 truncate mt-1" x-text="openAIAuthEmail"></p>
</div>
<button type="button" class="chatgpt-session-close" title="关闭"
@click="closeOpenAIAuth()" aria-label="关闭">&times;</button>
</div>
<div x-show="openAIAuthLoading" class="chatgpt-session-state">正在读取授权 JSON...</div>
<div x-show="openAIAuthError" x-text="openAIAuthError"
class="chatgpt-session-state chatgpt-session-state--error"></div>
<textarea x-show="!openAIAuthLoading && !openAIAuthError" readonly spellcheck="false"
class="chatgpt-session-output" :value="openAIAuthText" aria-label="OpenAI 授权 JSON"></textarea>
<div class="chatgpt-session-actions">
<button type="button" @click="closeOpenAIAuth()" class="btn btn-gray">关闭</button>
<button type="button" @click="copyOpenAIAuth()" class="btn btn-primary"
:disabled="openAIAuthLoading || !!openAIAuthError || !openAIAuthText">复制全部</button>
</div>
</div>
</div>
<!-- ===== 反重力 AC 凭证弹窗 ===== -->
<div x-show="showAgCredentialModal" x-cloak class="modal-backdrop"
@click.self="closeAgCredential()" @keydown.escape.window="closeAgCredential()">
<div class="modal-box modal-compact chatgpt-session-modal" role="dialog" aria-modal="true"
aria-labelledby="ag-credential-title">
<div class="chatgpt-session-header">
<div class="min-w-0">
<h3 id="ag-credential-title" class="text-base font-semibold text-gray-800">AC 凭证</h3>
<p class="text-xs text-gray-400 truncate mt-1" x-text="agCredentialEmail"></p>
</div>
<button type="button" class="chatgpt-session-close" title="关闭"
@click="closeAgCredential()" aria-label="关闭">&times;</button>
</div>
<div x-show="agCredentialLoading" class="chatgpt-session-state">正在读取 AC 凭证...</div>
<div x-show="agCredentialError" x-text="agCredentialError"
class="chatgpt-session-state chatgpt-session-state--error"></div>
<textarea x-show="!agCredentialLoading && !agCredentialError" readonly spellcheck="false"
class="chatgpt-session-output" :value="agCredentialText" aria-label="反重力 AC 凭证"></textarea>
<div class="chatgpt-session-actions">
<button type="button" @click="closeAgCredential()" class="btn btn-gray">关闭</button>
<button type="button" @click="copyAgCredential()" class="btn btn-primary"
:disabled="agCredentialLoading || !!agCredentialError || !agCredentialText">复制全部</button>
</div>
</div>
</div>
<!-- ===== Gemini CLI AC 凭证弹窗 ===== -->
<div x-show="showCliAcCredentialModal" x-cloak class="modal-backdrop"
@click.self="closeCliAcCredential()" @keydown.escape.window="closeCliAcCredential()">
<div class="modal-box modal-compact chatgpt-session-modal" role="dialog" aria-modal="true"
aria-labelledby="cli-ac-credential-title">
<div class="chatgpt-session-header">
<div class="min-w-0">
<h3 id="cli-ac-credential-title" class="text-base font-semibold text-gray-800">Cli_AC 凭证</h3>
<p class="text-xs text-gray-400 truncate mt-1" x-text="cliAcCredentialEmail"></p>
</div>
<button type="button" class="chatgpt-session-close" title="关闭"
@click="closeCliAcCredential()" aria-label="关闭">&times;</button>
</div>
<div x-show="cliAcCredentialLoading" class="chatgpt-session-state">正在读取 Cli_AC 凭证...</div>
<div x-show="cliAcCredentialError" x-text="cliAcCredentialError"
class="chatgpt-session-state chatgpt-session-state--error"></div>
<textarea x-show="!cliAcCredentialLoading && !cliAcCredentialError" readonly spellcheck="false"
class="chatgpt-session-output" :value="cliAcCredentialText" aria-label="Gemini CLI AC 凭证"></textarea>
<div class="chatgpt-session-actions">
<button type="button" @click="closeCliAcCredential()" class="btn btn-gray">关闭</button>
<button type="button" @click="copyCliAcCredential()" class="btn btn-primary"
:disabled="cliAcCredentialLoading || !!cliAcCredentialError || !cliAcCredentialText">复制全部</button>
</div>
</div>
</div>
<!-- ===== 单个邮箱的 AI Studio API Key(JSON)弹窗 ===== -->
<div x-show="showAistudioKeysModal" x-cloak class="modal-backdrop"
@click.self="showAistudioKeysModal=false" @keydown.escape.window="showAistudioKeysModal=false">
<div class="modal-box modal-compact chatgpt-session-modal" role="dialog" aria-modal="true"
aria-labelledby="aistudio-keys-title">
<div class="chatgpt-session-header">
<div class="min-w-0">
<h3 id="aistudio-keys-title" class="text-base font-semibold text-gray-800">AI Studio API Key(明文)</h3>
<p class="text-xs text-gray-400 truncate mt-1" x-text="aistudioKeyEmail"></p>
</div>
<button type="button" class="chatgpt-session-close" title="关闭"
@click="showAistudioKeysModal=false" aria-label="关闭">&times;</button>
</div>
<textarea readonly spellcheck="false" class="chatgpt-session-output"
:value="JSON.stringify(aistudioKeyRecord || {}, null, 2)" aria-label="单个邮箱 AI Studio API Key JSON"></textarea>
<div class="chatgpt-session-actions">
<button type="button" @click="showAistudioKeysModal=false" class="btn btn-gray">关闭</button>
<button type="button" @click="copyAistudioKeys()" class="btn btn-primary"
:disabled="!aistudioKeyRecord">复制全部</button>
</div>
</div>
</div>
<!-- 复制成功 Toast -->
<div x-show="copyToastVisible" x-cloak
style="position:fixed;bottom:32px;left:50%;transform:translateX(-50%);z-index:9999;pointer-events:none"
class="copy-toast">
✓ 已复制
</div>
<script src="/app.js"></script>
<!-- ===== 截图弹窗 ===== -->
<div x-show="showScreenshotModal" x-cloak
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75" @click.self="closeScreenshot()"
@keydown.escape.window="closeScreenshot()">
<div class="bg-white rounded-lg shadow-2xl" style="width:90vw;height:90vh;display:flex;flex-direction:column">
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-200 flex-shrink-0">
<span class="text-sm font-medium text-gray-700">截图 · <span class="text-gray-400"
x-text="screenshotEmail"></span></span>
<button @click="closeScreenshot()" class="text-gray-400 hover:text-gray-600 text-xl leading-none">✕</button>
</div>
<div class="p-4 overflow-auto flex-1 flex items-center justify-center">
<img :src="screenshotUrl" style="max-width:100%;max-height:100%;object-fit:contain"
class="rounded border border-gray-100" alt="失败截图">
</div>
</div>
</div>
<!-- 截图悬浮预览 -->
<div id="screenshotPreview" x-show="screenshotHover" x-cloak
style="position:fixed;z-index:9999;pointer-events:none;background:#fff;border-radius:8px;box-shadow:0 8px 30px rgba(0,0,0,.3);padding:4px;max-width:400px;max-height:320px">
<img :src="screenshotHover" style="max-width:100%;max-height:310px;object-fit:contain;border-radius:6px" alt="预览">
</div>
</body>
</html>