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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ Windows 桌面版在命令行中运行以下命令时写作 `cmd /c start /wait
- 不带参数时扫描本机 Chrome 账户,`--email` 或 `--profile` 选择明确的 Chrome 账户
- 隔离登录:`aistudio2api setup --login`,需要图形界面;没有图形界面时启动服务,在管理页面的“浏览器登录”中登录
- 文件导入:`aistudio2api setup --storage-state <file>`
- Cookie 导入:`aistudio2api setup --cookie-file <file>`、`aistudio2api setup --cookie "<Cookie 请求头>"` 或 `aistudio2api setup --cdp <调试地址>`,账户邮箱按 Cookie 识别,`--cdp` 导入的账户沿用该浏览器的语言与时区
- Cookie 导入:`aistudio2api setup --cookie-file <file>`、`aistudio2api setup --cookie "<Cookie 请求头>"` 或 `aistudio2api setup --cdp <调试地址>`,账户邮箱按 Cookie 识别,`--cdp` 导入的账户沿用该浏览器的语言与时区;Cookie 来自同时登录多个 Google 账户的浏览器时,以 `--authuser N` 选择官网地址 `/u/N/` 中第 N 个账户

`aistudio2api setup --remote https://<远程实例>`:向远程实例添加账户。先在远程实例的账户页点击“远程添加”取得配对令牌,再在能登录 Google 的电脑上运行该命令并粘贴令牌;可与 `--login` 等参数组合。

Expand Down
2 changes: 1 addition & 1 deletion README_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ On Windows, run these commands for the desktop app as `cmd /c start /wait aistud
- Without options it scans local Chrome accounts; `--email` or `--profile` selects a Chrome account
- Isolated login: `aistudio2api setup --login`, which needs a graphical display; without one, start the service and sign in with "Sign in with browser" in the console
- File import: `aistudio2api setup --storage-state <file>`
- Cookie import: `aistudio2api setup --cookie-file <file>`, `aistudio2api setup --cookie "<Cookie request header>"`, or `aistudio2api setup --cdp <debugging address>`; the account email is detected from the cookies, and an account imported with `--cdp` keeps the language and time zone of that browser
- Cookie import: `aistudio2api setup --cookie-file <file>`, `aistudio2api setup --cookie "<Cookie request header>"`, or `aistudio2api setup --cdp <debugging address>`; the account email is detected from the cookies, and an account imported with `--cdp` keeps the language and time zone of that browser; when the cookies come from a browser signed in to several Google accounts, `--authuser N` selects the account at `/u/N/` on the official site

`aistudio2api setup --remote https://<remote-instance>`: Adds accounts to a remote instance. Click "Add remotely" on the remote instance's Accounts page to get a pairing token, then run the command on a computer that can sign in to Google and paste the token. It combines with options such as `--login`.

Expand Down
4 changes: 2 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ go run ./cmd/aistudio2api
`setup` 的导入模式:

- 文件导入:`--storage-state`
- Cookie 导入:`--cookie`、`--cookie-file` 或 `--cdp`,账户邮箱按 Cookie 识别,也可用一个 `--email` 指定;只保留 google.com 及其子域的 Cookie,缺少 `__Secure-1PSID`、`SAPISID`、`__Secure-1PAPISID` 或 `__Secure-3PAPISID` 时报告缺少的 Cookie
- Cookie 导入:`--cookie`、`--cookie-file` 或 `--cdp`,账户邮箱按 Cookie 识别,也可用一个 `--email` 指定;`--authuser N` 选择 Cookie 中第 N 个 Google 会话(官网 `/u/N/`)的账户并验证其邮箱;只保留 google.com 及其子域的 Cookie,缺少 `__Secure-1PSID`、`SAPISID`、`__Secure-1PAPISID` 或 `__Secure-3PAPISID` 时报告缺少的 Cookie
- `--cdp` 接受 `host:port`、HTTP 调试地址或浏览器级 `ws://` 地址,经 CDP `Storage.getCookies` 读取该浏览器的全部未分区 Cookie,再附加到第一个页面读取 `navigator.language` 与 Intl 时区,未给 `--locale`、`--timezone` 时作为账户的语言与时区
- 管理页面导入 Cookie 只需粘贴 Cookie:服务以 Cookie 打开 AI Studio 对话页,从页面全局数据 `oPEP7c` 读取登录账户邮箱,再读取 `ListModels` 确认可用;页面转到登录页时返回 `signed_out`
- “从指纹浏览器导入”从本机进程命令行找出带 `--remote-debugging-port` 的浏览器(端口为 0 时读取用户数据目录的 `DevToolsActivePort`),逐个读取 Cookie 并识别账户,列出可导入、已导入与未登录的浏览器
Expand Down Expand Up @@ -279,7 +279,7 @@ POST /api/control/start

| 文件 | 内容 | 生命周期 |
| --- | --- | --- |
| `account.json` | label、enabled、proxy、locale、timezone | 创建或编辑账户时写入 |
| `account.json` | label、enabled、proxy、locale、timezone、auth_user | 创建或编辑账户时写入 |
| `storage-state.json` | Cookie、localStorage 与可选 Chrome OAuth/DBSC 续签材料 | 合并 `Set-Cookie` 或认证续签后原子写回 |
| `camoufox-fingerprint.json` | 账户固定的浏览器指纹、语言与时区 | 首次运行生成,空值、窗口几何、字体、语音、WebGL 与媒体设备按 Camoufox 官方启动器规则规范化;重新登录和 WAA runtime 继续复用 |
| `runtime-state.json` | 权益等级、模型资格、冷却与资源账户绑定 | 权益同步、首次模型结果或资源变化后原子写回 |
Expand Down
12 changes: 7 additions & 5 deletions docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Cookie 的 `name`、`value`、`domain`、`path`、`expires`、`httpOnly`、`secu

| 文件 | 内容 |
| --- | --- |
| `auth/<Google 邮箱>/account.json` | 邮箱、enabled、proxy、locale、timezone |
| `auth/<Google 邮箱>/account.json` | 邮箱、enabled、proxy、locale、timezone、auth_user |
| `auth/<Google 邮箱>/storage-state.json` | Cookie、localStorage 和可选 Chrome 续签材料 |
| `auth/<Google 邮箱>/camoufox-fingerprint.json` | 账户固定的 navigator、屏幕、字体、WebGL、语言、地区和时区配置 |
| `auth/<Google 邮箱>/runtime-state.json` | 账户权益、实测模型资格、冷却与 Drive/Veo 资源绑定 |
Expand Down Expand Up @@ -1184,7 +1184,7 @@ server content 的 index `0/1/2/4/5/6` 分别为 model content、turn complete
| 管理登录 | `GET /api/auth/session`、`POST /api/auth/login`、`POST /api/auth/logout` |
| 状态与模型 | `GET /api/status`、`GET /api/models` |
| 生成服务 | `POST /api/control/start`、`POST /api/control/stop` |
| 账户 | `GET /api/accounts`、`POST /api/accounts`、`GET/POST /api/accounts/import/chrome`、`GET /api/accounts/import/browsers`、`POST /api/accounts/import/cookies`、`PUT /api/accounts/{id}`、`DELETE /api/accounts/{id}` |
| 账户 | `GET /api/accounts`、`POST /api/accounts`、`GET/POST /api/accounts/import/chrome`、`GET /api/accounts/import/browsers`、`POST /api/accounts/import/cookies/discover`、`POST /api/accounts/import/cookies`、`PUT /api/accounts/{id}`、`DELETE /api/accounts/{id}` |
| 账户认证与刷新 | `POST /api/accounts/{id}/login`、`POST /api/accounts/{id}/refresh`、`GET /api/login-view/{view}`(WebSocket) |
| 远程配对 | `POST /api/pairing`、`POST /api/pairing/accounts` |
| 配置 | `GET /api/config`、`PUT /api/config` |
Expand All @@ -1203,6 +1203,7 @@ server content 的 index `0/1/2/4/5/6` 分别为 model content、turn complete
| `GET /api/accounts/import/chrome` | 200 | `{"profiles":[ChromeImportProfile,...]}` |
| `POST /api/accounts/import/chrome` | 201 | `{"accounts":[AdminAccount,...]}` |
| `GET /api/accounts/import/browsers` | 200 | `{"browsers":[CookieBrowser,...]}` |
| `POST /api/accounts/import/cookies/discover` | 200 | `{"accounts":[CookieBrowser,...]}` |
| `POST /api/accounts/import/cookies` | 201 | `{"account":AdminAccount}` |
| `POST /api/pairing` | 201 | `PairingToken` |
| `POST /api/pairing/accounts` | 201 | `{"account":AdminAccount}` |
Expand All @@ -1222,6 +1223,7 @@ server content 的 index `0/1/2/4/5/6` 分别为 model content、turn complete
| `GET /api/events` | 200 SSE | `{"type":"<TYPE>","data":<DTO>}` |

- `POST /api/pairing/accounts` 的令牌无效或过期时返回 401 `pairing_token_invalid`,账户已存在时返回 409 `account_exists`
- 一份 Cookie 可包含同一浏览器中多个 Google 账户的会话,第 N 个会话对应官网 `/u/N/` 与请求头 `X-Goog-AuthUser: N`。`POST /api/accounts/import/cookies/discover` 从 u/0 起依次打开各会话的对话页,列出每个会话登录的账户;`GET /api/accounts/import/browsers` 为每个浏览器的每个会话各列一项。`POST /api/accounts/import/cookies` 的 `auth_user` 选择导入的会话,省略时为 0;账户的 `auth_user` 写入 `account.json`,运行时以 `/u/N/` 页面与 `X-Goog-AuthUser: N` 访问官网,浏览器重新登录后为 0
- `POST /api/accounts/import/cookies` 的 Cookie 无法解析或缺少登录 Cookie 时返回 400 `invalid_cookies`,调试地址连接或读取失败时返回 502 `cdp_unavailable`,账户已存在时返回 409 `account_exists`
- `GET /api/requests/{id}/body` 在没有保存正文时返回 404 `request_body_not_found`
- `POST /api/accounts` 与 `POST /api/accounts/{id}/login` 的 body 可带 `view`,即管理页面为本次登录生成的登录窗口 ID;`POST /api/accounts/{id}/login` 也接受空 body。服务器没有图形界面,或请求来自非回环地址、带 `Forwarded` 或 `X-Forwarded-For` 时,登录画面经同一 ID 的 `GET /api/login-view/{view}` 传递,请求在登录结束后返回
Expand All @@ -1238,10 +1240,10 @@ server content 的 index `0/1/2/4/5/6` 分别为 model content、turn complete
| `AccountInput` | `label`、`enabled`、`proxy`、`locale`、`timezone` |
| `ChromeImportProfile` | `id`、`profile`、`display_name`、`email`、`locale` |
| `ChromeImportInput` | `account_ids`、`proxy`、`locale`、`timezone` |
| `CookieImportInput` | `label`、`cookies` 或 `cdp`、`proxy`、`locale`、`timezone`;`cdp` 导入时 `locale`、`timezone` 为空则取该浏览器页面的语言与时区 |
| `CookieBrowser` | `address`、`browser`、`email`、`locale`、`timezone`、`imported`、`signed_out`、`error` |
| `CookieImportInput` | `label`、`cookies` 或 `cdp`、`proxy`、`locale`、`timezone`、可选 `auth_user`(Cookie 中的会话序号 0–9,默认 0);`cdp` 导入时 `locale`、`timezone` 为空则取该浏览器页面的语言与时区 |
| `CookieBrowser` | `address`、`browser`、`email`、`auth_user`、`locale`、`timezone`、`imported`、`signed_out`、`error` |
| `PairingToken` | `token`、`expires_at` |
| `AccountStateInput` | `label`、`locale`、`timezone`、`storage_state`、可选 `fingerprint` |
| `AccountStateInput` | `label`、`locale`、`timezone`、`storage_state`、可选 `auth_user`(默认 0)、可选 `fingerprint` |
| `AdminCooldown` | `account_id`、`account_label`、`model_id`、`until`、可选 `reason` |
| `AdminRequest` | `id`、`model`、`account_id`、`account_label`、`state`、`started_at` |
| `AdminLog` | `time`、`level`、`source`、`message`、`event`;请求事件携带 `request`,包含 `id`、`state`、HTTP `status`、`model`、`duration_ms`、`tool_calls`、`usage` 与诊断字段,字段口径见 [logging.md](logging.md) |
Expand Down
4 changes: 4 additions & 0 deletions internal/aistudio/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ type AccountConfig struct {
Proxy string `json:"proxy"`
Locale string `json:"locale"`
Timezone string `json:"timezone"`
AuthUser int `json:"auth_user,omitempty"`
}

// ResourceBinding 记录上游资源的创建账户
Expand Down Expand Up @@ -583,6 +584,9 @@ func (c AccountConfig) Validate() error {
if strings.TrimSpace(c.Timezone) == "" {
return fmt.Errorf("账户 timezone 不能为空")
}
if c.AuthUser < 0 || c.AuthUser > 9 {
return fmt.Errorf("账户 auth_user 无效: %d", c.AuthUser)
}
if err := appconfig.ValidateProxy(c.Proxy); err != nil {
return fmt.Errorf("账户 proxy 无效: %w", err)
}
Expand Down
10 changes: 7 additions & 3 deletions internal/aistudio/runtime_go.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,11 @@ func newGoWAARuntime(ctx context.Context, options camoufoxnative.Options) (*goWA
}
runtime := &goWAARuntime{options: options, client: client, storage: storage}
runtime.profile, runtime.userAgent, runtime.acceptLanguage = profileFromFingerprint(fingerprint)
runtime.pageURL = aiStudioOrigin + "/prompts/new_chat?model=" + url.QueryEscape(options.Model)
pagePath := "/prompts/new_chat"
if options.AuthUser > 0 {
pagePath = fmt.Sprintf("/u/%d/prompts/new_chat", options.AuthUser)
}
runtime.pageURL = aiStudioOrigin + pagePath + "?model=" + url.QueryEscape(options.Model)
if options.TemporaryChat {
runtime.pageURL += "&temporary=true"
}
Expand Down Expand Up @@ -229,7 +233,7 @@ func newGoWAARuntime(ctx context.Context, options camoufoxnative.Options) (*goWA
runtime.headers = http.Header{}
runtime.headers.Set("user-agent", runtime.userAgent)
runtime.headers.Set("x-goog-api-key", apiKey)
runtime.headers.Set("x-goog-authuser", "0")
runtime.headers.Set("x-goog-authuser", strconv.Itoa(options.AuthUser))
runtime.headers.Set("x-user-agent", "grpc-web-javascript/0.1")
runtime.headers.Set("x-aistudio-visit-id", visitID)
if extension != "" {
Expand Down Expand Up @@ -462,7 +466,7 @@ func (runtime *goWAARuntime) rpcHeaders(apiKey string) (http.Header, error) {
headers.Set("Referer", aiStudioOrigin+"/")
headers.Set("Content-Type", "application/json+protobuf")
headers.Set("X-Goog-Api-Key", apiKey)
headers.Set("X-Goog-AuthUser", "0")
headers.Set("X-Goog-AuthUser", strconv.Itoa(runtime.options.AuthUser))
headers.Set("X-User-Agent", "grpc-web-javascript/0.1")
headers.Set("Authorization", authorization)
headers.Set("Origin", aiStudioOrigin)
Expand Down
6 changes: 6 additions & 0 deletions internal/aistudio/transport_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ func (t *MakerSuiteHTTPTransport) UploadAssistantFile(ctx context.Context, accou
header.Set("Sec-Fetch-Dest", "empty")
header.Set("Sec-Fetch-Mode", "cors")
header.Set("Sec-Fetch-Site", "same-origin")
if account.Config.AuthUser > 0 {
header.Set("X-Goog-Authuser", strconv.Itoa(account.Config.AuthUser))
}
if language := account.AcceptLanguage(); language != "" {
header.Set("Accept-Language", language)
}
Expand Down Expand Up @@ -363,6 +366,9 @@ func prepareProtocolHeaders(
headers.Add(name, value)
}
}
if account.Config.AuthUser > 0 {
headers.Set("X-Goog-Authuser", strconv.Itoa(account.Config.AuthUser))
}
for _, name := range []string{"User-Agent", "X-Goog-Api-Key", "X-Goog-Authuser", "X-User-Agent"} {
if strings.TrimSpace(headers.Get(name)) == "" {
return StorageState{}, nil, fmt.Errorf("账户动态公共头缺少 %s", name)
Expand Down
21 changes: 20 additions & 1 deletion internal/api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type AdminService interface {
CreateAccount(context.Context, AccountCreateInput, *camoufoxnative.LiveView) (AdminAccount, error)
ChromeImportProfiles(context.Context) ([]ChromeImportProfile, error)
CookieBrowsers(context.Context) ([]CookieBrowser, error)
DiscoverCookieAccounts(context.Context, CookieImportInput) ([]CookieBrowser, error)
ImportChromeAccounts(context.Context, ChromeImportInput) ([]AdminAccount, error)
ImportAccountState(context.Context, AccountStateInput) (AdminAccount, error)
UpdateAccount(context.Context, string, AccountInput) (AdminAccount, error)
Expand Down Expand Up @@ -197,6 +198,7 @@ type CookieImportInput struct {
Proxy string `json:"proxy"`
Locale string `json:"locale"`
Timezone string `json:"timezone"`
AuthUser int `json:"auth_user"`
}

// ChromeImportProfile 表示可从本机 Chrome 导入的账号
Expand All @@ -213,6 +215,7 @@ type CookieBrowser struct {
Address string `json:"address"`
Browser string `json:"browser"`
Email string `json:"email"`
AuthUser int `json:"auth_user"`
Locale string `json:"locale"`
Timezone string `json:"timezone"`
Imported bool `json:"imported"`
Expand Down Expand Up @@ -305,6 +308,7 @@ func (s *server) registerAdmin(mux *http.ServeMux) {
mux.HandleFunc("GET /api/accounts/import/chrome", s.handleChromeImportProfiles)
mux.HandleFunc("POST /api/accounts/import/chrome", s.handleImportChromeAccounts)
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)
mux.HandleFunc("POST /api/pairing", s.handleCreatePairing)
mux.HandleFunc("PUT /api/accounts/{id}", s.handleUpdateAccount)
Expand Down Expand Up @@ -414,6 +418,20 @@ func (s *server) handleCookieBrowsers(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string][]CookieBrowser{"browsers": browsers})
}

func (s *server) handleDiscoverCookieAccounts(w http.ResponseWriter, r *http.Request) {
var input CookieImportInput
if err := decodeJSON(r, &input); err != nil {
writeAdminError(w, http.StatusBadRequest, "invalid_request", err.Error())
return
}
accounts, err := s.config.Admin.DiscoverCookieAccounts(r.Context(), input)
if err != nil {
writeAdminUpstreamError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string][]CookieBrowser{"accounts": accounts})
}

func (s *server) handleImportCookieAccount(w http.ResponseWriter, r *http.Request) {
var input CookieImportInput
if err := decodeJSON(r, &input); err != nil {
Expand All @@ -440,7 +458,8 @@ func (s *server) handleImportCookieAccount(w http.ResponseWriter, r *http.Reques
return
}
account, err := s.config.Admin.ImportAccountState(r.Context(), AccountStateInput{
Label: input.Label, Proxy: input.Proxy, Locale: input.Locale, Timezone: input.Timezone, StorageState: state,
Label: input.Label, Proxy: input.Proxy, Locale: input.Locale, Timezone: input.Timezone,
AuthUser: input.AuthUser, StorageState: state,
})
if err != nil {
writeAdminUpstreamError(w, err)
Expand Down
1 change: 1 addition & 0 deletions internal/api/pairing.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type AccountStateInput struct {
Proxy string `json:"-"`
Locale string `json:"locale"`
Timezone string `json:"timezone"`
AuthUser int `json:"auth_user"`
StorageState aistudio.StorageState `json:"storage_state"`
Fingerprint json.RawMessage `json:"fingerprint,omitempty"`
}
Expand Down
Loading