feat: implement team registration management feature with backend API and frontend UI.
This commit is contained in:
@@ -154,6 +154,13 @@ func startServer(cfg *config.Config) {
|
|||||||
mux.HandleFunc("/api/monitor/settings", api.CORS(api.HandleGetMonitorSettings))
|
mux.HandleFunc("/api/monitor/settings", api.CORS(api.HandleGetMonitorSettings))
|
||||||
mux.HandleFunc("/api/monitor/settings/save", api.CORS(api.HandleSaveMonitorSettings))
|
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() {
|
if web.IsEmbedded() {
|
||||||
webFS := web.GetFileSystem()
|
webFS := web.GetFileSystem()
|
||||||
|
|||||||
536
backend/internal/api/team_reg_exec.go
Normal file
536
backend/internal/api/team_reg_exec.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
BIN
backend/team-reg
Normal file
BIN
backend/team-reg
Normal file
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
import { Routes, Route } from 'react-router-dom'
|
import { Routes, Route } from 'react-router-dom'
|
||||||
import { ConfigProvider, RecordsProvider } from './context'
|
import { ConfigProvider, RecordsProvider } from './context'
|
||||||
import { Layout } from './components/layout'
|
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() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -15,6 +15,7 @@ function App() {
|
|||||||
<Route path="accounts" element={<Accounts />} />
|
<Route path="accounts" element={<Accounts />} />
|
||||||
<Route path="monitor" element={<Monitor />} />
|
<Route path="monitor" element={<Monitor />} />
|
||||||
<Route path="cleaner" element={<Cleaner />} />
|
<Route path="cleaner" element={<Cleaner />} />
|
||||||
|
<Route path="team-reg" element={<TeamReg />} />
|
||||||
<Route path="config" element={<Config />} />
|
<Route path="config" element={<Config />} />
|
||||||
<Route path="config/s2a" element={<S2AConfig />} />
|
<Route path="config/s2a" element={<S2AConfig />} />
|
||||||
<Route path="config/email" element={<EmailConfig />} />
|
<Route path="config/email" element={<EmailConfig />} />
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Mail,
|
Mail,
|
||||||
Cog,
|
Cog,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
UserPlus,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
@@ -35,6 +36,7 @@ const navItems: NavItem[] = [
|
|||||||
{ to: '/accounts', icon: Users, label: '号池账号' },
|
{ to: '/accounts', icon: Users, label: '号池账号' },
|
||||||
{ to: '/monitor', icon: Activity, label: '号池监控' },
|
{ to: '/monitor', icon: Activity, label: '号池监控' },
|
||||||
{ to: '/cleaner', icon: Trash2, label: '定期清理' },
|
{ to: '/cleaner', icon: Trash2, label: '定期清理' },
|
||||||
|
{ to: '/team-reg', icon: UserPlus, label: 'Team 注册' },
|
||||||
{
|
{
|
||||||
to: '/config',
|
to: '/config',
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
|
|||||||
476
frontend/src/pages/TeamReg.tsx
Normal file
476
frontend/src/pages/TeamReg.tsx
Normal file
@@ -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<TeamRegStatus | null>(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<HTMLDivElement>(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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Team 自动注册</h1>
|
||||||
|
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
批量注册 ChatGPT Team 账号并自动支付
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={fetchStatus}
|
||||||
|
disabled={loading}
|
||||||
|
icon={<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 状态卡片 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
{/* 运行状态 */}
|
||||||
|
<Card className="stat-card card-hover">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-slate-500 dark:text-slate-400">运行状态</p>
|
||||||
|
<p className={`text-2xl font-bold ${isRunning ? 'text-green-500' : 'text-slate-400'}`}>
|
||||||
|
{isRunning ? '运行中' : '空闲'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={`h-12 w-12 rounded-xl flex items-center justify-center ${isRunning ? 'bg-green-100 dark:bg-green-900/30' : 'bg-slate-100 dark:bg-slate-800'
|
||||||
|
}`}>
|
||||||
|
{isRunning ? (
|
||||||
|
<Loader2 className="h-6 w-6 text-green-500 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Terminal className="h-6 w-6 text-slate-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 成功数 */}
|
||||||
|
<Card className="stat-card card-hover">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-slate-500 dark:text-slate-400">成功注册</p>
|
||||||
|
<p className="text-2xl font-bold text-green-500">{logStats.success}</p>
|
||||||
|
</div>
|
||||||
|
<div className="h-12 w-12 rounded-xl bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
|
||||||
|
<CheckCircle className="h-6 w-6 text-green-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 失败数 */}
|
||||||
|
<Card className="stat-card card-hover">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-slate-500 dark:text-slate-400">失败/重试</p>
|
||||||
|
<p className="text-2xl font-bold text-orange-500">{logStats.failed}</p>
|
||||||
|
</div>
|
||||||
|
<div className="h-12 w-12 rounded-xl bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center">
|
||||||
|
<AlertTriangle className="h-6 w-6 text-orange-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 已导入 */}
|
||||||
|
<Card className="stat-card card-hover">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-slate-500 dark:text-slate-400">已导入</p>
|
||||||
|
<p className="text-2xl font-bold text-blue-500">{importedCount}</p>
|
||||||
|
</div>
|
||||||
|
<div className="h-12 w-12 rounded-xl bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
|
||||||
|
<Users className="h-6 w-6 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* 配置面板 */}
|
||||||
|
<Card className="glass-card">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Settings className="h-5 w-5 text-blue-500" />
|
||||||
|
注册配置
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Input
|
||||||
|
label="注册数量"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
value={count}
|
||||||
|
onChange={(e) => setCount(Number(e.target.value))}
|
||||||
|
hint="要注册的 Team 账号数量 (1-100)"
|
||||||
|
disabled={isRunning}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="并发数"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
value={concurrency}
|
||||||
|
onChange={(e) => setConcurrency(Number(e.target.value))}
|
||||||
|
hint="同时进行的注册任务数 (1-10)"
|
||||||
|
disabled={isRunning}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="代理地址"
|
||||||
|
type="text"
|
||||||
|
value={proxy}
|
||||||
|
onChange={(e) => setProxy(e.target.value)}
|
||||||
|
placeholder="留空使用默认代理"
|
||||||
|
hint="HTTP 代理地址,如 http://127.0.0.1:7890"
|
||||||
|
disabled={isRunning}
|
||||||
|
/>
|
||||||
|
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
|
||||||
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={autoImport}
|
||||||
|
onChange={(e) => setAutoImport(e.target.checked)}
|
||||||
|
disabled={isRunning}
|
||||||
|
className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-slate-900 dark:text-slate-100">完成后自动导入</p>
|
||||||
|
<p className="text-xs text-slate-500">注册完成后自动将账号导入母号列表</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="flex gap-3 pt-2">
|
||||||
|
{!isRunning ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleStart}
|
||||||
|
loading={starting}
|
||||||
|
className="flex-1"
|
||||||
|
icon={<Play className="h-4 w-4" />}
|
||||||
|
>
|
||||||
|
开始注册
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={handleStop}
|
||||||
|
loading={stopping}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1 border-red-500 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||||
|
icon={<Square className="h-4 w-4" />}
|
||||||
|
>
|
||||||
|
停止
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 输出文件和导入按钮 */}
|
||||||
|
{outputFile && (
|
||||||
|
<div className="pt-4 border-t border-slate-200 dark:border-slate-700 space-y-3">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<FileJson className="h-4 w-4 text-blue-500" />
|
||||||
|
<span className="text-slate-500">输出文件:</span>
|
||||||
|
<span className="font-mono text-slate-900 dark:text-slate-100 truncate">
|
||||||
|
{outputFile.split(/[/\\]/).pop()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{!autoImport && (
|
||||||
|
<Button
|
||||||
|
onClick={handleImport}
|
||||||
|
loading={importing}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
icon={<Download className="h-4 w-4" />}
|
||||||
|
>
|
||||||
|
手动导入到母号列表
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 实时日志 */}
|
||||||
|
<Card className="glass-card lg:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Terminal className="h-5 w-5 text-green-500" />
|
||||||
|
实时日志
|
||||||
|
</CardTitle>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="flex items-center gap-2 text-sm text-slate-500">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={autoScroll}
|
||||||
|
onChange={(e) => setAutoScroll(e.target.checked)}
|
||||||
|
className="h-3 w-3 rounded"
|
||||||
|
/>
|
||||||
|
自动滚动
|
||||||
|
</label>
|
||||||
|
<span className="text-xs text-slate-400">
|
||||||
|
{logs.length} 条日志
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div
|
||||||
|
ref={logsContainerRef}
|
||||||
|
className="h-[500px] overflow-y-auto bg-slate-900 rounded-lg p-4 font-mono text-sm"
|
||||||
|
>
|
||||||
|
{logs.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center h-full text-slate-500">
|
||||||
|
<div className="text-center">
|
||||||
|
<Terminal className="h-12 w-12 mx-auto mb-3 opacity-30" />
|
||||||
|
<p>等待启动注册任务...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{logs.map((log, i) => (
|
||||||
|
<LogLine key={i} log={log} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 使用说明 */}
|
||||||
|
<Card className="glass-card">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Clock className="h-5 w-5 text-purple-500" />
|
||||||
|
使用说明
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-slate-900 dark:text-slate-100 mb-2">流程</h4>
|
||||||
|
<ol className="list-decimal list-inside space-y-1">
|
||||||
|
<li>设置注册数量和并发数</li>
|
||||||
|
<li>配置代理地址(可选)</li>
|
||||||
|
<li>点击"开始注册"启动任务</li>
|
||||||
|
<li>等待注册完成,观察实时日志</li>
|
||||||
|
<li>完成后自动/手动导入母号列表</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-slate-900 dark:text-slate-100 mb-2">注意事项</h4>
|
||||||
|
<ul className="list-disc list-inside space-y-1">
|
||||||
|
<li>需要在服务器上放置 <code className="bg-slate-100 dark:bg-slate-800 px-1 rounded">team-reg</code> 可执行文件</li>
|
||||||
|
<li>程序会自动处理 SEPA 支付</li>
|
||||||
|
<li>支持中断恢复,Ctrl+C 会保存已完成的账号</li>
|
||||||
|
<li>导入后的账号会出现在"母号管理"页面</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志行组件 - 根据内容着色
|
||||||
|
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 (
|
||||||
|
<div className={`${colorClass} leading-relaxed whitespace-pre-wrap break-all`}>
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,4 +7,6 @@ export { default as S2AConfig } from './S2AConfig'
|
|||||||
export { default as EmailConfig } from './EmailConfig'
|
export { default as EmailConfig } from './EmailConfig'
|
||||||
export { default as Monitor } from './Monitor'
|
export { default as Monitor } from './Monitor'
|
||||||
export { default as Cleaner } from './Cleaner'
|
export { default as Cleaner } from './Cleaner'
|
||||||
|
export { default as TeamReg } from './TeamReg'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user