feat: introduce a new Config page for managing site settings and proxy configurations, and add a Team Registration page with backend API support.
This commit is contained in:
@@ -44,6 +44,10 @@ type TeamRegState struct {
|
|||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
stdin io.WriteCloser
|
stdin io.WriteCloser
|
||||||
|
// 自动导入与退出控制
|
||||||
|
autoImporting bool
|
||||||
|
autoImported bool
|
||||||
|
exitSignaled bool
|
||||||
// 403 错误检测
|
// 403 错误检测
|
||||||
error403Count int // 403 错误计数
|
error403Count int // 403 错误计数
|
||||||
error403Start time.Time // 计数开始时间
|
error403Start time.Time // 计数开始时间
|
||||||
@@ -104,6 +108,9 @@ func HandleTeamRegStart(w http.ResponseWriter, r *http.Request) {
|
|||||||
teamRegState.Logs = make([]string, 0)
|
teamRegState.Logs = make([]string, 0)
|
||||||
teamRegState.OutputFile = ""
|
teamRegState.OutputFile = ""
|
||||||
teamRegState.Imported = 0
|
teamRegState.Imported = 0
|
||||||
|
teamRegState.autoImporting = false
|
||||||
|
teamRegState.autoImported = false
|
||||||
|
teamRegState.exitSignaled = false
|
||||||
teamRegState.error403Count = 0
|
teamRegState.error403Count = 0
|
||||||
teamRegState.error403Start = time.Now()
|
teamRegState.error403Start = time.Now()
|
||||||
teamRegState.mu.Unlock()
|
teamRegState.mu.Unlock()
|
||||||
@@ -379,31 +386,14 @@ func runTeamRegProcess(config TeamRegConfig) {
|
|||||||
// 自动导入
|
// 自动导入
|
||||||
if config.AutoImport {
|
if config.AutoImport {
|
||||||
addTeamRegLog("[系统] 自动导入账号到数据库...")
|
addTeamRegLog("[系统] 自动导入账号到数据库...")
|
||||||
count, err := importAccountsFromJSON(outputFile)
|
tryAutoImport(outputFile, config)
|
||||||
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)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送回车退出程序(如果还在运行)
|
// 发送回车退出程序(如果还在运行)
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
if stdin != nil {
|
signalTeamRegExit()
|
||||||
fmt.Fprintf(stdin, "\n")
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// readOutput 读取进程输出
|
// readOutput 读取进程输出
|
||||||
func readOutput(reader io.Reader, workDir string, config TeamRegConfig) {
|
func readOutput(reader io.Reader, workDir string, config TeamRegConfig) {
|
||||||
@@ -424,11 +414,14 @@ func readOutput(reader io.Reader, workDir string, config TeamRegConfig) {
|
|||||||
fileName := trimmed[idx : idx+endIdx+5] // 包含 .json
|
fileName := trimmed[idx : idx+endIdx+5] // 包含 .json
|
||||||
// 构建完整路径
|
// 构建完整路径
|
||||||
fullPath := filepath.Join(workDir, fileName)
|
fullPath := filepath.Join(workDir, fileName)
|
||||||
if _, err := os.Stat(fullPath); err == nil {
|
|
||||||
teamRegState.mu.Lock()
|
teamRegState.mu.Lock()
|
||||||
teamRegState.OutputFile = fullPath
|
teamRegState.OutputFile = fullPath
|
||||||
teamRegState.mu.Unlock()
|
teamRegState.mu.Unlock()
|
||||||
|
if config.AutoImport {
|
||||||
|
go tryAutoImport(fullPath, config)
|
||||||
}
|
}
|
||||||
|
// 发送回车提示退出(有些程序会在完成后等待回车)
|
||||||
|
signalTeamRegExit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -482,6 +475,80 @@ func check403AndStop() bool {
|
|||||||
return false
|
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 添加日志
|
// addTeamRegLog 添加日志
|
||||||
func addTeamRegLog(log string) {
|
func addTeamRegLog(log string) {
|
||||||
teamRegState.mu.Lock()
|
teamRegState.mu.Lock()
|
||||||
|
|||||||
@@ -23,11 +23,16 @@ export default function Config() {
|
|||||||
const { config, isConnected, refreshConfig } = useConfig()
|
const { config, isConnected, refreshConfig } = useConfig()
|
||||||
const [siteName, setSiteName] = useState('')
|
const [siteName, setSiteName] = useState('')
|
||||||
const [defaultProxy, setDefaultProxy] = useState('')
|
const [defaultProxy, setDefaultProxy] = useState('')
|
||||||
|
const [teamRegProxy, setTeamRegProxy] = useState('')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [savingProxy, setSavingProxy] = useState(false)
|
const [savingProxy, setSavingProxy] = useState(false)
|
||||||
|
const [savingTeamRegProxy, setSavingTeamRegProxy] = useState(false)
|
||||||
const [testingProxy, setTestingProxy] = useState(false)
|
const [testingProxy, setTestingProxy] = useState(false)
|
||||||
|
const [testingTeamRegProxy, setTestingTeamRegProxy] = useState(false)
|
||||||
const [proxyStatus, setProxyStatus] = useState<'unknown' | 'success' | 'error'>('unknown')
|
const [proxyStatus, setProxyStatus] = useState<'unknown' | 'success' | 'error'>('unknown')
|
||||||
|
const [teamRegProxyStatus, setTeamRegProxyStatus] = useState<'unknown' | 'success' | 'error'>('unknown')
|
||||||
const [proxyOriginIP, setProxyOriginIP] = useState('')
|
const [proxyOriginIP, setProxyOriginIP] = useState('')
|
||||||
|
const [teamRegProxyIP, setTeamRegProxyIP] = useState('')
|
||||||
const { toasts, toast, removeToast } = useToast()
|
const { toasts, toast, removeToast } = useToast()
|
||||||
|
|
||||||
// 加载站点名称和代理配置
|
// 加载站点名称和代理配置
|
||||||
@@ -39,6 +44,7 @@ export default function Config() {
|
|||||||
if (data.code === 0 && data.data) {
|
if (data.code === 0 && data.data) {
|
||||||
setSiteName(data.data.site_name || 'Codex Pool')
|
setSiteName(data.data.site_name || 'Codex Pool')
|
||||||
setDefaultProxy(data.data.default_proxy || '')
|
setDefaultProxy(data.data.default_proxy || '')
|
||||||
|
setTeamRegProxy(data.data.team_reg_proxy || '')
|
||||||
// 恢复代理测试状态
|
// 恢复代理测试状态
|
||||||
const testStatus = data.data.proxy_test_status
|
const testStatus = data.data.proxy_test_status
|
||||||
if (testStatus === 'success' || testStatus === 'error') {
|
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 = [
|
const configItems = [
|
||||||
{
|
{
|
||||||
to: '/config/s2a',
|
to: '/config/s2a',
|
||||||
@@ -307,6 +368,72 @@ export default function Config() {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 注册代理地址配置 */}
|
||||||
|
<div className="pt-4 border-t border-slate-200 dark:border-slate-700">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||||
|
注册代理地址
|
||||||
|
</label>
|
||||||
|
{/* 代理状态徽章 */}
|
||||||
|
{teamRegProxyStatus === 'success' && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
|
||||||
|
<Wifi className="h-3 w-3" />
|
||||||
|
代理可用
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{teamRegProxyStatus === 'error' && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400">
|
||||||
|
<WifiOff className="h-3 w-3" />
|
||||||
|
连接失败
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{teamRegProxyStatus === 'unknown' && teamRegProxy && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-400">
|
||||||
|
<HelpCircle className="h-3 w-3" />
|
||||||
|
未测试
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Input
|
||||||
|
value={teamRegProxy}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTeamRegProxy(e.target.value)
|
||||||
|
setTeamRegProxyStatus('unknown')
|
||||||
|
}}
|
||||||
|
placeholder="http://user:pass@host:port"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSaveTeamRegProxy}
|
||||||
|
disabled={savingTeamRegProxy}
|
||||||
|
icon={savingTeamRegProxy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
{savingTeamRegProxy ? '保存中...' : '保存'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleTestTeamRegProxy}
|
||||||
|
disabled={testingTeamRegProxy || !teamRegProxy.trim()}
|
||||||
|
icon={testingTeamRegProxy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Wifi className="h-4 w-4" />}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
{testingTeamRegProxy ? '测试中...' : '测试连接'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
Team 自动注册功能使用的代理地址。建议使用高质量住宅代理以避免被限制。
|
||||||
|
{teamRegProxyIP && (
|
||||||
|
<span className="ml-2 text-green-600 dark:text-green-400">
|
||||||
|
出口IP: {teamRegProxyIP}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export default function TeamReg() {
|
|||||||
// 配置表单
|
// 配置表单
|
||||||
const [count, setCount] = useState(5)
|
const [count, setCount] = useState(5)
|
||||||
const [concurrency, setConcurrency] = useState(2)
|
const [concurrency, setConcurrency] = useState(2)
|
||||||
|
const [useProxy, setUseProxy] = useState(false)
|
||||||
const [proxy, setProxy] = useState('')
|
const [proxy, setProxy] = useState('')
|
||||||
const [autoImport, setAutoImport] = useState(true)
|
const [autoImport, setAutoImport] = useState(true)
|
||||||
|
|
||||||
@@ -46,6 +47,27 @@ export default function TeamReg() {
|
|||||||
const logsContainerRef = useRef<HTMLDivElement>(null)
|
const logsContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const [autoScroll, setAutoScroll] = useState(true)
|
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 () => {
|
const fetchStatus = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -90,7 +112,7 @@ export default function TeamReg() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
count,
|
count,
|
||||||
concurrency,
|
concurrency,
|
||||||
proxy,
|
proxy: useProxy ? proxy : '',
|
||||||
auto_import: autoImport,
|
auto_import: autoImport,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -289,15 +311,42 @@ export default function TeamReg() {
|
|||||||
hint="同时进行的注册任务数 (1-10)"
|
hint="同时进行的注册任务数 (1-10)"
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
/>
|
/>
|
||||||
<Input
|
|
||||||
label="代理地址"
|
{/* 代理配置区域 */}
|
||||||
type="text"
|
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 space-y-3">
|
||||||
value={proxy}
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
onChange={(e) => setProxy(e.target.value)}
|
<input
|
||||||
placeholder="留空使用默认代理"
|
type="checkbox"
|
||||||
hint="HTTP 代理地址,如 http://127.0.0.1:7890"
|
checked={useProxy}
|
||||||
|
onChange={(e) => setUseProxy(e.target.checked)}
|
||||||
disabled={isRunning}
|
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>
|
||||||
|
|
||||||
|
{useProxy && proxy && (
|
||||||
|
<div className="ml-7 p-2 rounded bg-slate-100 dark:bg-slate-700">
|
||||||
|
<p className="text-sm text-slate-700 dark:text-slate-300 font-mono break-all">
|
||||||
|
{proxy}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
|
如需修改,请前往 <a href="/config" className="text-blue-500 hover:underline">系统配置</a> 页面
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{useProxy && !proxy && (
|
||||||
|
<p className="ml-7 text-xs text-orange-500">
|
||||||
|
⚠️ 未配置代理地址,请先在 <a href="/config" className="text-blue-500 hover:underline">系统配置</a> 中设置
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 自动导入开关 */}
|
||||||
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
|
<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">
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
|
|||||||
Reference in New Issue
Block a user