Skip to content

Commit 6d4cb8b

Browse files
committed
feat: 支持token级打码代理并完善有头Docker双镜像发布
1 parent cc6036a commit 6d4cb8b

File tree

15 files changed

+373
-70
lines changed

15 files changed

+373
-70
lines changed

.github/workflows/docker-publish.yml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ env:
1818
jobs:
1919
build-and-push:
2020
runs-on: ubuntu-latest
21+
strategy:
22+
fail-fast: false
23+
matrix:
24+
include:
25+
- variant: standard
26+
dockerfile: Dockerfile
27+
image_suffix: ""
28+
cache_scope: standard
29+
- variant: headed
30+
dockerfile: Dockerfile.headed
31+
image_suffix: -headed
32+
cache_scope: headed
2133
permissions:
2234
contents: read
2335
packages: write
@@ -44,21 +56,22 @@ jobs:
4456
id: meta
4557
uses: docker/metadata-action@v5
4658
with:
47-
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
59+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.image_suffix }}
4860
tags: |
4961
type=ref,event=branch
5062
type=ref,event=pr
5163
type=semver,pattern={{version}}
5264
type=semver,pattern={{major}}.{{minor}}
5365
type=raw,value=latest,enable={{is_default_branch}}
5466
55-
- name: Build and push Docker image
67+
- name: Build and push Docker image (${{ matrix.variant }})
5668
uses: docker/build-push-action@v5
5769
with:
5870
context: .
71+
file: ${{ matrix.dockerfile }}
5972
platforms: linux/amd64,linux/arm64
6073
push: ${{ github.event_name != 'pull_request' }}
6174
tags: ${{ steps.meta.outputs.tags }}
6275
labels: ${{ steps.meta.outputs.labels }}
63-
cache-from: type=gha
64-
cache-to: type=gha,mode=max
76+
cache-from: type=gha,scope=${{ matrix.cache_scope }}
77+
cache-to: type=gha,mode=max,scope=${{ matrix.cache_scope }}

Dockerfile.headed

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
FROM python:3.11-slim
2+
3+
WORKDIR /app
4+
5+
ENV PYTHONDONTWRITEBYTECODE=1 \
6+
PYTHONUNBUFFERED=1 \
7+
PLAYWRIGHT_BROWSERS_PATH=0 \
8+
ALLOW_DOCKER_HEADED_CAPTCHA=true \
9+
DISPLAY=:99 \
10+
XVFB_WHD=1920x1080x24
11+
12+
COPY requirements.txt ./
13+
14+
# 有头模式基础依赖:虚拟显示、窗口管理器。
15+
RUN apt-get update \
16+
&& apt-get install -y --no-install-recommends \
17+
ca-certificates \
18+
curl \
19+
xvfb \
20+
fluxbox \
21+
&& rm -rf /var/lib/apt/lists/*
22+
23+
RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt \
24+
&& python -m playwright install --with-deps chromium
25+
26+
COPY . .
27+
COPY docker/entrypoint.headed.sh /usr/local/bin/entrypoint.headed.sh
28+
RUN chmod +x /usr/local/bin/entrypoint.headed.sh
29+
30+
EXPOSE 8000
31+
32+
CMD ["/usr/local/bin/entrypoint.headed.sh"]

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232

3333
- 由于Flow增加了额外的验证码,你可以自行选择使用浏览器打码或第三发打码:
3434
注册[YesCaptcha](https://yescaptcha.com/i/13Xd8K)并获取api key,将其填入系统配置页面```YesCaptcha API密钥```区域
35+
- 默认 `docker-compose.yml` 建议搭配第三方打码(yescaptcha/capmonster/ezcaptcha/capsolver)。
36+
如需 Docker 内有头打码(browser/personal),请使用下方 `docker-compose.headed.yml`
3537

3638
- 自动更新st浏览器拓展:[Flow2API-Token-Updater](https://github.com/TheSmallHanCat/Flow2API-Token-Updater)
3739

@@ -61,6 +63,23 @@ docker-compose -f docker-compose.warp.yml up -d
6163
docker-compose -f docker-compose.warp.yml logs -f
6264
```
6365

66+
#### Docker 有头打码模式(browser / personal)
67+
68+
> 适用于你有虚拟化桌面需求、希望在容器里启用有头浏览器打码的场景。
69+
> 该模式默认启动 `Xvfb + Fluxbox` 实现容器内部可视化,并设置 `ALLOW_DOCKER_HEADED_CAPTCHA=true`
70+
> 仅开放应用端口,不提供任何远程桌面连接端口。
71+
72+
```bash
73+
# 启动有头模式(首次建议带 --build)
74+
docker compose -f docker-compose.headed.yml up -d --build
75+
76+
# 查看日志
77+
docker compose -f docker-compose.headed.yml logs -f
78+
```
79+
80+
- API 端口:`8000`
81+
- 进入管理后台后,将验证码方式设为 `browser``personal`
82+
6483
### 方式二:本地部署
6584

6685
```bash

docker-compose.headed.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
version: '3.8'
2+
3+
services:
4+
flow2api-headed:
5+
build:
6+
context: .
7+
dockerfile: Dockerfile.headed
8+
image: flow2api:headed
9+
container_name: flow2api-headed
10+
ports:
11+
- "8000:8000"
12+
volumes:
13+
- ./data:/app/data
14+
- ./config/setting.toml:/app/config/setting.toml
15+
environment:
16+
- PYTHONUNBUFFERED=1
17+
- ALLOW_DOCKER_HEADED_CAPTCHA=true
18+
- DISPLAY=:99
19+
- XVFB_WHD=1920x1080x24
20+
shm_size: "2gb"
21+
restart: unless-stopped

docker/entrypoint.headed.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
export DISPLAY="${DISPLAY:-:99}"
5+
export ALLOW_DOCKER_HEADED_CAPTCHA="${ALLOW_DOCKER_HEADED_CAPTCHA:-true}"
6+
export XVFB_WHD="${XVFB_WHD:-1920x1080x24}"
7+
8+
echo "[entrypoint] starting Xvfb on ${DISPLAY} (${XVFB_WHD})"
9+
Xvfb "${DISPLAY}" -screen 0 "${XVFB_WHD}" -ac -nolisten tcp +extension RANDR >/tmp/xvfb.log 2>&1 &
10+
11+
sleep 1
12+
13+
echo "[entrypoint] starting Fluxbox"
14+
fluxbox >/tmp/fluxbox.log 2>&1 &
15+
16+
if [ -z "${BROWSER_EXECUTABLE_PATH:-}" ]; then
17+
BROWSER_EXECUTABLE_PATH="$(python - <<'PY'
18+
from playwright.sync_api import sync_playwright
19+
20+
try:
21+
with sync_playwright() as p:
22+
print(p.chromium.executable_path)
23+
except Exception:
24+
print("")
25+
PY
26+
)"
27+
if [ -n "${BROWSER_EXECUTABLE_PATH}" ]; then
28+
export BROWSER_EXECUTABLE_PATH
29+
echo "[entrypoint] browser executable: ${BROWSER_EXECUTABLE_PATH}"
30+
fi
31+
fi
32+
33+
exec python main.py

src/api/admin.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ class AddTokenRequest(BaseModel):
229229
project_id: Optional[str] = None # 用户可选输入project_id
230230
project_name: Optional[str] = None
231231
remark: Optional[str] = None
232+
captcha_proxy_url: Optional[str] = None
232233
image_enabled: bool = True
233234
video_enabled: bool = True
234235
image_concurrency: int = -1
@@ -240,6 +241,7 @@ class UpdateTokenRequest(BaseModel):
240241
project_id: Optional[str] = None # 用户可选输入project_id
241242
project_name: Optional[str] = None
242243
remark: Optional[str] = None
244+
captcha_proxy_url: Optional[str] = None
243245
image_enabled: Optional[bool] = None
244246
video_enabled: Optional[bool] = None
245247
image_concurrency: Optional[int] = None
@@ -301,6 +303,7 @@ class ImportTokenItem(BaseModel):
301303
access_token: Optional[str] = None
302304
session_token: Optional[str] = None
303305
is_active: bool = True
306+
captcha_proxy_url: Optional[str] = None
304307
image_enabled: bool = True
305308
video_enabled: bool = True
306309
image_concurrency: int = -1
@@ -411,6 +414,7 @@ async def get_tokens(token: str = Depends(verify_admin_token)):
411414
"user_paygate_tier": row.get("user_paygate_tier"),
412415
"current_project_id": row.get("current_project_id"), # 🆕 项目ID
413416
"current_project_name": row.get("current_project_name"), # 🆕 项目名称
417+
"captcha_proxy_url": row.get("captcha_proxy_url") or "",
414418
"image_enabled": bool(row.get("image_enabled")),
415419
"video_enabled": bool(row.get("video_enabled")),
416420
"image_concurrency": row.get("image_concurrency"),
@@ -433,6 +437,7 @@ async def add_token(
433437
project_id=request.project_id, # 🆕 支持用户指定project_id
434438
project_name=request.project_name,
435439
remark=request.remark,
440+
captcha_proxy_url=request.captcha_proxy_url.strip() if request.captcha_proxy_url is not None else None,
436441
image_enabled=request.image_enabled,
437442
video_enabled=request.video_enabled,
438443
image_concurrency=request.image_concurrency,
@@ -495,6 +500,7 @@ async def update_token(
495500
project_id=request.project_id,
496501
project_name=request.project_name,
497502
remark=request.remark,
503+
captcha_proxy_url=request.captcha_proxy_url.strip() if request.captcha_proxy_url is not None else None,
498504
image_enabled=request.image_enabled,
499505
video_enabled=request.video_enabled,
500506
image_concurrency=request.image_concurrency,
@@ -695,6 +701,7 @@ async def import_tokens(
695701
st=st,
696702
at=at,
697703
at_expires=at_expires,
704+
captcha_proxy_url=item.captcha_proxy_url.strip() if item.captcha_proxy_url is not None else None,
698705
image_enabled=item.image_enabled,
699706
video_enabled=item.video_enabled,
700707
image_concurrency=item.image_concurrency,
@@ -707,6 +714,7 @@ async def import_tokens(
707714
existing.st = st
708715
existing.at = at
709716
existing.at_expires = at_expires
717+
existing.captcha_proxy_url = item.captcha_proxy_url
710718
existing.image_enabled = item.image_enabled
711719
existing.video_enabled = item.video_enabled
712720
existing.image_concurrency = item.image_concurrency
@@ -716,6 +724,7 @@ async def import_tokens(
716724
# 添加新Token
717725
new_token = await token_manager.add_token(
718726
st=st,
727+
captcha_proxy_url=item.captcha_proxy_url.strip() if item.captcha_proxy_url is not None else None,
719728
image_enabled=item.image_enabled,
720729
video_enabled=item.video_enabled,
721730
image_concurrency=item.image_concurrency,

src/core/database.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ async def check_and_migrate_db(self, config_dict: dict = None):
283283
("video_enabled", "BOOLEAN DEFAULT 1"),
284284
("image_concurrency", "INTEGER DEFAULT -1"),
285285
("video_concurrency", "INTEGER DEFAULT -1"),
286+
("captcha_proxy_url", "TEXT"), # token级打码代理
286287
("ban_reason", "TEXT"), # 禁用原因
287288
("banned_at", "TIMESTAMP"), # 禁用时间
288289
]
@@ -406,6 +407,7 @@ async def init_db(self):
406407
video_enabled BOOLEAN DEFAULT 1,
407408
image_concurrency INTEGER DEFAULT -1,
408409
video_concurrency INTEGER DEFAULT -1,
410+
captcha_proxy_url TEXT,
409411
ban_reason TEXT,
410412
banned_at TIMESTAMP
411413
)
@@ -653,13 +655,13 @@ async def add_token(self, token: Token) -> int:
653655
cursor = await db.execute("""
654656
INSERT INTO tokens (st, at, at_expires, email, name, remark, is_active,
655657
credits, user_paygate_tier, current_project_id, current_project_name,
656-
image_enabled, video_enabled, image_concurrency, video_concurrency)
657-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
658+
image_enabled, video_enabled, image_concurrency, video_concurrency, captcha_proxy_url)
659+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
658660
""", (token.st, token.at, token.at_expires, token.email, token.name, token.remark,
659661
token.is_active, token.credits, token.user_paygate_tier,
660662
token.current_project_id, token.current_project_name,
661663
token.image_enabled, token.video_enabled,
662-
token.image_concurrency, token.video_concurrency))
664+
token.image_concurrency, token.video_concurrency, token.captcha_proxy_url))
663665
await db.commit()
664666
token_id = cursor.lastrowid
665667

src/core/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ class Token(BaseModel):
3838
image_concurrency: int = -1 # -1表示无限制
3939
video_concurrency: int = -1 # -1表示无限制
4040

41+
# 打码代理(token 级,可覆盖全局浏览器打码代理)
42+
captcha_proxy_url: Optional[str] = None
43+
4144
# 429禁用相关
4245
ban_reason: Optional[str] = None # 禁用原因: "429_rate_limit" 或 None
4346
banned_at: Optional[datetime] = None # 禁用时间

src/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ async def lifespan(app: FastAPI):
105105
elif captcha_config.captcha_method == "browser":
106106
from .services.browser_captcha import BrowserCaptchaService
107107
browser_service = await BrowserCaptchaService.get_instance(db)
108-
print("✓ Browser captcha service initialized (headless mode)")
108+
print("✓ Browser captcha service initialized (headed mode)")
109109

110110
# Initialize concurrency manager
111111
tokens = await token_manager.get_all_tokens()

0 commit comments

Comments
 (0)