Skip to content
Open
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
67 changes: 66 additions & 1 deletion internal/api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ type AdminService interface {
Accounts(context.Context) ([]AdminAccount, error)
CreateAccount(context.Context, AccountCreateInput, *camoufoxnative.LiveView) (AdminAccount, error)
ChromeImportProfiles(context.Context) ([]ChromeImportProfile, error)
BrowserImportProfiles(ctx context.Context, browser string) ([]BrowserImportProfile, error)
CookieBrowsers(context.Context) ([]CookieBrowser, error)
DiscoverCookieAccounts(context.Context, CookieImportInput) ([]CookieBrowser, error)
ImportChromeAccounts(context.Context, ChromeImportInput) ([]AdminAccount, error)
ImportBrowserAccounts(ctx context.Context, input BrowserImportInput) ([]AdminAccount, []ImportFailure, error)
ImportAccountState(context.Context, AccountStateInput) (AdminAccount, error)
UpdateAccount(context.Context, string, AccountInput) (AdminAccount, error)
DeleteAccount(context.Context, string) error
Expand Down Expand Up @@ -247,6 +249,33 @@ type ChromeImportInput struct {
Timezone string `json:"timezone"`
}

// BrowserImportProfile 表示可从本机浏览器导入的账号
type BrowserImportProfile struct {
ID string `json:"id"`
Browser string `json:"browser,omitempty"`
Profile string `json:"profile"`
DisplayName string `json:"display_name"`
Email string `json:"email"`
Locale string `json:"locale"`
AuthUser int `json:"auth_user,omitempty"`
Imported bool `json:"imported"`
}

// BrowserImportInput 表示批量导入的浏览器账号与账户环境
type BrowserImportInput struct {
Browser string `json:"browser"`
AccountIDs []string `json:"account_ids"`
Proxy string `json:"proxy"`
Locale string `json:"locale"`
Timezone string `json:"timezone"`
}

// ImportFailure 描述导入失败的账号与原因
type ImportFailure struct {
Target string `json:"target"`
Error string `json:"error"`
}

// RuntimeConfig 表示全局运行配置
type RuntimeConfig struct {
AutoStart bool `json:"auto_start"`
Expand Down Expand Up @@ -334,6 +363,8 @@ func (s *server) registerAdmin(mux *http.ServeMux) {
mux.HandleFunc("POST /api/accounts", s.handleCreateAccount)
mux.HandleFunc("GET /api/accounts/import/chrome", s.handleChromeImportProfiles)
mux.HandleFunc("POST /api/accounts/import/chrome", s.handleImportChromeAccounts)
mux.HandleFunc("GET /api/accounts/import/browser", s.handleBrowserImportProfiles)
mux.HandleFunc("POST /api/accounts/import/browser", s.handleImportBrowserAccounts)
mux.HandleFunc("GET /api/accounts/import/browsers", s.handleCookieBrowsers)
mux.HandleFunc("POST /api/accounts/import/cookies/discover", s.handleDiscoverCookieAccounts)
mux.HandleFunc("POST /api/accounts/import/cookies", s.handleImportCookieAccount)
Expand Down Expand Up @@ -429,6 +460,36 @@ func (s *server) handleImportChromeAccounts(w http.ResponseWriter, r *http.Reque
writeJSON(w, http.StatusCreated, map[string][]AdminAccount{"accounts": accounts})
}

func (s *server) handleBrowserImportProfiles(w http.ResponseWriter, r *http.Request) {
browser := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("browser")))
if browser == "" {
browser = "chrome"
}
profiles, err := s.config.Admin.BrowserImportProfiles(r.Context(), browser)
if err != nil {
writeAdminUpstreamError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"browser": browser, "profiles": profiles})
}

func (s *server) handleImportBrowserAccounts(w http.ResponseWriter, r *http.Request) {
var input BrowserImportInput
if err := decodeJSON(r, &input); err != nil {
writeAdminError(w, http.StatusBadRequest, "invalid_request", err.Error())
return
}
if input.Browser == "" {
input.Browser = "chrome"
}
accounts, failures, err := s.config.Admin.ImportBrowserAccounts(r.Context(), input)
if err != nil && len(accounts) == 0 {
writeAdminUpstreamError(w, err)
return
}
writeJSON(w, http.StatusCreated, map[string]any{"accounts": accounts, "failures": failures})
}

func (s *server) handleCookieBrowsers(w http.ResponseWriter, r *http.Request) {
browsers, err := s.config.Admin.CookieBrowsers(r.Context())
if err != nil {
Expand Down Expand Up @@ -671,5 +732,9 @@ func writeAdminUpstreamError(w http.ResponseWriter, err error) {
if errors.Is(err, context.Canceled) {
return
}
writeAdminError(w, statusFromError(err), codeFromError(err, "upstream_error"), err.Error())
status := statusFromError(err)
if status == http.StatusUnauthorized {
status = http.StatusBadGateway
}
writeAdminError(w, status, codeFromError(err, "upstream_error"), err.Error())
}
141 changes: 110 additions & 31 deletions internal/app/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,28 +175,48 @@ func (admin *runtimeAdmin) CreateAccount(ctx context.Context, input api.AccountC
return created, nil
}

func (admin *runtimeAdmin) ChromeImportProfiles(context.Context) ([]api.ChromeImportProfile, error) {
root, err := chromeauth.DefaultChromeRoot()
func (admin *runtimeAdmin) BrowserImportProfiles(ctx context.Context, browser string) ([]api.BrowserImportProfile, error) {
if browser == "" {
browser = "chrome"
}
root, err := chromeauth.DefaultBrowserRoot(browser)
if err != nil {
return nil, err
}
accounts, err := chromeauth.Discover(root)
accounts, err := chromeauth.DiscoverBrowser(browser, root)
if err != nil {
return nil, err
}
existing := admin.accountIDs()
profiles := make([]api.ChromeImportProfile, 0, len(accounts))
profiles := make([]api.BrowserImportProfile, 0, len(accounts))
for _, account := range accounts {
email := strings.ToLower(strings.TrimSpace(account.Email))
if !account.Importable || email == "" {
continue
}
if _, exists := existing[email]; exists {
_, isImported := existing[email]
profiles = append(profiles, api.BrowserImportProfile{
ID: account.ID, Browser: browser, Profile: account.Profile, DisplayName: account.DisplayName,
Email: email, Locale: account.Locale, AuthUser: account.AuthUser, Imported: isImported,
})
}
return profiles, nil
}

func (admin *runtimeAdmin) ChromeImportProfiles(ctx context.Context) ([]api.ChromeImportProfile, error) {
bp, err := admin.BrowserImportProfiles(ctx, "chrome")
if err != nil || len(bp) == 0 {
if braveBp, braveErr := admin.BrowserImportProfiles(ctx, "brave"); braveErr == nil && len(braveBp) > 0 {
bp = braveBp
}
}
profiles := make([]api.ChromeImportProfile, 0, len(bp))
for _, p := range bp {
if p.Imported {
continue
}
existing[email] = struct{}{}
profiles = append(profiles, api.ChromeImportProfile{
ID: account.ID, Profile: account.Profile, DisplayName: account.DisplayName, Email: email, Locale: account.Locale,
ID: p.ID, Profile: p.Profile, DisplayName: p.DisplayName, Email: p.Email, Locale: p.Locale,
})
}
return profiles, nil
Expand Down Expand Up @@ -291,28 +311,59 @@ func (admin *runtimeAdmin) accountIDs() map[string]struct{} {
}

func (admin *runtimeAdmin) ImportChromeAccounts(ctx context.Context, input api.ChromeImportInput) ([]api.AdminAccount, error) {
accounts, _, err := admin.ImportBrowserAccounts(ctx, api.BrowserImportInput{
Browser: "chrome", AccountIDs: input.AccountIDs, Proxy: input.Proxy, Locale: input.Locale, Timezone: input.Timezone,
})
if err != nil && len(accounts) == 0 {
if bAccs, _, bErr := admin.ImportBrowserAccounts(ctx, api.BrowserImportInput{
Browser: "brave", AccountIDs: input.AccountIDs, Proxy: input.Proxy, Locale: input.Locale, Timezone: input.Timezone,
}); bErr == nil && len(bAccs) > 0 {
return bAccs, nil
}
}
return accounts, err
}

func (admin *runtimeAdmin) ImportBrowserAccounts(ctx context.Context, input api.BrowserImportInput) ([]api.AdminAccount, []api.ImportFailure, error) {
if len(input.AccountIDs) == 0 {
return nil, invalidAccount(fmt.Errorf("未选择 Chrome 账号"))
return nil, nil, invalidAccount(fmt.Errorf("未选择账号"))
}
root, err := chromeauth.DefaultChromeRoot()
browser := input.Browser
if browser == "" {
browser = "chrome"
}
root, err := chromeauth.DefaultBrowserRoot(browser)
if err != nil {
return nil, err
return nil, nil, err
}
accountProxy := strings.TrimSpace(input.Proxy)
results, err := chromeauth.Import(ctx, chromeauth.ImportOptions{
ChromeRoot: root, Proxy: admin.effectiveProxy(accountProxy), AccountIDs: input.AccountIDs,
res, err := chromeauth.ImportWithDetails(ctx, chromeauth.ImportOptions{
Browser: browser,
ChromeRoot: root,
Proxy: admin.effectiveProxy(accountProxy),
AccountIDs: input.AccountIDs,
})
if err != nil {
return nil, err
if err != nil && len(res.Results) == 0 {
var failures []api.ImportFailure
for _, f := range res.Failures {
failures = append(failures, api.ImportFailure{Target: f.Target, Error: f.Error})
}
return nil, failures, err
}

var failures []api.ImportFailure
for _, f := range res.Failures {
failures = append(failures, api.ImportFailure{Target: f.Target, Error: f.Error})
}
configs := make([]aistudio.AccountConfig, len(results))
seen := make(map[string]struct{}, len(results))
for index, result := range results {

existingAccounts := make(map[string]struct{})
for _, status := range admin.pool.Status() {
existingAccounts[strings.ToLower(strings.TrimSpace(status.ID))] = struct{}{}
}

accounts := make([]api.AdminAccount, 0, len(res.Results))
for _, result := range res.Results {
email := strings.ToLower(strings.TrimSpace(result.Email))
if _, exists := seen[email]; exists {
return nil, invalidAccount(fmt.Errorf("Chrome 账号重复: %s", email))
}
seen[email] = struct{}{}
accountConfig := aistudio.DefaultAccountConfig(email)
accountConfig.Proxy = accountProxy
if locale := strings.TrimSpace(input.Locale); locale != "" {
Expand All @@ -324,23 +375,51 @@ func (admin *runtimeAdmin) ImportChromeAccounts(ctx context.Context, input api.C
accountConfig.Timezone = timezone
}
if err := accountConfig.Validate(); err != nil {
return nil, invalidAccount(err)
failures = append(failures, api.ImportFailure{Target: email, Error: err.Error()})
continue
}
if _, err := aistudio.NewSigner().Authorization(result.State); err != nil {
return nil, fmt.Errorf("认证状态无法用于 AI Studio: %w", err)
failures = append(failures, api.ImportFailure{Target: email, Error: fmt.Sprintf("认证状态无法用于 AI Studio: %v", err)})
continue
}
configs[index] = accountConfig
}
accounts := make([]api.AdminAccount, 0, len(results))
for index, result := range results {
account, err := admin.addAccount(ctx, configs[index], result.State, "")

if _, exists := existingAccounts[email]; exists {
lease, leaseErr := admin.pool.AcquireAccount(ctx, email)
if leaseErr == nil {
account := lease.Account()
_ = admin.workers.Reset(account.ID)
saveErr := lease.SaveStorageState(result.State)
_ = admin.service.changeModels(func() error {
return errors.Join(
admin.pool.MarkReady(account.ID),
admin.pool.ResetModelAccess(account.ID),
)
})
_ = lease.Release()
if saveErr == nil {
admin.syncAccountModelCatalog(ctx, account)
if updated, err := admin.account(account.ID); err == nil {
accounts = append(accounts, updated)
admin.requests.log("auth", "INFO", browser+" 账户已覆盖更新 | 账户="+updated.Label)
continue
}
}
}
}

account, err := admin.addAccount(ctx, accountConfig, result.State, "")
if err != nil {
return nil, err
failures = append(failures, api.ImportFailure{Target: email, Error: err.Error()})
continue
}
accounts = append(accounts, account)
admin.requests.log("auth", "INFO", "Chrome 账户已导入 | 账户="+account.Label)
admin.requests.log("auth", "INFO", browser+" 账户已导入 | 账户="+account.Label)
}
return accounts, nil

if len(accounts) == 0 && len(failures) > 0 {
return nil, failures, fmt.Errorf("所有所选账号均导入失败: %s", failures[0].Error)
}
return accounts, failures, nil
}

// ImportAccountState 导入远程配对上传或 Cookie 导入的认证状态与 Camoufox 指纹,未给标签时以 Cookie 中第 auth_user 个会话登录的账户邮箱为标签
Expand Down
16 changes: 16 additions & 0 deletions internal/app/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ type generationAdmin interface {
Accounts(context.Context) ([]api.AdminAccount, error)
CreateAccount(context.Context, api.AccountCreateInput, *camoufoxnative.LiveView) (api.AdminAccount, error)
ChromeImportProfiles(context.Context) ([]api.ChromeImportProfile, error)
BrowserImportProfiles(ctx context.Context, browser string) ([]api.BrowserImportProfile, error)
CookieBrowsers(context.Context) ([]api.CookieBrowser, error)
DiscoverCookieAccounts(context.Context, api.CookieImportInput) ([]api.CookieBrowser, error)
ImportChromeAccounts(context.Context, api.ChromeImportInput) ([]api.AdminAccount, error)
ImportBrowserAccounts(ctx context.Context, input api.BrowserImportInput) ([]api.AdminAccount, []api.ImportFailure, error)
ImportAccountState(context.Context, api.AccountStateInput) (api.AdminAccount, error)
UpdateAccount(context.Context, string, api.AccountInput) (api.AdminAccount, error)
DeleteAccount(context.Context, string) error
Expand Down Expand Up @@ -454,6 +456,20 @@ func (manager *runtimeManager) ChromeImportProfiles(ctx context.Context) ([]api.
return manager.current.admin.ChromeImportProfiles(ctx)
}

// BrowserImportProfiles 返回当前生成服务可导入的指定浏览器账号
func (manager *runtimeManager) BrowserImportProfiles(ctx context.Context, browser string) ([]api.BrowserImportProfile, error) {
manager.mu.RLock()
defer manager.mu.RUnlock()
return manager.current.admin.BrowserImportProfiles(ctx, browser)
}

// ImportBrowserAccounts 在当前生成服务批量导入指定浏览器账号
func (manager *runtimeManager) ImportBrowserAccounts(ctx context.Context, input api.BrowserImportInput) ([]api.AdminAccount, []api.ImportFailure, error) {
manager.mu.RLock()
defer manager.mu.RUnlock()
return manager.current.admin.ImportBrowserAccounts(ctx, input)
}

// CookieBrowsers 返回当前生成服务发现的本机调试浏览器
func (manager *runtimeManager) CookieBrowsers(ctx context.Context) ([]api.CookieBrowser, error) {
manager.mu.RLock()
Expand Down
4 changes: 4 additions & 0 deletions internal/chromeauth/abe_unsupported.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ import "fmt"
func retrieveV20Key(string) ([]byte, error) {
return nil, fmt.Errorf("自动读取 Chrome App-Bound 主密钥仅支持 Windows amd64")
}

func retrieveV20KeyForBrowser(string, string) ([]byte, error) {
return nil, fmt.Errorf("自动读取 App-Bound 主密钥仅支持 Windows amd64")
}
Loading
Loading