flow_captcha_service 是给 flow2api 使用的自托管打码服务。
它的目标不是接第三方打码平台,而是自己托管 Playwright + Chromium
有头浏览器能力,并支持单机、主从集群、用户门户、管理后台和额度体系。
如果你只想快速理解一句话,可以直接记住下面这句:
standalone本地自己打码,master只调度不本地打码,subnode负责执行浏览器打码并向master上报心跳。
第一次部署,建议只走脚本入口,不要先手动 mkdir / cp / docker compose -f ...。
先跑单机:
.\scripts\deploy.ps1 standalone想本机验证最小集群:
.\scripts\deploy.ps1 stack单独部署子节点:
.\scripts\deploy.ps1 subnode `
-MasterBaseUrl http://host.docker.internal:8060 `
-MasterClusterKey <你的主节点 key> `
-NodePublicBaseUrl http://host.docker.internal:8061bash ./scripts/deploy.sh standalone
bash ./scripts/deploy.sh stack- 创建
data/data/master/data/subnode - 不存在时自动复制
config/setting_example.toml - 为
stack/subnode自动生成本地 env 文件 - 执行对应的
docker compose up -d --build - 等待健康检查通过后再打印访问地址
- 第一次建议先跑
standalone,跑通后再尝试stack - 配置优先级始终是:环境变量 >
data/setting.toml> 默认值
另外需要注意:
stack更适合本机演示集群,不等于跨机器生产部署subnode跨机器部署时,node_public_base_url必须填主节点真正能访问到的地址,不能填127.0.0.1/localhost/0.0.0.0
flow2api 在某些场景下需要一个独立的、有头浏览器驱动的验证码服务。
flow_captcha_service 就是为这个场景准备的独立服务端。
它提供:
- 有头浏览器打码能力
solve -> finish/error会话协议YesCaptcha兼容协议(createTask / getTaskResult / getBalance)- 浏览器槽位复用与空闲回收
standalone / master / subnode三种部署角色- 用户门户、管理员后台、API Key、额度和日志
- 主从集群调度
它不提供:
capsolver等其它第三方平台协议- 子节点上的用户注册/登录首页
flow2api
|
| HTTP
v
[standalone] 直接本地打码
或
flow2api
|
| HTTP
v
[master] 只调度,不本地起浏览器
|
| 路由转发
v
[subnode] 本地执行有头浏览器打码
- 单机模式
- 本地直接执行浏览器打码
- 适合本地调试、单机部署、小规模使用
- 只负责调度和转发
- 不执行本地浏览器打码
- 适合集群入口节点
- 执行本地浏览器打码
- 向
master注册和发送心跳 - 首页是子节点状态页,不是用户登录/注册页
/:用户门户/portal:用户门户别名/admin:管理后台/api/v1/health:健康检查
/:子节点状态页/portal:同样返回子节点状态页/subnode:子节点状态页显式入口/admin:管理后台/api/v1/health:健康检查
从这个版本开始,
subnode不再展示用户注册/登录首页。 子节点首页只展示角色、健康状态和后台入口。
- 上游调用
POST /api/v1/solve - 服务返回
session_id、token、node_name - 上游拿着 token 继续完成图片/视频等后续业务
- 业务成功后调用
POST /api/v1/sessions/{session_id}/finish - 业务失败后调用
POST /api/v1/sessions/{session_id}/error
solve只负责拿 token,不代表整个上游业务已经完成finish/error是业务会话回收协议- 成功时不会强制关闭共享浏览器
- 只有明确命中验证码失败类错误时,才会回收对应浏览器槽位
如果当前角色是 master,返回的 session_id 可能是:
nodeId:childSessionId
这表示:
- 前半段是 master 侧的节点路由信息
- 后半段是 subnode 的真实会话 ID
- 之后的
finish/error会按这个路由再转发回原子节点
- Python 本地运行:建议
Python 3.11 - Docker 部署:需要
Docker和Docker Compose - 有头浏览器模式依赖
Playwright + Chromium
如果你使用前面的 Docker 脚本入口,这一步会自动完成。
只有在你准备走本地 Python 或手工命令时,才需要自己准备运行目录:
mkdir -p data
cp config/setting_example.toml data/setting.toml本地非 Docker 启动时,除了安装 Python 依赖,还要安装 Chromium:
pip install -r requirements.txt
python -m playwright install chromiumLinux 如果缺系统依赖,可以改用:
python -m playwright install --with-deps chromium- 模板文件:
config/setting_example.toml - 运行配置:
data/setting.toml - 兼容迁移:如果存在旧的
config/setting.toml,启动时会迁移到data/setting.toml
建议记住这一条:
环境变量 > data/setting.toml > 默认配置
config/setting_example.toml 只是初始化模板,不是最终优先级来源。
如果你想看按角色拆开的详细填写说明,直接看:
docs/ROLE_CONFIG_GUIDE.md
FCS_CONFIG_FILEFCS_SERVER_HOSTFCS_SERVER_PORTFCS_DB_PATHFCS_ADMIN_USERNAMEFCS_ADMIN_PASSWORDFCS_LOG_LEVELFCS_NODE_NAME
FCS_LOG_STORAGE_BACKENDFCS_LOG_REDIS_URLFCS_LOG_REDIS_KEY_PREFIXFCS_LOG_REDIS_MAX_ENTRIESFCS_LOG_STARTUP_CLEAR_ON_BOOT
FCS_BROWSER_COUNTFCS_BROWSER_PROXY_ENABLEDFCS_BROWSER_PROXY_URLFCS_BROWSER_LAUNCH_BACKGROUNDFCS_BROWSER_SCORE_DOM_WAIT_SECONDSFCS_BROWSER_RECAPTCHA_SETTLE_SECONDSFCS_BROWSER_SCORE_TEST_WARMUP_SECONDSFCS_BROWSER_IDLE_TTL_SECONDSFCS_FLOW_TIMEOUTFCS_UPSAMPLE_TIMEOUTFCS_SESSION_TTL_SECONDS
FCS_CLUSTER_ROLEFCS_CLUSTER_MASTER_BASE_URLFCS_CLUSTER_MASTER_CLUSTER_KEYFCS_CLUSTER_NODE_PUBLIC_BASE_URLFCS_CLUSTER_NODE_API_KEYFCS_CLUSTER_HEARTBEAT_INTERVAL_SECONDSFCS_CLUSTER_NODE_WEIGHTFCS_CLUSTER_NODE_MAX_CONCURRENCYFCS_CLUSTER_MASTER_NODE_STALE_SECONDSFCS_CLUSTER_MASTER_DISPATCH_TIMEOUT_SECONDS
默认行为:
storage_backend = "sqlite"时,继续走 SQLitestorage_backend = "redis"时,请求日志、子节点心跳历史、子节点错误历史写入 Rediscluster_nodes的当前节点状态快照仍保留在 SQLite,因为它是覆盖写,不是历史累积日志startup_clear_on_boot = true时,服务启动后会自动清理 SQLite 历史日志,并执行数据库压缩auto_clear_interval_minutes > 0时,服务会按间隔自动清空请求日志、子节点心跳历史和子节点错误历史- 不会清空用户剩余次数、已用次数,也不会删除
session_quota_events
- 不会清空用户剩余次数、已用次数,也不会删除
配置示例:
[log]
level = "INFO"
storage_backend = "redis" # sqlite | redis
redis_url = "redis://127.0.0.1:6379/0"
redis_key_prefix = "fcs"
redis_max_entries = 20000
startup_clear_on_boot = true
auto_clear_interval_minutes = 0等价环境变量:
FCS_LOG_STORAGE_BACKEND=redisFCS_LOG_REDIS_URL=redis://127.0.0.1:6379/0FCS_LOG_REDIS_KEY_PREFIX=fcsFCS_LOG_REDIS_MAX_ENTRIES=20000FCS_LOG_STARTUP_CLEAR_ON_BOOT=trueFCS_LOG_AUTO_CLEAR_INTERVAL_MINUTES=30
关闭 Redis,恢复默认 SQLite:
FCS_LOG_STORAGE_BACKEND=sqlite
说明:
subnode接收来自master的内部调用时,不再持久化请求日志,避免重复写入redis_max_entries用于限制 Redis 历史日志长度,避免 Redis 继续无限增长- 定时清理日志只删除日志历史,不会影响额度扣减结果和用户使用次数统计
cluster.node_max_concurrency = 0
实际含义是:
- 不单独指定固定并发
- 自动跟随
browser_count
如果你只是想把服务跑起来,优先用前面的脚本入口。 这一节只保留“不使用脚本时”的手工命令。
python -m venv .venvWindows:
.venv\Scripts\activateLinux / macOS:
source .venv/bin/activatepip install -r requirements.txt
python -m playwright install chromiummkdir -p data
cp config/setting_example.toml data/setting.toml确认 data/setting.toml 中:
[cluster]
role = "standalone"python main.py- 用户门户:
http://127.0.0.1:8060/ - 管理后台:
http://127.0.0.1:8060/admin - 健康检查:
http://127.0.0.1:8060/api/v1/health
对应文件:docker-compose.headed.yml
推荐脚本:
.\scripts\deploy.ps1 standalone手工方式:
mkdir -p data
cp config/setting_example.toml data/setting.toml
docker compose -f docker-compose.headed.yml up -d --build访问:
- 用户门户:
http://127.0.0.1:8060/ - 管理后台:
http://127.0.0.1:8060/admin - 健康检查:
http://127.0.0.1:8060/api/v1/health
说明:
- 该模式使用
Dockerfile.headed - 镜像内已安装
Playwright Chromium + Xvfb + fluxbox - 默认角色是
standalone ./data:/app/data必须保留,否则数据库、日志、密钥等状态会丢失
下面这部分是“服务已经启动之后,怎么真正接入”。
访问:
http://127.0.0.1:8060/admin
默认管理员账号通常是:
admin / admin
首次登录后建议马上修改。
你有两种常见用法:
- 直接在后台创建服务 API Key,给上游程序直接调用
- 让用户走门户注册/登录,再在门户里创建自己的 API Key
只适用于 standalone 或 master。
http://127.0.0.1:8060/
门户支持:
- 登录
- 注册
- 查看额度
- 兑换 CDK
- 查看调用记录
- 自助创建自己的 API Key
登录后进入 API Key 页面,创建自己的 Key。
下面是最常见的接入方式。
curl -X POST "http://127.0.0.1:8060/api/v1/solve" \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"project_id": "demo-project",
"action": "IMAGE_GENERATION"
}'返回示例:
{
"success": true,
"session_id": "123456",
"token": "recaptcha-token",
"fingerprint": {},
"node_name": "standalone-node",
"expires_in_seconds": 1200
}如果当前入口是 master,session_id 可能是:
12:child-session-id
这属于正常现象,后续 finish/error 原样带回即可。
curl -X POST "http://127.0.0.1:8060/api/v1/sessions/<SESSION_ID>/finish" \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"status": "success"
}'curl -X POST "http://127.0.0.1:8060/api/v1/sessions/<SESSION_ID>/error" \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"error_reason": "upstream_error"
}'如果你要做自定义 score 调试,可以调用:
POST /api/v1/custom-score
请求体包含:
website_urlwebsite_keyverify_urlactionenterprise
如果上游已经按 YesCaptcha 的请求格式接入,可以直接调用:
POST /createTask
POST /getTaskResult
POST /getBalance
当前支持的 task.type:
NoCaptchaTaskProxylessRecaptchaV2TaskProxylessRecaptchaV2EnterpriseTaskProxylessRecaptchaV3TaskProxylessRecaptchaV3TaskProxylessM1RecaptchaV3TaskProxylessM1S7RecaptchaV3TaskProxylessM1S9RecaptchaV3EnterpriseTaskProxylessTurnstileTaskProxylessTurnstileTaskProxylessM1TurnstileTaskProxylessM1S1TurnstileTaskProxylessM1S2TurnstileTaskProxylessM1S3
-
NoCaptchaTaskProxyless、RecaptchaV2TaskProxyless- 本地统一映射为
reCAPTCHA V2 - 默认按
isInvisible=true走 invisible;如果请求里显式传isInvisible=false,会按普通 V2 widget 方式执行 - 返回
solution.gRecaptchaResponse和solution.token
- 本地统一映射为
-
RecaptchaV2EnterpriseTaskProxyless- 本地映射为
reCAPTCHA V2 Enterprise - 仍然支持
isInvisible - 返回
solution.gRecaptchaResponse和solution.token
- 本地映射为
-
RecaptchaV3TaskProxyless、RecaptchaV3TaskProxylessM1、RecaptchaV3TaskProxylessM1S7、RecaptchaV3TaskProxylessM1S9- 本地统一映射为同一条
reCAPTCHA V3求解链路 pageAction、action、websiteAction会被归一化成同一个action- 这些
M1 / S7 / S9在当前服务里只是协议兼容别名,不额外承诺官方同名任务的特殊求解策略 - 返回
solution.gRecaptchaResponse和solution.token
- 本地统一映射为同一条
-
RecaptchaV3EnterpriseTaskProxyless- 本地映射为
reCAPTCHA V3 Enterprise pageAction、action、websiteAction会被归一化成同一个action- 返回
solution.gRecaptchaResponse和solution.token
- 本地映射为
-
TurnstileTaskProxyless、TurnstileTaskProxylessM1、TurnstileTaskProxylessM1S1、TurnstileTaskProxylessM1S2、TurnstileTaskProxylessM1S3- 本地统一映射为同一条
Cloudflare Turnstile求解链路 - 这些
M1 / S1 / S2 / S3在当前服务里也是协议兼容别名,不额外区分不同本地策略 - 返回
solution.token Turnstile不会返回solution.gRecaptchaResponse
- 本地统一映射为同一条
说明:
- 原有
/api/v1/solve、/finish、/error保持不变 - 当前只支持
proxyless的通用 token 任务 - 当前不支持
hCaptcha、FunCaptcha、代理型任务 getBalance返回的是当前 API Key 剩余次数;无限额度 Key 会返回999999999YesCaptcha兼容层是标准轮询模式,只保证token ready- 如果你需要“打码后继续等待上游业务请求完成再回收”的会话语义,请继续使用原生
/api/v1/solve -> finish/error
- 如果你需要“打码后继续等待上游业务请求完成再回收”的会话语义,请继续使用原生
createTask只是创建异步任务,不会像原生/api/v1/solve一样建立一条等待finish/error回收的业务会话getTaskResult返回status = ready,表示当前token已经可用ready不代表你的上游业务请求已经成功,也不代表服务还会继续替你等待真实业务完成- 标准
YesCaptcha协议本身没有finish/error这类回调,所以它天然无法完整复刻原生接口“获取 token 后继续等待上游完成”的语义 - 如果你的业务必须依赖“同一浏览器上下文继续保活,直到真实请求结束”,请继续使用原生
/api/v1/solve -> finish/error - 如果你的业务只是想兼容
YesCaptcha createTask/getTaskResult的调用格式,并且目标只是拿到可用token,直接用当前兼容层即可
curl -X POST "http://127.0.0.1:8060/createTask" \
-H "Content-Type: application/json" \
-d '{
"clientKey": "<YOUR_API_KEY>",
"task": {
"type": "RecaptchaV3TaskProxyless",
"websiteURL": "https://example.com/login",
"websiteKey": "site-key",
"pageAction": "login"
}
}'返回示例:
{
"errorId": 0,
"taskId": 1774442208718
}curl -X POST "http://127.0.0.1:8060/getTaskResult" \
-H "Content-Type: application/json" \
-d '{
"clientKey": "<YOUR_API_KEY>",
"taskId": 1774442208718
}'处理中:
{
"errorId": 0,
"taskId": 1774442208718,
"status": "processing"
}完成后,如果是 reCAPTCHA,返回示例:
{
"errorId": 0,
"taskId": 1774442208718,
"status": "ready",
"solution": {
"gRecaptchaResponse": "token-value",
"token": "token-value",
"userAgent": "Mozilla/5.0 ..."
}
}完成后,如果是 Turnstile,返回示例:
{
"errorId": 0,
"taskId": 1774442208718,
"status": "ready",
"solution": {
"token": "turnstile-token",
"userAgent": "Mozilla/5.0 ..."
}
}curl -X POST "http://127.0.0.1:8060/getBalance" \
-H "Content-Type: application/json" \
-d '{
"clientKey": "<YOUR_API_KEY>"
}'如果你只是想先把集群跑起来,优先使用前面的脚本入口。
这一节只保留:
- 每种集群模式适用于什么场景
- 手工排查时最短的 compose 命令
- 哪些关键变量必须自己确认
对应文件:docker-compose.cluster.master.yml
- 作为集群统一入口
- 只调度,不本地打码
- 镜像更轻,不安装 Chromium
推荐脚本:
.\scripts\deploy.ps1 master手工方式:
mkdir -p data/master
cp config/setting_example.toml data/master/setting.toml
docker compose -f docker-compose.cluster.master.yml up -d --build默认访问入口:
- 管理后台:
http://127.0.0.1:8060/admin - 用户门户:
http://127.0.0.1:8060/ - 健康检查:
http://127.0.0.1:8060/api/v1/health
说明:
master使用Dockerfile.master- 不安装
Playwright/Chromium - 当前 compose 默认挂载是
./data/master:/app/data - 当前 compose 会同时启动
Redis - 当前 compose 默认将日志后端设为
redis - 如果你不想启用 Redis,可在启动前覆盖:
FCS_LOG_STORAGE_BACKEND=sqlite- 或直接改
data/master/setting.toml
对应文件:docker-compose.cluster.subnode.yml
- 集群执行节点
- 本地有头浏览器打码
- 向
master注册与发送心跳
推荐脚本:
.\scripts\deploy.ps1 subnode `
-MasterBaseUrl http://host.docker.internal:8060 `
-MasterClusterKey <你的主节点 key> `
-NodePublicBaseUrl http://host.docker.internal:8061手工方式:
mkdir -p data/subnode deploy
cp config/setting_example.toml data/subnode/setting.toml
cp deploy/subnode.env.example deploy/subnode.env.local然后修改 deploy/subnode.env.local。
必须确认以下 4 个值:
FCS_CLUSTER_MASTER_BASE_URL- 子节点访问主节点的地址
FCS_CLUSTER_MASTER_CLUSTER_KEY- 主节点当前使用的 cluster key
FCS_CLUSTER_NODE_PUBLIC_BASE_URL- 主节点回调当前子节点的地址
FCS_CLUSTER_NODE_API_KEY- 子节点内部认证 key
最后启动:
docker compose --env-file deploy/subnode.env.local -f docker-compose.cluster.subnode.yml up -d --build默认访问入口:
- 子节点状态页:
http://127.0.0.1:8061/ - 管理后台:
http://127.0.0.1:8061/admin - 健康检查:
http://127.0.0.1:8061/api/v1/health
非常重要的注意事项:
FCS_CLUSTER_NODE_PUBLIC_BASE_URL不能填127.0.0.1- 不能填
localhost - 不能填
0.0.0.0 - 必须是
master真正能访问到的地址
另外:
host.docker.internal只适合本机开发或 Docker Desktop 场景- Linux 服务器或跨机器部署时,应改成真实地址
对应文件:docker-compose.cluster.stack.yml
这是最推荐的最小集群示例。
推荐脚本:
.\scripts\deploy.ps1 stack手工方式:
mkdir -p data/master data/subnode deploy
cp config/setting_example.toml data/master/setting.toml
cp config/setting_example.toml data/subnode/setting.toml
cp deploy/stack.env.example deploy/stack.env.local然后修改 deploy/stack.env.local。
至少要确认这些值:
FCS_CLUSTER_MASTER_CLUSTER_KEYFCS_CLUSTER_NODE_API_KEY- 如果不是容器内互联场景,还要改
FCS_CLUSTER_MASTER_BASE_URL和FCS_CLUSTER_NODE_PUBLIC_BASE_URL
最后启动:
docker compose --env-file deploy/stack.env.local -f docker-compose.cluster.stack.yml up -d --build访问:
- master 用户门户:
http://127.0.0.1:8060/ - master 管理后台:
http://127.0.0.1:8060/admin - master 健康检查:
http://127.0.0.1:8060/api/v1/health - subnode 状态页:
http://127.0.0.1:8061/ - subnode 健康检查:
http://127.0.0.1:8061/api/v1/health
这个 compose 的优点
- 已把
master和subnode的数据目录拆开 - 已内置
Redis服务给日志/心跳/错误历史使用 - 默认更适合作为第一次验证集群调度的示例
- 默认会把日志后端设为
redis - 如果你想退回 SQLite,把
deploy/stack.env.local里的FCS_LOG_STORAGE_BACKEND=sqlite
挂载如下:
./data/master:/app/data./data/subnode:/app/data./data/redis:/data
- 一般可以直接启动
- 按需调整
browser_count - 按需调整代理
- 建议修改管理员账号密码
- 建议修改
FCS_NODE_NAME - 建议修改管理员账号密码
- 如果有多个子节点,建议统一规划 cluster key
- 如果不想使用 Redis 日志后端,显式设置
FCS_LOG_STORAGE_BACKEND=sqlite
- 必改
FCS_CLUSTER_MASTER_BASE_URL - 必改
FCS_CLUSTER_MASTER_CLUSTER_KEY - 必改
FCS_CLUSTER_NODE_PUBLIC_BASE_URL - 必改
FCS_CLUSTER_NODE_API_KEY - 建议修改
FCS_NODE_NAME - 推荐放在
deploy/subnode.env.local
- 必改
FCS_CLUSTER_MASTER_CLUSTER_KEY - 必改
FCS_CLUSTER_NODE_API_KEY - 如果不是容器内互联场景,要改 master/subnode 的实际对外地址
- 默认已启用 Redis 日志后端;如需退回 SQLite,改
FCS_LOG_STORAGE_BACKEND=sqlite - 推荐放在
deploy/stack.env.local
curl http://127.0.0.1:8060/api/v1/health返回示例:
{
"success": true,
"status": "ok",
"node_name": "master-1",
"role": "master"
}standalone:/应该是用户门户master:/应该是用户门户subnode:/应该是子节点状态页
在 master 管理后台查看:
- 子节点是否出现
- 心跳是否持续更新
- 有效容量是否正常
- 节点是否健康
仓库支持通过 GHCR 发布镜像:
ghcr.io/<owner>/flow_captcha_service-masterghcr.io/<owner>/flow_captcha_service-headed
拉取示例:
docker pull ghcr.io/genz27/flow_captcha_service-master:latest
docker pull ghcr.io/genz27/flow_captcha_service-headed:latest说明:
master镜像用于主节点headed镜像用于standalone或subnode
这是当前版本的设计。
subnode 首页现在是子节点状态页,只用于:
- 看当前角色
- 看健康状态
- 进后台排查
用户门户只应该放在 master 或 standalone。
因为会共用:
- 数据库
- API Key
- 集群状态
- 日志
结果会导致状态混乱。
优先检查:
- 是否执行过
python -m playwright install chromium - Linux 是否缺依赖,必要时使用
python -m playwright install --with-deps chromium - 代理或显示环境是否异常
表示:
- 不手动固定并发值
- 自动跟随
browser_count
不会。
master 只负责调度和转发,真正执行浏览器打码的是 subnode
或者 standalone。
如果你是第一次接触这个项目,最推荐的顺序是:
- 先跑
standalone - 用
/admin创建 API Key - 用
solve -> finish/error跑通一遍 - 再尝试
master + subnode集群
如果你看到子节点首页不是登录页,而是状态页,这不是异常, 而是当前版本的预期行为。