feat: Implement batch team owner processing with file upload, configuration, and real-time status monitoring.
This commit is contained in:
@@ -71,25 +71,50 @@ func HandleSaveMonitorSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if database.Instance == nil {
|
if database.Instance == nil {
|
||||||
|
logger.Error("保存监控设置失败: 数据库未初始化", "", "monitor")
|
||||||
Error(w, http.StatusInternalServerError, "数据库未初始化")
|
Error(w, http.StatusInternalServerError, "数据库未初始化")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var settings MonitorSettings
|
var settings MonitorSettings
|
||||||
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
|
||||||
|
logger.Error("保存监控设置失败: 解析请求失败 - "+err.Error(), "", "monitor")
|
||||||
Error(w, http.StatusBadRequest, "解析请求失败")
|
Error(w, http.StatusBadRequest, "解析请求失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Info("收到保存监控设置请求: target="+strconv.Itoa(settings.Target)+
|
||||||
|
", auto_add="+strconv.FormatBool(settings.AutoAdd)+
|
||||||
|
", polling="+strconv.FormatBool(settings.PollingEnabled), "", "monitor")
|
||||||
|
|
||||||
// 保存到数据库
|
// 保存到数据库
|
||||||
database.Instance.SetConfig("monitor_target", strconv.Itoa(settings.Target))
|
var saveErrors []string
|
||||||
database.Instance.SetConfig("monitor_auto_add", strconv.FormatBool(settings.AutoAdd))
|
if err := database.Instance.SetConfig("monitor_target", strconv.Itoa(settings.Target)); err != nil {
|
||||||
database.Instance.SetConfig("monitor_min_interval", strconv.Itoa(settings.MinInterval))
|
saveErrors = append(saveErrors, "target: "+err.Error())
|
||||||
database.Instance.SetConfig("monitor_polling_enabled", strconv.FormatBool(settings.PollingEnabled))
|
}
|
||||||
database.Instance.SetConfig("monitor_polling_interval", strconv.Itoa(settings.PollingInterval))
|
if err := database.Instance.SetConfig("monitor_auto_add", strconv.FormatBool(settings.AutoAdd)); err != nil {
|
||||||
|
saveErrors = append(saveErrors, "auto_add: "+err.Error())
|
||||||
|
}
|
||||||
|
if err := database.Instance.SetConfig("monitor_min_interval", strconv.Itoa(settings.MinInterval)); err != nil {
|
||||||
|
saveErrors = append(saveErrors, "min_interval: "+err.Error())
|
||||||
|
}
|
||||||
|
if err := database.Instance.SetConfig("monitor_polling_enabled", strconv.FormatBool(settings.PollingEnabled)); err != nil {
|
||||||
|
saveErrors = append(saveErrors, "polling_enabled: "+err.Error())
|
||||||
|
}
|
||||||
|
if err := database.Instance.SetConfig("monitor_polling_interval", strconv.Itoa(settings.PollingInterval)); err != nil {
|
||||||
|
saveErrors = append(saveErrors, "polling_interval: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(saveErrors) > 0 {
|
||||||
|
errMsg := "保存监控设置部分失败: " + saveErrors[0]
|
||||||
|
logger.Error(errMsg, "", "monitor")
|
||||||
|
Error(w, http.StatusInternalServerError, errMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 输出日志
|
// 输出日志
|
||||||
logger.Info("监控设置已保存: target="+strconv.Itoa(settings.Target)+
|
logger.Success("监控设置已保存: target="+strconv.Itoa(settings.Target)+
|
||||||
|
", auto_add="+strconv.FormatBool(settings.AutoAdd)+
|
||||||
", polling="+strconv.FormatBool(settings.PollingEnabled)+
|
", polling="+strconv.FormatBool(settings.PollingEnabled)+
|
||||||
", interval="+strconv.Itoa(settings.PollingInterval)+"s", "", "monitor")
|
", interval="+strconv.Itoa(settings.PollingInterval)+"s", "", "monitor")
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"codex-pool/internal/auth"
|
"codex-pool/internal/auth"
|
||||||
"codex-pool/internal/config"
|
"codex-pool/internal/config"
|
||||||
|
"codex-pool/internal/database"
|
||||||
"codex-pool/internal/invite"
|
"codex-pool/internal/invite"
|
||||||
"codex-pool/internal/logger"
|
"codex-pool/internal/logger"
|
||||||
"codex-pool/internal/mail"
|
"codex-pool/internal/mail"
|
||||||
@@ -21,9 +22,10 @@ import (
|
|||||||
type TeamProcessRequest struct {
|
type TeamProcessRequest struct {
|
||||||
// Owner 账号列表
|
// Owner 账号列表
|
||||||
Owners []struct {
|
Owners []struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
AccountID string `json:"account_id"` // 已存储的 account_id,如有则直接使用
|
||||||
} `json:"owners"`
|
} `json:"owners"`
|
||||||
// 配置
|
// 配置
|
||||||
MembersPerTeam int `json:"members_per_team"` // 每个 Team 的成员数
|
MembersPerTeam int `json:"members_per_team"` // 每个 Team 的成员数
|
||||||
@@ -31,6 +33,7 @@ type TeamProcessRequest struct {
|
|||||||
BrowserType string `json:"browser_type"` // "chromedp" 或 "rod"
|
BrowserType string `json:"browser_type"` // "chromedp" 或 "rod"
|
||||||
Headless bool `json:"headless"` // 是否无头模式
|
Headless bool `json:"headless"` // 是否无头模式
|
||||||
Proxy string `json:"proxy"` // 代理设置
|
Proxy string `json:"proxy"` // 代理设置
|
||||||
|
IncludeOwner bool `json:"include_owner"` // 母号也入库到 S2A
|
||||||
}
|
}
|
||||||
|
|
||||||
// TeamProcessResult 团队处理结果
|
// TeamProcessResult 团队处理结果
|
||||||
@@ -76,11 +79,34 @@ func HandleTeamProcess(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证参数
|
// 如果没有传入 owners,从数据库获取待处理的母号
|
||||||
if len(req.Owners) == 0 {
|
if len(req.Owners) == 0 {
|
||||||
Error(w, http.StatusBadRequest, "请提供至少一个 Owner 账号")
|
pendingOwners, err := database.Instance.GetPendingOwners()
|
||||||
return
|
if err != nil {
|
||||||
|
Error(w, http.StatusInternalServerError, fmt.Sprintf("获取待处理账号失败: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(pendingOwners) == 0 {
|
||||||
|
Error(w, http.StatusBadRequest, "没有待处理的母号,请先上传账号文件")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 转换为请求格式(包含已存储的 account_id)
|
||||||
|
for _, o := range pendingOwners {
|
||||||
|
req.Owners = append(req.Owners, struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
}{
|
||||||
|
Email: o.Email,
|
||||||
|
Password: o.Password,
|
||||||
|
Token: o.Token,
|
||||||
|
AccountID: o.AccountID, // 直接使用数据库中存储的 account_id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
logger.Info(fmt.Sprintf("从数据库加载 %d 个待处理母号", len(req.Owners)), "", "team")
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.MembersPerTeam <= 0 {
|
if req.MembersPerTeam <= 0 {
|
||||||
req.MembersPerTeam = 4
|
req.MembersPerTeam = 4
|
||||||
}
|
}
|
||||||
@@ -225,17 +251,27 @@ func processSingleTeam(idx int, req TeamProcessRequest) TeamProcessResult {
|
|||||||
logPrefix := fmt.Sprintf("[Team %d]", idx+1)
|
logPrefix := fmt.Sprintf("[Team %d]", idx+1)
|
||||||
logger.Info(fmt.Sprintf("%s Starting with owner: %s", logPrefix, owner.Email), owner.Email, "team")
|
logger.Info(fmt.Sprintf("%s Starting with owner: %s", logPrefix, owner.Email), owner.Email, "team")
|
||||||
|
|
||||||
// Step 1: 获取 Team ID
|
// Step 1: 获取 Team ID(优先使用已存储的 account_id)
|
||||||
|
var teamID string
|
||||||
inviter := invite.NewWithProxy(owner.Token, req.Proxy)
|
inviter := invite.NewWithProxy(owner.Token, req.Proxy)
|
||||||
teamID, err := inviter.GetAccountID()
|
|
||||||
if err != nil {
|
if owner.AccountID != "" {
|
||||||
result.Errors = append(result.Errors, fmt.Sprintf("获取 Team ID 失败: %v", err))
|
// 直接使用数据库中存储的 account_id
|
||||||
result.DurationMs = time.Since(startTime).Milliseconds()
|
teamID = owner.AccountID
|
||||||
logger.Error(fmt.Sprintf("%s Failed to get Team ID: %v", logPrefix, err), owner.Email, "team")
|
logger.Info(fmt.Sprintf("%s 使用已存储的 Team ID: %s", logPrefix, teamID), owner.Email, "team")
|
||||||
return result
|
} else {
|
||||||
|
// 如果没有存储,才请求 API 获取
|
||||||
|
var err error
|
||||||
|
teamID, err = inviter.GetAccountID()
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("获取 Team ID 失败: %v", err))
|
||||||
|
result.DurationMs = time.Since(startTime).Milliseconds()
|
||||||
|
logger.Error(fmt.Sprintf("%s Failed to get Team ID: %v", logPrefix, err), owner.Email, "team")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
logger.Success(fmt.Sprintf("%s 获取到 Team ID: %s", logPrefix, teamID), owner.Email, "team")
|
||||||
}
|
}
|
||||||
result.TeamID = teamID
|
result.TeamID = teamID
|
||||||
logger.Success(fmt.Sprintf("%s Team ID: %s", logPrefix, teamID), owner.Email, "team")
|
|
||||||
|
|
||||||
// Step 2: 生成成员邮箱并发送邀请
|
// Step 2: 生成成员邮箱并发送邀请
|
||||||
type MemberAccount struct {
|
type MemberAccount struct {
|
||||||
@@ -328,7 +364,7 @@ func processSingleTeam(idx int, req TeamProcessRequest) TeamProcessResult {
|
|||||||
}
|
}
|
||||||
logger.Info(fmt.Sprintf("%s Registered: %d/%d", logPrefix, result.Registered, req.MembersPerTeam), owner.Email, "team")
|
logger.Info(fmt.Sprintf("%s Registered: %d/%d", logPrefix, result.Registered, req.MembersPerTeam), owner.Email, "team")
|
||||||
|
|
||||||
// Step 4: S2A 授权入库
|
// Step 4: S2A 授权入库(成员)
|
||||||
for i, child := range registeredChildren {
|
for i, child := range registeredChildren {
|
||||||
if !teamProcessState.Running {
|
if !teamProcessState.Running {
|
||||||
break
|
break
|
||||||
@@ -373,6 +409,44 @@ func processSingleTeam(idx int, req TeamProcessRequest) TeamProcessResult {
|
|||||||
logger.Success(fmt.Sprintf("%s [Member %d] Added to S2A", logPrefix, i+1), child.Email, "team")
|
logger.Success(fmt.Sprintf("%s [Member %d] Added to S2A", logPrefix, i+1), child.Email, "team")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 5: 母号也入库(如果开启)
|
||||||
|
if req.IncludeOwner && teamProcessState.Running {
|
||||||
|
logger.Info(fmt.Sprintf("%s 开始将母号入库到 S2A", logPrefix), owner.Email, "team")
|
||||||
|
|
||||||
|
s2aResp, err := auth.GenerateS2AAuthURL(config.Global.S2AApiBase, config.Global.S2AAdminKey, config.Global.ProxyID)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("Owner auth URL: %v", err))
|
||||||
|
} else {
|
||||||
|
var code string
|
||||||
|
if req.BrowserType == "rod" {
|
||||||
|
code, err = auth.CompleteWithRod(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy)
|
||||||
|
} else {
|
||||||
|
code, err = auth.CompleteWithChromedp(s2aResp.Data.AuthURL, owner.Email, owner.Password, teamID, req.Headless, req.Proxy)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("Owner browser: %v", err))
|
||||||
|
} else {
|
||||||
|
_, err = auth.SubmitS2AOAuth(
|
||||||
|
config.Global.S2AApiBase,
|
||||||
|
config.Global.S2AAdminKey,
|
||||||
|
s2aResp.Data.SessionID,
|
||||||
|
code,
|
||||||
|
owner.Email,
|
||||||
|
config.Global.Concurrency,
|
||||||
|
config.Global.Priority,
|
||||||
|
config.Global.GroupIDs,
|
||||||
|
config.Global.ProxyID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("Owner S2A: %v", err))
|
||||||
|
} else {
|
||||||
|
result.AddedToS2A++
|
||||||
|
logger.Success(fmt.Sprintf("%s [Owner] Added to S2A", logPrefix), owner.Email, "team")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result.DurationMs = time.Since(startTime).Milliseconds()
|
result.DurationMs = time.Since(startTime).Milliseconds()
|
||||||
logger.Success(fmt.Sprintf("%s Complete: %d registered, %d in S2A", logPrefix, result.Registered, result.AddedToS2A), owner.Email, "team")
|
logger.Success(fmt.Sprintf("%s Complete: %d registered, %d in S2A", logPrefix, result.Registered, result.AddedToS2A), owner.Email, "team")
|
||||||
|
|
||||||
|
|||||||
72
frontend/src/components/common/Switch.tsx
Normal file
72
frontend/src/components/common/Switch.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { useId } from 'react'
|
||||||
|
|
||||||
|
interface SwitchProps {
|
||||||
|
checked: boolean
|
||||||
|
onChange: (checked: boolean) => void
|
||||||
|
disabled?: boolean
|
||||||
|
label?: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Switch({
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
className = '',
|
||||||
|
}: SwitchProps) {
|
||||||
|
const id = useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center justify-between ${className}`}>
|
||||||
|
{(label || description) && (
|
||||||
|
<div className="flex-1 mr-3">
|
||||||
|
{label && (
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className="text-sm font-medium text-slate-700 dark:text-slate-300 cursor-pointer"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
id={id}
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => !disabled && onChange(!checked)}
|
||||||
|
className={`
|
||||||
|
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full
|
||||||
|
border-2 border-transparent transition-colors duration-200 ease-in-out
|
||||||
|
focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
|
||||||
|
${checked
|
||||||
|
? 'bg-blue-500'
|
||||||
|
: 'bg-slate-200 dark:bg-slate-700'
|
||||||
|
}
|
||||||
|
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`
|
||||||
|
pointer-events-none inline-block h-5 w-5 transform rounded-full
|
||||||
|
bg-white shadow-lg ring-0 transition duration-200 ease-in-out
|
||||||
|
${checked ? 'translate-x-5' : 'translate-x-0'}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { SwitchProps }
|
||||||
@@ -31,3 +31,6 @@ export { ToastProvider, useToast } from './Toast'
|
|||||||
|
|
||||||
export { Tabs } from './Tabs'
|
export { Tabs } from './Tabs'
|
||||||
export type { TabItem } from './Tabs'
|
export type { TabItem } from './Tabs'
|
||||||
|
|
||||||
|
export { default as Switch } from './Switch'
|
||||||
|
export type { SwitchProps } from './Switch'
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
Save,
|
Save,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
|
import { Card, CardHeader, CardTitle, CardContent, Button, Input, Switch } from '../components/common'
|
||||||
import type { DashboardStats } from '../types'
|
import type { DashboardStats } from '../types'
|
||||||
|
|
||||||
interface PoolStatus {
|
interface PoolStatus {
|
||||||
@@ -148,11 +148,19 @@ export default function Monitor() {
|
|||||||
polling_interval: pollingInterval,
|
polling_interval: pollingInterval,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
const data = await res.json()
|
||||||
console.error('保存设置失败:', res.status)
|
if (!res.ok || data.code !== 0) {
|
||||||
|
console.error('保存设置失败:', data.message || res.status)
|
||||||
|
alert('保存设置失败: ' + (data.message || '未知错误'))
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
console.log('保存设置成功:', data)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存设置失败:', e)
|
console.error('保存设置失败:', e)
|
||||||
|
alert('保存设置失败: ' + (e instanceof Error ? e.message : '网络错误'))
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// 更新本地状态
|
// 更新本地状态
|
||||||
setPoolStatus(prev => prev ? {
|
setPoolStatus(prev => prev ? {
|
||||||
@@ -438,96 +446,98 @@ export default function Monitor() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 配置面板 */}
|
{/* 配置面板 - 使用 flex 布局让两卡片等高,底部按钮对齐 */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{/* 目标设置 */}
|
{/* 目标设置 */}
|
||||||
<Card className="glass-card">
|
<Card className="glass-card flex flex-col">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Target className="h-5 w-5 text-blue-500" />
|
<Target className="h-5 w-5 text-blue-500" />
|
||||||
号池目标设置
|
号池目标设置
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="flex flex-col flex-1">
|
||||||
<Input
|
<div className="space-y-4 flex-1">
|
||||||
label="目标账号数"
|
<Input
|
||||||
type="number"
|
label="目标账号数"
|
||||||
min={1}
|
type="number"
|
||||||
max={1000}
|
min={1}
|
||||||
value={targetInput}
|
max={1000}
|
||||||
onChange={(e) => setTargetInput(Number(e.target.value))}
|
value={targetInput}
|
||||||
hint="期望保持的活跃账号数量"
|
onChange={(e) => setTargetInput(Number(e.target.value))}
|
||||||
/>
|
hint="期望保持的活跃账号数量"
|
||||||
<div className="flex items-center gap-3">
|
/>
|
||||||
<input
|
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
|
||||||
type="checkbox"
|
<Switch
|
||||||
id="autoAdd"
|
checked={autoAdd}
|
||||||
checked={autoAdd}
|
onChange={setAutoAdd}
|
||||||
onChange={(e) => setAutoAdd(e.target.checked)}
|
label="启用自动补号"
|
||||||
className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
description="开启后,当号池不足时自动补充账号"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="最小间隔 (秒)"
|
||||||
|
type="number"
|
||||||
|
min={60}
|
||||||
|
max={3600}
|
||||||
|
value={minInterval}
|
||||||
|
onChange={(e) => setMinInterval(Number(e.target.value))}
|
||||||
|
hint="两次自动补号的最小间隔"
|
||||||
|
disabled={!autoAdd}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="autoAdd" className="text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
启用自动补号
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<div className="mt-4">
|
||||||
label="最小间隔 (秒)"
|
<Button onClick={handleSetTarget} loading={loading} className="w-full">
|
||||||
type="number"
|
保存设置
|
||||||
min={60}
|
</Button>
|
||||||
max={3600}
|
</div>
|
||||||
value={minInterval}
|
|
||||||
onChange={(e) => setMinInterval(Number(e.target.value))}
|
|
||||||
hint="两次自动补号的最小间隔"
|
|
||||||
disabled={!autoAdd}
|
|
||||||
/>
|
|
||||||
<Button onClick={handleSetTarget} loading={loading} className="w-full">
|
|
||||||
保存设置
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 轮询控制 */}
|
{/* 轮询控制 */}
|
||||||
<Card className="glass-card">
|
<Card className="glass-card flex flex-col">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Activity className="h-5 w-5 text-green-500" />
|
<Activity className="h-5 w-5 text-green-500" />
|
||||||
实时监控设置
|
实时监控设置
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="flex flex-col flex-1">
|
||||||
<div className="w-full">
|
<div className="space-y-4 flex-1">
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
<div className="w-full">
|
||||||
轮询间隔 (秒)
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
</label>
|
轮询间隔 (秒)
|
||||||
<input
|
</label>
|
||||||
type="number"
|
<input
|
||||||
min={10}
|
type="number"
|
||||||
max={600}
|
min={10}
|
||||||
value={pollingInterval}
|
max={600}
|
||||||
onChange={(e) => setPollingInterval(Number(e.target.value) || 60)}
|
value={pollingInterval}
|
||||||
className="w-full px-3 py-2 text-sm rounded-lg border transition-colors
|
onChange={(e) => setPollingInterval(Number(e.target.value) || 60)}
|
||||||
bg-white dark:bg-slate-800
|
className="w-full px-3 py-2 text-sm rounded-lg border transition-colors
|
||||||
text-slate-900 dark:text-slate-100
|
bg-white dark:bg-slate-800
|
||||||
border-slate-300 dark:border-slate-600
|
text-slate-900 dark:text-slate-100
|
||||||
focus:border-blue-500 focus:ring-blue-500
|
border-slate-300 dark:border-slate-600
|
||||||
focus:outline-none focus:ring-2"
|
focus:border-blue-500 focus:ring-blue-500
|
||||||
/>
|
focus:outline-none focus:ring-2"
|
||||||
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
/>
|
||||||
自动刷新号池状态的间隔时间 (10-600秒)
|
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||||||
</p>
|
自动刷新号池状态的间隔时间 (10-600秒)
|
||||||
</div>
|
</p>
|
||||||
<div className="p-4 rounded-lg bg-slate-50 dark:bg-slate-800/50">
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="p-4 rounded-lg bg-slate-50 dark:bg-slate-800/50">
|
||||||
<div>
|
<div className="flex items-center justify-between">
|
||||||
<p className="font-medium text-slate-900 dark:text-slate-100">监控状态</p>
|
<div>
|
||||||
<p className="text-sm text-slate-500">
|
<p className="font-medium text-slate-900 dark:text-slate-100">监控状态</p>
|
||||||
{pollingEnabled ? '正在实时监控号池状态' : '监控已暂停'}
|
<p className="text-sm text-slate-500">
|
||||||
</p>
|
{pollingEnabled ? '正在实时监控号池状态' : '监控已暂停'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={`status-dot ${pollingEnabled ? 'online' : 'offline'}`} />
|
||||||
</div>
|
</div>
|
||||||
<div className={`status-dot ${pollingEnabled ? 'online' : 'offline'}`} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3 mt-4">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSavePollingSettings}
|
onClick={handleSavePollingSettings}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { FileDropzone } from '../components/upload'
|
import { FileDropzone } from '../components/upload'
|
||||||
import LogStream from '../components/upload/LogStream'
|
import LogStream from '../components/upload/LogStream'
|
||||||
import OwnerList from '../components/upload/OwnerList'
|
import OwnerList from '../components/upload/OwnerList'
|
||||||
import { Card, CardHeader, CardTitle, CardContent, Button, Tabs, Input } from '../components/common'
|
import { Card, CardHeader, CardTitle, CardContent, Button, Tabs, Input, Switch } from '../components/common'
|
||||||
import { useConfig } from '../hooks/useConfig'
|
import { useConfig } from '../hooks/useConfig'
|
||||||
|
|
||||||
interface OwnerStats {
|
interface OwnerStats {
|
||||||
@@ -71,6 +71,7 @@ export default function Upload() {
|
|||||||
const [concurrentTeams, setConcurrentTeams] = useState(2)
|
const [concurrentTeams, setConcurrentTeams] = useState(2)
|
||||||
const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp')
|
const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp')
|
||||||
const [proxy, setProxy] = useState('')
|
const [proxy, setProxy] = useState('')
|
||||||
|
const [includeOwner, setIncludeOwner] = useState(false) // 母号也入库
|
||||||
|
|
||||||
const hasConfig = config.s2a.apiBase && config.s2a.adminKey
|
const hasConfig = config.s2a.apiBase && config.s2a.adminKey
|
||||||
|
|
||||||
@@ -175,6 +176,7 @@ export default function Upload() {
|
|||||||
browser_type: browserType,
|
browser_type: browserType,
|
||||||
headless: true, // 始终使用无头模式
|
headless: true, // 始终使用无头模式
|
||||||
proxy,
|
proxy,
|
||||||
|
include_owner: includeOwner, // 母号也入库
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -190,7 +192,7 @@ export default function Upload() {
|
|||||||
alert('启动失败')
|
alert('启动失败')
|
||||||
}
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}, [stats, membersPerTeam, concurrentTeams, browserType, proxy, fetchStatus])
|
}, [stats, membersPerTeam, concurrentTeams, browserType, proxy, includeOwner, fetchStatus])
|
||||||
|
|
||||||
// 停止处理
|
// 停止处理
|
||||||
const handleStop = useCallback(async () => {
|
const handleStop = useCallback(async () => {
|
||||||
@@ -427,6 +429,16 @@ export default function Upload() {
|
|||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
|
||||||
|
<Switch
|
||||||
|
checked={includeOwner}
|
||||||
|
onChange={setIncludeOwner}
|
||||||
|
disabled={isRunning}
|
||||||
|
label="母号也入库"
|
||||||
|
description="开启后,母号(Owner)账号也会被注册到 S2A"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-2">
|
||||||
{isRunning ? (
|
{isRunning ? (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user