From f8670d0ed33d3f3d1238d4dd5dec9b19d937afa5 Mon Sep 17 00:00:00 2001 From: hangone <56105779+hangone@users.noreply.github.com> Date: Sat, 30 May 2026 23:22:42 +0800 Subject: [PATCH 1/2] feat: Build multi-variant Docker images with browser options --- .github/workflows/build.yml | 55 +++++++++++++++++++++++--------- Dockerfile | 31 +++++++++++++------ README.md | 54 ++++++++++++++++++++++++++++++-- captcha.py | 62 ++++++++++++++++++++++++++++--------- main.py | 16 +++++++++- 5 files changed, 177 insertions(+), 41 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d22b2e..bf3a171 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,15 +63,23 @@ jobs: if-no-files-found: error docker: - name: Docker ${{ matrix.platform }} + name: Docker ${{ matrix.variant }} ${{ matrix.platform }} runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - - platform: amd64 + - variant: with-browser + platform: amd64 runner: ubuntu-latest - - platform: arm64 + - variant: with-browser + platform: arm64 + runner: ubuntu-24.04-arm + - variant: without-browser + platform: amd64 + runner: ubuntu-latest + - variant: without-browser + platform: arm64 runner: ubuntu-24.04-arm steps: - uses: actions/checkout@v6 @@ -87,19 +95,21 @@ jobs: id: build with: context: . + file: Dockerfile + target: ${{ matrix.variant }} platforms: linux/${{ matrix.platform }} outputs: type=image,push-by-digest=true,name=${{ secrets.DOCKERHUB_USERNAME }}/weban,push=true provenance: false - cache-from: type=gha,scope=${{ matrix.platform }} - cache-to: type=gha,scope=${{ matrix.platform }},mode=max + cache-from: type=gha,scope=docker-${{ matrix.platform }} + cache-to: type=gha,scope=docker-${{ matrix.platform }},mode=max - name: Export digest - run: echo "${{ steps.build.outputs.digest }}" > digest-${{ matrix.platform }}.txt + run: echo "${{ steps.build.outputs.digest }}" > digest-${{ matrix.variant }}-${{ matrix.platform }}.txt - uses: actions/upload-artifact@v7 with: - name: digest-${{ matrix.platform }} - path: digest-${{ matrix.platform }}.txt + name: digest-${{ matrix.variant }}-${{ matrix.platform }} + path: digest-${{ matrix.variant }}-${{ matrix.platform }}.txt docker-merge: name: Docker merge @@ -120,27 +130,44 @@ jobs: merge-multiple: true - uses: docker/metadata-action@v6 - id: meta + id: meta-with-browser with: images: ${{ secrets.DOCKERHUB_USERNAME }}/weban tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest + type=raw,value=with-browser + + - uses: docker/metadata-action@v6 + id: meta-without-browser + with: + images: ${{ secrets.DOCKERHUB_USERNAME }}/weban + tags: | + type=semver,pattern={{version}}-without-browser + type=raw,value=without-browser + + - name: Merge with-browser manifests + run: | + img="${{ secrets.DOCKERHUB_USERNAME }}/weban" + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "${{ steps.meta-with-browser.outputs.json }}") \ + "$img@$(cat digests/digest-with-browser-amd64.txt)" \ + "$img@$(cat digests/digest-with-browser-arm64.txt)" - - name: Merge multi-platform manifests + - name: Merge without-browser manifests run: | img="${{ secrets.DOCKERHUB_USERNAME }}/weban" docker buildx imagetools create \ - $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - "$img@$(cat digests/digest-amd64.txt)" \ - "$img@$(cat digests/digest-arm64.txt)" + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "${{ steps.meta-without-browser.outputs.json }}") \ + "$img@$(cat digests/digest-without-browser-amd64.txt)" \ + "$img@$(cat digests/digest-without-browser-arm64.txt)" release: name: Release permissions: contents: write - needs: [build, docker-merge] + needs: [build, docker, docker-merge] if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: diff --git a/Dockerfile b/Dockerfile index c3558a7..8027790 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,14 @@ +# WeBan Docker 镜像 +# 目标: +# with-browser — 内置浏览器,开箱即用 +# without-browser — 通过 CDP 连接宿主机浏览器 + # ── Builder: PyInstaller 打包 ────────────────────────────── -FROM python:3.12-slim-bookworm AS builder +FROM python:3.12-slim AS builder +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv RUN apt-get update && apt-get install -y --no-install-recommends binutils \ && rm -rf /var/lib/apt/lists/* -RUN pip install --no-cache-dir uv WORKDIR /build @@ -25,20 +30,26 @@ RUN --mount=type=cache,target=/root/.cache/uv \ --hidden-import numpy \ --hidden-import cv2 \ --collect-submodules nodriver \ - main.py + main.py \ + && strip /build/dist/WeBan + +# ── with-browser: 内置 Chrome headless shell ─────────────── +FROM chromedp/headless-shell:stable AS with-browser + +COPY --from=builder /build/dist/WeBan /app/WeBan +WORKDIR /app + +ENV CHROMIUM_BINARY=/headless-shell/headless-shell +ENV WEBAN_NO_SANDBOX=1 -# ── Runtime ──────────────────────────────────────────────── -FROM debian:bookworm-slim +ENTRYPOINT ["/app/WeBan"] -RUN apt-get update && apt-get install -y --no-install-recommends --no-install-suggests \ - chromium \ - && rm -rf /var/lib/apt/lists/* /var/cache/apt/* /tmp/* \ - && mkdir -p /app/logs +# ── without-browser: 纯二进制,CDP 连接宿主机浏览器 ──────── +FROM debian:stable-slim AS without-browser COPY --from=builder /build/dist/WeBan /app/WeBan WORKDIR /app -ENV CHROMIUM_BINARY=/usr/bin/chromium ENV WEBAN_NO_SANDBOX=1 ENTRYPOINT ["/app/WeBan"] diff --git a/README.md b/README.md index 3277769..79c1c24 100644 --- a/README.md +++ b/README.md @@ -49,19 +49,69 @@ python main.py # 或 uv run main.py ### Docker -**完整镜像**(内置 Chromium,开箱即用): +提供两种镜像变体: + +| 镜像 | Tag | 说明 | +|------|-----|------| +| 内置浏览器 | `latest` / `with-browser` / `<版本号>` | 开箱即用 | +| 轻量镜像 | `without-browser` / `<版本号>-without-browser` | 通过 CDP 连接宿主机浏览器 | + +#### 完整镜像(内置浏览器) + +```bash +docker run -it --rm \ + -v "$PWD/config.toml":/app/config.toml:ro \ + -v "$PWD/logs":/app/logs \ + hangyi/weban:latest +``` + +#### 轻量镜像(CDP 连接宿主机浏览器) + +容器会自动检测 Docker 环境并尝试连接宿主机的 Chrome,无需手动配置 CDP。 + +**第一步:在宿主机启动 Chrome 远程调试** + +打开 Chrome,地址栏输入 `chrome://inspect/#remote-debugging`,勾选 **Allow remote debugging for this browser instance**。 + +或者直接命令行启动带远程调试的 Chrome: + +```bash +# macOS +"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222 + +# Linux +google-chrome --remote-debugging-port=9222 + +# Windows +"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 +``` + +> 参考:[Chrome DevTools: Debug your browser session](https://developer.chrome.com/blog/chrome-devtools-mcp-debug-your-browser-session) + +**第二步:运行容器** ```bash docker run -it --rm \ -v "$PWD/config.toml":/app/config.toml:ro \ -v "$PWD/logs":/app/logs \ - hangyi/weban + hangyi/weban:without-browser ``` +如需自定义 CDP 地址,可在 `config.toml` 中配置 `cdp_host` 和 `cdp_port`。 + ## 配置说明 首次使用先从 [config.example.toml](config.example.toml) 复制一份 `config.toml` 并填写账号信息。账号级配置可覆盖全局设置。 +### 浏览器检测 + +程序按以下优先级自动检测可用的浏览器,无需手动配置: + +1. **用户指定**:环境变量 `CHROMIUM_BINARY` / 配置文件 `browser_path` +2. **CDP 远程调试**:配置文件 `cdp_host` + `cdp_port`,或 Docker 环境下自动尝试 `host.docker.internal:9222` +3. **Playwright 浏览器**:`pip install playwright && playwright install chromium` +4. **系统浏览器**:自动查找已安装的 Chrome / Chromium + ## 功能特性 - **课程学习**:自动遍历项目 → 分类 → 课程,模拟翻页、答题、等待学习时长后完课 diff --git a/captcha.py b/captcha.py index abccdda..a5d2662 100644 --- a/captcha.py +++ b/captcha.py @@ -652,17 +652,37 @@ def recognize(cls, image: bytes, log) -> Optional[str]: # ── 浏览器自动检测 ────────────────────────────────────── +def _find_playwright_browser() -> Optional[str]: + """检测 Playwright 安装的 Chrome/Chromium 路径。""" + pw_dir = Path.home() / ".cache" / "ms-playwright" + if not pw_dir.is_dir(): + return None + system = platform.system() + patterns = { + "Linux": "chromium-*/chrome-linux/chrome", + "Darwin": "chromium-*/chrome-mac/Chromium.app/Contents/MacOS/Chromium", + "Windows": "chromium-*/chrome-win/chrome.exe", + } + pattern = patterns.get(system) + if pattern: + matches = sorted(pw_dir.glob(pattern), reverse=True) + if matches: + return str(matches[0]) + return None + + def detect_browser() -> Optional[str]: - """自动检测系统中已安装的 Chrome/Chromium 浏览器路径。 + """自动检测本地已安装的 Chrome/Chromium 浏览器路径。 - 检查顺序:环境变量 CHROMIUM_BINARY / CHROME_BINARY → 平台默认路径 + 检查顺序:Playwright → 系统默认路径 + 环境变量和 CDP 由 check_browser_health 处理。 """ - # 环境变量优先(Docker 等场景) - for env_var in ("CHROMIUM_BINARY", "CHROME_BINARY"): - path = os.environ.get(env_var, "") - if path and os.path.isfile(path): - return path + # 1. Playwright 安装的浏览器 + pw = _find_playwright_browser() + if pw: + return pw + # 2. 系统默认路径 system = platform.system() candidates: list[str] = [] if system == "Darwin": @@ -708,21 +728,35 @@ def check_browser_health( cdp_host: Optional[str] = None, cdp_port: Optional[int] = None, ) -> str: - """检测浏览器是否能正常启动。 + """按优先级检测可用的浏览器:用户指定 → CDP → Playwright → 系统。 - :param browser_path: 浏览器可执行文件路径,留空则自动查找 - :param cdp_host: CDP 远程调试地址(CDP 模式跳过本地启动检测) + :param browser_path: 浏览器可执行文件路径(环境变量或配置文件指定) + :param cdp_host: CDP 远程调试地址 :param cdp_port: CDP 远程调试端口 - :return: 浏览器路径或地址 - :raises RuntimeError: 浏览器无法启动时 + :return: 浏览器路径或 CDP 地址 + :raises RuntimeError: 无可用浏览器时 """ + # 1. 用户显式指定的浏览器路径 + for env_var in ("CHROMIUM_BINARY", "CHROME_BINARY"): + env_path = os.environ.get(env_var, "") + if env_path and os.path.isfile(env_path): + return env_path + if browser_path and os.path.isfile(browser_path): + return browser_path + + # 2. CDP 远程调试 if cdp_host and cdp_port: return f"{cdp_host}:{cdp_port}" - resolved = browser_path or detect_browser() + # 3. Playwright / 系统浏览器(detect_browser 内部按 Playwright → 系统排序) + resolved = detect_browser() if not resolved: raise RuntimeError( - "未找到浏览器,请安装 Chrome 或设置 browser_path / CHROMIUM_BINARY 环境变量" + "未找到浏览器。请通过以下方式之一提供:\n" + " 1. 设置环境变量 CHROMIUM_BINARY 或配置 browser_path\n" + " 2. 配置 cdp_host 和 cdp_port 连接远程浏览器\n" + " 3. 安装 Playwright: pip install playwright && playwright install chromium\n" + " 4. 安装 Chrome 或 Chromium" ) try: args = [ diff --git a/main.py b/main.py index fb607fc..19067b5 100644 --- a/main.py +++ b/main.py @@ -350,15 +350,29 @@ def load_all_config(): accounts = valid_accounts logger.info(f"共加载到 {len(accounts)} 个账号") - # 检测浏览器是否可用 + # 检测浏览器是否可用(优先级:用户指定 → CDP → Playwright → 系统) browser_path = global_settings.get("browser_path", "") or None cdp_host = global_settings.get("cdp_host", "") or None cdp_port = int(global_settings.get("cdp_port", 0)) or None + + # Docker 环境下未配置浏览器和 CDP 时,自动尝试默认 CDP 地址 + in_docker = os.path.exists("/.dockerenv") or os.path.isfile("/run/.containerenv") + if not browser_path and not cdp_host and not cdp_port and in_docker: + cdp_host, cdp_port = "host.docker.internal", 9222 + logger.info("检测到 Docker 环境,自动尝试 CDP 连接 host.docker.internal:9222") + try: resolved = check_browser_health(browser_path, cdp_host, cdp_port) logger.info(f"浏览器检测通过: {resolved}") except RuntimeError as e: logger.error(f"浏览器检测失败: {e}") + if in_docker: + logger.warning( + "请在宿主机 Chrome 中开启远程调试,然后重启容器:\n" + " 1. 地址栏输入 chrome://inspect/#remote-debugging\n" + " 2. 勾选 Allow remote debugging for this browser instance\n" + " 3. 或在 config.toml 中配置 cdp_host 和 cdp_port" + ) sys.exit(1) # 是否多线程 From 0399dd5271c1f25298002cc5edc4f903748e61ce Mon Sep 17 00:00:00 2001 From: hangone <56105779+hangone@users.noreply.github.com> Date: Sat, 30 May 2026 23:23:27 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=E9=A2=98?= =?UTF-8?q?=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- answer/answer.json | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/answer/answer.json b/answer/answer.json index 45f2c17..1f94fe9 100644 --- a/answer/answer.json +++ b/answer/answer.json @@ -625,6 +625,19 @@ ], "type": 1 }, + "In China, how many types of fire are there?": { + "optionList": [ + { + "content": "Six.", + "isCorrect": 1 + }, + { + "content": "Four.", + "isCorrect": 2 + } + ], + "type": 1 + }, "In addition to the explosion, how often does it take to control the fire after a fire?": { "optionList": [ { @@ -3672,6 +3685,19 @@ ], "type": 1 }, + "一个心地善良的人会以为别人都是善良的;一个经常算计别人的人就会觉得别人也在算计他,这就是心理学上说的:": { + "optionList": [ + { + "content": "投射效应", + "isCorrect": 1 + }, + { + "content": "羊群效应", + "isCorrect": 2 + } + ], + "type": 1 + }, "一个有同理心的人,一般具有哪些品质?": { "optionList": [ { @@ -32028,6 +32054,10 @@ { "content": "积极心态。", "isCorrect": 1 + }, + { + "content": "消极心态。", + "isCorrect": 2 } ], "type": 1 @@ -33589,6 +33619,27 @@ ], "type": 1 }, + "常被骗的人,可能存在的共同特征之一是?": { + "optionList": [ + { + "content": "尊重他人", + "isCorrect": 2 + }, + { + "content": "容易被小便宜吸引", + "isCorrect": 1 + }, + { + "content": "不轻易转账", + "isCorrect": 2 + }, + { + "content": "警惕性强", + "isCorrect": 2 + } + ], + "type": 1 + }, "常见的地震前兆不包括哪个?": { "optionList": [ { @@ -37465,6 +37516,10 @@ { "content": "立即接听", "isCorrect": 1 + }, + { + "content": "骗子太多,赶快挂掉", + "isCorrect": 2 } ], "type": 1 @@ -37943,6 +37998,10 @@ { "content": "对", "isCorrect": 1 + }, + { + "content": "错", + "isCorrect": 2 } ], "type": 1 @@ -39170,6 +39229,18 @@ { "content": "努力带着溺水者游向安全地带。", "isCorrect": 2 + }, + { + "content": "放手自沉,使溺水者手松开,再进行救护", + "isCorrect": 1 + }, + { + "content": "努力带着溺水者游向安全地带", + "isCorrect": 2 + }, + { + "content": "强行掰开对方手臂拉扯", + "isCorrect": 2 } ], "type": 1 @@ -48220,6 +48291,19 @@ ], "type": 1 }, + "破窗理论是谁提出的?": { + "optionList": [ + { + "content": "詹姆士·威尔逊和乔治·凯林", + "isCorrect": 1 + }, + { + "content": "詹姆士·威尔逊", + "isCorrect": 2 + } + ], + "type": 1 + }, "确定是流感或疑似流感并有并发症时,最好在多长时间内进行治疗?": { "optionList": [ {