From 28bdc9d509302ccb4d8be26da29eb85165b46f92 Mon Sep 17 00:00:00 2001
From: kyx236
Date: Sun, 1 Feb 2026 07:33:35 +0800
Subject: [PATCH] feat: introduce a new Config page for managing site settings
and proxy configurations, and add a Team Registration page with backend API
support.
---
backend/internal/api/team_reg_exec.go | 151 +++++++++++++++++++-------
frontend/src/pages/Config.tsx | 127 ++++++++++++++++++++++
frontend/src/pages/TeamReg.tsx | 69 ++++++++++--
3 files changed, 295 insertions(+), 52 deletions(-)
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() {
)}
+
+ {/* 注册代理地址配置 */}
+
+
+
+ {/* 代理状态徽章 */}
+ {teamRegProxyStatus === 'success' && (
+
+
+ 代理可用
+
+ )}
+ {teamRegProxyStatus === 'error' && (
+
+
+ 连接失败
+
+ )}
+ {teamRegProxyStatus === 'unknown' && teamRegProxy && (
+
+
+ 未测试
+
+ )}
+
+
+ {
+ setTeamRegProxy(e.target.value)
+ setTeamRegProxyStatus('unknown')
+ }}
+ placeholder="http://user:pass@host:port"
+ className="flex-1"
+ />
+ : }
+ className="shrink-0"
+ >
+ {savingTeamRegProxy ? '保存中...' : '保存'}
+
+ : }
+ className="shrink-0"
+ >
+ {testingTeamRegProxy ? '测试中...' : '测试连接'}
+
+
+
+ Team 自动注册功能使用的代理地址。建议使用高质量住宅代理以避免被限制。
+ {teamRegProxyIP && (
+
+ 出口IP: {teamRegProxyIP}
+
+ )}
+
+
diff --git a/frontend/src/pages/TeamReg.tsx b/frontend/src/pages/TeamReg.tsx
index 45b8bbc..021e821 100644
--- a/frontend/src/pages/TeamReg.tsx
+++ b/frontend/src/pages/TeamReg.tsx
@@ -39,6 +39,7 @@ export default function TeamReg() {
// 配置表单
const [count, setCount] = useState(5)
const [concurrency, setConcurrency] = useState(2)
+ const [useProxy, setUseProxy] = useState(false)
const [proxy, setProxy] = useState('')
const [autoImport, setAutoImport] = useState(true)
@@ -46,6 +47,27 @@ export default function TeamReg() {
const logsContainerRef = useRef(null)
const [autoScroll, setAutoScroll] = useState(true)
+ // 加载保存的代理配置(从配置页面读取)
+ const loadProxyConfig = useCallback(async () => {
+ try {
+ const res = await fetch('/api/config')
+ if (res.ok) {
+ const data = await res.json()
+ if (data.code === 0 && data.data?.team_reg_proxy) {
+ setProxy(data.data.team_reg_proxy)
+ setUseProxy(true)
+ }
+ }
+ } catch (e) {
+ console.error('加载代理配置失败:', e)
+ }
+ }, [])
+
+ // 初始化时加载代理配置
+ useEffect(() => {
+ loadProxyConfig()
+ }, [loadProxyConfig])
+
// 获取状态
const fetchStatus = useCallback(async () => {
setLoading(true)
@@ -90,7 +112,7 @@ export default function TeamReg() {
body: JSON.stringify({
count,
concurrency,
- proxy,
+ proxy: useProxy ? proxy : '',
auto_import: autoImport,
}),
})
@@ -289,15 +311,42 @@ export default function TeamReg() {
hint="同时进行的注册任务数 (1-10)"
disabled={isRunning}
/>
- setProxy(e.target.value)}
- placeholder="留空使用默认代理"
- hint="HTTP 代理地址,如 http://127.0.0.1:7890"
- disabled={isRunning}
- />
+
+ {/* 代理配置区域 */}
+
+
+
+ {useProxy && proxy && (
+
+
+ {proxy}
+
+
+ 如需修改,请前往 系统配置 页面
+
+
+ )}
+
+ {useProxy && !proxy && (
+
+ ⚠️ 未配置代理地址,请先在 系统配置 中设置
+
+ )}
+
+
+ {/* 自动导入开关 */}