Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions internal/aistudio/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2193,6 +2193,18 @@ func (p *AccountPool) ClearCooldownIfGeneration(
return err
}

// ClearCooldown 清除账户在作用域上的冷却,作用域为全局 *、模型 ID 或 build:<模型 ID>
func (p *AccountPool) ClearCooldown(accountID string, scope string) error {
_, err := p.updateRuntime(accountID, func(_ *Account, runtimeState *accountRuntimeState) (bool, func(*Account), error) {
if _, exists := runtimeState.Cooldowns[scope]; !exists {
return false, nil, nil
}
delete(runtimeState.Cooldowns, scope)
return true, nil, nil
})
return err
}

// BindResource 将资源 ID 固定到创建账户
func (p *AccountPool) BindResource(resourceID string, accountID string) error {
return p.BindResourceKind(resourceID, accountID, "")
Expand Down
1 change: 1 addition & 0 deletions internal/aistudio/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ type IsolatedLoginResult struct {
Email string
VerifiedAt time.Time
DriveError string
CloseError string
}

// IsolatedLoginDriver 定义独立浏览器登录
Expand Down
3 changes: 2 additions & 1 deletion internal/aistudio/login_native.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ func (driver *NativeLoginDriver) Login(ctx context.Context, request IsolatedLogi
return IsolatedLoginResult{}, err
}
return IsolatedLoginResult{
StorageState: state, Email: result.Email, VerifiedAt: result.VerifiedAt, DriveError: result.DriveError,
StorageState: state, Email: result.Email, VerifiedAt: result.VerifiedAt,
DriveError: result.DriveError, CloseError: result.CloseError,
}, nil
}

Expand Down
11 changes: 11 additions & 0 deletions internal/api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type AdminService interface {
RuntimeConfig(context.Context) (RuntimeConfig, error)
UpdateRuntimeConfig(context.Context, RuntimeConfig) (RuntimeConfig, error)
Cooldowns(context.Context) ([]AdminCooldown, error)
ClearCooldown(ctx context.Context, accountID string, scope string) error
Requests(context.Context) ([]AdminRequest, error)
CancelRequest(context.Context, string) error
Events(context.Context) (<-chan AdminEvent, error)
Expand Down Expand Up @@ -241,6 +242,7 @@ type AdminCooldown struct {
AccountLabel string `json:"account_label"`
Channel string `json:"channel"`
ModelID string `json:"model_id"`
Scope string `json:"scope"`
Until time.Time `json:"until"`
Reason string `json:"reason,omitempty"`
}
Expand Down Expand Up @@ -278,6 +280,7 @@ func (s *server) registerAdmin(mux *http.ServeMux) {
mux.HandleFunc("GET /api/config", s.handleRuntimeConfig)
mux.HandleFunc("PUT /api/config", s.handleUpdateRuntimeConfig)
mux.HandleFunc("GET /api/cooldowns", s.handleCooldowns)
mux.HandleFunc("DELETE /api/accounts/{id}/cooldowns/{scope}", s.handleClearCooldown)
mux.HandleFunc("GET /api/requests", s.handleRequests)
mux.HandleFunc("POST /api/requests/{id}/cancel", s.handleCancelRequest)
mux.HandleFunc("GET /api/events", s.handleAdminEvents)
Expand Down Expand Up @@ -457,6 +460,14 @@ func (s *server) handleCooldowns(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string][]AdminCooldown{"cooldowns": cooldowns})
}

func (s *server) handleClearCooldown(w http.ResponseWriter, r *http.Request) {
if err := s.config.Admin.ClearCooldown(r.Context(), r.PathValue("id"), r.PathValue("scope")); err != nil {
writeAdminUpstreamError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}

func (s *server) handleRequests(w http.ResponseWriter, r *http.Request) {
requests, err := s.config.Admin.Requests(r.Context())
if err != nil {
Expand Down
30 changes: 29 additions & 1 deletion internal/app/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ func (admin *runtimeAdmin) CreateAccount(ctx context.Context, input api.AccountC
if result.DriveError != "" {
admin.requests.log("auth", "WARN", "账户添加 | Drive 授权失败 | 错误="+result.DriveError)
}
if result.CloseError != "" {
admin.requests.log("auth", "WARN", "账户添加 | 关闭 Camoufox 失败 | 错误="+result.CloseError)
}
admin.requests.log("auth", "INFO", "账户添加 | 2/2 | 保存认证状态")
if _, err := aistudio.NewSigner().Sign(result.StorageState); err != nil {
return api.AdminAccount{}, fmt.Errorf("认证状态无法用于 AI Studio: %w", err)
Expand Down Expand Up @@ -507,6 +510,9 @@ func (admin *runtimeAdmin) LoginAccount(ctx context.Context, accountID string) (
if result.DriveError != "" {
admin.requests.log(account.Config.Label, "WARN", "账户登录 | Drive 授权失败 | 错误="+result.DriveError)
}
if result.CloseError != "" {
admin.requests.log(account.Config.Label, "WARN", "账户登录 | 关闭 Camoufox 失败 | 错误="+result.CloseError)
}
admin.requests.log(account.Config.Label, "INFO", "账户登录 | 2/2 | 保存认证状态")
if _, err := aistudio.NewSigner().Sign(result.StorageState); err != nil {
return api.AdminAccount{}, errors.Join(fmt.Errorf("认证状态无法用于 AI Studio: %w", err), lease.Release())
Expand Down Expand Up @@ -840,9 +846,11 @@ func (admin *runtimeAdmin) Cooldowns(context.Context) ([]api.AdminCooldown, erro
models[modelID] = struct{}{}
}
effective := make(map[string]aistudio.CooldownState)
scopes := make(map[string]string)
if global, ok := account.Cooldowns["*"]; ok && global.Active(now) {
for modelID := range models {
effective[modelID] = global
scopes[modelID] = "*"
}
}
for key, cooldown := range account.Cooldowns {
Expand All @@ -855,6 +863,7 @@ func (admin *runtimeAdmin) Cooldowns(context.Context) ([]api.AdminCooldown, erro
}
if current, ok := effective[key]; !ok || cooldown.Until.After(current.Until) {
effective[key] = cooldown
scopes[key] = key
}
}
modelIDs := make([]string, 0, len(effective))
Expand All @@ -873,13 +882,32 @@ func (admin *runtimeAdmin) Cooldowns(context.Context) ([]api.AdminCooldown, erro
}
cooldowns = append(cooldowns, api.AdminCooldown{
AccountID: account.ID, AccountLabel: account.Label, Channel: channel,
ModelID: modelID, Until: cooldown.Until, Reason: cooldown.Reason,
ModelID: modelID, Scope: scopes[key], Until: cooldown.Until, Reason: cooldown.Reason,
})
}
}
return cooldowns, nil
}

// ClearCooldown 手动清除账户在一个作用域上的冷却,并推送新的冷却列表与账户状态
func (admin *runtimeAdmin) ClearCooldown(ctx context.Context, accountID string, scope string) error {
account, err := admin.account(accountID)
if err != nil {
return err
}
if err := admin.pool.ClearCooldown(accountID, scope); err != nil {
return err
}
admin.requests.log(account.Label, "INFO", "冷却已手动清除 | 范围="+scope)
if cooldowns, err := admin.Cooldowns(ctx); err == nil {
admin.requests.publish(api.AdminEvent{Type: "cooldowns", Data: cooldowns})
}
if accounts, err := admin.Accounts(ctx); err == nil {
admin.requests.publish(api.AdminEvent{Type: "accounts", Data: map[string]any{"accounts": accounts}})
}
return nil
}

func (admin *runtimeAdmin) Requests(context.Context) ([]api.AdminRequest, error) {
return admin.requests.list(), nil
}
Expand Down
7 changes: 7 additions & 0 deletions internal/app/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,13 @@ func (manager *runtimeManager) Cooldowns(ctx context.Context) ([]api.AdminCooldo
return manager.current.admin.Cooldowns(ctx)
}

// ClearCooldown 手动清除账户在一个作用域上的冷却
func (manager *runtimeManager) ClearCooldown(ctx context.Context, accountID string, scope string) error {
manager.mu.RLock()
defer manager.mu.RUnlock()
return manager.current.admin.ClearCooldown(ctx, accountID, scope)
}

// Requests 返回进程级活动请求
func (manager *runtimeManager) Requests(ctx context.Context) ([]api.AdminRequest, error) {
manager.mu.RLock()
Expand Down
22 changes: 10 additions & 12 deletions internal/camoufoxnative/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ var bidiEndpointPattern = regexp.MustCompile(`ws://[^\s]+`)

const browserProcessCloseTimeout = 5 * time.Second

type browserProcessTerminator func(context.Context, *exec.Cmd) error
type browserProcessTerminator func(context.Context, *browserProcess) error

type browserProcess struct {
command *exec.Cmd
done chan struct{}
waitErr error
profile string
lock *flock.Flock
tree browserTree
mu sync.Mutex
closed bool
}
Expand Down Expand Up @@ -140,7 +141,8 @@ func launchBrowser(ctx context.Context, options Options, config map[string]any)
_ = removeProfile(profile, profileLock)
return nil, "", fmt.Errorf("启动 Camoufox: %w", err)
}
if err := attachBrowserProcess(command); err != nil {
tree, err := attachBrowserProcess(command)
if err != nil {
_ = command.Process.Kill()
_ = command.Wait()
_ = removeProfile(profile, profileLock)
Expand All @@ -151,6 +153,7 @@ func launchBrowser(ctx context.Context, options Options, config map[string]any)
done: make(chan struct{}),
profile: profile,
lock: profileLock,
tree: tree,
}
launched = true
go func() {
Expand Down Expand Up @@ -181,9 +184,8 @@ func launchBrowser(ctx context.Context, options Options, config map[string]any)
case <-process.done:
process.mu.Lock()
err := process.waitErr
process.closed = true
process.mu.Unlock()
_ = removeProfile(profile, profileLock)
_ = process.Close()
if err == nil {
err = errors.New("Camoufox 在报告 BiDi 端点前退出")
}
Expand Down Expand Up @@ -217,16 +219,11 @@ func (process *browserProcess) close(timeout time.Duration, terminate browserPro
}
closeCtx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var closeErr error
closeErr := terminate(closeCtx, process)
select {
case <-process.done:
default:
closeErr = terminate(closeCtx, process.command)
select {
case <-process.done:
case <-closeCtx.Done():
closeErr = errors.Join(closeErr, fmt.Errorf("等待 Camoufox 进程退出: %w", closeCtx.Err()))
}
case <-closeCtx.Done():
closeErr = errors.Join(closeErr, fmt.Errorf("等待 Camoufox 进程退出: %w", closeCtx.Err()))
}
closeErr = errors.Join(closeErr, removeProfile(process.profile, process.lock))
if closeErr != nil {
Expand All @@ -235,6 +232,7 @@ func (process *browserProcess) close(timeout time.Duration, terminate browserPro
process.mu.Lock()
process.closed = true
process.mu.Unlock()
releaseBrowserTree(process.tree)
return nil
}

Expand Down
8 changes: 7 additions & 1 deletion internal/camoufoxnative/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type LoginResult struct {
PageURL string
VerifiedAt time.Time
DriveError string
CloseError string
}

type loginSession struct {
Expand All @@ -65,7 +66,12 @@ func Login(ctx context.Context, options LoginOptions) (result LoginResult, err e
return LoginResult{}, err
}
defer func() {
err = errors.Join(err, session.Close())
closeErr := session.Close()
if err == nil && closeErr != nil {
result.CloseError = closeErr.Error()
return
}
err = errors.Join(err, closeErr)
}()
origins := make(map[string]storageOrigin)
pageURL, err := session.waitLogin(loginCtx, origins)
Expand Down
13 changes: 10 additions & 3 deletions internal/camoufoxnative/process_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,20 @@ func configureBrowserProcess(command *exec.Cmd, _ bool) {
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}

// browserTree 在非 Windows 平台由进程组表示,不需要额外句柄
type browserTree struct{}

// attachBrowserProcess 在非 Windows 平台由进程组负责回收
func attachBrowserProcess(*exec.Cmd) error {
return nil
func attachBrowserProcess(*exec.Cmd) (browserTree, error) {
return browserTree{}, nil
}

// releaseBrowserTree 在非 Windows 平台无需释放
func releaseBrowserTree(browserTree) {}

// terminateBrowserProcess 结束 Camoufox 进程组
func terminateBrowserProcess(ctx context.Context, command *exec.Cmd) error {
func terminateBrowserProcess(ctx context.Context, process *browserProcess) error {
command := process.command
if command == nil || command.Process == nil {
return nil
}
Expand Down
Loading