feat: Implement core backend infrastructure including configuration management, SQLite database with team owners and app settings, and initial owner-related APIs and frontend components.

This commit is contained in:
2026-01-31 03:16:24 +08:00
parent 634b493524
commit f590fe0c7a
6 changed files with 810 additions and 20 deletions

View File

@@ -85,6 +85,9 @@ func main() {
// 启动错误账号定期清理服务(需在配置中启用) // 启动错误账号定期清理服务(需在配置中启用)
api.StartErrorCleanerService() api.StartErrorCleanerService()
// 启动母号封禁检查服务(需在配置中启用)
api.StartBanCheckService()
// 启动服务器 // 启动服务器
startServer(cfg) startServer(cfg)
} }
@@ -121,6 +124,11 @@ func startServer(cfg *config.Config) {
mux.HandleFunc("/api/db/owners/refetch-account-ids", api.CORS(api.HandleRefetchAccountIDs)) mux.HandleFunc("/api/db/owners/refetch-account-ids", api.CORS(api.HandleRefetchAccountIDs))
mux.HandleFunc("/api/upload/validate", api.CORS(api.HandleUploadValidate)) mux.HandleFunc("/api/upload/validate", api.CORS(api.HandleUploadValidate))
// 母号封禁检查 API
mux.HandleFunc("/api/db/owners/ban-check", api.CORS(api.HandleManualBanCheck)) // 手动触发检查
mux.HandleFunc("/api/db/owners/ban-check/status", api.CORS(api.HandleBanCheckStatus)) // 检查状态
mux.HandleFunc("/api/db/owners/ban-check/settings", api.CORS(api.HandleBanCheckSettings)) // 配置
// 注册测试 API // 注册测试 API
mux.HandleFunc("/api/register/test", api.CORS(handleRegisterTest)) mux.HandleFunc("/api/register/test", api.CORS(handleRegisterTest))

View File

@@ -0,0 +1,417 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"sync"
"sync/atomic"
"time"
"codex-pool/internal/config"
"codex-pool/internal/database"
"codex-pool/internal/invite"
"codex-pool/internal/logger"
)
// 封禁检查服务状态
var (
banCheckRunning bool
banCheckStopChan chan struct{}
banCheckMu sync.Mutex
lastBanCheckTime time.Time
banCheckTaskState BanCheckTaskState
)
// BanCheckTaskState 封禁检查任务状态
type BanCheckTaskState struct {
Running bool `json:"running"`
StartedAt time.Time `json:"started_at"`
Total int32 `json:"total"`
Checked int32 `json:"checked"`
Banned int32 `json:"banned"`
Valid int32 `json:"valid"`
Failed int32 `json:"failed"`
}
// BanCheckResult 单个检查结果
type BanCheckResult struct {
Email string `json:"email"`
Status string `json:"status"` // valid, banned, error
Message string `json:"message,omitempty"`
}
// StartBanCheckService 启动定期封禁检查服务
func StartBanCheckService() {
banCheckMu.Lock()
if banCheckRunning {
banCheckMu.Unlock()
return
}
banCheckRunning = true
banCheckStopChan = make(chan struct{})
banCheckMu.Unlock()
logger.Info("母号封禁检查服务已启动", "", "ban-check")
go func() {
// 默认检查间隔 30 分钟
checkInterval := 1800
for {
// 动态读取检查间隔配置
if database.Instance != nil {
if val, _ := database.Instance.GetConfig("ban_check_interval"); val != "" {
if v, err := strconv.Atoi(val); err == nil && v >= 60 {
checkInterval = v
}
}
}
select {
case <-banCheckStopChan:
logger.Info("母号封禁检查服务已停止", "", "ban-check")
return
case <-time.After(time.Duration(checkInterval) * time.Second):
runScheduledBanCheck()
}
}
}()
}
// StopBanCheckService 停止定期封禁检查服务
func StopBanCheckService() {
banCheckMu.Lock()
defer banCheckMu.Unlock()
if banCheckRunning && banCheckStopChan != nil {
close(banCheckStopChan)
banCheckRunning = false
}
}
// runScheduledBanCheck 执行定期封禁检查
func runScheduledBanCheck() {
if database.Instance == nil {
return
}
// 检查是否开启定期检查
enabled := false
if val, _ := database.Instance.GetConfig("ban_check_enabled"); val == "true" {
enabled = true
}
if !enabled {
return
}
// 检查是否有任务在运行
if banCheckTaskState.Running || teamProcessState.Running {
logger.Info("有其他任务在运行,跳过定期封禁检查", "", "ban-check")
return
}
// 获取检查间隔(小时)
checkIntervalHours := 24
if val, _ := database.Instance.GetConfig("ban_check_hours"); val != "" {
if v, err := strconv.Atoi(val); err == nil && v > 0 {
checkIntervalHours = v
}
}
// 获取需要检查的母号
owners, err := database.Instance.GetOwnersForBanCheck(checkIntervalHours)
if err != nil {
logger.Error(fmt.Sprintf("获取待检查母号失败: %v", err), "", "ban-check")
return
}
if len(owners) == 0 {
logger.Info("没有需要检查的母号", "", "ban-check")
return
}
logger.Info(fmt.Sprintf("定期封禁检查: 发现 %d 个需要检查的母号", len(owners)), "", "ban-check")
// 执行检查
go runBanCheckTask(owners, 2)
}
// HandleBanCheckSettings 获取/设置封禁检查配置
func HandleBanCheckSettings(w http.ResponseWriter, r *http.Request) {
if database.Instance == nil {
Error(w, http.StatusInternalServerError, "数据库未初始化")
return
}
switch r.Method {
case http.MethodGet:
settings := map[string]interface{}{
"enabled": false,
"interval": 1800, // 检查服务间隔(秒)
"check_hours": 24, // 多少小时后重新检查
"last_check": lastBanCheckTime,
"task_state": banCheckTaskState,
"service_running": banCheckRunning,
}
if val, _ := database.Instance.GetConfig("ban_check_enabled"); val == "true" {
settings["enabled"] = true
}
if val, _ := database.Instance.GetConfig("ban_check_interval"); val != "" {
if v, err := strconv.Atoi(val); err == nil {
settings["interval"] = v
}
}
if val, _ := database.Instance.GetConfig("ban_check_hours"); val != "" {
if v, err := strconv.Atoi(val); err == nil {
settings["check_hours"] = v
}
}
Success(w, settings)
case http.MethodPut:
var req struct {
Enabled *bool `json:"enabled"`
Interval *int `json:"interval"`
CheckHours *int `json:"check_hours"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, http.StatusBadRequest, "请求格式错误")
return
}
if req.Enabled != nil {
database.Instance.SetConfig("ban_check_enabled", strconv.FormatBool(*req.Enabled))
}
if req.Interval != nil && *req.Interval >= 60 {
database.Instance.SetConfig("ban_check_interval", strconv.Itoa(*req.Interval))
}
if req.CheckHours != nil && *req.CheckHours > 0 {
database.Instance.SetConfig("ban_check_hours", strconv.Itoa(*req.CheckHours))
}
logger.Success("封禁检查配置已更新", "", "ban-check")
Success(w, map[string]string{"message": "配置已更新"})
default:
Error(w, http.StatusMethodNotAllowed, "不支持的方法")
}
}
// HandleManualBanCheck 手动触发封禁检查
func HandleManualBanCheck(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
Error(w, http.StatusMethodNotAllowed, "不支持的方法")
return
}
if database.Instance == nil {
Error(w, http.StatusInternalServerError, "数据库未初始化")
return
}
// 检查是否有任务在运行
if banCheckTaskState.Running {
Error(w, http.StatusConflict, "封禁检查任务正在运行中")
return
}
if teamProcessState.Running {
Error(w, http.StatusConflict, "Team 处理任务正在运行中,请稍后再试")
return
}
var req struct {
IDs []int64 `json:"ids"` // 指定检查的母号 ID为空则检查所有有效母号
Concurrency int `json:"concurrency"` // 并发数
ForceCheck bool `json:"force_check"` // 是否强制检查(忽略上次检查时间)
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
// 允许空 body
req.Concurrency = 2
}
if req.Concurrency <= 0 {
req.Concurrency = 2
}
if req.Concurrency > 5 {
req.Concurrency = 5
}
var owners []database.TeamOwner
var err error
if len(req.IDs) > 0 {
// 检查指定的母号
for _, id := range req.IDs {
ownerList, _, err := database.Instance.GetTeamOwners("", 1000, 0)
if err != nil {
continue
}
for _, o := range ownerList {
if o.ID == id && o.Status == "valid" {
owners = append(owners, o)
break
}
}
}
} else if req.ForceCheck {
// 强制检查所有有效母号
owners, err = database.Instance.GetPendingOwners()
} else {
// 检查需要检查的母号
checkIntervalHours := 24
if val, _ := database.Instance.GetConfig("ban_check_hours"); val != "" {
if v, err := strconv.Atoi(val); err == nil && v > 0 {
checkIntervalHours = v
}
}
owners, err = database.Instance.GetOwnersForBanCheck(checkIntervalHours)
}
if err != nil {
Error(w, http.StatusInternalServerError, fmt.Sprintf("获取母号失败: %v", err))
return
}
if len(owners) == 0 {
Success(w, map[string]interface{}{
"message": "没有需要检查的母号",
"total": 0,
})
return
}
// 异步执行检查
go runBanCheckTask(owners, req.Concurrency)
Success(w, map[string]interface{}{
"message": "封禁检查任务已启动",
"total": len(owners),
"concurrency": req.Concurrency,
})
}
// HandleBanCheckStatus 获取封禁检查任务状态
func HandleBanCheckStatus(w http.ResponseWriter, r *http.Request) {
Success(w, banCheckTaskState)
}
// runBanCheckTask 执行封禁检查任务
func runBanCheckTask(owners []database.TeamOwner, concurrency int) {
banCheckTaskState = BanCheckTaskState{
Running: true,
StartedAt: time.Now(),
Total: int32(len(owners)),
}
defer func() {
banCheckTaskState.Running = false
lastBanCheckTime = time.Now()
}()
logger.Info(fmt.Sprintf("开始封禁检查: 共 %d 个母号, 并发数: %d", len(owners), concurrency), "", "ban-check")
// 任务队列
taskChan := make(chan database.TeamOwner, len(owners))
var wg sync.WaitGroup
// 获取代理配置
proxy := ""
if config.Global != nil {
proxy = config.Global.GetProxy()
}
// 启动 worker
for w := 0; w < concurrency; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for owner := range taskChan {
result := checkSingleOwnerBan(owner, proxy)
// 更新计数
atomic.AddInt32(&banCheckTaskState.Checked, 1)
switch result.Status {
case "valid":
atomic.AddInt32(&banCheckTaskState.Valid, 1)
case "banned":
atomic.AddInt32(&banCheckTaskState.Banned, 1)
case "error":
atomic.AddInt32(&banCheckTaskState.Failed, 1)
}
}
}()
}
// 发送任务
for _, owner := range owners {
taskChan <- owner
}
close(taskChan)
// 等待完成
wg.Wait()
logger.Success(fmt.Sprintf("封禁检查完成: 共 %d, 有效 %d, 封禁 %d, 失败 %d",
banCheckTaskState.Total, banCheckTaskState.Valid, banCheckTaskState.Banned, banCheckTaskState.Failed), "", "ban-check")
}
// checkSingleOwnerBan 检查单个母号的封禁状态
// 使用 accounts/check API 直接检测,不发送邀请
func checkSingleOwnerBan(owner database.TeamOwner, proxy string) BanCheckResult {
result := BanCheckResult{
Email: owner.Email,
Status: "error",
}
// 创建检查器
var checker *invite.TeamInviter
if proxy != "" {
checker = invite.NewWithProxy(owner.Token, proxy)
} else {
checker = invite.New(owner.Token)
}
// 调用 accounts/check API 检测状态
accountStatus := checker.CheckAccountStatus()
// 更新最后检查时间
database.Instance.UpdateOwnerLastCheckedAtByEmail(owner.Email)
switch accountStatus.Status {
case "active":
// 账户正常
logger.Info(fmt.Sprintf("母号有效: %s (plan: %s)", owner.Email, accountStatus.PlanType), owner.Email, "ban-check")
result.Status = "valid"
result.Message = fmt.Sprintf("母号状态正常 (plan: %s)", accountStatus.PlanType)
// 如果获取到了 account_id 且数据库中没有,则更新
if accountStatus.AccountID != "" && owner.AccountID == "" {
database.Instance.UpdateOwnerAccountID(owner.ID, accountStatus.AccountID)
}
case "banned":
// 账户被封禁
logger.Warning(fmt.Sprintf("母号被封禁: %s - %s", owner.Email, accountStatus.Error), owner.Email, "ban-check")
database.Instance.MarkOwnerAsInvalid(owner.Email)
result.Status = "banned"
result.Message = accountStatus.Error
case "token_expired":
// Token 过期
logger.Warning(fmt.Sprintf("母号 Token 过期: %s", owner.Email), owner.Email, "ban-check")
database.Instance.MarkOwnerAsInvalid(owner.Email)
result.Status = "banned"
result.Message = "Token 已过期"
default:
// 其他错误
logger.Error(fmt.Sprintf("检查失败: %s - %s", owner.Email, accountStatus.Error), owner.Email, "ban-check")
result.Message = accountStatus.Error
}
return result
}

View File

@@ -174,6 +174,9 @@ func InitFromDB() *Config {
cfg.MailServices = services cfg.MailServices = services
} }
} }
if v, _ := configDB.GetConfig("site_name"); v != "" {
cfg.SiteName = v
}
Global = cfg Global = cfg
return cfg return cfg
@@ -210,6 +213,10 @@ func SaveToDB() error {
configDB.SetConfig("mail_services", string(data)) configDB.SetConfig("mail_services", string(data))
} }
if cfg.SiteName != "" {
configDB.SetConfig("site_name", cfg.SiteName)
}
return nil return nil
} }

View File

@@ -17,6 +17,7 @@ type TeamOwner struct {
AccountID string `json:"account_id"` AccountID string `json:"account_id"`
Status string `json:"status"` Status string `json:"status"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
LastCheckedAt *time.Time `json:"last_checked_at,omitempty"`
} }
// DB 数据库管理器 // DB 数据库管理器
@@ -40,10 +41,31 @@ func Init(dbPath string) error {
return fmt.Errorf("创建表失败: %w", err) return fmt.Errorf("创建表失败: %w", err)
} }
// 执行数据库迁移
if err := Instance.migrate(); err != nil {
fmt.Printf("[数据库] 迁移警告: %v\n", err)
}
fmt.Printf("[数据库] SQLite 已连接: %s\n", dbPath) fmt.Printf("[数据库] SQLite 已连接: %s\n", dbPath)
return nil return nil
} }
// migrate 数据库迁移
func (d *DB) migrate() error {
// 添加 last_checked_at 列(如果不存在)
_, err := d.db.Exec(`ALTER TABLE team_owners ADD COLUMN last_checked_at DATETIME`)
if err != nil && !isColumnExistsError(err) {
return err
}
return nil
}
// isColumnExistsError 判断是否是列已存在的错误
func isColumnExistsError(err error) bool {
return err != nil && (err.Error() == "duplicate column name: last_checked_at" ||
err.Error() == "table team_owners already has a column named last_checked_at")
}
// createTables 创建表 // createTables 创建表
func (d *DB) createTables() error { func (d *DB) createTables() error {
_, err := d.db.Exec(` _, err := d.db.Exec(`
@@ -54,7 +76,8 @@ func (d *DB) createTables() error {
token TEXT, token TEXT,
account_id TEXT NOT NULL, account_id TEXT NOT NULL,
status TEXT DEFAULT 'valid', status TEXT DEFAULT 'valid',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_checked_at DATETIME
); );
CREATE INDEX IF NOT EXISTS idx_team_owners_email ON team_owners(email); CREATE INDEX IF NOT EXISTS idx_team_owners_email ON team_owners(email);
@@ -173,7 +196,7 @@ func (d *DB) AddTeamOwners(owners []TeamOwner) (int, error) {
// GetTeamOwners 获取列表 // GetTeamOwners 获取列表
func (d *DB) GetTeamOwners(status string, limit, offset int) ([]TeamOwner, int, error) { func (d *DB) GetTeamOwners(status string, limit, offset int) ([]TeamOwner, int, error) {
query := "SELECT id, email, password, token, account_id, status, created_at FROM team_owners WHERE 1=1" query := "SELECT id, email, password, token, account_id, status, created_at, last_checked_at FROM team_owners WHERE 1=1"
countQuery := "SELECT COUNT(*) FROM team_owners WHERE 1=1" countQuery := "SELECT COUNT(*) FROM team_owners WHERE 1=1"
args := []interface{}{} args := []interface{}{}
@@ -202,10 +225,14 @@ func (d *DB) GetTeamOwners(status string, limit, offset int) ([]TeamOwner, int,
var owners []TeamOwner var owners []TeamOwner
for rows.Next() { for rows.Next() {
var owner TeamOwner var owner TeamOwner
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt) var lastCheckedAt sql.NullTime
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt, &lastCheckedAt)
if err != nil { if err != nil {
continue continue
} }
if lastCheckedAt.Valid {
owner.LastCheckedAt = &lastCheckedAt.Time
}
owners = append(owners, owner) owners = append(owners, owner)
} }
@@ -215,7 +242,7 @@ func (d *DB) GetTeamOwners(status string, limit, offset int) ([]TeamOwner, int,
// GetPendingOwners 获取待处理(排除已使用和处理中的) // GetPendingOwners 获取待处理(排除已使用和处理中的)
func (d *DB) GetPendingOwners() ([]TeamOwner, error) { func (d *DB) GetPendingOwners() ([]TeamOwner, error) {
rows, err := d.db.Query(` rows, err := d.db.Query(`
SELECT id, email, password, token, account_id, status, created_at SELECT id, email, password, token, account_id, status, created_at, last_checked_at
FROM team_owners WHERE status = 'valid' FROM team_owners WHERE status = 'valid'
ORDER BY created_at ASC ORDER BY created_at ASC
`) `)
@@ -227,15 +254,62 @@ func (d *DB) GetPendingOwners() ([]TeamOwner, error) {
var owners []TeamOwner var owners []TeamOwner
for rows.Next() { for rows.Next() {
var owner TeamOwner var owner TeamOwner
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt) var lastCheckedAt sql.NullTime
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt, &lastCheckedAt)
if err != nil { if err != nil {
continue continue
} }
if lastCheckedAt.Valid {
owner.LastCheckedAt = &lastCheckedAt.Time
}
owners = append(owners, owner) owners = append(owners, owner)
} }
return owners, nil return owners, nil
} }
// GetOwnersForBanCheck 获取需要检查封禁状态的母号
func (d *DB) GetOwnersForBanCheck(checkIntervalHours int) ([]TeamOwner, error) {
// 获取 valid 状态且 超过检查间隔 或 从未检查过 的母号
rows, err := d.db.Query(`
SELECT id, email, password, token, account_id, status, created_at, last_checked_at
FROM team_owners
WHERE status = 'valid'
AND (last_checked_at IS NULL OR last_checked_at < datetime('now', ?))
ORDER BY last_checked_at ASC NULLS FIRST, created_at ASC
`, fmt.Sprintf("-%d hours", checkIntervalHours))
if err != nil {
return nil, err
}
defer rows.Close()
var owners []TeamOwner
for rows.Next() {
var owner TeamOwner
var lastCheckedAt sql.NullTime
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt, &lastCheckedAt)
if err != nil {
continue
}
if lastCheckedAt.Valid {
owner.LastCheckedAt = &lastCheckedAt.Time
}
owners = append(owners, owner)
}
return owners, nil
}
// UpdateOwnerLastCheckedAt 更新母号的最后检查时间
func (d *DB) UpdateOwnerLastCheckedAt(id int64) error {
_, err := d.db.Exec("UPDATE team_owners SET last_checked_at = CURRENT_TIMESTAMP WHERE id = ?", id)
return err
}
// UpdateOwnerLastCheckedAtByEmail 通过邮箱更新母号的最后检查时间
func (d *DB) UpdateOwnerLastCheckedAtByEmail(email string) error {
_, err := d.db.Exec("UPDATE team_owners SET last_checked_at = CURRENT_TIMESTAMP WHERE email = ?", email)
return err
}
// MarkOwnerAsUsed 标记 Owner 为已使用 // MarkOwnerAsUsed 标记 Owner 为已使用
func (d *DB) MarkOwnerAsUsed(email string) error { func (d *DB) MarkOwnerAsUsed(email string) error {
_, err := d.db.Exec("UPDATE team_owners SET status = 'used' WHERE email = ?", email) _, err := d.db.Exec("UPDATE team_owners SET status = 'used' WHERE email = ?", email)
@@ -290,7 +364,7 @@ func (d *DB) ClearUsedOwners() (int64, error) {
// GetOwnersWithoutAccountID 获取缺少 account_id 的 owners // GetOwnersWithoutAccountID 获取缺少 account_id 的 owners
func (d *DB) GetOwnersWithoutAccountID() ([]TeamOwner, error) { func (d *DB) GetOwnersWithoutAccountID() ([]TeamOwner, error) {
rows, err := d.db.Query(` rows, err := d.db.Query(`
SELECT id, email, password, token, account_id, status, created_at SELECT id, email, password, token, account_id, status, created_at, last_checked_at
FROM team_owners WHERE account_id = '' OR account_id IS NULL FROM team_owners WHERE account_id = '' OR account_id IS NULL
ORDER BY created_at DESC ORDER BY created_at DESC
`) `)
@@ -302,10 +376,14 @@ func (d *DB) GetOwnersWithoutAccountID() ([]TeamOwner, error) {
var owners []TeamOwner var owners []TeamOwner
for rows.Next() { for rows.Next() {
var owner TeamOwner var owner TeamOwner
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt) var lastCheckedAt sql.NullTime
err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt, &lastCheckedAt)
if err != nil { if err != nil {
continue continue
} }
if lastCheckedAt.Valid {
owner.LastCheckedAt = &lastCheckedAt.Time
}
owners = append(owners, owner) owners = append(owners, owner)
} }
return owners, nil return owners, nil

View File

@@ -36,6 +36,14 @@ type AccountCheckResponse struct {
} `json:"accounts"` } `json:"accounts"`
} }
// AccountStatus 账户状态检查结果
type AccountStatus struct {
Status string // active, banned, token_expired, error
PlanType string // team, plus, free 等
AccountID string // 账户 ID
Error string // 错误信息
}
// New 创建邀请器 (使用默认代理) // New 创建邀请器 (使用默认代理)
func New(accessToken string) *TeamInviter { func New(accessToken string) *TeamInviter {
c, _ := client.New(DefaultProxy) c, _ := client.New(DefaultProxy)
@@ -59,6 +67,77 @@ func (t *TeamInviter) SetAccountID(accountID string) {
t.accountID = accountID t.accountID = accountID
} }
// CheckAccountStatus 检查账户状态(封禁检测)
// 基于 check_ban.py 的逻辑:
// - HTTP 200 + 有账户数据 → active
// - HTTP 200 + 无账户数据 → banned
// - HTTP 401 → token_expired
// - HTTP 403 → banned
func (t *TeamInviter) CheckAccountStatus() AccountStatus {
result := AccountStatus{Status: "error"}
req, _ := http.NewRequest("GET", "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27", nil)
req.Header.Set("Authorization", "Bearer "+t.accessToken)
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")
resp, err := t.client.Do(req)
if err != nil {
result.Error = fmt.Sprintf("请求失败: %v", err)
return result
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
switch resp.StatusCode {
case 200:
var checkResp AccountCheckResponse
if err := json.Unmarshal(body, &checkResp); err != nil {
result.Error = fmt.Sprintf("解析失败: %v", err)
return result
}
// 检查是否有账户数据
if len(checkResp.Accounts) == 0 {
result.Status = "banned"
result.Error = "无账户数据"
return result
}
// 查找非 default 的账户
for accountID, info := range checkResp.Accounts {
if accountID == "default" {
continue
}
result.Status = "active"
result.AccountID = accountID
result.PlanType = info.Account.PlanType
t.accountID = accountID
return result
}
// 只有 default 账户,也视为正常
result.Status = "active"
result.PlanType = "default"
return result
case 401:
result.Status = "token_expired"
result.Error = "Token 已过期"
return result
case 403:
result.Status = "banned"
result.Error = "账户被封禁 (403)"
return result
default:
result.Error = fmt.Sprintf("HTTP %d", resp.StatusCode)
return result
}
}
// GetAccountID 获取 Team 的 account_id (workspace_id) // GetAccountID 获取 Team 的 account_id (workspace_id)
func (t *TeamInviter) GetAccountID() (string, error) { func (t *TeamInviter) GetAccountID() (string, error) {
req, _ := http.NewRequest("GET", "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27", nil) req, _ := http.NewRequest("GET", "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27", nil)

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square } from 'lucide-react' import { Users, Trash2, RefreshCw, ChevronLeft, ChevronRight, Key, CheckSquare, Square, ShieldCheck, Settings, Clock } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button } from '../common' import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../common'
interface TeamOwner { interface TeamOwner {
id: number id: number
@@ -8,6 +8,25 @@ interface TeamOwner {
account_id: string account_id: string
status: string status: string
created_at: string created_at: string
last_checked_at?: string
}
interface BanCheckTaskState {
running: boolean
started_at: string
total: number
checked: number
banned: number
valid: number
failed: number
}
interface BanCheckSettings {
enabled: boolean
interval: number
check_hours: number
service_running: boolean
task_state: BanCheckTaskState
} }
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
@@ -42,6 +61,12 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
const [deleting, setDeleting] = useState(false) const [deleting, setDeleting] = useState(false)
const limit = 20 const limit = 20
// 封禁检查相关状态
const [banCheckSettings, setBanCheckSettings] = useState<BanCheckSettings | null>(null)
const [showBanCheckSettings, setShowBanCheckSettings] = useState(false)
const [banCheckRunning, setBanCheckRunning] = useState(false)
const [checkHours, setCheckHours] = useState(24)
const loadOwners = async () => { const loadOwners = async () => {
setLoading(true) setLoading(true)
try { try {
@@ -72,6 +97,99 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
setSelectedIds(new Set()) setSelectedIds(new Set())
}, [page, filter]) }, [page, filter])
// 加载封禁检查配置
const loadBanCheckSettings = useCallback(async () => {
try {
const res = await fetch('/api/db/owners/ban-check/settings')
const data = await res.json()
if (data.code === 0) {
setBanCheckSettings(data.data)
setCheckHours(data.data.check_hours || 24)
setBanCheckRunning(data.data.task_state?.running || false)
}
} catch (e) {
console.error('Failed to load ban check settings:', e)
}
}, [])
useEffect(() => {
loadBanCheckSettings()
// 轮询检查状态
const interval = setInterval(() => {
if (banCheckRunning) {
loadBanCheckSettings()
}
}, 2000)
return () => clearInterval(interval)
}, [loadBanCheckSettings, banCheckRunning])
// 手动触发封禁检查
const handleBanCheck = async (forceCheck = false) => {
if (banCheckRunning) return
setBanCheckRunning(true)
try {
const body: { ids?: number[], force_check?: boolean } = {}
if (selectedIds.size > 0) {
body.ids = Array.from(selectedIds)
}
if (forceCheck) {
body.force_check = true
}
const res = await fetch('/api/db/owners/ban-check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
if (data.code === 0) {
// 开始轮询状态
const pollInterval = setInterval(async () => {
const statusRes = await fetch('/api/db/owners/ban-check/status')
const statusData = await statusRes.json()
if (statusData.code === 0) {
if (!statusData.data.running) {
clearInterval(pollInterval)
setBanCheckRunning(false)
loadOwners()
onStatsChange?.()
loadBanCheckSettings()
}
}
}, 2000)
} else {
alert(data.message || '启动检查失败')
setBanCheckRunning(false)
}
} catch (e) {
console.error('Failed to start ban check:', e)
alert('启动检查失败')
setBanCheckRunning(false)
}
}
// 保存封禁检查配置
const handleSaveBanCheckSettings = async (enabled: boolean) => {
try {
const res = await fetch('/api/db/owners/ban-check/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
enabled,
check_hours: checkHours,
}),
})
const data = await res.json()
if (data.code === 0) {
loadBanCheckSettings()
} else {
alert(data.message || '保存失败')
}
} catch (e) {
console.error('Failed to save ban check settings:', e)
alert('保存失败')
}
}
// 单个删除 // 单个删除
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
if (!confirm('确认删除此账号?')) return if (!confirm('确认删除此账号?')) return
@@ -247,6 +365,29 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
> >
{refetching ? '获取中...' : '重新获取ID'} {refetching ? '获取中...' : '重新获取ID'}
</Button> </Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleBanCheck(false)}
disabled={banCheckRunning}
icon={<ShieldCheck className={`h-4 w-4 ${banCheckRunning ? 'animate-pulse' : ''}`} />}
className="text-emerald-500 hover:text-emerald-600"
>
{banCheckRunning
? `检查中 (${banCheckSettings?.task_state?.checked || 0}/${banCheckSettings?.task_state?.total || 0})`
: selectedIds.size > 0
? `检查封禁 (${selectedIds.size})`
: '检查封禁'}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setShowBanCheckSettings(!showBanCheckSettings)}
icon={<Settings className="h-4 w-4" />}
className={showBanCheckSettings ? 'text-blue-500' : 'text-slate-500 hover:text-slate-600'}
>
</Button>
{selectedIds.size > 0 && ( {selectedIds.size > 0 && (
<Button <Button
variant="ghost" variant="ghost"
@@ -280,6 +421,62 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
{/* 封禁检查设置面板 */}
{showBanCheckSettings && (
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-800/50 border-b border-slate-200 dark:border-slate-700">
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
<label className="text-sm text-slate-600 dark:text-slate-400">:</label>
<button
onClick={() => handleSaveBanCheckSettings(!banCheckSettings?.enabled)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
banCheckSettings?.enabled ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-slate-600'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
banCheckSettings?.enabled ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-slate-400" />
<label className="text-sm text-slate-600 dark:text-slate-400">:</label>
<Input
type="number"
value={checkHours}
onChange={(e) => setCheckHours(parseInt(e.target.value) || 24)}
className="w-20 h-8 text-sm"
min={1}
max={168}
/>
<span className="text-sm text-slate-500"></span>
<Button
size="sm"
variant="outline"
onClick={() => handleSaveBanCheckSettings(banCheckSettings?.enabled || false)}
>
</Button>
</div>
<div className="flex items-center gap-2 ml-auto text-xs text-slate-500">
{banCheckSettings?.task_state?.running ? (
<span className="text-emerald-500">
: {banCheckSettings.task_state.checked}/{banCheckSettings.task_state.total}
(: {banCheckSettings.task_state.valid}, : {banCheckSettings.task_state.banned})
</span>
) : banCheckSettings?.enabled ? (
<span></span>
) : (
<span></span>
)}
</div>
</div>
</div>
)}
<CardContent className="flex-1 overflow-hidden p-0"> <CardContent className="flex-1 overflow-hidden p-0">
<div className="h-full overflow-auto"> <div className="h-full overflow-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
@@ -298,19 +495,20 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400">Account ID</th> <th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400">Account ID</th>
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400"></th> <th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400"></th>
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400"></th> <th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400"></th>
<th className="text-left p-3 font-medium text-slate-600 dark:text-slate-400"></th>
<th className="text-center p-3 font-medium text-slate-600 dark:text-slate-400"></th> <th className="text-center p-3 font-medium text-slate-600 dark:text-slate-400"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{loading ? ( {loading ? (
<tr> <tr>
<td colSpan={6} className="text-center py-8 text-slate-500"> <td colSpan={7} className="text-center py-8 text-slate-500">
... ...
</td> </td>
</tr> </tr>
) : owners.length === 0 ? ( ) : owners.length === 0 ? (
<tr> <tr>
<td colSpan={6} className="text-center py-8 text-slate-500"> <td colSpan={7} className="text-center py-8 text-slate-500">
</td> </td>
</tr> </tr>
@@ -336,6 +534,9 @@ export default function OwnerList({ onStatsChange }: OwnerListProps) {
</span> </span>
</td> </td>
<td className="p-3 text-slate-500 text-xs">{formatTime(owner.created_at)}</td> <td className="p-3 text-slate-500 text-xs">{formatTime(owner.created_at)}</td>
<td className="p-3 text-slate-500 text-xs">
{owner.last_checked_at ? formatTime(owner.last_checked_at) : '-'}
</td>
<td className="p-3 text-center"> <td className="p-3 text-center">
<button <button
onClick={() => handleDelete(owner.id)} onClick={() => handleDelete(owner.id)}