diff --git a/README.md b/README.md index 8db4aed0..d163b61a 100644 --- a/README.md +++ b/README.md @@ -7,607 +7,216 @@ [![FastAPI](https://img.shields.io/badge/fastapi-0.119.0-green.svg)](https://fastapi.tiangolo.com/) [![Docker](https://img.shields.io/badge/docker-supported-blue.svg)](https://www.docker.com/) -**一个功能完整的 OpenAI 兼容 API 服务,为 Flow 提供统一的接口** +**A full-featured OpenAI-compatible API service that provides a unified interface for Google Flow.** -## ❤️赞助商 - - - - - - -
- - useapi.net - - - 感谢 useapi.net 赞助了本项目!一个订阅,全部 API:useapi.net 为你自己的 Google 账号提供托管 API:Google Flow(Veo 3.1、带原生音频的 Omni 1.1 Flash、Nano Banana Pro)、Google Vids(Gemini Omni 1.1 Flash 带配音数字人,无需验证码,额度与 Flow 分开,同一账号同时使用 Flow 和 Vids,Omni 产能约翻倍)、Google Flow Music(Lyria 3.5 整首歌曲)和 Gemini Notebook(NotebookLM)。新上线 Meta Muse(muse.ai):用你自己的 Muse 账号生成图片和约 10 秒带台词配音的视频,免费账号可用。无需自己部署。同一订阅还包含 Dreamina、可灵 Kling、PixVerse、海螺 MiniMax、Mureka、Runway 等 API,每月固定 $15。Flow 中文教程 · Vids 中文教程 · Muse 中文教程 · 订阅

- Thanks to useapi.net for sponsoring this project! One subscription, every API: useapi.net runs hosted APIs on your own Google account: Google Flow (Veo 3.1, audio-native Omni 1.1 Flash, Nano Banana Pro), Google Vids (Gemini Omni 1.1 Flash with voiced avatars, no captcha, an allowance separate from Flow, so using both on one account roughly doubles your Omni capacity), Google Flow Music (full songs with Lyria 3.5) and Gemini Notebook (NotebookLM). New: Meta Muse (muse.ai): images and about 10-second videos with spoken dialogue on your own Muse account, free plan included. Nothing to self-host. The same flat $15/month also covers Dreamina, Kling, PixVerse, MiniMax, Mureka, Runway and more. Flow docs · Vids docs · Muse docs · Subscribe -
- - - - - - -
- - ProxyLane - - - 感谢 ProxyLane 赞助了本项目!ProxyLane 是住宅代理服务,覆盖 195 个国家,95%+ IP 获 Scamalytics 低风险评级,支持 HTTP / SOCKS5 和账号密码认证。在 Token 的「协议代理」「打码代理」或全局「请求代理地址」里填入代理,用户名中写上城市和会话名,每个 Token 最长 72 小时使用同一个住宅 IP,换 IP 也留在同一城市。中文界面,支持 USDT 付款,首次购买使用优惠码 FLOW2API30 享 7 折,通过此链接注册即可。

- Thanks to ProxyLane for sponsoring this project! ProxyLane is a residential proxy service in 195 countries, with 95%+ of IPs rated low risk by Scamalytics and HTTP / SOCKS5 with username and password. Put the proxy into a Token's protocol or captcha proxy, or into the global request proxy, with a city and a session name in the username: each Token keeps one residential IP for up to 72 hours, and a replacement IP stays in the same city. New customers get 30% off the first purchase with code FLOW2API30. Sign up here to get started. -
- -## ✨ 核心特性 - -- 🎨 **文生图** / **图生图** -- 🎬 **文生视频** / **图生视频** -- 🎞️ **首尾帧视频** -- 🔄 **Cookie 自动刷新** - 定时刷新完整 Google/Flow Cookie 并验证登录态,支持 browser/personal 浏览器兜底;登录失效后需重新同步 Cookie -- 📊 **余额显示** - 实时查询和显示 VideoFX Credits -- 🚀 **负载均衡** - 多 Token 轮询和并发控制 -- 🌐 **代理支持** - 支持 HTTP/SOCKS5 代理 -- 📱 **Web 管理界面** - 直观的 Token 和配置管理 -- 🎨 **图片生成连续对话** -- 🧩 **Gemini 官方请求体兼容** - 支持 `generateContent` / `streamGenerateContent`、`systemInstruction`、`contents.parts.text/inlineData/fileData` -- ✅ **Gemini 官方格式已实测出图** - 已使用真实 Token 验证 `/models/{model}:generateContent` 可正常返回官方 `candidates[].content.parts[].inlineData` - -## 🚀 快速开始 - -### 前置要求 - -- Docker 和 Docker Compose(推荐) -- 或 Python 3.8+ - -- 由于 Flow 增加了额外的验证码,你可以自行选择使用浏览器打码或第三方打码: - - 注册 [YesCaptcha](https://yescaptcha.com/i/13Xd8K) 并获取 API Key,填入系统配置页面的 `YesCaptcha API 密钥`。 - - 注册 [CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=z95yVf1HvixI) 并获取 API Key,填入系统配置页面的 `CapSolver API 密钥`。 - - 注册 [Captcha.run](https://captcha.run/sso?inviter=36179602-dbd1-4227-8ec8-98f8c0170c59) 并获取 API Key,填入系统配置页面的 `Captcha.run API 密钥`。 - - 注册 [2Captcha](https://2captcha.com/auth/register/?from=24953866) 并获取 API Key,填入系统配置页面的 `2Captcha API 密钥`。 -- YesCaptcha 支持在管理页切换 `type`:`RecaptchaV3TaskProxyless`、`RecaptchaV3TaskProxylessM1`、`RecaptchaV3TaskProxylessM1S7`、`RecaptchaV3TaskProxylessM1S9`;当前默认推荐 `M1S9`,S7/S9 会强制提交 `minScore` 0.7/0.9。 -- 默认 `docker-compose.yml` 建议搭配第三方打码(yescaptcha/captcharun/twocaptcha/capmonster/ezcaptcha/capsolver)。 -如需 Docker 内有头打码(browser/personal),请使用下方 `docker-compose.headed.yml`。 - -- 自动推送新版 Flow 登录态的浏览器扩展:[Flow2API-Token-Updater](https://github.com/TheSmallHanCat/Flow2API-Token-Updater)。 - -### 方式一:Docker 部署(推荐) - -#### 标准模式(不使用代理) +## Sponsors -```bash -# 克隆项目 -git clone https://github.com/TheSmallHanCat/flow2api.git -cd flow2api +Thanks to [useapi.net](https://useapi.net/docs/api-google-flow-v1?utm_source=flow2api&utm_medium=referral&utm_campaign=flow2api-sponsor) for sponsoring this project. Its hosted APIs cover Google Flow (Veo 3.1, Omni 1.1 Flash, and Nano Banana Pro), Google Vids, Flow Music, Gemini Notebook, Meta Muse, and other services under one subscription. See the sponsor's website for current features and pricing. -# 启动服务 -docker-compose up -d +Thanks to [ProxyLane](https://proxylane.dev/?utm_source=flow2api&utm_medium=partnership&utm_campaign=flow2api_sponsor_202610&utm_content=github_readme) for sponsoring this project. ProxyLane offers residential proxies in 195 countries, HTTP/SOCKS5 support, and username/password authentication. See its website for current terms and offers. -# 查看日志 -docker-compose logs -f -``` +## Key features + +- Text-to-image and image-to-image generation. +- Text-to-video, image-to-video, first/last-frame video, reference-image video, and video extension. +- Automatic Google/Flow cookie refresh and session validation. +- VideoFX Credits balance lookup. +- Token rotation, load balancing, and concurrency controls. +- HTTP and SOCKS5 proxy support. +- Web-based administration for tokens and configuration. +- Multi-turn image-generation conversations. +- Gemini API request compatibility, including `generateContent`, `streamGenerateContent`, `systemInstruction`, and text/image/file parts. + +## Getting started + +### Requirements + +- Docker and Docker Compose (recommended), or Python 3.8+. +- A supported captcha-solving provider may be required because Flow can request additional reCAPTCHA challenges. Available integrations are configured in the admin console and example TOML file. + +For automatic cookie updates, see [Flow2API-Token-Updater](https://github.com/TheSmallHanCat/Flow2API-Token-Updater). -> 说明:Compose 已默认挂载 `./tmp:/app/tmp`。如果把缓存超时设为 `0`,语义是“不自动过期删除”;若希望容器重建后仍保留缓存文件,也需要保留这个 `tmp` 挂载。 +### Option 1: Docker (recommended) -#### WARP 模式(使用代理) +Clone the repository and start the service: ```bash -# 使用 WARP 代理启动 -docker-compose -f docker-compose.proxy.yml up -d +git clone https://github.com/kattmio/flow2api.git +cd flow2api +docker compose up -d +docker compose logs -f +``` + +The default Compose configuration mounts `./tmp:/app/tmp`. A cache timeout of `0` disables automatic cache expiry. Keep the `tmp` mount if cache files must survive container recreation. + +#### WARP proxy mode -# 查看日志 -docker-compose -f docker-compose.proxy.yml logs -f +```bash +docker compose -f docker-compose.proxy.yml up -d +docker compose -f docker-compose.proxy.yml logs -f ``` -#### Docker 有头打码模式(browser / personal) +#### Headed browser captcha mode -> 适用于你有虚拟化桌面需求、希望在容器里启用有头浏览器打码的场景。 -> 该模式默认启动 `Xvfb + Fluxbox` 实现容器内部可视化,并设置 `ALLOW_DOCKER_HEADED_CAPTCHA=true`。 -> 仅开放应用端口,不提供任何远程桌面连接端口。 -> `personal` 内置浏览器现在默认按有头模式启动;如需临时切回无头,可额外设置环境变量 `PERSONAL_BROWSER_HEADLESS=true`。 +Use this mode when captcha solving needs a graphical browser inside the container. It starts Xvfb and Fluxbox, enables `ALLOW_DOCKER_HEADED_CAPTCHA=true`, and exposes only the application port—not a remote desktop port. ```bash -# 启动有头模式(首次建议带 --build) docker compose -f docker-compose.headed.yml up -d --build - -# 查看日志 docker compose -f docker-compose.headed.yml logs -f ``` -- API 端口:`8000` -- 进入管理后台后,将验证码方式设为 `browser` 或 `personal` +The API listens on port `8000`. In the admin console, choose `browser` or `personal` as the captcha method. The built-in personal browser runs in headed mode by default; set `PERSONAL_BROWSER_HEADLESS=true` to temporarily use headless mode. -### 方式二:本地部署 +### Option 2: Local Python installation ```bash -# 克隆项目 -git clone https://github.com/TheSmallHanCat/flow2api.git +git clone https://github.com/kattmio/flow2api.git cd flow2api - -# 创建虚拟环境 python -m venv venv +``` -# 激活虚拟环境 +Activate the environment and install dependencies: + +```bash # Windows -venv\Scripts\activate -# Linux/Mac +venv\\Scripts\\activate + +# Linux / macOS source venv/bin/activate -# 安装依赖 pip install -r requirements.txt - -# 启动服务 python main.py ``` -### 首次访问 +Open **http://localhost:8000** to access the admin console. The default initial credentials are username `admin` and password `admin`. **Change the password immediately after the first sign-in.** -服务启动后,访问管理后台: **http://localhost:8000**,首次登录后请立即修改密码! +## Monitoring endpoints -- **用户名**: `admin` -- **密码**: `admin` +- `GET /health` — public health summary, including service status, active tokens, tokens expiring soon, expired tokens, and tokens disabled after HTTP 429 responses. +- `GET /metrics` — Prometheus metrics. +- `GET /api/tokens` — token status fields such as `at_expires`, `at_expired`, `at_expiring_within_1h`, `ban_reason`, and `consecutive_error_count`. -## 📈 监控接口 +Prometheus can scrape `/metrics` directly. For Kubernetes deployments, prefer scraping from inside the cluster and restrict external access to this endpoint at the Ingress or Gateway layer. -- `GET /health`:公开健康检查,返回服务是否存活、活跃 Token 数、即将过期 Token 数、已过期 Token 数、429 禁用数等摘要 -- `GET /metrics`:Prometheus 指标接口 -- `GET /api/tokens`:管理接口,返回 `at_expires`、`at_expired`、`at_expiring_within_1h`、`ban_reason`、`consecutive_error_count` 等 Token 状态 +## Model testing page -Prometheus 可直接抓 `/metrics`。如果部署到 Kubernetes,建议只在集群内抓取,并在 Ingress/Gateway 层单独限制 `/metrics` 的外部访问。 +Open **http://localhost:8000/test** to use the built-in model tester. It supports browsing models by category, streaming generation progress, uploading images for image-to-image and image-to-video requests, and previewing generated images or videos. -### 模型测试页面 +## Supported models -访问 **http://localhost:8000/test** 可打开内置的模型测试页面,支持: +The model names below describe the public naming scheme. Exact availability depends on the current upstream Flow service and the configuration of your instance. -- 按分类浏览所有可用模型(图片生成、文/图生视频、多图视频、视频放大等) -- 输入提示词一键测试,流式显示生成进度 -- 图生图 / 图生视频场景支持上传图片 -- 生成完成后直接预览图片或视频 +### Image generation -## 📋 支持的模型 +The `/v1/models` and Gemini `/models` endpoints expose Nano Banana 2.1 and Nano Banana 2 Lite, with landscape, portrait, square, 4:3, and 3:4 aspect ratios in default, 2K, and 4K variants. Existing `gemini-3.1-flash-image-*` names are retained and routed to Nano Banana 2.1 (`BELUGA`). Legacy `gemini-3.0-pro-image-*` names still route to Nano Banana Pro (`GEM_PIX_2`) but are not listed. Legacy Imagen models are no longer supported, and unknown model names do not fall back to legacy upstream fields. -### 图片生成 +Nano Banana 2 Lite uses the `gemini-3.1-flash-lite-image-*` prefix with the same aspect-ratio and resolution suffixes. The simplified name `gemini-3.1-flash-lite-image` can also be used with `aspectRatio` and `imageSize` parameters. -当前 `/v1/models` 与 Gemini `/models` 公开 Nano Banana 2.1 和 Nano Banana 2 Lite 两个图片系列,共 30 个比例与分辨率变体。已有 `gemini-3.1-flash-image-*` 名称继续保留,调用新版 Nano Banana 2.1(`BELUGA`)。旧 Pro 名称 `gemini-3.0-pro-image-*` 继续调用 Nano Banana Pro(`GEM_PIX_2`),但不出现在模型列表中。旧 Imagen 模型已停止支持;模型不存在时不会回退到旧上游字段。 +The 2K/4K variants upscale the generated image; they do not represent native Lite output at those resolutions. The main program submits the model, aspect ratio, and reference images through the image RPC. Browser-based and third-party captcha integrations provide captcha solutions only. The `browser` and `personal` modes do not require a third-party captcha API key, and the Lite model does not automatically fall back to another image model. -| 当前公开名称 | 分辨率 | 比例 | -|---|---:|---:| -| `gemini-3.1-flash-image-landscape` | 默认 | 横屏 | -| `gemini-3.1-flash-image-portrait` | 默认 | 竖屏 | -| `gemini-3.1-flash-image-square` | 默认 | 方图 | -| `gemini-3.1-flash-image-four-three` | 默认 | 横屏 4:3 | -| `gemini-3.1-flash-image-three-four` | 默认 | 竖屏 3:4 | -| `gemini-3.1-flash-image-landscape-2k` | 2K | 横屏 | -| `gemini-3.1-flash-image-portrait-2k` | 2K | 竖屏 | -| `gemini-3.1-flash-image-square-2k` | 2K | 方图 | -| `gemini-3.1-flash-image-four-three-2k` | 2K | 横屏 4:3 | -| `gemini-3.1-flash-image-three-four-2k` | 2K | 竖屏 3:4 | -| `gemini-3.1-flash-image-landscape-4k` | 4K | 横屏 | -| `gemini-3.1-flash-image-portrait-4k` | 4K | 竖屏 | -| `gemini-3.1-flash-image-square-4k` | 4K | 方图 | -| `gemini-3.1-flash-image-four-three-4k` | 4K | 横屏 4:3 | -| `gemini-3.1-flash-image-three-four-4k` | 4K | 竖屏 3:4 | +### Video generation -Nano Banana 2 Lite 使用 `gemini-3.1-flash-lite-image-*`,比例及分辨率后缀与上表一致,例如 `gemini-3.1-flash-lite-image-landscape`、`gemini-3.1-flash-lite-image-portrait-2k`、`gemini-3.1-flash-lite-image-square-4k`。也支持简化名称 `gemini-3.1-flash-lite-image` 搭配 `aspectRatio` / `imageSize` 参数。 +The `/v1/models` and Gemini `/models` endpoints expose current Flow model families. Legacy underscore-style names remain compatible but are not listed. -2K/4K 变体通过生成后的高清放大实现,不代表 Lite 原生输出这些分辨率。主程序通过图片 RPC 明确提交模型、比例和参考图;浏览器打码与第三方打码均只提供验证码。`browser` / `personal` 模式不需要第三方打码密钥,图片、上传、放大及视频请求按所需 action 获取验证码。Lite 不会自动回退到其他图片模型。 +| Public model pattern | Capability | +| --- | --- | +| `veo-3.1-lite`, `veo-3.1-fast`, `veo-3.1-quality`, `omni-1.1-flash` | Default 8-second landscape text-to-video aliases | +| `veo-3.1-{lite,fast,quality}-{4,6,8}s-{landscape,portrait}` | Text-to-video | +| `omni-1.1-flash-{4,6,8,10}s-{landscape,portrait}` | Text-to-video or reference-image video | +| `veo-3.1-{lite,fast,quality}-i2v-{4,6}s-{landscape,portrait}` | First-frame or first/last-frame video | +| `veo-3.1-{lite,fast}-r2v-8s-{landscape,portrait}` | Video generation from multiple reference images | +| `veo-3.1-{lite,fast,quality}-extend-8s-{landscape,portrait}` | Video extension | -### 视频生成 +Braces indicate alternatives. For example, `veo-3.1-fast-{4,6,8}s-{landscape,portrait}` represents combinations of 4, 6, or 8 seconds and landscape or portrait orientation. -当前 `/v1/models` 与 Gemini `/models` 只公开 Flow 现行模型族。旧下划线模型名继续兼容,但不再出现在模型列表中。 +#### Omni 1.1 Flash -| 当前公开名称 | 上游模型族 | 能力 | -|---|---|---| -| `veo-3.1-lite` / `veo-3.1-fast` / `veo-3.1-quality` / `omni-1.1-flash` | 当前默认模型 | 8 秒横屏文生视频 | -| `veo-3.1-lite-{4,6,8}s-{landscape,portrait}` | Veo 3.1 - Lite | 文生视频 | -| `veo-3.1-fast-{4,6,8}s-{landscape,portrait}` | Veo 3.1 - Fast | 文生视频 | -| `veo-3.1-quality-{4,6,8}s-{landscape,portrait}` | Veo 3.1 - Quality | 文生视频 | -| `omni-1.1-flash-{4,6,8,10}s-{landscape,portrait}` | Omni 1.1 Flash (`abra`) | 文生/参考图视频 | -| `veo-3.1-{lite,fast,quality}-i2v-{4,6}s-{landscape,portrait}` | Veo 3.1 | 首帧/首尾帧视频 | -| `veo-3.1-{lite,fast}-r2v-8s-{landscape,portrait}` | Veo 3.1 | 多参考图视频 | -| `veo-3.1-{lite,fast,quality}-extend-8s-{landscape,portrait}` | Veo 3.1 | 视频续写 | +Omni 1.1 Flash supports both text-to-video and reference-image video. The service selects the mode based on whether the request contains images: -表格中的花括号表示任选其中一个值。例如 `veo-3.1-fast-{4,6,8}s-{landscape,portrait}` 包含 4/6/8 秒与横屏/竖屏组合。 +- `omni-1.1-flash` is an alias for `omni-1.1-flash-8s-landscape`. +- Full names follow `omni-1.1-flash-{4,6,8,10}s-{landscape,portrait}`. +- Text-only requests use text-to-video mode. Requests containing 1–3 `image_url` images use reference-image mode. +- Duration and orientation are encoded in the model name; no matching parameters are required. -#### Omni 1.1 Flash +#### Legacy model aliases -`Omni 1.1 Flash` 同时支持文生视频和参考图视频。调用方使用同一组模型名,服务会根据请求中是否包含图片自动选择生成模式: - -- `omni-1.1-flash` 是 `omni-1.1-flash-8s-landscape` 的默认别名。 -- 完整模型名为 `omni-1.1-flash-{4,6,8,10}s-{landscape,portrait}`,可选择 4、6、8、10 秒以及横屏或竖屏。 -- 仅传入文本时使用文生视频模式;传入 1–3 张 `image_url` 图片时使用参考图视频模式。 -- 时长和画面方向由模型名指定,无需额外传入同名参数。 - -参考图视频请求示例: - -```json -{ - "model": "omni-1.1-flash-8s-landscape", - "messages": [ - { - "role": "user", - "content": [ - { "type": "text", "text": "让画面中的人物自然转身并看向镜头" }, - { - "type": "image_url", - "image_url": { - "url": "data:image/jpeg;base64,<参考图base64>" - } - } - ] - } - ], - "stream": true -} -``` +Legacy aliases include text-to-video (`T2V`), image-to-video (`I2V`), reference-images-to-video (`R2V`), and video upscaling. For I2V, one image supplies the first frame and two images supply the first and last frames. Lite I2V aliases may be limited to first-frame-only or interpolation-only behavior. R2V currently accepts up to three reference images. -#### 兼容旧名:文生视频 (T2V - Text to Video) -⚠️ **不支持上传图片** - -| 模型名称 | 说明| 尺寸 | -|---------|---------|--------| -| `veo_3_1_t2v_fast_portrait` | 文生视频 | 竖屏 | -| `veo_3_1_t2v_fast_landscape` | 文生视频 | 横屏 | -| `veo_3_1_t2v_fast_portrait_ultra` | 文生视频 | 竖屏 | -| `veo_3_1_t2v_fast_ultra` | 文生视频 | 横屏 | -| `veo_3_1_t2v_fast_portrait_ultra_relaxed` | 文生视频 | 竖屏 | -| `veo_3_1_t2v_fast_ultra_relaxed` | 文生视频 | 横屏 | -| `veo_3_1_t2v_portrait` | 文生视频 | 竖屏 | -| `veo_3_1_t2v_landscape` | 文生视频 | 横屏 | -| `veo_3_1_t2v_landscape_4s` | 文生视频 4秒 | 横屏 | -| `veo_3_1_t2v_portrait_4s` | 文生视频 4秒 | 竖屏 | -| `veo_3_1_t2v_landscape_6s` | 文生视频 6秒 | 横屏 | -| `veo_3_1_t2v_portrait_6s` | 文生视频 6秒 | 竖屏 | -| `veo_3_1_t2v_fast_landscape_4s` | 文生视频 Fast 4秒 | 横屏 | -| `veo_3_1_t2v_fast_portrait_4s` | 文生视频 Fast 4秒 | 竖屏 | -| `veo_3_1_t2v_fast_landscape_6s` | 文生视频 Fast 6秒 | 横屏 | -| `veo_3_1_t2v_fast_portrait_6s` | 文生视频 Fast 6秒 | 竖屏 | -| `veo_3_1_t2v_lite_portrait` | 文生视频 Lite | 竖屏 | -| `veo_3_1_t2v_lite_landscape` | 文生视频 Lite | 横屏 | -| `veo_3_1_t2v_lite_4s_portrait` | 文生视频 Lite 4秒 | 竖屏 | -| `veo_3_1_t2v_lite_4s_landscape` | 文生视频 Lite 4秒 | 横屏 | -| `veo_3_1_t2v_lite_6s_portrait` | 文生视频 Lite 6秒 | 竖屏 | -| `veo_3_1_t2v_lite_6s_landscape` | 文生视频 Lite 6秒 | 横屏 | - -#### 兼容旧名:首尾帧模型 (I2V - Image to Video) -📸 **支持1-2张图片:1张作为首帧,2张作为首尾帧** - -> 💡 **自动适配**:系统会根据图片数量自动选择对应的 model_key -> - **单帧模式**(1张图):使用首帧生成视频 -> - **双帧模式**(2张图):使用首帧+尾帧生成过渡视频 -> - `veo_3_1_i2v_lite_*` 仅支持 **1 张** 首帧图片 -> - `veo_3_1_interpolation_lite_*` 仅支持 **2 张** 首尾帧图片 - -| 模型名称 | 说明| 尺寸 | -|---------|---------|--------| -| `veo_3_1_i2v_s_fast_portrait_fl` | 图生视频 | 竖屏 | -| `veo_3_1_i2v_s_fast_fl` | 图生视频 | 横屏 | -| `veo_3_1_i2v_s_fast_portrait_ultra_fl` | 图生视频 | 竖屏 | -| `veo_3_1_i2v_s_fast_ultra_fl` | 图生视频 | 横屏 | -| `veo_3_1_i2v_s_fast_portrait_ultra_relaxed` | 图生视频 | 竖屏 | -| `veo_3_1_i2v_s_fast_ultra_relaxed` | 图生视频 | 横屏 | -| `veo_3_1_i2v_s_portrait` | 图生视频 | 竖屏 | -| `veo_3_1_i2v_s_landscape` | 图生视频 | 横屏 | -| `veo_3_1_i2v_s_landscape_4s` | 图生视频 4秒 | 横屏 | -| `veo_3_1_i2v_s_portrait_4s` | 图生视频 4秒 | 竖屏 | -| `veo_3_1_i2v_s_landscape_6s` | 图生视频 6秒 | 横屏 | -| `veo_3_1_i2v_s_portrait_6s` | 图生视频 6秒 | 竖屏 | -| `veo_3_1_i2v_s_fast_landscape_4s_fl` | 图生视频 Fast 4秒 | 横屏 | -| `veo_3_1_i2v_s_fast_portrait_4s_fl` | 图生视频 Fast 4秒 | 竖屏 | -| `veo_3_1_i2v_s_fast_landscape_6s_fl` | 图生视频 Fast 6秒 | 横屏 | -| `veo_3_1_i2v_s_fast_portrait_6s_fl` | 图生视频 Fast 6秒 | 竖屏 | -| `veo_3_1_i2v_lite_portrait` | 图生视频 Lite(仅首帧) | 竖屏 | -| `veo_3_1_i2v_lite_landscape` | 图生视频 Lite(仅首帧) | 横屏 | -| `veo_3_1_i2v_lite_4s_portrait` | 图生视频 Lite 4秒(仅首帧) | 竖屏 | -| `veo_3_1_i2v_lite_4s_landscape` | 图生视频 Lite 4秒(仅首帧) | 横屏 | -| `veo_3_1_i2v_lite_6s_portrait` | 图生视频 Lite 6秒(仅首帧) | 竖屏 | -| `veo_3_1_i2v_lite_6s_landscape` | 图生视频 Lite 6秒(仅首帧) | 横屏 | -| `veo_3_1_interpolation_lite_portrait` | 图生视频 Lite(首尾帧过渡) | 竖屏 | -| `veo_3_1_interpolation_lite_landscape` | 图生视频 Lite(首尾帧过渡) | 横屏 | -| `veo_3_1_interpolation_lite_4s_portrait` | 图生视频 Lite 4秒(首尾帧过渡) | 竖屏 | -| `veo_3_1_interpolation_lite_4s_landscape` | 图生视频 Lite 4秒(首尾帧过渡) | 横屏 | -| `veo_3_1_interpolation_lite_6s_portrait` | 图生视频 Lite 6秒(首尾帧过渡) | 竖屏 | -| `veo_3_1_interpolation_lite_6s_landscape` | 图生视频 Lite 6秒(首尾帧过渡) | 横屏 | - -#### 兼容旧名:多图生成 (R2V - Reference Images to Video) -🖼️ **支持多张图片** - -> **2026-03-06 更新** -> -> - 已同步上游新版 `R2V` 视频请求体 -> - `textInput` 已切换为 `structuredPrompt.parts` -> - 顶层新增 `mediaGenerationContext.batchId` -> - 顶层新增 `useV2ModelConfig: true` -> - 横屏 / 竖屏 `R2V` 模型共用同一套新版请求体 -> - 横屏 `R2V` 的上游 `videoModelKey` 已切换为 `*_landscape` 形式 -> - 根据当前上游协议,`referenceImages` 当前最多传 **3 张** - -| 模型名称 | 说明| 尺寸 | -|---------|---------|--------| -| `veo_3_1_r2v_fast_portrait` | 图生视频 | 竖屏 | -| `veo_3_1_r2v_fast_landscape` | 图生视频 | 横屏 | -| `veo_3_1_r2v_fast_portrait_ultra` | 图生视频 | 竖屏 | -| `veo_3_1_r2v_fast_landscape_ultra` | 图生视频 | 横屏 | -| `veo_3_1_r2v_fast_portrait_ultra_relaxed` | 图生视频 | 竖屏 | -| `veo_3_1_r2v_fast_landscape_ultra_relaxed` | 图生视频 | 横屏 | - -#### 兼容旧名:视频放大模型 (Upsample) - -这些模型不是直接调用上游 upsampler key,而是先用对应的 Veo 3.1 普通模型生成视频,再提交 1080P/4K 放大请求。 - -| 模型名称 | 说明 | 输出 | -|---------|---------|--------| -| `veo_3_1_t2v_landscape_4k` | 文生视频放大 | 4K | -| `veo_3_1_t2v_portrait_4k` | 文生视频放大 | 4K | -| `veo_3_1_t2v_landscape_1080p` | 文生视频放大 | 1080P | -| `veo_3_1_t2v_portrait_1080p` | 文生视频放大 | 1080P | -| `veo_3_1_t2v_landscape_4s_4k` | 文生视频 4秒放大 | 4K | -| `veo_3_1_t2v_portrait_4s_4k` | 文生视频 4秒放大 | 4K | -| `veo_3_1_t2v_landscape_4s_1080p` | 文生视频 4秒放大 | 1080P | -| `veo_3_1_t2v_portrait_4s_1080p` | 文生视频 4秒放大 | 1080P | -| `veo_3_1_t2v_landscape_6s_4k` | 文生视频 6秒放大 | 4K | -| `veo_3_1_t2v_portrait_6s_4k` | 文生视频 6秒放大 | 4K | -| `veo_3_1_t2v_landscape_6s_1080p` | 文生视频 6秒放大 | 1080P | -| `veo_3_1_t2v_portrait_6s_1080p` | 文生视频 6秒放大 | 1080P | -| `veo_3_1_t2v_fast_portrait_4k` | 文生视频放大 | 4K | -| `veo_3_1_t2v_fast_4k` | 文生视频放大 | 4K | -| `veo_3_1_t2v_fast_portrait_ultra_4k` | 文生视频放大 | 4K | -| `veo_3_1_t2v_fast_ultra_4k` | 文生视频放大 | 4K | -| `veo_3_1_t2v_fast_portrait_1080p` | 文生视频放大 | 1080P | -| `veo_3_1_t2v_fast_1080p` | 文生视频放大 | 1080P | -| `veo_3_1_t2v_fast_portrait_ultra_1080p` | 文生视频放大 | 1080P | -| `veo_3_1_t2v_fast_ultra_1080p` | 文生视频放大 | 1080P | -| `veo_3_1_i2v_s_fast_portrait_ultra_fl_4k` | 图生视频放大 | 4K | -| `veo_3_1_i2v_s_fast_ultra_fl_4k` | 图生视频放大 | 4K | -| `veo_3_1_i2v_s_fast_portrait_ultra_fl_1080p` | 图生视频放大 | 1080P | -| `veo_3_1_i2v_s_fast_ultra_fl_1080p` | 图生视频放大 | 1080P | -| `veo_3_1_i2v_s_landscape_4k` | 图生视频放大 | 4K | -| `veo_3_1_i2v_s_portrait_4k` | 图生视频放大 | 4K | -| `veo_3_1_i2v_s_landscape_1080p` | 图生视频放大 | 1080P | -| `veo_3_1_i2v_s_portrait_1080p` | 图生视频放大 | 1080P | -| `veo_3_1_i2v_s_landscape_4s_4k` | 图生视频 4秒放大 | 4K | -| `veo_3_1_i2v_s_portrait_4s_4k` | 图生视频 4秒放大 | 4K | -| `veo_3_1_i2v_s_landscape_4s_1080p` | 图生视频 4秒放大 | 1080P | -| `veo_3_1_i2v_s_portrait_4s_1080p` | 图生视频 4秒放大 | 1080P | -| `veo_3_1_i2v_s_landscape_6s_4k` | 图生视频 6秒放大 | 4K | -| `veo_3_1_i2v_s_portrait_6s_4k` | 图生视频 6秒放大 | 4K | -| `veo_3_1_i2v_s_landscape_6s_1080p` | 图生视频 6秒放大 | 1080P | -| `veo_3_1_i2v_s_portrait_6s_1080p` | 图生视频 6秒放大 | 1080P | -| `veo_3_1_r2v_fast_portrait_ultra_4k` | 多图视频放大 | 4K | -| `veo_3_1_r2v_fast_landscape_ultra_4k` | 多图视频放大 | 4K | -| `veo_3_1_r2v_fast_portrait_ultra_1080p` | 多图视频放大 | 1080P | -| `veo_3_1_r2v_fast_landscape_ultra_1080p` | 多图视频放大 | 1080P | - -## 📡 API 使用示例(需要使用流式) - -> 除了下方 `OpenAI-compatible` 示例,服务也支持 Gemini 官方格式: -> - `POST /v1beta/models/{model}:generateContent` -> - `POST /models/{model}:generateContent` -> - `POST /v1beta/models/{model}:streamGenerateContent` -> - `POST /models/{model}:streamGenerateContent` -> -> Gemini 官方格式支持以下认证方式: -> - `Authorization: Bearer ` -> - `x-goog-api-key: ` -> - `?key=` -> -> Gemini 官方图片请求体已兼容: -> - `systemInstruction` -> - `contents[].parts[].text` -> - `contents[].parts[].inlineData` -> - `contents[].parts[].fileData.fileUri` -> - `generationConfig.responseModalities` -> - `generationConfig.imageConfig.aspectRatio` -> - `generationConfig.imageConfig.imageSize` - -### Gemini 官方 generateContent(文生图) - -> 已使用真实 Token 实测通过。 -> 如需流式返回,可将路径替换为 `:streamGenerateContent?alt=sse`。 +Video upscaling aliases first generate a video with the corresponding standard Veo 3.1 model and then submit a 1080p or 4K upscaling request. + +## API examples + +The service supports OpenAI-compatible chat completions and the official Gemini request format. Gemini endpoints include: + +- `POST /v1beta/models/{model}:generateContent` +- `POST /models/{model}:generateContent` +- `POST /v1beta/models/{model}:streamGenerateContent` +- `POST /models/{model}:streamGenerateContent` + +Gemini authentication can use `Authorization: Bearer `, `x-goog-api-key: `, or `?key=`. Supported image request fields include `systemInstruction`, `contents[].parts[].text`, `inlineData`, `fileData.fileUri`, `generationConfig.responseModalities`, and `generationConfig.imageConfig.aspectRatio/imageSize`. + +### Gemini text-to-image ```bash curl -X POST "http://localhost:8000/models/gemini-3.1-flash-image:generateContent" \ - -H "x-goog-api-key: han1234" \ + -H "x-goog-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ - "systemInstruction": { - "parts": [ - { - "text": "Return an image only." - } - ] - }, - "contents": [ - { - "role": "user", - "parts": [ - { - "text": "一颗放在木桌上的红苹果,棚拍光线,极简背景" - } - ] - } - ], + "systemInstruction": {"parts": [{"text": "Return an image only."}]}, + "contents": [{"role": "user", "parts": [{"text": "A red apple on a wooden table, studio lighting, minimalist background"}]}], "generationConfig": { "responseModalities": ["IMAGE"], - "imageConfig": { - "aspectRatio": "1:1", - "imageSize": "1K" - } + "imageConfig": {"aspectRatio": "1:1", "imageSize": "1K"} } }' ``` -### 文生图 - -```bash -curl -X POST "http://localhost:8000/v1/chat/completions" \ - -H "Authorization: Bearer han1234" \ - -H "Content-Type: application/json" \ - -d '{ - "model": "gemini-3.1-flash-image-landscape", - "messages": [ - { - "role": "user", - "content": "一只可爱的猫咪在花园里玩耍" - } - ], - "stream": true - }' -``` +For streaming, use `:streamGenerateContent?alt=sse` instead of `:generateContent`. -### 图生图 +### OpenAI-compatible text-to-image ```bash curl -X POST "http://localhost:8000/v1/chat/completions" \ - -H "Authorization: Bearer han1234" \ + -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gemini-3.1-flash-image-landscape", - "messages": [ - { - "role": "user", - "content": [ - { - "type": "text", - "text": "将这张图片变成水彩画风格" - }, - { - "type": "image_url", - "image_url": { - "url": "data:image/jpeg;base64," - } - } - ] - } - ], + "messages": [{"role": "user", "content": "A cute cat playing in a garden"}], "stream": true }' ``` -### 文生视频 +### Image-to-image -```bash -curl -X POST "http://localhost:8000/v1/chat/completions" \ - -H "Authorization: Bearer han1234" \ - -H "Content-Type: application/json" \ - -d '{ - "model": "veo-3.1-fast-8s-landscape", - "messages": [ - { - "role": "user", - "content": "一只小猫在草地上追逐蝴蝶" - } - ], - "stream": true - }' -``` +Use a text part and an `image_url` part in the user's message. For example, the text prompt can be: `Turn this image into a watercolor painting`. The image can be provided as a data URL such as `data:image/jpeg;base64,`. -### 首尾帧生成视频 +### Text-to-video ```bash curl -X POST "http://localhost:8000/v1/chat/completions" \ - -H "Authorization: Bearer han1234" \ + -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ - "model": "veo-3.1-fast-i2v-4s-landscape", - "messages": [ - { - "role": "user", - "content": [ - { - "type": "text", - "text": "从第一张图过渡到第二张图" - }, - { - "type": "image_url", - "image_url": { - "url": "data:image/jpeg;base64,<首帧base64>" - } - }, - { - "type": "image_url", - "image_url": { - "url": "data:image/jpeg;base64,<尾帧base64>" - } - } - ] - } - ], - "stream": true - }' -``` - -### 多图生成视频 - -> `R2V` 会由服务端自动组装新版视频请求体,调用方仍然使用 OpenAI 兼容输入即可。 -> 服务端会将横屏 `R2V` 自动映射到最新的 `*_landscape` 上游模型键。 -> 当前最多传 **3 张参考图**。 - -```bash -curl -X POST "http://localhost:8000/v1/chat/completions" \ - -H "Authorization: Bearer han1234" \ - -H "Content-Type: application/json" \ - -d '{ - "model": "veo-3.1-fast-r2v-8s-portrait", - "messages": [ - { - "role": "user", - "content": [ - { - "type": "text", - "text": "以三张参考图的人物和场景为基础,生成一段镜头平滑推进的竖屏视频" - }, - { - "type": "image_url", - "image_url": { - "url": "data:image/jpeg;base64/<参考图1base64>" - } - }, - { - "type": "image_url", - "image_url": { - "url": "data:image/jpeg;base64/<参考图2base64>" - } - }, - { - "type": "image_url", - "image_url": { - "url": "data:image/jpeg;base64/<参考图3base64>" - } - } - ] - } - ], + "model": "veo-3.1-fast-8s-landscape", + "messages": [{"role": "user", "content": "A kitten chasing butterflies in a meadow"}], "stream": true }' ``` ---- - -## 📄 许可证 - -本项目采用 MIT 许可证。详见 [LICENSE](LICENSE) 文件。 - ---- - -## 🙏 致谢 - -- [PearNoDec](https://github.com/PearNoDec) 提供的YesCaptcha打码方案 -- [raomaiping](https://github.com/raomaiping) 提供的无头打码方案 -感谢所有贡献者和使用者的支持! +### First/last-frame video ---- +Use the `veo-3.1-fast-i2v-4s-landscape` model and include two `image_url` parts. The text prompt can be `Transition from the first image to the second image`. -## 📞 联系方式 +### Reference-image video -- 提交 Issue:[GitHub Issues](https://github.com/TheSmallHanCat/flow2api/issues) +R2V request bodies are assembled by the server. Callers can use the OpenAI-compatible input format, and landscape requests are mapped to the latest upstream `*_landscape` model key. Up to three reference images are currently accepted. ---- +## License -**⭐ 如果这个项目对你有帮助,请给个 Star!** +This project is licensed under the MIT License. See [LICENSE](LICENSE) for details. -## Star History +## Acknowledgements -[![Star History Chart](https://star-history.dera.page/svg?repos=TheSmallHanCat/flow2api&type=date&legend=top-left)](https://star-history.dera.page/#TheSmallHanCat/flow2api&type=date&legend=top-left) +- [PearNoDec](https://github.com/PearNoDec) — upstream project and implementation contributions. diff --git a/config/setting_example.toml b/config/setting_example.toml index ac24809f..01279d4c 100644 --- a/config/setting_example.toml +++ b/config/setting_example.toml @@ -42,34 +42,34 @@ image_timeout = 300 video_timeout = 1500 [call_logic] -call_mode = "default" # default=随机轮询策略, polling=顺序轮询策略 +call_mode = "default" # default=randomized round-robin; polling=sequential round-robin [admin] error_ban_threshold = 3 [cache] enabled = false -timeout = 7200 # 缓存超时时间(秒), 默认2小时; 设置为0表示不自动删除缓存 -base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址 +timeout = 7200 # Cache timeout in seconds; defaults to 2 hours. Set to 0 to disable automatic cache deletion +base_url = "" # Base URL for accessing cached files; leave empty to use the server address [captcha] -captcha_method = "yescaptcha" # 打码方式: yescaptcha/flow2api_team(内置团队打码,地址与密钥固定)/captcharun/twocaptcha/capsolver/ezcaptcha/browser/personal/remote_browser -browser_recaptcha_settle_seconds = 3.0 # 自定义站点分数测试在 reload/clr 就绪后的额外稳态等待 -browser_flow_page_warmup_seconds = 6.0 # browser harvest 打开 Flow 页面后的稳定等待 -browser_count = 1 # browser 模式的有头浏览器实例数量 -browser_captcha_max_retries = 5 # browser 模式单次获取验证码的最大重试次数 -browser_captcha_generation_retries = 6 # 生成接口遇到 reCAPTCHA evaluation failed 时的总重试次数 -personal_solve_strategy = "auto" # auto=优先 trusted JS、失败切原生 harvest;direct/harvest 可强制指定 -personal_project_pool_size = 4 # personal 模式下单个 Token 默认维护的项目池数量(仅影响项目轮换,不决定打码标签页数量) -personal_max_resident_tabs = 5 # personal 模式共享打码标签页上限,所有 Token/project 共用这组 tab -browser_personal_fresh_restart_every_n_solves = 10 # personal 模式成功获取多少个码后清理并重启浏览器,0 表示禁用 -personal_idle_tab_ttl_seconds = 600 # personal 模式标签页空闲回收时间(秒) -yescaptcha_api_key = "" # YesCaptcha API密钥 +captcha_method = "yescaptcha" # Captcha provider: yescaptcha/flow2api_team (built-in team solver with fixed URL and key)/captcharun/twocaptcha/capsolver/ezcaptcha/browser/personal/remote_browser +browser_recaptcha_settle_seconds = 3.0 # Additional stabilization delay after reload/clr is ready during custom site-score testing +browser_flow_page_warmup_seconds = 6.0 # Stabilization delay after browser harvest opens the Flow page +browser_count = 1 # Number of headed browser instances in browser mode +browser_captcha_max_retries = 5 # Maximum retries for a single captcha retrieval in browser mode +browser_captcha_generation_retries = 6 # Total retries when the generation endpoint encounters reCAPTCHA evaluation failed +personal_solve_strategy = "auto" # auto=prefer trusted JS, falling back to native harvesting on failure; direct/harvest can be selected explicitly +personal_project_pool_size = 4 # Default project-pool size maintained per token in personal mode (affects project rotation only, not the number of captcha tabs) +personal_max_resident_tabs = 5 # Maximum number of shared captcha tabs in personal mode; all tokens/projects share this set of tabs +browser_personal_fresh_restart_every_n_solves = 10 # In personal mode, clean up and restart the browser after this many successful captcha solves; 0 disables this behavior +personal_idle_tab_ttl_seconds = 600 # Idle tab cleanup timeout in seconds for personal mode +yescaptcha_api_key = "" # YesCaptcha API key yescaptcha_base_url = "https://api.yescaptcha.com" -yescaptcha_task_type = "RecaptchaV3TaskProxylessM1S9" # 可选: RecaptchaV3TaskProxyless/RecaptchaV3TaskProxylessM1/RecaptchaV3TaskProxylessM1S7/RecaptchaV3TaskProxylessM1S9 -remote_browser_base_url = "" # 远程有头打码服务地址 -remote_browser_api_key = "" # 远程有头打码服务 API Key -remote_browser_timeout = 60 # 远程有头打码请求超时(秒) +yescaptcha_task_type = "RecaptchaV3TaskProxylessM1S9" # Options: RecaptchaV3TaskProxyless/RecaptchaV3TaskProxylessM1/RecaptchaV3TaskProxylessM1S7/RecaptchaV3TaskProxylessM1S9 +remote_browser_base_url = "" # Remote headed captcha service URL +remote_browser_api_key = "" # Remote headed captcha service API key +remote_browser_timeout = 60 # Remote headed captcha request timeout in seconds capsolver_api_key = "" capsolver_base_url = "https://api.capsolver.com" captcharun_api_key = "" diff --git a/extension/options.html b/extension/options.html index 4bded69c..6f12bef7 100644 --- a/extension/options.html +++ b/extension/options.html @@ -1,100 +1,41 @@ - + Flow2API Captcha Worker Settings

Flow2API Captcha Worker

-

给当前浏览器实例设置一个独立的路由键,Flow2API 后端就能把对应账号的验证码请求固定发到这个实例。

- +

Assign a dedicated routing key to this browser instance so the Flow2API backend can route captcha requests for the corresponding account to this instance.

- - + - - + - - - - + +
-
管理台 token 里填写同样的 Route Key,比如 9223 对 9223。后端会使用 API Key 校验 WebSocket 连接。
+
Enter the same Route Key in the admin console's token settings (for example, 9223 on both sides). The backend validates WebSocket connections using the API key.
- + \ No newline at end of file diff --git a/extension/options.js b/extension/options.js index 09ebcdb7..558af369 100644 --- a/extension/options.js +++ b/extension/options.js @@ -50,24 +50,24 @@ function saveSettings() { }); if (!isValidWsUrl(settings.serverUrl)) { - setStatus("WebSocket URL 必须以 ws:// 或 wss:// 开头。", true); + setStatus("The WebSocket URL must start with ws:// or wss://.", true); return; } if (!settings.apiKey) { - setStatus("请填写 Flow2API API Key。", true); + setStatus("Please enter the Flow2API API key.", true); return; } chrome.storage.local.set(settings, () => { if (chrome.runtime.lastError) { - setStatus(`保存失败:${chrome.runtime.lastError.message}`, true); + setStatus(`Could not save settings: ${chrome.runtime.lastError.message}`, true); return; } - setStatus("已保存,后台连接会自动重连。"); + setStatus("Settings saved. The background connection will reconnect automatically."); }); } document.addEventListener("DOMContentLoaded", () => { loadSettings(); $("saveBtn").addEventListener("click", saveSettings); -}); +}); \ No newline at end of file diff --git a/src/api/admin.py b/src/api/admin.py index 91f9708e..dfa4d994 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -60,8 +60,8 @@ # Store active admin session tokens (in production, use Redis or database) active_admin_tokens = set() ADMIN_SESSION_COOKIE_NAME = "admin_session" -# 历史版本曾把「账号邮箱与 Cookie 不匹配」当作失败原因写进 last_st_refresh_result。 -# 现在邮箱一律以 Flow 页面为准自动更正,这类旧记录不再回显给前端。 +# Older versions recorded “account email does not match cookies” as a failure reason in last_st_refresh_result. +# Email addresses are now corrected using the Flow page as the source of truth; these legacy records are no longer returned to the frontend. _LEGACY_IDENTITY_ERROR_MARKERS = ( "登录账号与当前 Token", "请同步对应账号的", @@ -69,7 +69,7 @@ def _sanitize_refresh_result(text: Any) -> str: - """旧版本的邮箱不匹配报错直接丢弃,不再回显给前端。""" + """Discard legacy email-mismatch errors instead of returning them to the frontend.""" value = str(text or "").strip() if not value: return "" @@ -105,7 +105,7 @@ def _truncate_text(text: Any, limit: int = 240) -> str: def _extract_error_summary(payload: Any) -> str: - """从响应体里提取用户可读的错误摘要。""" + """Extract a user-readable error summary from the response body.""" if payload is None: return "" @@ -153,7 +153,7 @@ def _extract_error_summary(payload: Any) -> str: def _guess_client_hints_from_user_agent(user_agent: str) -> Dict[str, str]: - """根据 UA 补全常见的 sec-ch-* 头。""" + """Populate common sec-ch-* headers based on the user agent.""" ua = (user_agent or "").strip() if not ua: return {} @@ -198,7 +198,7 @@ def _validate_browser_proxy_url_local(proxy_url: str) -> tuple[bool, Optional[st r"^(socks5h?|socks5|http|https)://(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$", normalized, ): - return False, "代理格式错误" + return False, "Invalid proxy format" return True, None @@ -215,12 +215,12 @@ async def _prepare_captcha_runtime(method: str): if runtime_method == "browser": start_runtime_prepare( runtime_method, - "已开始准备有头浏览器打码运行环境,安装进度将自动显示。", + "Preparing the headed-browser captcha environment. Installation progress will be shown automatically.", ) else: start_runtime_prepare( runtime_method, - "已开始准备内置浏览器打码运行环境,安装进度将自动显示。", + "Preparing the built-in browser captcha environment. Installation progress will be shown automatically.", ) if runtime_method == "browser": @@ -235,7 +235,7 @@ async def _prepare_captcha_runtime(method: str): await service.warmup_browser_slots() finish_runtime_prepare( runtime_method, - "Chromium 浏览器环境已就绪,可以开始使用有头浏览器打码。", + "The Chromium browser environment is ready. Headed-browser captcha solving is available.", ) return @@ -246,13 +246,13 @@ async def _prepare_captcha_runtime(method: str): service = await service_cls.get_instance(db) await service.reload_config() finish_runtime_prepare( - runtime_method, "内置浏览器环境已就绪,可以开始使用 personal 打码。" + runtime_method, "The built-in browser environment is ready. Personal captcha solving is available." ) except HTTPException: raise except Exception as e: fail_runtime_prepare( - runtime_method, f"浏览器环境准备失败: {type(e).__name__}: {e}" + runtime_method, f"Failed to prepare the browser environment: {type(e).__name__}: {e}" ) finally: captcha_runtime_prepare_tasks.pop(runtime_method, None) @@ -263,7 +263,7 @@ def _schedule_captcha_runtime_prepare(method: str) -> bool: task = captcha_runtime_prepare_tasks.get(runtime_method) if task and not task.done(): progress_runtime_prepare( - runtime_method, "浏览器环境准备任务仍在进行中,请稍候..." + runtime_method, "Browser environment setup is still in progress. Please wait..." ) return False @@ -274,7 +274,7 @@ def _schedule_captcha_runtime_prepare(method: str) -> bool: def _guess_impersonate_from_user_agent(user_agent: str) -> str: - """使用 curl_cffi 当前安装版本支持的 Chrome 指纹别名。""" + """Use Chrome impersonation aliases supported by the installed curl_cffi version.""" return "chrome" @@ -288,11 +288,11 @@ def _build_proxy_map(proxy_url: str) -> Optional[Dict[str, str]]: def _normalize_http_base_url(base_url: str) -> str: normalized = (base_url or "").strip().rstrip("/") if not normalized: - raise RuntimeError("远程打码服务地址未配置") + raise RuntimeError("Remote captcha service URL is not configured") parsed = urlparse(normalized) if parsed.scheme not in {"http", "https"} or not parsed.netloc: - raise RuntimeError("远程打码服务地址格式错误,必须是 http(s)://host[:port]") + raise RuntimeError("Invalid remote captcha service URL. Expected http(s)://host[:port].") return normalized @@ -301,7 +301,7 @@ def _get_remote_browser_client_config() -> tuple[str, str, int]: base_url = _normalize_http_base_url(config.remote_browser_base_url) api_key = (config.remote_browser_api_key or "").strip() if not api_key: - raise RuntimeError("远程打码服务 API Key 未配置") + raise RuntimeError("Remote captcha service API key is not configured") timeout = max(5, int(config.remote_browser_timeout or 60)) return base_url, api_key, timeout @@ -371,7 +371,7 @@ def do_request() -> tuple[int, str]: try: status_code, text = await asyncio.to_thread(do_request) except Exception as e: - raise RuntimeError(f"远程打码服务请求失败: {e}") from e + raise RuntimeError(f"Remote captcha service request failed: {e}") from e return status_code, _parse_json_response_text(text), text @@ -406,8 +406,8 @@ async def _sync_json_http_request( ) try: - # remote_browser 控制面是服务间 JSON API,使用 httpx 避免 curl_cffi 在当前 - # Windows + impersonate 场景下 POST body 丢失导致 FastAPI 直接判定 body 缺失。 + # The remote_browser control plane is a service-to-service JSON API. Use httpx to avoid curl_cffi in the current + # Windows + impersonate scenario, where POST bodies can be lost and FastAPI may report a missing request body. async with httpx.AsyncClient( follow_redirects=False, trust_env=False ) as session: @@ -417,7 +417,7 @@ async def _sync_json_http_request( **request_kwargs, ) except Exception as e: - raise RuntimeError(f"远程打码服务请求失败: {e}") from e + raise RuntimeError(f"Remote captcha service request failed: {e}") from e status_code = int(getattr(response, "status_code", 0) or 0) text = response.text or "" @@ -430,10 +430,10 @@ async def _resolve_score_test_verify_proxy( captcha_method: str, browser_proxy_enabled: bool, browser_proxy_url: str ) -> tuple[Optional[Dict[str, str]], bool, str, str]: """ - 选择 score-test 的 verify 请求代理,优先与浏览器打码代理保持一致。 - 返回: (proxies, used, source, proxy_url) + Choose a proxy for score-test verification, preferring the same proxy used for browser captcha solving. + Returns: (proxies, used, source, proxy_url) """ - # 浏览器打码模式优先使用 browser_proxy,确保与取 token 出口一致 + # Browser captcha mode prefers browser_proxy so token retrieval uses the same network exit. if ( captcha_method in {"browser", "personal"} and browser_proxy_enabled @@ -443,7 +443,7 @@ async def _resolve_score_test_verify_proxy( if proxy_map: return proxy_map, True, "captcha_browser_proxy", browser_proxy_url - # 退回请求代理配置 + # Fall back to the request-proxy configuration try: if proxy_manager: proxy_cfg = await proxy_manager.get_proxy_config() @@ -464,9 +464,9 @@ async def _solve_recaptcha_with_api_service( action: str, enterprise: bool = False, ) -> Optional[str]: - """使用当前配置的第三方打码服务获取 token。""" + """Use the configured third-party captcha provider to obtain a token.""" if method == FLOW2API_TEAM_CAPTCHA_METHOD: - # Flow2API 团队打码:内置服务,地址与密钥固定。 + # Flow2API Team captcha solving: built in, with a fixed URL and key. client_key = FLOW2API_TEAM_CAPTCHA_CLIENT_KEY base_url = FLOW2API_TEAM_CAPTCHA_BASE_URL task_type = FLOW2API_TEAM_CAPTCHA_TASK_TYPE @@ -511,10 +511,10 @@ async def _solve_recaptcha_with_api_service( ) return solution["gRecaptchaResponse"] else: - raise RuntimeError(f"不支持的打码方式: {method}") + raise RuntimeError(f"Unsupported captcha method: {method}") if not client_key: - raise RuntimeError(f"{method} API Key 未配置") + raise RuntimeError(f"{method} API key is not configured") task: Dict[str, Any] = { "websiteURL": website_url, @@ -550,7 +550,7 @@ async def _solve_recaptcha_with_api_service( payload = response.json() task_id = str(payload.get("taskId") or payload.get("id") or "") if response.status_code >= 400 or not task_id: - raise RuntimeError(f"captcharun 创建任务失败: {payload}") + raise RuntimeError(f"Captcha.run task creation failed: {payload}") for _ in range(60): await asyncio.sleep(3) response = await session.get( @@ -566,15 +566,15 @@ async def _solve_recaptcha_with_api_service( token = solution.get("gRecaptchaResponse") if token: return str(token) - raise RuntimeError("captcharun 返回结果中缺少 token") + raise RuntimeError("Captcha.run response is missing a token") if status in {"failed", "failure", "error", "cancelled"}: - raise RuntimeError(f"captcharun 任务失败: {payload}") - raise RuntimeError("captcharun 获取 token 超时") + raise RuntimeError(f"Captcha.run task failed: {payload}") + raise RuntimeError("Timed out while retrieving a token from Captcha.run") create_url = f"{base_url.rstrip('/')}/createTask" get_url = f"{base_url.rstrip('/')}/getTaskResult" - # 获取代理配置 + # Get proxy configuration proxies = None try: if proxy_manager: @@ -605,7 +605,7 @@ async def _solve_recaptcha_with_api_service( or create_json.get("errorMessage") or str(create_json) ) - raise RuntimeError(f"{method} createTask 失败: {error_desc}") + raise RuntimeError(f"{method} createTask failed: {error_desc}") for _ in range(40): poll_resp = await session.post( @@ -621,7 +621,7 @@ async def _solve_recaptcha_with_api_service( token = solution.get("gRecaptchaResponse") or solution.get("token") if token: return token - raise RuntimeError(f"{method} 返回结果缺少 token: {poll_json}") + raise RuntimeError(f"{method} Response is missing a token: {poll_json}") if poll_json.get("errorId") not in (None, 0): error_desc = ( @@ -629,11 +629,11 @@ async def _solve_recaptcha_with_api_service( or poll_json.get("errorMessage") or str(poll_json) ) - raise RuntimeError(f"{method} getTaskResult 失败: {error_desc}") + raise RuntimeError(f"{method} getTaskResult failed: {error_desc}") await asyncio.sleep(3) - raise RuntimeError(f"{method} 获取 token 超时") + raise RuntimeError(f"{method} Timed out while retrieving a token") async def _score_test_with_remote_browser_service( @@ -643,7 +643,7 @@ async def _score_test_with_remote_browser_service( action: str, enterprise: bool = False, ) -> Dict[str, Any]: - """调用远程有头打码服务执行页面内打码+分数校验。""" + """Call the remote headed-captcha service to solve the challenge and verify the score on the page.""" base_url, api_key, timeout = _get_remote_browser_client_config() endpoint = f"{base_url}/api/v1/custom-score" request_payload = { @@ -673,11 +673,11 @@ async def _score_test_with_remote_browser_service( if not detail: detail = (response_text or "").strip() raise RuntimeError( - f"远程打码服务请求失败 (HTTP {status_code}): {detail or '未知错误'}" + f"Remote captcha service request failed (HTTP {status_code}): {detail or 'Unknown error'}" ) if not isinstance(response_payload, dict): - raise RuntimeError("远程打码服务返回格式错误") + raise RuntimeError("Remote captcha service returned an invalid format") return response_payload @@ -705,7 +705,7 @@ class LoginRequest(BaseModel): class AddTokenRequest(BaseModel): st: str = "" - project_id: Optional[str] = None # 用户可选输入project_id + project_id: Optional[str] = None # Optional user-supplied project_id project_name: Optional[str] = None remark: Optional[str] = None captcha_proxy_url: Optional[str] = None @@ -716,7 +716,7 @@ class AddTokenRequest(BaseModel): video_concurrency: int = -1 protocol_mode: Literal["protocol"] = "protocol" google_cookies: str = Field(min_length=1, max_length=524288) - # 可选:留空时由 Cookie/会话自动识别,识别失败则使用占位邮箱 + # Optional: if blank, identify the account from cookies/session; use a placeholder email if identification fails. login_account: Optional[str] = Field(default=None, max_length=254) login_password: Optional[str] = None proxy_url: Optional[str] = None @@ -738,7 +738,7 @@ def validate_login_account(cls, value: Optional[str]) -> Optional[str]: class UpdateTokenRequest(BaseModel): st: str = "" - project_id: Optional[str] = None # 用户可选输入project_id + project_id: Optional[str] = None # Optional user-supplied project_id project_name: Optional[str] = None remark: Optional[str] = None captcha_proxy_url: Optional[str] = None @@ -875,7 +875,7 @@ def validate_unique_accounts(self): class TokenRefreshConfigRequest(BaseModel): enabled: Optional[bool] = None refresh_interval_minutes: Optional[int] = Field(default=None, ge=1, le=10080) - # 同步改写全部账号的刷新间隔,避免旧账号继续沿用历史默认值 + # Update the refresh interval for all accounts so older accounts do not keep the historical default. apply_to_tokens: bool = False @@ -885,7 +885,7 @@ class PluginTokenUpdateRequest(BaseModel): model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) google_cookies: str = Field(min_length=1, max_length=524288) - # 可选:留空时按 Cookie 会话定位账号,定位不到则使用占位邮箱 + # Optional: if blank, identify the account from the cookie session; use a placeholder email if no account is found. login_account: Optional[str] = Field(default=None, max_length=254) protocol_mode: Literal["protocol"] = "protocol" @@ -973,7 +973,7 @@ async def admin_logout(response: Response, token: str = Depends(verify_admin_tok """Admin logout - invalidate session token""" active_admin_tokens.discard(token) response.delete_cookie(ADMIN_SESSION_COOKIE_NAME, path="/") - return {"success": True, "message": "退出登录成功"} + return {"success": True, "message": "Signed out successfully"} @router.post("/api/admin/change-password") @@ -985,7 +985,7 @@ async def change_password( # Verify old password if not AuthManager.verify_admin(admin_config.username, request.old_password): - raise HTTPException(status_code=400, detail="旧密码错误") + raise HTTPException(status_code=400, detail="The current password is incorrect") # Update password and username in database update_params = {"password": request.new_password} @@ -1000,7 +1000,7 @@ async def change_password( # 🔑 Invalidate all admin session tokens (force re-login for security) active_admin_tokens.clear() - return {"success": True, "message": "密码修改成功,请重新登录"} + return {"success": True, "message": "Password changed successfully. Please sign in again."} # ========== Token Management ========== @@ -1029,10 +1029,10 @@ def normalize_dt(value): { "id": row.get("id"), "st": row.get("st"), # Session Token for editing - "at": row.get("at"), # Access Token for editing (从ST转换而来) + "at": row.get("at"), # Access Token for editing (converted from ST) "at_expires": to_iso(row.get("at_expires")) if row.get("at_expires") - else None, # 🆕 AT过期时间 + else None, # 🆕 AT expiration time "at_expired": bool( normalize_dt(row.get("at_expires")) and normalize_dt(row.get("at_expires")) <= now @@ -1042,7 +1042,7 @@ def normalize_dt(value): and normalize_dt(row.get("at_expires")) > now and (normalize_dt(row.get("at_expires")) - now).total_seconds() < 3600 ), - "token": row.get("at"), # 兼容前端 token.token 的访问方式 + "token": row.get("at"), # Keep compatibility with frontend access through token.token "email": row.get("email"), "name": row.get("name"), "remark": row.get("remark"), @@ -1054,10 +1054,10 @@ def normalize_dt(value): if row.get("last_used_at") else None, "use_count": row.get("use_count"), - "credits": row.get("credits"), # 🆕 余额 + "credits": row.get("credits"), # 🆕 Credits "user_paygate_tier": row.get("user_paygate_tier"), - "current_project_id": row.get("current_project_id"), # 🆕 项目ID - "current_project_name": row.get("current_project_name"), # 🆕 项目名称 + "current_project_id": row.get("current_project_id"), # 🆕 Project ID + "current_project_name": row.get("current_project_name"), # 🆕 Project name "captcha_proxy_url": row.get("captcha_proxy_url") or "", "extension_route_key": row.get("extension_route_key") or "", "protocol_mode": row.get("protocol_mode") or "session", @@ -1089,7 +1089,7 @@ def normalize_dt(value): "banned_at": to_iso(row.get("banned_at")) if row.get("banned_at") else None, } for row in token_rows - ] # 直接返回数组,兼容前端 + ] # Return an array directly for frontend compatibility @router.post("/api/tokens") @@ -1098,7 +1098,7 @@ async def add_token(request: AddTokenRequest, token: str = Depends(verify_admin_ try: new_token = await token_manager.add_token( st=request.st, - project_id=request.project_id, # 🆕 支持用户指定project_id + project_id=request.project_id, # 🆕 Allows the user to specify a project_id project_name=request.project_name, remark=request.remark, captcha_proxy_url=request.captcha_proxy_url.strip() @@ -1120,7 +1120,7 @@ async def add_token(request: AddTokenRequest, token: str = Depends(verify_admin_ refresh_interval_minutes=request.refresh_interval_minutes, ) - # 热更新并发限制,避免必须重启服务 + # Apply concurrency-limit changes immediately without restarting the service if concurrency_manager: await concurrency_manager.reset_token( new_token.id, @@ -1130,7 +1130,7 @@ async def add_token(request: AddTokenRequest, token: str = Depends(verify_admin_ return { "success": True, - "message": "Token添加成功", + "message": "Token added successfully", "token": { "id": new_token.id, "email": new_token.email, @@ -1142,7 +1142,7 @@ async def add_token(request: AddTokenRequest, token: str = Depends(verify_admin_ except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: - raise HTTPException(status_code=500, detail=f"添加Token失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to add token: {str(e)}") @router.put("/api/tokens/{token_id}") @@ -1164,7 +1164,7 @@ async def update_token( at = result["access_token"] expires = result.get("expires") - # 解析过期时间 + # Parse the expiration time from datetime import datetime at_expires = None @@ -1174,12 +1174,12 @@ async def update_token( except: pass - # 更新token (包含AT、ST、AT过期时间、project_id和project_name) + # Update the token (including AT, ST, AT expiration time, project_id, and project_name) await token_manager.update_token( token_id=token_id, st=session_value, at=at, - at_expires=at_expires, # 🆕 更新AT过期时间 + at_expires=at_expires, # 🆕 Update AT expiration time email=request.login_account, project_id=request.project_id, project_name=request.project_name, @@ -1207,7 +1207,7 @@ async def update_token( token_id, request.login_account ) - # 热更新并发限制,确保管理台修改立即生效 + # Apply concurrency-limit changes immediately so admin-console changes take effect if concurrency_manager: updated_token = await token_manager.get_token(token_id) if updated_token: @@ -1217,7 +1217,7 @@ async def update_token( video_concurrency=updated_token.video_concurrency, ) - return {"success": True, "message": "Token更新成功"} + return {"success": True, "message": "Token updated successfully"} except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: @@ -1231,7 +1231,7 @@ async def delete_token(token_id: int, token: str = Depends(verify_admin_token)): await token_manager.delete_token(token_id) if concurrency_manager: await concurrency_manager.remove_token(token_id) - return {"success": True, "message": "Token删除成功"} + return {"success": True, "message": "Token deleted successfully"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -1240,47 +1240,47 @@ async def delete_token(token_id: int, token: str = Depends(verify_admin_token)): async def enable_token(token_id: int, token: str = Depends(verify_admin_token)): """Enable token""" await token_manager.enable_token(token_id) - return {"success": True, "message": "Token已启用"} + return {"success": True, "message": "Token enabled"} @router.post("/api/tokens/{token_id}/disable") async def disable_token(token_id: int, token: str = Depends(verify_admin_token)): """Disable token""" await token_manager.disable_token(token_id) - return {"success": True, "message": "Token已禁用"} + return {"success": True, "message": "Token disabled"} @router.post("/api/tokens/{token_id}/refresh-credits") async def refresh_credits(token_id: int, token: str = Depends(verify_admin_token)): - """刷新Token余额 🆕""" + """Refresh token credits 🆕""" try: credits = await token_manager.refresh_credits(token_id) - return {"success": True, "message": "余额刷新成功", "credits": credits} + return {"success": True, "message": "Credits refreshed successfully", "credits": credits} except Exception as e: - raise HTTPException(status_code=500, detail=f"刷新余额失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to refresh credits: {str(e)}") @router.post("/api/tokens/{token_id}/refresh-at") async def refresh_at(token_id: int, token: str = Depends(verify_admin_token)): - """刷新完整 Google/Flow Cookie 并验证当前登录态。""" + """Refresh the full Google/Flow cookies and validate the current login session.""" from ..core.logger import debug_logger from ..core.config import config debug_logger.log_info( - f"[API] 手动刷新 AT 请求: token_id={token_id}, captcha_method={config.captcha_method}" + f"[API] Manual AT refresh request: token_id={token_id}, captcha_method={config.captcha_method}" ) try: - # 手动刷新完整 Cookie,不依赖 AT 标识是否到期。 + # Manually refresh the full cookies regardless of whether the AT appears to have expired. success = await token_manager.refresh_cookies(token_id) if success: - # 获取更新后的token信息 + # Retrieve the updated token information updated_token = await token_manager.get_token(token_id) - message = "Cookie 刷新并验证成功" + message = "Cookies refreshed and session validated successfully" - debug_logger.log_info(f"[API] AT 刷新成功: token_id={token_id}") + debug_logger.log_info(f"[API] AT refresh succeeded: token_id={token_id}") return { "success": True, @@ -1294,24 +1294,24 @@ async def refresh_at(token_id: int, token: str = Depends(verify_admin_token)): }, } else: - debug_logger.log_error(f"[API] AT 刷新失败: token_id={token_id}") + debug_logger.log_error(f"[API] AT refresh failed: token_id={token_id}") snapshot = await token_manager.get_token(token_id) error_detail = _sanitize_refresh_result( snapshot.last_st_refresh_result if snapshot else None - ) or "Cookie 刷新失败,请重新同步完整的 Google Cookies" + ) or "Cookie refresh failed. Please sync the full Google cookies again." raise HTTPException(status_code=500, detail=error_detail) except HTTPException: raise except Exception as e: - debug_logger.log_error(f"[API] 刷新AT异常: {str(e)}") - raise HTTPException(status_code=500, detail=f"刷新AT失败: {str(e)}") + debug_logger.log_error(f"[API] AT refresh exception: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to refresh AT: {str(e)}") @router.post("/api/tokens/st2at") async def st_to_at(request: ST2ATRequest, token: str = Depends(verify_admin_token)): - """Convert Session Token to Access Token (仅转换,不添加到数据库)""" + """Convert Session Token to Access Token (conversion only; does not add the token to the database)""" try: result = await token_manager.flow_client.st_to_at( request.st, @@ -1354,7 +1354,7 @@ async def import_tokens( email = str(item.login_account or "").strip().lower() detected_email = "" if not email: - # 未提供账号时,从 Cookie 读取 Flow 登录邮箱并保存最新登录态。 + # If no account is provided, read the Flow login email from cookies and save the latest session. identity = await token_manager.detect_account_identity( google_cookies, proxy_url=item.proxy_url ) @@ -1468,18 +1468,18 @@ async def import_tokens( added += 1 except Exception as e: - errors.append(f"第{idx + 1}项 ({email or detected_email}): {str(e)}") + errors.append(f"Item {idx + 1} ({email or detected_email}): {str(e)}") except Exception as e: - errors.append(f"第{idx + 1}项: {str(e)}") + errors.append(f"Item {idx + 1}: {str(e)}") return { "success": not errors or added + updated > 0, "added": added, "updated": updated, "errors": errors if errors else None, - "message": f"导入完成: 新增 {added} 个, 更新 {updated} 个" - + (f", {len(errors)} 个失败" if errors else ""), + "message": f"Import complete: added {added}, updated {updated}" + + (f", ; {len(errors)} failed" if errors else ""), } @@ -1527,7 +1527,7 @@ async def update_proxy_config_alias( ) except ValueError as e: return {"success": False, "message": str(e)} - return {"success": True, "message": "代理配置更新成功"} + return {"success": True, "message": "Proxy settings updated successfully"} @router.post("/api/config/proxy") @@ -1544,21 +1544,21 @@ async def update_proxy_config( ) except ValueError as e: return {"success": False, "message": str(e)} - return {"success": True, "message": "代理配置更新成功"} + return {"success": True, "message": "Proxy settings updated successfully"} @router.post("/api/proxy/test") async def test_proxy_connectivity( request: ProxyTestRequest, token: str = Depends(verify_admin_token) ): - """测试代理是否可访问目标站点(默认 https://flow.google.com/)""" + """Test whether a proxy can reach the target site (default: https://flow.google.com/).""" proxy_input = (request.proxy_url or "").strip() test_url = (request.test_url or "https://flow.google.com/").strip() timeout_seconds = int(request.timeout_seconds or 15) timeout_seconds = max(5, min(timeout_seconds, 60)) if not proxy_input: - return {"success": False, "message": "代理地址为空", "test_url": test_url} + return {"success": False, "message": "Proxy URL is empty", "test_url": test_url} try: proxy_url = proxy_manager.normalize_proxy_url(proxy_input) @@ -1585,9 +1585,9 @@ async def test_proxy_connectivity( return { "success": ok, - "message": "代理可用" + "message": "Proxy is working" if ok - else f"代理可连通,但目标返回状态码 {status_code}", + else f"Proxy is reachable, but the target returned status code {status_code}", "test_url": test_url, "final_url": final_url, "status_code": status_code, @@ -1597,7 +1597,7 @@ async def test_proxy_connectivity( elapsed_ms = int((time.time() - start_time) * 1000) return { "success": False, - "message": f"代理测试失败: {str(e)}", + "message": f"Proxy test failed: {str(e)}", "test_url": test_url, "elapsed_ms": elapsed_ms, } @@ -1631,7 +1631,7 @@ async def update_generation_config( # 🔥 Hot reload: sync database config to memory await db.reload_config_to_memory() - return {"success": True, "message": "生成配置更新成功"} + return {"success": True, "message": "Generation settings updated successfully"} @router.get("/api/call-logic/config") @@ -1670,7 +1670,7 @@ async def update_call_logic_config( return { "success": True, - "message": "Token轮询模式保存成功", + "message": "Token rotation mode saved successfully", "config": { "call_mode": call_mode, "polling_mode_enabled": call_mode == "polling", @@ -1770,7 +1770,7 @@ async def get_log_detail(log_id: int, token: str = Depends(verify_admin_token)): """Get single request log detail (payload loaded on demand)""" log = await db.get_log_detail(log_id) if not log: - raise HTTPException(status_code=404, detail="日志不存在") + raise HTTPException(status_code=404, detail="Log not found") error_summary = _extract_error_summary(log.get("response_body")) @@ -1797,7 +1797,7 @@ async def clear_logs(token: str = Depends(verify_admin_token)): """Clear all logs""" try: await db.clear_all_logs() - return {"success": True, "message": "所有日志已清空"} + return {"success": True, "message": "All logs have been cleared"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -1823,7 +1823,7 @@ async def update_admin_config( # Update error_ban_threshold in database await db.update_admin_config(error_ban_threshold=request.error_ban_threshold) - return {"success": True, "message": "配置更新成功"} + return {"success": True, "message": "Settings updated successfully"} @router.post("/api/admin/password") @@ -1845,7 +1845,7 @@ async def update_api_key( # 🔥 Hot reload: sync database config to memory await db.reload_config_to_memory() - return {"success": True, "message": "API Key更新成功"} + return {"success": True, "message": "API key updated successfully"} @router.post("/api/admin/debug") @@ -1890,7 +1890,7 @@ async def update_generation_timeout( # 🔥 Hot reload: sync database config to memory await db.reload_config_to_memory() - return {"success": True, "message": "生成配置更新成功"} + return {"success": True, "message": "Generation settings updated successfully"} # ========== AT Auto Refresh Config ========== @@ -1918,7 +1918,7 @@ async def update_token_refresh_enabled( enabled = (request or {}).get("enabled") if enabled is not None: await db.update_token_refresh_config(enabled=bool(enabled)) - return {"success": True, "message": "刷新配置已更新"} + return {"success": True, "message": "Refresh settings updated"} @router.post("/api/token-refresh/config") @@ -1990,7 +1990,7 @@ async def update_cache_enabled(request: dict, token: str = Depends(verify_admin_ await db.reload_config_to_memory() await _sync_runtime_cache_config() - return {"success": True, "message": f"缓存已{'启用' if enabled else '禁用'}"} + return {"success": True, "message": f"Cache {'enabled' if enabled else 'disabled'}"} @router.post("/api/cache/config") @@ -2006,9 +2006,9 @@ async def update_cache_config_full( try: timeout = int(timeout) except (TypeError, ValueError): - raise HTTPException(status_code=400, detail="缓存超时时间必须为整数") + raise HTTPException(status_code=400, detail="Cache timeout must be an integer") if timeout < 0: - raise HTTPException(status_code=400, detail="缓存超时时间不能小于 0") + raise HTTPException(status_code=400, detail="Cache timeout cannot be less than 0") await db.update_cache_config(enabled=enabled, timeout=timeout, base_url=base_url) @@ -2016,7 +2016,7 @@ async def update_cache_config_full( await db.reload_config_to_memory() await _sync_runtime_cache_config() - return {"success": True, "message": "缓存配置更新成功"} + return {"success": True, "message": "Cache settings updated successfully"} @router.post("/api/cache/base-url") @@ -2031,7 +2031,7 @@ async def update_cache_base_url( await db.reload_config_to_memory() await _sync_runtime_cache_config() - return {"success": True, "message": "缓存Base URL更新成功"} + return {"success": True, "message": "Cache base URL updated successfully"} @router.post("/api/captcha/config") @@ -2056,10 +2056,10 @@ async def update_captcha_config( twocaptcha_api_key = request.get("twocaptcha_api_key") twocaptcha_base_url = request.get("twocaptcha_base_url") if twocaptcha_api_key is not None and not isinstance(twocaptcha_api_key, str): - return {"success": False, "message": "2Captcha API Key 必须是字符串"} + return {"success": False, "message": "2Captcha API key must be a string"} if twocaptcha_base_url is not None: if not isinstance(twocaptcha_base_url, str): - return {"success": False, "message": "2Captcha API 地址必须是字符串"} + return {"success": False, "message": "2Captcha API URL must be a string"} try: twocaptcha_base_url = _normalize_http_base_url(twocaptcha_base_url.strip() or "https://api.2captcha.com") except RuntimeError as exc: @@ -2078,7 +2078,7 @@ async def update_captcha_config( ) personal_idle_tab_ttl_seconds = request.get("personal_idle_tab_ttl_seconds") - # 验证浏览器代理URL格式 + # Validate browser proxy URL format if browser_proxy_enabled and browser_proxy_url: is_valid, error_msg = _validate_browser_proxy_url_local(browser_proxy_url) if not is_valid: @@ -2093,11 +2093,11 @@ async def update_captcha_config( try: remote_browser_timeout = max(5, int(remote_browser_timeout or 60)) except Exception: - return {"success": False, "message": "远程打码超时时间必须是整数秒"} + return {"success": False, "message": "Remote captcha timeout must be an integer number of seconds"} try: browser_count = max(1, min(20, int(browser_count or 1))) except Exception: - return {"success": False, "message": "浏览器实例数量必须是整数"} + return {"success": False, "message": "Browser instance count must be an integer"} try: browser_personal_fresh_restart_every_n_solves = max( 0, @@ -2108,18 +2108,18 @@ async def update_captcha_config( ), ) except Exception: - return {"success": False, "message": "重置码数必须是整数,0 表示禁用"} + return {"success": False, "message": "Captcha restart threshold must be an integer; 0 disables it"} if captcha_method == "remote_browser": if not (remote_browser_base_url or "").strip(): return { "success": False, - "message": "remote_browser 模式需要配置远程打码服务地址", + "message": "remote_browser mode requires a remote captcha service URL", } if not (remote_browser_api_key or "").strip(): return { "success": False, - "message": "remote_browser 模式需要配置远程打码服务 API Key", + "message": "remote_browser mode requires a remote captcha service API key", } await db.update_captcha_config( @@ -2161,16 +2161,16 @@ async def update_captcha_config( runtime_prepare_started = _schedule_captcha_runtime_prepare(captcha_method) if captcha_method == "browser": runtime_prepare_message = ( - "已开始准备有头浏览器打码运行环境,安装进度将自动显示。" + "Preparing the headed-browser captcha environment. Installation progress will be shown automatically." ) else: runtime_prepare_message = ( - "已开始准备内置浏览器打码运行环境,安装进度将自动显示。" + "Preparing the built-in browser captcha environment. Installation progress will be shown automatically." ) return { "success": True, - "message": "验证码配置更新成功", + "message": "Captcha settings updated successfully", "runtime_prepare_started": runtime_prepare_started, "runtime_prepare_message": runtime_prepare_message, "runtime_status_method": runtime_status_method, @@ -2242,7 +2242,7 @@ async def test_captcha_score( _request: Optional[CaptchaScoreTestRequest] = None, _token: str = Depends(verify_admin_token), ): - """使用当前打码方式获取 token,并提交到 antcpt 校验分数。""" + """Use the configured captcha method to obtain a token and submit it to antcpt for score verification.""" req = _request or CaptchaScoreTestRequest() website_url = (req.website_url or "https://antcpt.com/score_detector/").strip() website_key = ( @@ -2351,7 +2351,7 @@ async def test_captcha_score( if isinstance(score_payload, dict): if score_payload.get("success") is False: raise RuntimeError( - score_payload.get("message") or "远程打码分数测试失败" + score_payload.get("message") or "Remote captcha score test failed" ) token_value = score_payload.get("token") verify_elapsed_ms = int(score_payload.get("verify_elapsed_ms") or 0) @@ -2385,7 +2385,7 @@ async def test_captcha_score( token_elapsed_ms = int((time.time() - token_start) * 1000) return { "success": True, - "message": "CapSolver不支持antcpt。已成功用 Google Labs 测试连通性", + "message": "CapSolver does not support antcpt. Connectivity to Google Labs was tested successfully.", "captcha_method": captcha_method, "website_url": "https://flow.google.com/", "website_key": "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV", @@ -2398,7 +2398,7 @@ async def test_captcha_score( "verify_elapsed_ms": 0, "verify_http_status": 200, "score": 0.9, - "verify_result": {"success": True, "message": "跳过分数校验"}, + "verify_result": {"success": True, "message": "Score verification skipped"}, "verify_request_meta": {}, "browser_proxy_enabled": browser_proxy_enabled, "browser_proxy_url": browser_proxy_url @@ -2418,7 +2418,7 @@ async def test_captcha_score( else: return { "success": False, - "message": f"当前打码方式不支持分数测试: {captcha_method}", + "message": f"The current captcha method does not support score testing: {captcha_method}", "captcha_method": captcha_method, "website_url": website_url, "website_key": website_key, @@ -2431,8 +2431,8 @@ async def test_captcha_score( if token_elapsed_ms <= 0: token_elapsed_ms = int((time.time() - token_start) * 1000) - # 远程有头打码的 custom-score 可能由页面内直接完成校验, - # 在部分实现里不会显式回传 token,本地按 verify_result 兜底判定。 + # Custom-score verification for remote headed captcha may be completed directly on the page, + # and some implementations do not explicitly return a token; the local fallback uses verify_result. if ( captcha_method == "remote_browser" and not token_value @@ -2448,7 +2448,7 @@ async def test_captcha_score( if not token_value: return { "success": False, - "message": "未获取到 reCAPTCHA token", + "message": "No reCAPTCHA token was obtained", "captcha_method": captcha_method, "website_url": website_url, "website_key": website_key, @@ -2559,7 +2559,7 @@ async def test_captcha_score( return { "success": verify_success, - "message": "分数校验成功" if verify_success else "分数校验未通过", + "message": "Score verification succeeded" if verify_success else "Score verification failed", "captcha_method": captcha_method, "website_url": website_url, "website_key": website_key, @@ -2596,7 +2596,7 @@ async def test_captcha_score( except Exception as e: return { "success": False, - "message": f"分数测试失败: {str(e)}", + "message": f"Score test failed: {str(e)}", "captcha_method": captcha_method, "website_url": website_url, "website_key": website_key, @@ -2706,7 +2706,7 @@ async def get_plugin_config(request: Request, token: str = Depends(verify_admin_ async def update_plugin_config(request: dict, token: str = Depends(verify_admin_token)): """Update plugin configuration""" connection_token = request.get("connection_token", "") - auto_enable_on_update = request.get("auto_enable_on_update", True) # 默认开启 + auto_enable_on_update = request.get("auto_enable_on_update", True) # Enabled by default # Generate random token if empty if not connection_token: @@ -2718,7 +2718,7 @@ async def update_plugin_config(request: dict, token: str = Depends(verify_admin_ return { "success": True, - "message": "插件配置更新成功", + "message": "Extension settings updated successfully", "connection_token": connection_token, "auto_enable_on_update": auto_enable_on_update, } @@ -2753,14 +2753,14 @@ async def plugin_update_token( except Exception as cookie_error: raise HTTPException( status_code=400, - detail=f"Google Cookie 验证失败:{str(cookie_error)}", + detail=f"Google cookie validation failed: {str(cookie_error)}", ) from cookie_error cookie_hash = hashlib.sha256(google_cookies.encode("utf-8")).hexdigest()[:32] session_token = f"frontend-{cookie_hash}" - # Step 2: 定位已有账号。邮箱可选:先用邮箱,未提供则按 Cookie 会话(st)反查, - # 都没有则生成与 add_token 一致的占位邮箱,避免“必须填 Google 账号”的硬性要求。 + # Step 2: Locate an existing account. Email is optional: use the email first; if omitted, look it up through the cookie session (ST), + # and if neither is available, generate a placeholder email as in add_token, avoiding a strict Google-account requirement. existing_token = await db.get_token_by_email(email) if email else None if existing_token is None: existing_token = await db.get_token_by_st(session_token) diff --git a/src/api/routes.py b/src/api/routes.py index fcd7f4b2..d01efbfc 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -197,14 +197,14 @@ async def retrieve_image_data(url: str) -> Optional[bytes]: if data: return data except Exception as exc: - debug_logger.log_warning(f"[CONTEXT] 本地缓存读取失败: {str(exc)}") + debug_logger.log_warning(f"[CONTEXT] Failed to read local cache: {str(exc)}") proxy_url = None try: if file_cache and hasattr(file_cache, "_resolve_download_proxy"): proxy_url = await file_cache._resolve_download_proxy("image") except Exception as exc: - debug_logger.log_warning(f"[CONTEXT] 图片下载代理解析失败: {str(exc)}") + debug_logger.log_warning(f"[CONTEXT] Failed to resolve the image-download proxy: {str(exc)}") try: async with AsyncSession() as session: @@ -225,10 +225,10 @@ async def retrieve_image_data(url: str) -> Optional[bytes]: if response.status_code == 200 and response.content: return response.content debug_logger.log_warning( - f"[CONTEXT] 图片下载失败,状态码: {response.status_code}" + f"[CONTEXT] Image download failed with status code: {response.status_code}" ) except Exception as exc: - debug_logger.log_error(f"[CONTEXT] 图片下载异常: {str(exc)}") + debug_logger.log_error(f"[CONTEXT] Image download exception: {str(exc)}") return None @@ -327,7 +327,7 @@ async def _extract_prompt_and_images_from_openai_messages( prompt_parts.append(text) elif item_type == "image_url": image_url = item.get("image_url", {}).get("url", "") - # extend://MEDIA_ID 用于视频续写 + # extend://MEDIA_ID used for video extension if image_url.startswith("extend://"): video_media_id = image_url[len("extend://"):] else: @@ -346,7 +346,7 @@ async def _append_openai_reference_images( if not model_config or model_config["type"] != "image" or len(messages) <= 1: return images - debug_logger.log_info(f"[CONTEXT] 开始查找历史参考图,消息数量: {len(messages)}") + debug_logger.log_info(f"[CONTEXT] Searching for historical reference images; message count: {len(messages)}") for msg in reversed(messages[:-1]): if msg.role == "assistant" and isinstance(msg.content, str): @@ -362,14 +362,14 @@ async def _append_openai_reference_images( if downloaded_bytes: images.insert(0, downloaded_bytes) debug_logger.log_info( - f"[CONTEXT] ✅ 添加历史参考图: {image_url}" + f"[CONTEXT] ✅ Added historical reference image: {image_url}" ) return images debug_logger.log_warning( - f"[CONTEXT] 图片下载失败或为空,尝试下一个: {image_url}" + f"[CONTEXT] Image download failed or was empty; trying the next one: {image_url}" ) except Exception as exc: - debug_logger.log_error(f"[CONTEXT] 处理参考图时出错: {str(exc)}") + debug_logger.log_error(f"[CONTEXT] Error processing reference image: {str(exc)}") return images @@ -424,12 +424,12 @@ def _resolve_request_model( model=model, request=request, model_config=MODEL_CONFIG, images=images ) if resolved_model != model: - debug_logger.log_info(f"[ROUTE] 模型名已转换: {model} → {resolved_model}") + debug_logger.log_info(f"[ROUTE] Model name resolved: {model} → {resolved_model}") return resolved_model def _get_request_base_url(request: Request) -> Optional[str]: - """根据实际请求头推导对外可访问的基础地址。""" + """Infer the externally accessible base URL from the actual request headers.""" forwarded_proto = ( (request.headers.get("x-forwarded-proto") or "").split(",")[0].strip() ) @@ -499,7 +499,7 @@ async def _normalize_gemini_request( if system_instruction: if media_model and _should_ignore_media_system_instruction(system_instruction): debug_logger.log_warning( - f"[GEMINI] 忽略媒体模型的 systemInstruction: model={resolved_model}, len={len(system_instruction)}" + f"[GEMINI] Ignoring systemInstruction for media model: model={resolved_model}, len={len(system_instruction)}" ) else: if media_model: diff --git a/src/core/config.py b/src/core/config.py index f4d368cb..f74948ef 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -13,7 +13,7 @@ "RecaptchaV3TaskProxylessM1S9": 0.9, } -# Flow2API 团队打码:内置服务,地址与密钥写死在代码里,不随配置/数据库改变。 +# Flow2API Team captcha solving is built in; its URL and key are hard-coded and do not change with configuration or database values. FLOW2API_TEAM_CAPTCHA_METHOD = "flow2api_team" FLOW2API_TEAM_CAPTCHA_BASE_URL = "https://captcha.haqimi.uk" FLOW2API_TEAM_CAPTCHA_CLIENT_KEY = "sk-flow-mint-2f9a7c1e4b8d6350" @@ -21,7 +21,7 @@ def normalize_captcha_method(method: Optional[str]) -> str: - """把打码方式收敛到内置支持的取值(未知值原样转小写返回)。""" + """Normalize the captcha method to a supported built-in value (unknown values are returned in lowercase).""" normalized = (method or "").strip().lower() if normalized in {FLOW2API_TEAM_CAPTCHA_METHOD, "flow2api-team", "flow2apiteam"}: return FLOW2API_TEAM_CAPTCHA_METHOD @@ -29,7 +29,7 @@ def normalize_captcha_method(method: Optional[str]) -> str: def is_yes_style_captcha_method(method: Optional[str]) -> bool: - """Flow2API 团队打码与 YesCaptcha 共用 createTask/getTaskResult 协议。""" + """Flow2API Team captcha solving uses the same createTask/getTaskResult protocol as YesCaptcha.""" return normalize_captcha_method(method) in { "yescaptcha", FLOW2API_TEAM_CAPTCHA_METHOD, @@ -63,8 +63,8 @@ def _load_config(self) -> Dict[str, Any]: if config_path.exists() and not config_path.is_file(): print( - f"[Config] 检测到 {config_path} 不是普通文件," - f"将回退到 {fallback_path.name}。请检查 Docker 挂载或本地配置路径。" + f"[Config] Detected that {config_path} is not a regular file; " + f"falling back to {fallback_path.name}. Check the Docker mount or local configuration path." ) config_path = fallback_path elif not config_path.exists(): @@ -72,8 +72,8 @@ def _load_config(self) -> Dict[str, Any]: if not config_path.is_file(): raise FileNotFoundError( - f"配置文件不存在或不可读取: {config_path}. " - f"请确认 config 目录下存在可用的 setting.toml 或 setting_example.toml" + f"Configuration file does not exist or cannot be read: {config_path}. " + f"Ensure the config directory contains a usable setting.toml or setting_example.toml." ) with open(config_path, "rb") as f: @@ -142,7 +142,7 @@ def set_flow_max_retries(self, retries: int): @property def flow_image_request_timeout(self) -> int: - """图片生成单次 HTTP 请求超时(秒)。""" + """Timeout in seconds for a single image-generation HTTP request.""" default_timeout = min(self.flow_timeout, 40) timeout = self._config.get("flow", {}).get( "image_request_timeout", default_timeout @@ -154,7 +154,7 @@ def flow_image_request_timeout(self) -> int: @property def flow_image_timeout_retry_count(self) -> int: - """图片生成遇到网络超时时的快速重试次数。""" + """Number of quick retries when image generation encounters a network timeout.""" retry_count = self._config.get("flow", {}).get("image_timeout_retry_count", 1) try: return max(0, min(3, int(retry_count))) @@ -163,7 +163,7 @@ def flow_image_timeout_retry_count(self) -> int: @property def flow_image_timeout_retry_delay(self) -> float: - """图片生成网络超时重试前等待秒数。""" + """Seconds to wait before retrying image generation after a network timeout.""" delay = self._config.get("flow", {}).get("image_timeout_retry_delay", 0.8) try: return max(0.0, min(5.0, float(delay))) @@ -172,7 +172,7 @@ def flow_image_timeout_retry_delay(self) -> float: @property def flow_image_timeout_use_media_proxy_fallback(self) -> bool: - """网络超时时是否切换媒体代理重试。""" + """Whether to retry through the media proxy after a network timeout.""" return bool( self._config.get("flow", {}).get( "image_timeout_use_media_proxy_fallback", True @@ -181,12 +181,12 @@ def flow_image_timeout_use_media_proxy_fallback(self) -> bool: @property def flow_image_prefer_media_proxy(self) -> bool: - """图片生成是否优先走媒体代理链路。""" + """Whether image generation should prefer the media-proxy route.""" return bool(self._config.get("flow", {}).get("image_prefer_media_proxy", False)) @property def flow_image_slot_wait_timeout(self) -> float: - """图片硬并发槽位等待超时(秒)。""" + """Timeout in seconds while waiting for an image-generation concurrency slot.""" timeout = self._config.get("flow", {}).get("image_slot_wait_timeout", 120) try: return max(1.0, min(600.0, float(timeout))) @@ -195,7 +195,7 @@ def flow_image_slot_wait_timeout(self) -> float: @property def flow_image_launch_soft_limit(self) -> int: - """图片生成前置发车软并发上限(0 表示关闭软整形,仅使用硬并发)。""" + """Soft concurrency limit before starting image generation (0 disables soft shaping and uses only the hard limit).""" value = self._config.get("flow", {}).get("image_launch_soft_limit", 0) try: return max(0, min(200, int(value))) @@ -204,7 +204,7 @@ def flow_image_launch_soft_limit(self) -> int: @property def flow_image_launch_wait_timeout(self) -> float: - """图片前置发车软并发等待超时(秒)。""" + """Timeout in seconds while waiting for the soft image-generation concurrency limit.""" timeout = self._config.get("flow", {}).get("image_launch_wait_timeout", 180) try: return max(1.0, min(600.0, float(timeout))) @@ -213,7 +213,7 @@ def flow_image_launch_wait_timeout(self) -> float: @property def flow_image_launch_stagger_ms(self) -> int: - """图片请求前置发车间隔(毫秒),用于平滑同批突发。""" + """Delay in milliseconds between image request launches to smooth bursts in the same batch.""" value = self._config.get("flow", {}).get("image_launch_stagger_ms", 0) try: return max(0, min(5000, int(value))) @@ -222,7 +222,7 @@ def flow_image_launch_stagger_ms(self) -> int: @property def flow_video_slot_wait_timeout(self) -> float: - """视频硬并发槽位等待超时(秒)。""" + """Timeout in seconds while waiting for a video-generation concurrency slot.""" timeout = self._config.get("flow", {}).get("video_slot_wait_timeout", 120) try: return max(1.0, min(600.0, float(timeout))) @@ -231,7 +231,7 @@ def flow_video_slot_wait_timeout(self) -> float: @property def flow_video_launch_soft_limit(self) -> int: - """视频生成前置发车软并发上限(0 表示关闭软整形,仅使用硬并发)。""" + """Soft concurrency limit before starting video generation (0 disables soft shaping and uses only the hard limit).""" value = self._config.get("flow", {}).get("video_launch_soft_limit", 0) try: return max(0, min(200, int(value))) @@ -240,7 +240,7 @@ def flow_video_launch_soft_limit(self) -> int: @property def flow_video_launch_wait_timeout(self) -> float: - """视频前置发车软并发等待超时(秒)。""" + """Timeout in seconds while waiting for the soft video-generation concurrency limit.""" timeout = self._config.get("flow", {}).get("video_launch_wait_timeout", 180) try: return max(1.0, min(600.0, float(timeout))) @@ -249,7 +249,7 @@ def flow_video_launch_wait_timeout(self) -> float: @property def flow_video_launch_stagger_ms(self) -> int: - """视频请求前置发车间隔(毫秒),用于平滑同批突发。""" + """Delay in milliseconds between video request launches to smooth bursts in the same batch.""" value = self._config.get("flow", {}).get("video_launch_stagger_ms", 0) try: return max(0, min(5000, int(value))) @@ -424,18 +424,18 @@ def set_captcha_method(self, method: str): @property def browser_launch_background(self) -> bool: - """有头浏览器打码是否默认后台启动,避免抢占前台窗口。""" + """Whether headed-browser captcha solving should start in the background by default to avoid taking over the foreground window.""" return self._config.get("captcha", {}).get("browser_launch_background", True) def set_browser_launch_background(self, enabled: bool): - """设置有头浏览器打码是否后台启动。""" + """Set whether headed-browser captcha solving starts in the background.""" if "captcha" not in self._config: self._config["captcha"] = {} self._config["captcha"]["browser_launch_background"] = bool(enabled) @property def browser_count(self) -> int: - """浏览器打码实例数量,browser/personal 模式共用。""" + """Number of browser instances used by both browser and personal captcha modes.""" value = self._config.get("captcha", {}).get("browser_count", 1) try: return max(1, min(20, int(value))) @@ -443,14 +443,14 @@ def browser_count(self) -> int: return 1 def set_browser_count(self, value: int): - """设置浏览器打码实例数量。""" + """Set the number of browser captcha instances.""" if "captcha" not in self._config: self._config["captcha"] = {} self._config["captcha"]["browser_count"] = max(1, min(20, int(value))) @property def browser_recaptcha_settle_seconds(self) -> float: - """自定义站点分数测试在 reload/clr 就绪后的额外等待秒数。""" + """Additional wait in seconds after reload/clr is ready during custom site score tests.""" value = self._config.get("captcha", {}).get( "browser_recaptcha_settle_seconds", 3.0 ) @@ -461,7 +461,7 @@ def browser_recaptcha_settle_seconds(self) -> float: @property def browser_flow_page_warmup_seconds(self) -> float: - """browser harvest 打开真实 Flow 页面后的稳定等待秒数。""" + """Stabilization delay in seconds after browser harvest opens the real Flow page.""" value = self._config.get("captcha", {}).get( "browser_flow_page_warmup_seconds", 6.0 ) @@ -471,7 +471,7 @@ def browser_flow_page_warmup_seconds(self) -> float: return 6.0 def set_browser_flow_page_warmup_seconds(self, value: float): - """设置 browser harvest 页面预热等待秒数。""" + """Set the page warm-up delay for browser harvest.""" if "captcha" not in self._config: self._config["captcha"] = {} self._config["captcha"]["browser_flow_page_warmup_seconds"] = max( @@ -488,7 +488,7 @@ def browser_idle_ttl_seconds(self) -> int: @property def browser_captcha_max_retries(self) -> int: - """browser 模式单次打码最大重试次数。""" + """Maximum captcha-solving retries per attempt in browser mode.""" value = self._config.get("captcha", {}).get("browser_captcha_max_retries", 5) try: return max(1, min(20, int(value))) @@ -497,7 +497,7 @@ def browser_captcha_max_retries(self) -> int: @property def browser_captcha_generation_retries(self) -> int: - """生成接口因 reCAPTCHA 评估失败时允许的总重试次数。""" + """Total retries allowed when the generation endpoint fails reCAPTCHA evaluation.""" value = self._config.get("captcha", {}).get( "browser_captcha_generation_retries", 6 ) @@ -508,14 +508,14 @@ def browser_captcha_generation_retries(self) -> int: @property def personal_solve_strategy(self) -> str: - """personal 打码策略:direct 用 trusted JS,harvest 原生截获,auto 自动降级。""" + """Personal captcha strategy: direct uses trusted JS, harvest uses native interception, and auto falls back automatically.""" value = str( self._config.get("captcha", {}).get("personal_solve_strategy", "auto") ).strip().lower() return value if value in {"direct", "harvest", "auto"} else "auto" def set_personal_solve_strategy(self, value: str): - """设置 personal 打码策略。""" + """Set the personal captcha-solving strategy.""" normalized = str(value or "").strip().lower() if normalized not in {"direct", "harvest", "auto"}: normalized = "auto" @@ -525,16 +525,16 @@ def set_personal_solve_strategy(self, value: str): @property def personal_max_resident_tabs(self) -> int: - """内置浏览器打码单实例共享标签页上限""" + """Maximum number of shared captcha tabs per built-in browser instance.""" value = self._config.get("captcha", {}).get("personal_max_resident_tabs", 5) try: - return max(1, min(50, int(value))) # 限制在1-50之间 + return max(1, min(50, int(value))) # Limit to 1–50 except Exception: return 5 @property def personal_project_pool_size(self) -> int: - """单个 Token 默认维护的项目池数量,仅影响项目轮换。""" + """Default project-pool size maintained per token; affects project rotation only.""" value = self._config.get("captcha", {}).get("personal_project_pool_size", 4) try: return max(1, min(50, int(value))) @@ -543,7 +543,7 @@ def personal_project_pool_size(self) -> int: @property def personal_idle_tab_ttl_seconds(self) -> int: - """内置浏览器打码标签页空闲超时(秒)""" + """Idle timeout in seconds for built-in browser captcha tabs.""" value = self._config.get("captcha", {}).get( "personal_idle_tab_ttl_seconds", 600 ) @@ -554,14 +554,14 @@ def personal_idle_tab_ttl_seconds(self) -> int: @property def personal_headless(self) -> bool: - """personal 内置浏览器是否强制无头;默认按有头模式运行。""" + """Whether to force the built-in personal browser into headless mode; headed mode is the default.""" env_value = os.getenv("PERSONAL_BROWSER_HEADLESS") if env_value is not None: return str(env_value).strip().lower() in {"1", "true", "yes", "on"} return bool(self._config.get("captcha", {}).get("personal_headless", False)) def set_personal_max_resident_tabs(self, value: int): - """设置内置浏览器打码单实例共享标签页上限""" + """设置Maximum number of shared captcha tabs per built-in browser instance.""" if "captcha" not in self._config: self._config["captcha"] = {} self._config["captcha"]["personal_max_resident_tabs"] = max( @@ -569,7 +569,7 @@ def set_personal_max_resident_tabs(self, value: int): ) def set_personal_project_pool_size(self, value: int): - """设置单个 Token 默认维护的项目池数量,仅影响项目轮换""" + """Set the default project-pool size maintained per token; affects project rotation only.""" if "captcha" not in self._config: self._config["captcha"] = {} self._config["captcha"]["personal_project_pool_size"] = max( @@ -577,14 +577,14 @@ def set_personal_project_pool_size(self, value: int): ) def set_personal_idle_tab_ttl_seconds(self, value: int): - """设置内置浏览器打码标签页空闲超时(秒)""" + """设置Idle timeout in seconds for built-in browser captcha tabs.""" if "captcha" not in self._config: self._config["captcha"] = {} self._config["captcha"]["personal_idle_tab_ttl_seconds"] = max(60, int(value)) @property def browser_personal_fresh_restart_every_n_solves(self) -> int: - """内置浏览器成功打码多少次后使用全新 profile 重启,0 表示禁用。""" + """Restart the built-in browser with a fresh profile after this many successful captcha solves; 0 disables this behavior.""" value = self._config.get("captcha", {}).get( "browser_personal_fresh_restart_every_n_solves", 10 ) @@ -594,7 +594,7 @@ def browser_personal_fresh_restart_every_n_solves(self) -> int: return 10 def set_browser_personal_fresh_restart_every_n_solves(self, value: int): - """设置内置浏览器 fresh profile 轮换阈值。""" + """Set the fresh-profile rotation threshold for the built-in browser.""" if "captcha" not in self._config: self._config["captcha"] = {} self._config["captcha"]["browser_personal_fresh_restart_every_n_solves"] = max( diff --git a/src/core/database.py b/src/core/database.py index 1f2b44f3..21a15a92 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -372,7 +372,7 @@ async def _ensure_config_rows(self, db, config_dict: dict = None): ), ) - # 清理历史版本的邮箱不匹配报错记录(现在邮箱按 Flow 页面自动更正) + # Clean up email-mismatch errors from older versions (email addresses are now corrected automatically using the Flow page). try: await db.execute( "UPDATE tokens SET last_st_refresh_result = '' " @@ -533,19 +533,19 @@ async def check_and_migrate_db(self, config_dict: dict = None): ("video_enabled", "BOOLEAN DEFAULT 1"), ("image_concurrency", "INTEGER DEFAULT -1"), ("video_concurrency", "INTEGER DEFAULT -1"), - ("captcha_proxy_url", "TEXT"), # token级打码代理 - ("extension_route_key", "TEXT"), # extension 模式路由键 - ("protocol_mode", "TEXT DEFAULT 'session'"), # ST 刷新模式 - ("google_cookies", "TEXT DEFAULT ''"), # 协议登录 Google Cookies - ("login_account", "TEXT DEFAULT ''"), # 协议登录账号提示 - ("login_password", "TEXT DEFAULT ''"), # 预留 - ("proxy_url", "TEXT DEFAULT ''"), # 协议刷新代理 + ("captcha_proxy_url", "TEXT"), # Per-token captcha proxy + ("extension_route_key", "TEXT"), # Routing key for extension mode + ("protocol_mode", "TEXT DEFAULT 'session'"), # ST refresh mode + ("google_cookies", "TEXT DEFAULT ''"), # Google cookies for protocol login + ("login_account", "TEXT DEFAULT ''"), # Account hint for protocol login + ("login_password", "TEXT DEFAULT ''"), # Reserved + ("proxy_url", "TEXT DEFAULT ''"), # Proxy for protocol refresh ("auto_refresh_enabled", "BOOLEAN DEFAULT 1"), ("refresh_interval_minutes", "INTEGER DEFAULT 30"), ("last_st_refresh_at", "TIMESTAMP"), ("last_st_refresh_result", "TEXT DEFAULT ''"), - ("ban_reason", "TEXT"), # 禁用原因 - ("banned_at", "TIMESTAMP"), # 禁用时间 + ("ban_reason", "TEXT"), # Ban reason + ("banned_at", "TIMESTAMP"), # Time disabled ] for col_name, col_type in columns_to_add: @@ -660,7 +660,7 @@ async def check_and_migrate_db(self, config_dict: dict = None): ("today_video_count", "INTEGER DEFAULT 0"), ("today_error_count", "INTEGER DEFAULT 0"), ("today_date", "DATE"), - ("consecutive_error_count", "INTEGER DEFAULT 0"), # 🆕 连续错误计数 + ("consecutive_error_count", "INTEGER DEFAULT 0"), # 🆕 Consecutive error count ] for col_name, col_type in stats_columns_to_add: @@ -676,7 +676,7 @@ async def check_and_migrate_db(self, config_dict: dict = None): # Check and add missing columns to plugin_config table if await self._table_exists(db, "plugin_config"): plugin_columns_to_add = [ - ("auto_enable_on_update", "BOOLEAN DEFAULT 1"), # 默认开启 + ("auto_enable_on_update", "BOOLEAN DEFAULT 1"), # Enabled by default ] for col_name, col_type in plugin_columns_to_add: @@ -749,7 +749,7 @@ async def init_db(self): async with self._connect(write=True) as db: await db.execute("PRAGMA journal_mode = WAL") await db.execute("PRAGMA synchronous = NORMAL") - # Tokens table (Flow2API版本) + # Tokens table (Flow2API version) await db.execute(""" CREATE TABLE IF NOT EXISTS tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -787,7 +787,7 @@ async def init_db(self): ) """) - # Projects table (新增) + # Projects table (New) await db.execute(""" CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -1000,7 +1000,7 @@ async def init_db(self): # Migrate request_logs table if needed await self._migrate_request_logs(db) - # Request logs query indexes (列表按 created_at 排序 / token 过滤) + # Request logs query indexes (List sorted by created_at / filtered by token) await db.execute( "CREATE INDEX IF NOT EXISTS idx_request_logs_created_at ON request_logs(created_at DESC)" ) @@ -1119,7 +1119,7 @@ async def _ensure_token_identity_available( query += " LIMIT 1" cursor = await db.execute(query, params) if await cursor.fetchone(): - raise ValueError(f"账号已存在: {identities[0]}") + raise ValueError(f"Account already exists: {identities[0]}") # Token operations async def add_token(self, token: Token) -> int: @@ -2448,7 +2448,7 @@ async def update_captcha_config( ) new_personal_max_tabs = max( 1, min(50, int(new_personal_max_tabs)) - ) # 限制1-50 + ) # Limit: 1–50 new_personal_fresh_restart_every = max( 0, int(new_personal_fresh_restart_every) ) diff --git a/src/main.py b/src/main.py index 1ee6effb..90815f5a 100644 --- a/src/main.py +++ b/src/main.py @@ -118,13 +118,13 @@ async def lifespan(app: FastAPI): await db.check_and_migrate_db(config_dict) print("Database migration check completed.") - # 启动时统一把数据库配置同步到内存,避免 personal/brower 相关运行时配置遗漏。 + # Synchronize database configuration into memory at startup so personal/browser runtime settings are not missed. await db.reload_config_to_memory() generation_handler.file_cache.set_timeout(config.cache_timeout) cache_cleanup_enabled = await generation_handler.file_cache.refresh_cleanup_task() captcha_config = await db.get_captcha_config() - # 尽量在浏览器服务启动前就拿到 token 快照,后续并发管理和预热共用。 + # Capture a token snapshot before starting the browser service when possible, so concurrency management and warm-up can share it. tokens = await token_manager.get_all_tokens() # Initialize browser captcha service if needed @@ -175,7 +175,7 @@ async def lifespan(app: FastAPI): elif tokens: print("Browser captcha resident warmup skipped: no tab warmed successfully") else: - # 没有任何可用 token 时,打开登录窗口供用户手动操作 + # If no usable token is available, open a login window for the user to sign in manually. await browser_service.open_login_window() print("No active token found, opened login window for manual setup") elif captcha_config.captcha_method == "browser": @@ -203,10 +203,10 @@ async def lifespan(app: FastAPI): import asyncio async def auto_unban_task(): - """定时任务:每小时检查并解禁429被禁用的token""" + """Scheduled task: check hourly and re-enable tokens disabled by HTTP 429.""" while True: try: - await asyncio.sleep(3600) # 每小时执行一次 + await asyncio.sleep(3600) # Run once per hour await token_manager.auto_unban_429_tokens() except Exception as e: print(f"Auto-unban task error: {e}") @@ -263,7 +263,7 @@ async def auto_unban_task(): load_balancer, db, concurrency_manager, - proxy_manager, # 添加 proxy_manager 参数 + proxy_manager, # Add the proxy_manager parameter ) # Set dependencies diff --git a/static/login.html b/static/login.html index 115cea85..30280bcf 100644 --- a/static/login.html +++ b/static/login.html @@ -1,9 +1,9 @@ - + - 登录 - Flow2API + Sign in - Flow2API