diff --git a/backend/internal/api/team_reg_exec.go b/backend/internal/api/team_reg_exec.go index c251aaa..39c1d6c 100644 --- a/backend/internal/api/team_reg_exec.go +++ b/backend/internal/api/team_reg_exec.go @@ -44,6 +44,10 @@ type TeamRegState struct { cmd *exec.Cmd cancel context.CancelFunc stdin io.WriteCloser + // 自动导入与退出控制 + autoImporting bool + autoImported bool + exitSignaled bool // 403 错误检测 error403Count int // 403 错误计数 error403Start time.Time // 计数开始时间 @@ -104,6 +108,9 @@ func HandleTeamRegStart(w http.ResponseWriter, r *http.Request) { teamRegState.Logs = make([]string, 0) teamRegState.OutputFile = "" teamRegState.Imported = 0 + teamRegState.autoImporting = false + teamRegState.autoImported = false + teamRegState.exitSignaled = false teamRegState.error403Count = 0 teamRegState.error403Start = time.Now() teamRegState.mu.Unlock() @@ -368,42 +375,25 @@ func runTeamRegProcess(config TeamRegConfig) { addTeamRegLog("[系统] 进程正常完成") } - // 查找输出文件 - outputFile := findLatestOutputFile(workDir) - if outputFile != "" { - teamRegState.mu.Lock() - teamRegState.OutputFile = outputFile - teamRegState.mu.Unlock() - addTeamRegLog(fmt.Sprintf("[系统] 输出文件: %s", filepath.Base(outputFile))) + // 查找输出文件 + 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)) - - // 导入成功后删除 JSON 文件 - if err := os.Remove(outputFile); err != nil { - addTeamRegLog(fmt.Sprintf("[警告] 删除临时文件失败: %v", err)) - } else { - addTeamRegLog(fmt.Sprintf("[系统] 已清理临时文件: %s", filepath.Base(outputFile))) - } + // 自动导入 + if config.AutoImport { + addTeamRegLog("[系统] 自动导入账号到数据库...") + tryAutoImport(outputFile, config) } } - } - // 发送回车退出程序(如果还在运行) - time.Sleep(500 * time.Millisecond) - if stdin != nil { - fmt.Fprintf(stdin, "\n") + // 发送回车退出程序(如果还在运行) + time.Sleep(500 * time.Millisecond) + signalTeamRegExit() } -} // readOutput 读取进程输出 func readOutput(reader io.Reader, workDir string, config TeamRegConfig) { @@ -415,23 +405,26 @@ func readOutput(reader io.Reader, workDir string, config TeamRegConfig) { if trimmed != "" { addTeamRegLog(trimmed) - // 检测输出文件名(例如:结果已保存到: accounts-2-20260201-071558.json) - if strings.Contains(trimmed, "结果已保存到") || strings.Contains(trimmed, "accounts-") && strings.Contains(trimmed, ".json") { - // 尝试提取文件名 - if idx := strings.Index(trimmed, "accounts-"); idx >= 0 { - endIdx := strings.Index(trimmed[idx:], ".json") - if endIdx > 0 { - fileName := trimmed[idx : idx+endIdx+5] // 包含 .json - // 构建完整路径 - fullPath := filepath.Join(workDir, fileName) - if _, err := os.Stat(fullPath); err == nil { + // 检测输出文件名(例如:结果已保存到: accounts-2-20260201-071558.json) + if strings.Contains(trimmed, "结果已保存到") || strings.Contains(trimmed, "accounts-") && strings.Contains(trimmed, ".json") { + // 尝试提取文件名 + if idx := strings.Index(trimmed, "accounts-"); idx >= 0 { + endIdx := strings.Index(trimmed[idx:], ".json") + if endIdx > 0 { + fileName := trimmed[idx : idx+endIdx+5] // 包含 .json + // 构建完整路径 + fullPath := filepath.Join(workDir, fileName) teamRegState.mu.Lock() teamRegState.OutputFile = fullPath teamRegState.mu.Unlock() + if config.AutoImport { + go tryAutoImport(fullPath, config) + } + // 发送回车提示退出(有些程序会在完成后等待回车) + signalTeamRegExit() } } } - } // 检测 403 错误 if strings.Contains(trimmed, "403") { @@ -482,6 +475,80 @@ func check403AndStop() bool { return false } +// tryAutoImport 尝试自动导入(仅执行一次) +func tryAutoImport(filePath string, config TeamRegConfig) { + if !config.AutoImport || filePath == "" { + return + } + + teamRegState.mu.Lock() + if teamRegState.autoImported || teamRegState.autoImporting { + teamRegState.mu.Unlock() + return + } + teamRegState.autoImporting = true + teamRegState.mu.Unlock() + + var ( + count int + err error + ) + + // 等待文件稳定写入(最多重试几次) + for i := 0; i < 5; i++ { + if _, statErr := os.Stat(filePath); statErr != nil { + err = statErr + } else { + count, err = importAccountsFromJSON(filePath) + } + if err == nil { + break + } + time.Sleep(300 * time.Millisecond) + } + + teamRegState.mu.Lock() + teamRegState.autoImporting = false + if err == nil { + teamRegState.Imported = count + teamRegState.autoImported = true + } + teamRegState.mu.Unlock() + + if err != nil { + addTeamRegLog(fmt.Sprintf("[错误] 导入失败: %v", err)) + return + } + + addTeamRegLog(fmt.Sprintf("[系统] 成功导入 %d 个账号", count)) + + // 导入成功后删除 JSON 文件 + if err := os.Remove(filePath); err != nil { + addTeamRegLog(fmt.Sprintf("[警告] 删除临时文件失败: %v", err)) + } else { + addTeamRegLog(fmt.Sprintf("[系统] 已清理临时文件: %s", filepath.Base(filePath))) + } +} + +// signalTeamRegExit 发送回车并关闭 stdin,提示程序退出 +func signalTeamRegExit() { + teamRegState.mu.Lock() + if teamRegState.exitSignaled { + teamRegState.mu.Unlock() + return + } + teamRegState.exitSignaled = true + stdin := teamRegState.stdin + teamRegState.mu.Unlock() + + if stdin == nil { + return + } + + _, _ = fmt.Fprintln(stdin) + _ = stdin.Close() +} + // addTeamRegLog 添加日志 func addTeamRegLog(log string) { teamRegState.mu.Lock() diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index 716d3a3..40ab1ab 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -23,11 +23,16 @@ export default function Config() { const { config, isConnected, refreshConfig } = useConfig() const [siteName, setSiteName] = useState('') const [defaultProxy, setDefaultProxy] = useState('') + const [teamRegProxy, setTeamRegProxy] = useState('') const [saving, setSaving] = useState(false) const [savingProxy, setSavingProxy] = useState(false) + const [savingTeamRegProxy, setSavingTeamRegProxy] = useState(false) const [testingProxy, setTestingProxy] = useState(false) + const [testingTeamRegProxy, setTestingTeamRegProxy] = useState(false) const [proxyStatus, setProxyStatus] = useState<'unknown' | 'success' | 'error'>('unknown') + const [teamRegProxyStatus, setTeamRegProxyStatus] = useState<'unknown' | 'success' | 'error'>('unknown') const [proxyOriginIP, setProxyOriginIP] = useState('') + const [teamRegProxyIP, setTeamRegProxyIP] = useState('') const { toasts, toast, removeToast } = useToast() // 加载站点名称和代理配置 @@ -39,6 +44,7 @@ export default function Config() { if (data.code === 0 && data.data) { setSiteName(data.data.site_name || 'Codex Pool') setDefaultProxy(data.data.default_proxy || '') + setTeamRegProxy(data.data.team_reg_proxy || '') // 恢复代理测试状态 const testStatus = data.data.proxy_test_status if (testStatus === 'success' || testStatus === 'error') { @@ -135,6 +141,61 @@ export default function Config() { } } + // 保存注册代理地址 + const handleSaveTeamRegProxy = async () => { + setSavingTeamRegProxy(true) + try { + const res = await fetch('/api/config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ team_reg_proxy: teamRegProxy }), + }) + const data = await res.json() + if (data.code === 0) { + toast.success('注册代理地址已保存') + refreshConfig() + } else { + toast.error(data.message || '保存失败') + } + } catch { + toast.error('网络错误') + } finally { + setSavingTeamRegProxy(false) + } + } + + // 测试注册代理连接 + const handleTestTeamRegProxy = async () => { + if (!teamRegProxy.trim()) { + toast.error('请先输入注册代理地址') + return + } + setTestingTeamRegProxy(true) + setTeamRegProxyStatus('unknown') + setTeamRegProxyIP('') + try { + const res = await fetch('/api/proxy/test', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ proxy_url: teamRegProxy }), + }) + const data = await res.json() + if (data.code === 0 && data.data?.connected) { + setTeamRegProxyStatus('success') + setTeamRegProxyIP(data.data.origin_ip || '') + toast.success(`注册代理连接成功${data.data.origin_ip ? `, 出口IP: ${data.data.origin_ip}` : ''}`) + } else { + setTeamRegProxyStatus('error') + toast.error(data.message || '注册代理连接失败') + } + } catch (e) { + setTeamRegProxyStatus('error') + toast.error(e instanceof Error ? e.message : '网络错误') + } finally { + setTestingTeamRegProxy(false) + } + } + const configItems = [ { to: '/config/s2a', @@ -307,6 +368,72 @@ export default function Config() { )}
+ + {/* 注册代理地址配置 */} ++ Team 自动注册功能使用的代理地址。建议使用高质量住宅代理以避免被限制。 + {teamRegProxyIP && ( + + 出口IP: {teamRegProxyIP} + + )} +
++ {proxy} +
++ 如需修改,请前往 系统配置 页面 +
++ ⚠️ 未配置代理地址,请先在 系统配置 中设置 +
+ )} +