diff --git a/backend/cmd/main.go b/backend/cmd/main.go index b6dd95f..8db06b0 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -154,6 +154,13 @@ func startServer(cfg *config.Config) { mux.HandleFunc("/api/monitor/settings", api.CORS(api.HandleGetMonitorSettings)) mux.HandleFunc("/api/monitor/settings/save", api.CORS(api.HandleSaveMonitorSettings)) + // Team-Reg 自动注册 API + mux.HandleFunc("/api/team-reg/start", api.CORS(api.HandleTeamRegStart)) + mux.HandleFunc("/api/team-reg/stop", api.CORS(api.HandleTeamRegStop)) + mux.HandleFunc("/api/team-reg/status", api.CORS(api.HandleTeamRegStatus)) + mux.HandleFunc("/api/team-reg/logs", api.HandleTeamRegLogs) // SSE + mux.HandleFunc("/api/team-reg/import", api.CORS(api.HandleTeamRegImport)) + // 嵌入的前端静态文件 if web.IsEmbedded() { webFS := web.GetFileSystem() diff --git a/backend/internal/api/team_reg_exec.go b/backend/internal/api/team_reg_exec.go new file mode 100644 index 0000000..bda7089 --- /dev/null +++ b/backend/internal/api/team_reg_exec.go @@ -0,0 +1,536 @@ +package api + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "sort" + "strings" + "sync" + "time" + + "codex-pool/internal/database" + "codex-pool/internal/logger" +) + +// TeamRegConfig 注册配置 +type TeamRegConfig struct { + Count int `json:"count"` // 注册数量 + Concurrency int `json:"concurrency"` // 并发线程数 + Proxy string `json:"proxy"` // 代理地址 + AutoImport bool `json:"auto_import"` // 完成后自动导入 +} + +// TeamRegState 运行状态 +type TeamRegState struct { + Running bool `json:"running"` + StartedAt time.Time `json:"started_at"` + Config TeamRegConfig `json:"config"` + Logs []string `json:"logs"` + OutputFile string `json:"output_file"` // 生成的 JSON 文件 + Imported int `json:"imported"` // 已导入数量 + mu sync.Mutex + cmd *exec.Cmd + cancel context.CancelFunc + stdin io.WriteCloser +} + +var teamRegState = &TeamRegState{ + Logs: make([]string, 0), +} + +// HandleTeamRegStart POST /api/team-reg/start - 启动注册进程 +func HandleTeamRegStart(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + teamRegState.mu.Lock() + if teamRegState.Running { + teamRegState.mu.Unlock() + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": false, + "message": "已有注册任务在运行中", + }) + return + } + + var config TeamRegConfig + if err := json.NewDecoder(r.Body).Decode(&config); err != nil { + teamRegState.mu.Unlock() + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + // 验证参数 + if config.Count < 1 { + config.Count = 1 + } + if config.Count > 100 { + config.Count = 100 + } + if config.Concurrency < 1 { + config.Concurrency = 1 + } + if config.Concurrency > 10 { + config.Concurrency = 10 + } + + // 重置状态 + teamRegState.Running = true + teamRegState.StartedAt = time.Now() + teamRegState.Config = config + teamRegState.Logs = make([]string, 0) + teamRegState.OutputFile = "" + teamRegState.Imported = 0 + teamRegState.mu.Unlock() + + // 启动进程 + go runTeamRegProcess(config) + + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "注册任务已启动", + }) +} + +// HandleTeamRegStop POST /api/team-reg/stop - 停止注册进程 +func HandleTeamRegStop(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + teamRegState.mu.Lock() + defer teamRegState.mu.Unlock() + + if !teamRegState.Running { + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": false, + "message": "没有正在运行的任务", + }) + return + } + + // 发送 Ctrl+C 信号 + if teamRegState.cancel != nil { + teamRegState.cancel() + } + + // 如果进程还在,强制终止 + if teamRegState.cmd != nil && teamRegState.cmd.Process != nil { + teamRegState.cmd.Process.Kill() + } + + teamRegState.Running = false + addTeamRegLog("[系统] 任务已被用户停止") + + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "任务已停止", + }) +} + +// HandleTeamRegStatus GET /api/team-reg/status - 获取状态和日志 +func HandleTeamRegStatus(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + teamRegState.mu.Lock() + state := map[string]interface{}{ + "running": teamRegState.Running, + "started_at": teamRegState.StartedAt, + "config": teamRegState.Config, + "logs": teamRegState.Logs, + "output_file": teamRegState.OutputFile, + "imported": teamRegState.Imported, + } + teamRegState.mu.Unlock() + + json.NewEncoder(w).Encode(state) +} + +// HandleTeamRegLogs GET /api/team-reg/logs - SSE 实时日志流 +func HandleTeamRegLogs(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Streaming not supported", http.StatusInternalServerError) + return + } + + lastIndex := 0 + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-r.Context().Done(): + return + case <-ticker.C: + teamRegState.mu.Lock() + running := teamRegState.Running + logs := teamRegState.Logs + teamRegState.mu.Unlock() + + // 发送新日志 + if len(logs) > lastIndex { + for i := lastIndex; i < len(logs); i++ { + fmt.Fprintf(w, "data: %s\n\n", logs[i]) + } + lastIndex = len(logs) + flusher.Flush() + } + + // 发送状态 + if !running { + fmt.Fprintf(w, "event: done\ndata: finished\n\n") + flusher.Flush() + return + } + } + } +} + +// HandleTeamRegImport POST /api/team-reg/import - 导入生成的 JSON 到数据库 +func HandleTeamRegImport(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + teamRegState.mu.Lock() + outputFile := teamRegState.OutputFile + teamRegState.mu.Unlock() + + if outputFile == "" { + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": false, + "message": "没有可导入的文件", + }) + return + } + + count, err := importAccountsFromJSON(outputFile) + if err != nil { + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": false, + "message": fmt.Sprintf("导入失败: %v", err), + }) + return + } + + teamRegState.mu.Lock() + teamRegState.Imported = count + teamRegState.mu.Unlock() + + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": fmt.Sprintf("成功导入 %d 个账号", count), + "count": count, + }) +} + +// runTeamRegProcess 执行 team-reg 进程 +func runTeamRegProcess(config TeamRegConfig) { + defer func() { + teamRegState.mu.Lock() + teamRegState.Running = false + teamRegState.mu.Unlock() + }() + + // 查找 team-reg 可执行文件 + execPath := findTeamRegExecutable() + if execPath == "" { + addTeamRegLog("[错误] 找不到 team-reg 可执行文件") + addTeamRegLog("[提示] 请确保 team-reg 文件位于 backend 目录下") + return + } + + addTeamRegLog(fmt.Sprintf("[系统] 找到可执行文件: %s", execPath)) + + // Linux/macOS 上自动设置执行权限 + if runtime.GOOS != "windows" { + if err := os.Chmod(execPath, 0755); err != nil { + addTeamRegLog(fmt.Sprintf("[警告] 设置执行权限失败: %v", err)) + } else { + addTeamRegLog("[系统] 已设置执行权限 (chmod +x)") + } + } + + addTeamRegLog(fmt.Sprintf("[系统] 配置: 数量=%d, 并发=%d, 代理=%s", + config.Count, config.Concurrency, config.Proxy)) + + // 创建上下文用于取消 + ctx, cancel := context.WithCancel(context.Background()) + teamRegState.mu.Lock() + teamRegState.cancel = cancel + teamRegState.mu.Unlock() + + // 创建命令 + cmd := exec.CommandContext(ctx, execPath) + + // 设置工作目录(输出文件会保存在这里) + workDir := filepath.Dir(execPath) + cmd.Dir = workDir + + // 获取 stdin, stdout, stderr + stdin, err := cmd.StdinPipe() + if err != nil { + addTeamRegLog(fmt.Sprintf("[错误] 无法获取 stdin: %v", err)) + return + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + addTeamRegLog(fmt.Sprintf("[错误] 无法获取 stdout: %v", err)) + return + } + + stderr, err := cmd.StderrPipe() + if err != nil { + addTeamRegLog(fmt.Sprintf("[错误] 无法获取 stderr: %v", err)) + return + } + + teamRegState.mu.Lock() + teamRegState.cmd = cmd + teamRegState.stdin = stdin + teamRegState.mu.Unlock() + + // 启动进程 + addTeamRegLog("[系统] 启动 team-reg 进程...") + if err := cmd.Start(); err != nil { + addTeamRegLog(fmt.Sprintf("[错误] 启动失败: %v", err)) + return + } + + // 合并 stdout 和 stderr 读取 + go readOutput(stdout, workDir, config) + go readOutput(stderr, workDir, config) + + // 等待一小段时间让程序启动 + time.Sleep(500 * time.Millisecond) + + // 发送输入参数 + addTeamRegLog(fmt.Sprintf("[输入] 注册数量: %d", config.Count)) + fmt.Fprintf(stdin, "%d\n", config.Count) + time.Sleep(200 * time.Millisecond) + + addTeamRegLog(fmt.Sprintf("[输入] 并发线程数: %d", config.Concurrency)) + fmt.Fprintf(stdin, "%d\n", config.Concurrency) + time.Sleep(200 * time.Millisecond) + + addTeamRegLog(fmt.Sprintf("[输入] 代理地址: %s", config.Proxy)) + fmt.Fprintf(stdin, "%s\n", config.Proxy) + + // 等待进程完成 + err = cmd.Wait() + if err != nil { + if ctx.Err() == context.Canceled { + addTeamRegLog("[系统] 进程已被取消") + } else { + addTeamRegLog(fmt.Sprintf("[系统] 进程退出: %v", err)) + } + } else { + addTeamRegLog("[系统] 进程正常完成") + } + + // 查找输出文件 + outputFile := findLatestOutputFile(workDir) + if outputFile != "" { + teamRegState.mu.Lock() + teamRegState.OutputFile = outputFile + teamRegState.mu.Unlock() + addTeamRegLog(fmt.Sprintf("[系统] 输出文件: %s", filepath.Base(outputFile))) + + // 自动导入 + if config.AutoImport { + addTeamRegLog("[系统] 自动导入账号到数据库...") + count, err := importAccountsFromJSON(outputFile) + if err != nil { + addTeamRegLog(fmt.Sprintf("[错误] 导入失败: %v", err)) + } else { + teamRegState.mu.Lock() + teamRegState.Imported = count + teamRegState.mu.Unlock() + addTeamRegLog(fmt.Sprintf("[系统] 成功导入 %d 个账号", count)) + } + } + } + + // 发送回车退出程序(如果还在运行) + time.Sleep(500 * time.Millisecond) + if stdin != nil { + fmt.Fprintf(stdin, "\n") + } +} + +// readOutput 读取进程输出 +func readOutput(reader io.Reader, workDir string, config TeamRegConfig) { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := scanner.Text() + // 过滤空行和只有空格的行 + trimmed := strings.TrimSpace(line) + if trimmed != "" { + addTeamRegLog(trimmed) + } + } +} + +// addTeamRegLog 添加日志 +func addTeamRegLog(log string) { + teamRegState.mu.Lock() + defer teamRegState.mu.Unlock() + + timestamp := time.Now().Format("15:04:05") + fullLog := fmt.Sprintf("[%s] %s", timestamp, log) + teamRegState.Logs = append(teamRegState.Logs, fullLog) + + // 限制日志数量 + if len(teamRegState.Logs) > 1000 { + teamRegState.Logs = teamRegState.Logs[len(teamRegState.Logs)-1000:] + } + + // 同时输出到系统日志 + logger.Info(fmt.Sprintf("[TeamReg] %s", log), "", "team-reg") +} + +// findTeamRegExecutable 查找 team-reg 可执行文件 +func findTeamRegExecutable() string { + // 可能的文件名 + var names []string + if runtime.GOOS == "windows" { + names = []string{"team-reg.exe", "team-reg"} + } else { + names = []string{"team-reg", "team-reg.exe"} + } + + // 可能的路径 + paths := []string{ + ".", // 当前目录 + "..", // 上级目录 + "../", // 项目根目录 + filepath.Join("..", ".."), // 更上级 + } + + // 获取可执行文件所在目录 + execDir, err := os.Executable() + if err == nil { + execDir = filepath.Dir(execDir) + paths = append(paths, execDir, filepath.Join(execDir, "..")) + } + + for _, basePath := range paths { + for _, name := range names { + fullPath := filepath.Join(basePath, name) + if absPath, err := filepath.Abs(fullPath); err == nil { + if _, err := os.Stat(absPath); err == nil { + return absPath + } + } + } + } + + return "" +} + +// findLatestOutputFile 查找最新的输出文件 +func findLatestOutputFile(dir string) string { + pattern := filepath.Join(dir, "accounts-*.json") + matches, err := filepath.Glob(pattern) + if err != nil || len(matches) == 0 { + return "" + } + + // 按修改时间排序,取最新的 + sort.Slice(matches, func(i, j int) bool { + fi, _ := os.Stat(matches[i]) + fj, _ := os.Stat(matches[j]) + if fi == nil || fj == nil { + return false + } + return fi.ModTime().After(fj.ModTime()) + }) + + // 确保是最近创建的文件(5分钟内) + fi, err := os.Stat(matches[0]) + if err != nil { + return "" + } + if time.Since(fi.ModTime()) > 5*time.Minute { + return "" + } + + return matches[0] +} + +// TeamRegAccount team-reg 输出的账号格式 +type TeamRegAccount struct { + Account string `json:"account"` + Password string `json:"password"` + Token string `json:"token"` + AccountID string `json:"account_id"` + PlanType string `json:"plan_type"` +} + +// importAccountsFromJSON 从 JSON 文件导入账号 +func importAccountsFromJSON(filePath string) (int, error) { + if database.Instance == nil { + return 0, fmt.Errorf("数据库未初始化") + } + + data, err := os.ReadFile(filePath) + if err != nil { + return 0, err + } + + var accounts []TeamRegAccount + if err := json.Unmarshal(data, &accounts); err != nil { + return 0, err + } + + // 转换为 database.TeamOwner 格式 + var owners []database.TeamOwner + for _, acc := range accounts { + if acc.Account == "" || acc.Password == "" { + continue + } + + // 提取 account_id(去掉 org- 前缀如果有的话) + accountID := acc.AccountID + if strings.HasPrefix(accountID, "org-") { + accountID = strings.TrimPrefix(accountID, "org-") + } + + owners = append(owners, database.TeamOwner{ + Email: acc.Account, + Password: acc.Password, + Token: acc.Token, + AccountID: accountID, + }) + } + + // 批量导入 + count, err := database.Instance.AddTeamOwners(owners) + if err != nil { + return 0, err + } + + return count, nil +} diff --git a/backend/team-reg b/backend/team-reg new file mode 100644 index 0000000..a0cd0bd Binary files /dev/null and b/backend/team-reg differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 050af0e..6629b65 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,7 @@ import { Routes, Route } from 'react-router-dom' import { ConfigProvider, RecordsProvider } from './context' import { Layout } from './components/layout' -import { Dashboard, Upload, Records, Accounts, Config, S2AConfig, EmailConfig, Monitor, Cleaner } from './pages' +import { Dashboard, Upload, Records, Accounts, Config, S2AConfig, EmailConfig, Monitor, Cleaner, TeamReg } from './pages' function App() { return ( @@ -15,6 +15,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index f4ff33d..dea4c7b 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -14,6 +14,7 @@ import { Mail, Cog, Trash2, + UserPlus, } from 'lucide-react' interface SidebarProps { @@ -35,6 +36,7 @@ const navItems: NavItem[] = [ { to: '/accounts', icon: Users, label: '号池账号' }, { to: '/monitor', icon: Activity, label: '号池监控' }, { to: '/cleaner', icon: Trash2, label: '定期清理' }, + { to: '/team-reg', icon: UserPlus, label: 'Team 注册' }, { to: '/config', icon: Settings, diff --git a/frontend/src/pages/TeamReg.tsx b/frontend/src/pages/TeamReg.tsx new file mode 100644 index 0000000..45b8bbc --- /dev/null +++ b/frontend/src/pages/TeamReg.tsx @@ -0,0 +1,476 @@ +import { useState, useEffect, useRef, useCallback } from 'react' +import { + Play, + Square, + Download, + Terminal, + Settings, + CheckCircle, + AlertTriangle, + Clock, + Loader2, + RefreshCw, + FileJson, + Users, +} from 'lucide-react' +import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common' + +interface TeamRegStatus { + running: boolean + started_at: string + config: { + count: number + concurrency: number + proxy: string + auto_import: boolean + } + logs: string[] + output_file: string + imported: number +} + +export default function TeamReg() { + const [status, setStatus] = useState(null) + const [loading, setLoading] = useState(false) + const [starting, setStarting] = useState(false) + const [stopping, setStopping] = useState(false) + const [importing, setImporting] = useState(false) + + // 配置表单 + const [count, setCount] = useState(5) + const [concurrency, setConcurrency] = useState(2) + const [proxy, setProxy] = useState('') + const [autoImport, setAutoImport] = useState(true) + + // 日志相关 + const logsContainerRef = useRef(null) + const [autoScroll, setAutoScroll] = useState(true) + + // 获取状态 + const fetchStatus = useCallback(async () => { + setLoading(true) + try { + const res = await fetch('/api/team-reg/status') + if (res.ok) { + const data = await res.json() + setStatus(data) + } + } catch (e) { + console.error('获取状态失败:', e) + } + setLoading(false) + }, []) + + // 初始化和轮询 + useEffect(() => { + fetchStatus() + + // 如果正在运行,每秒刷新状态 + const interval = setInterval(() => { + fetchStatus() + }, 1000) + + return () => clearInterval(interval) + }, [fetchStatus]) + + // 自动滚动到底部 + useEffect(() => { + if (autoScroll && logsContainerRef.current) { + logsContainerRef.current.scrollTop = logsContainerRef.current.scrollHeight + } + }, [status?.logs, autoScroll]) + + // 启动注册 + const handleStart = async () => { + setStarting(true) + try { + const res = await fetch('/api/team-reg/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + count, + concurrency, + proxy, + auto_import: autoImport, + }), + }) + const data = await res.json() + if (data.success) { + await fetchStatus() + } else { + alert(data.message || '启动失败') + } + } catch (e) { + console.error('启动失败:', e) + alert('启动失败: ' + (e instanceof Error ? e.message : '网络错误')) + } + setStarting(false) + } + + // 停止注册 + const handleStop = async () => { + if (!confirm('确定要停止当前注册任务吗?')) return + setStopping(true) + try { + const res = await fetch('/api/team-reg/stop', { method: 'POST' }) + const data = await res.json() + if (data.success) { + await fetchStatus() + } else { + alert(data.message || '停止失败') + } + } catch (e) { + console.error('停止失败:', e) + } + setStopping(false) + } + + // 手动导入 + const handleImport = async () => { + setImporting(true) + try { + const res = await fetch('/api/team-reg/import', { method: 'POST' }) + const data = await res.json() + if (data.success) { + alert(data.message) + await fetchStatus() + } else { + alert(data.message || '导入失败') + } + } catch (e) { + console.error('导入失败:', e) + alert('导入失败: ' + (e instanceof Error ? e.message : '网络错误')) + } + setImporting(false) + } + + const isRunning = status?.running || false + const logs = status?.logs || [] + const outputFile = status?.output_file || '' + const importedCount = status?.imported || 0 + + // 解析日志中的成功/失败计数 + const parseLogStats = () => { + let success = 0 + let failed = 0 + logs.forEach(log => { + if (log.includes('[OK]') && (log.includes('注册成功') || log.includes('支付成功'))) { + success++ + } + if (log.includes('[!]') || log.includes('[错误]')) { + failed++ + } + }) + return { success: Math.floor(success / 2), failed } // 注册+支付各算一次 + } + + const logStats = parseLogStats() + + return ( +
+ {/* Header */} +
+
+

Team 自动注册

+

+ 批量注册 ChatGPT Team 账号并自动支付 +

+
+
+ +
+
+ + {/* 状态卡片 */} +
+ {/* 运行状态 */} + + +
+
+

运行状态

+

+ {isRunning ? '运行中' : '空闲'} +

+
+
+ {isRunning ? ( + + ) : ( + + )} +
+
+
+
+ + {/* 成功数 */} + + +
+
+

成功注册

+

{logStats.success}

+
+
+ +
+
+
+
+ + {/* 失败数 */} + + +
+
+

失败/重试

+

{logStats.failed}

+
+
+ +
+
+
+
+ + {/* 已导入 */} + + +
+
+

已导入

+

{importedCount}

+
+
+ +
+
+
+
+
+ +
+ {/* 配置面板 */} + + + + + 注册配置 + + + + setCount(Number(e.target.value))} + hint="要注册的 Team 账号数量 (1-100)" + disabled={isRunning} + /> + setConcurrency(Number(e.target.value))} + hint="同时进行的注册任务数 (1-10)" + disabled={isRunning} + /> + setProxy(e.target.value)} + placeholder="留空使用默认代理" + hint="HTTP 代理地址,如 http://127.0.0.1:7890" + disabled={isRunning} + /> +
+ +
+ + {/* 操作按钮 */} +
+ {!isRunning ? ( + + ) : ( + + )} +
+ + {/* 输出文件和导入按钮 */} + {outputFile && ( +
+
+ + 输出文件: + + {outputFile.split(/[/\\]/).pop()} + +
+ {!autoImport && ( + + )} +
+ )} +
+
+ + {/* 实时日志 */} + + + + + 实时日志 + +
+ + + {logs.length} 条日志 + +
+
+ +
+ {logs.length === 0 ? ( +
+
+ +

等待启动注册任务...

+
+
+ ) : ( +
+ {logs.map((log, i) => ( + + ))} +
+ )} +
+
+
+
+ + {/* 使用说明 */} + + + + + 使用说明 + + + +
+
+

流程

+
    +
  1. 设置注册数量和并发数
  2. +
  3. 配置代理地址(可选)
  4. +
  5. 点击"开始注册"启动任务
  6. +
  7. 等待注册完成,观察实时日志
  8. +
  9. 完成后自动/手动导入母号列表
  10. +
+
+
+

注意事项

+
    +
  • 需要在服务器上放置 team-reg 可执行文件
  • +
  • 程序会自动处理 SEPA 支付
  • +
  • 支持中断恢复,Ctrl+C 会保存已完成的账号
  • +
  • 导入后的账号会出现在"母号管理"页面
  • +
+
+
+
+
+
+ ) +} + +// 日志行组件 - 根据内容着色 +function LogLine({ log }: { log: string }) { + let colorClass = 'text-slate-300' + + if (log.includes('[OK]') || log.includes('成功')) { + colorClass = 'text-green-400' + } else if (log.includes('[!]') || log.includes('重试') || log.includes('[错误]')) { + colorClass = 'text-orange-400' + } else if (log.includes('[系统]') || log.includes('[输入]')) { + colorClass = 'text-blue-400' + } else if (log.includes('[W1]')) { + colorClass = 'text-cyan-400' + } else if (log.includes('[W2]')) { + colorClass = 'text-purple-400' + } else if (log.includes('[W3]')) { + colorClass = 'text-yellow-400' + } else if (log.includes('[W4]')) { + colorClass = 'text-pink-400' + } else if (log.includes('完成') || log.includes('结果已保存')) { + colorClass = 'text-emerald-400 font-medium' + } + + return ( +
+ {log} +
+ ) +} diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index 2f11d96..27ca6a2 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -7,4 +7,6 @@ export { default as S2AConfig } from './S2AConfig' export { default as EmailConfig } from './EmailConfig' export { default as Monitor } from './Monitor' export { default as Cleaner } from './Cleaner' +export { default as TeamReg } from './TeamReg' +