From c580e0e8c0caefce503a68574e35a8c1b6c72059 Mon Sep 17 00:00:00 2001 From: visail Date: Fri, 3 Jul 2026 10:40:11 +0800 Subject: [PATCH 1/9] feat(proxy): add --host / CSSWITCH_BIND_HOST for containerized deployments --- proxy/csswitch_proxy.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/proxy/csswitch_proxy.py b/proxy/csswitch_proxy.py index 60780dd..ebfada9 100644 --- a/proxy/csswitch_proxy.py +++ b/proxy/csswitch_proxy.py @@ -640,6 +640,8 @@ def _replay_as_sse(self, aresp): ap = argparse.ArgumentParser() ap.add_argument("--provider", default=os.environ.get("CSSWITCH_PROVIDER", "deepseek"), choices=list(PROVIDERS.keys())) + ap.add_argument("--host", default=os.environ.get("CSSWITCH_BIND_HOST", "127.0.0.1"), + help="绑定地址(默认 127.0.0.1;容器内可设 0.0.0.0)") ap.add_argument("--port", type=int, default=18991) ap.add_argument("--env-file", default=None) ap.add_argument("--log", default=None) @@ -648,6 +650,7 @@ def _replay_as_sse(self, aresp): PROV_NAME = args.provider PROV = PROVIDERS[PROV_NAME] LOG = args.log + HOST = args.host KEY = load_key(PROV, args) AUTH_SECRET = os.environ.get("CSSWITCH_AUTH_TOKEN") or args.auth_token _up = os.environ.get("CSSWITCH_UPSTREAM_URL") @@ -657,14 +660,14 @@ def _replay_as_sse(self, aresp): if not KEY: print(f"找不到 {PROV['key_env']}。用环境变量或 --env-file <路径> 提供。", file=sys.stderr) sys.exit(1) - log(f"CSSwitch 代理启动 127.0.0.1:{args.port} provider={PROV_NAME} " + log(f"CSSwitch 代理启动 {HOST}:{args.port} provider={PROV_NAME} " f"key=已加载(未显示) 上游={PROV['url']}") # 绑定重试:上次会话遗留的孤儿代理可能还占着端口(app 侧会主动清,但退干净需一点时间)。 # 重试 ~3s 等端口释放,避免一次绑不上就直接失败(Errno 48)。 srv = None for attempt in range(10): try: - srv = ThreadingHTTPServer(("127.0.0.1", args.port), H) + srv = ThreadingHTTPServer((HOST, args.port), H) break except OSError as e: if attempt == 9: From aca03de4bd0f1a8646a458c237003bddac01416e Mon Sep 17 00:00:00 2001 From: visail Date: Fri, 3 Jul 2026 10:40:42 +0800 Subject: [PATCH 2/9] feat(docker): add Dockerfile and build artifacts for containerized proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 基于 python:3.11-slim,非 root 运行(csswitch 用户),零 pip 依赖。 .dockerignore 排除 .git、.sandbox、findings、desktop 等无关文件。 .env.example 提供完整的环境变量模板与注释。 Co-Authored-By: Claude --- docker/.dockerignore | 35 +++++++++++++++++++++++++++++++++++ docker/.env.example | 29 +++++++++++++++++++++++++++++ docker/Dockerfile | 22 ++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 docker/.dockerignore create mode 100644 docker/.env.example create mode 100644 docker/Dockerfile diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000..8dd407e --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,35 @@ +# 版本管理 +.git +.gitignore +.gitattributes +.gitmodules +.gitleaks.toml + +# GitHub +.github + +# 运行时 +.sandbox +.sandbox/ + +# 项目特有 +findings/ +findings/auto-maint/ +desktop/ +docs/assets/ +node_modules/ + +# Python +__pycache__/ +*.pyc +*.pyo +*.egg-info/ + +# 日志 +*.log +logs/ + +# 本地 env(按 .env.example 模板手写,不提交) +.env +.env.local +.env.production diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..1a4b715 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,29 @@ +# CSSwitch 代理容器环境变量配置 +# 复制本文件为 .env,填入你的 key 后启动: +# cp .env.example .env +# docker compose -f docker/docker-compose.yml up -d + +# ----- 必须设置一个(视 provider 而定)----- + +# DeepSeek API Key(选择 deepseek provider 时必填) +# 获取:https://platform.deepseek.com/api_keys +DEEPSEEK_API_KEY= + +# 阿里通义千问(DashScope)API Key(选择 qwen provider 时必填) +# 获取:https://bailian.console.aliyun.com/ +DASHSCOPE_API_KEY= + +# ----- 可选配置(按需修改)----- + +# Provider:deepseek(默认)或 qwen +CSSWITCH_PROVIDER=deepseek + +# 代理监听端口(需与 docker-compose.yml ports 映射一致) +CSSWITCH_PROXY_PORT=18991 + +# 路径 secret 鉴权(建议设置随机字符串,防未经授权的访问) +# 生成:openssl rand -hex 16 +CSSWITCH_AUTH_TOKEN= + +# 上游 URL 覆盖(仅高级用法,一般无需设置) +# CSSWITCH_UPSTREAM_URL=https://api.deepseek.com/anthropic/v1/messages diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..c1cc34a --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,22 @@ +# CSSwitch 翻译代理容器镜像 +# - 纯 Python 标准库,零 pip 依赖 +# - 非 root 运行,最小攻击面 +# - 默认监听 0.0.0.0:18991(`--host` 与 `--port` 可通过 CMD 覆盖) +FROM python:3.11-slim + +# 创建非 root 用户 +RUN addgroup --system --gid 1001 csswitch \ + && adduser --system --uid 1001 --ingroup csswitch --no-create-home csswitch + +WORKDIR /app + +# 只拷代理脚本及支持文件(纯 stdlib,无 requirements.txt) +COPY proxy/csswitch_proxy.py /app/csswitch_proxy.py + +# 容器内默认绑定 0.0.0.0 监听所有接口 +ENV PYTHONUNBUFFERED=1 +EXPOSE 18991 + +USER csswitch + +ENTRYPOINT ["python3", "/app/csswitch_proxy.py", "--host", "0.0.0.0"] From d5f67b7d9c8e3b9da05e3a6892e759c4a747c3d7 Mon Sep 17 00:00:00 2001 From: visail Date: Fri, 3 Jul 2026 10:41:16 +0800 Subject: [PATCH 3/9] feat(docker): add docker-compose.yml with healthcheck and optional Caddy TLS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compose 编排:csswitch-proxy 服务(env_file、端口映射、健康检查、 自动重启、日志卷)。提供可选 Caddy TLS 反代(--profile tls), 在 WSL2 内部用自签名证书保护 path secret 的传输安全。 Co-Authored-By: Claude --- docker/Caddyfile | 22 ++++++++++++ docker/docker-compose.yml | 71 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 docker/Caddyfile create mode 100644 docker/docker-compose.yml diff --git a/docker/Caddyfile b/docker/Caddyfile new file mode 100644 index 0000000..7ddb537 --- /dev/null +++ b/docker/Caddyfile @@ -0,0 +1,22 @@ +# CSSwitch Caddy TLS 反代配置 +# 用于 docker-compose.yml 的 caddy 服务(需启用 tls profile) +# 提供本地 HTTPS 保护 path secret 不在 WSL2 内部明文传输。 +# +# 使用自签名证书(tls internal),适合本地/内网环境。 +# 生产环境请替换 tls internal 为你的真实证书。 + +:8443 { + # 自签名 TLS(本地/内网适用) + tls internal + + # 反代到 csswitch-proxy 容器 + reverse_proxy csswitch-proxy:{$CSSWITCH_PROXY_PORT:-18991} { + # 透传原始 Host 与 path(含 path secret) + header_up Host {host} + } + + # 访问日志 + log { + output file /data/access.log + } +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..a3bd7ea --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,71 @@ +# CSSwitch 容器化代理 — Docker Compose 编排 +# +# 快速启动: +# cp docker/.env.example docker/.env +# # 编辑 docker/.env,填入你的 API key +# docker compose -f docker/docker-compose.yml up -d +# +# 可选 Caddy TLS 反代(保护 path secret 不在明文 HTTP 上传输): +# 取消下方 caddy 服务的注释,并: +# docker compose -f docker/docker-compose.yml --profile tls up -d +# +# 健康检查: +# curl http://localhost:18991//health + +name: csswitch + +services: + csswitch-proxy: + image: csswitch-proxy:latest + build: + context: .. + dockerfile: docker/Dockerfile + container_name: csswitch-proxy + env_file: + - .env + ports: + - "${CSSWITCH_PROXY_PORT:-18991}:${CSSWITCH_PROXY_PORT:-18991}" + healthcheck: + test: ["CMD", "python3", "-c", + "import urllib.request, os, sys; \ + t=os.environ.get('CSSWITCH_AUTH_TOKEN',''); \ + p=os.environ.get('CSSWITCH_PROXY_PORT','18991'); \ + u=f'http://127.0.0.1:{p}/{\"\" if not t else t}/health'; \ + sys.exit(0 if 200==urllib.request.urlopen(u,timeout=3).getcode() else 1)"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + volumes: + - csswitch-logs:/app/logs + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +# ── 可选:Caddy 本地 HTTPS/TLS 反代 ────────────────────────── +# 启用方式:docker compose -f docker/docker-compose.yml --profile tls up -d +# +# 注意:Caddy 使用自签名证书(tls internal),适合本地/内网环境。 +# 浏览器首次访问需确认安全例外;curl 加 -k 或 --insecure。 +# +# caddy: +# profiles: ["tls"] +# image: caddy:2-alpine +# container_name: csswitch-caddy +# restart: unless-stopped +# ports: +# - "8443:8443" +# volumes: +# - ./Caddyfile:/etc/caddy/Caddyfile:ro +# - caddy-data:/data +# - caddy-config:/config +# depends_on: +# - csswitch-proxy + +volumes: + csswitch-logs: + caddy-data: + caddy-config: From d69932036502dd341fc81aae22597db62e246fc1 Mon Sep 17 00:00:00 2001 From: visail Date: Fri, 3 Jul 2026 10:47:06 +0800 Subject: [PATCH 4/9] feat: add container deploy scripts --- scripts/docker-build.sh | 10 +++++++ scripts/wsl2-deploy.sh | 65 +++++++++++++++++++++++++++++++++++++++++ scripts/wsl2-logs.sh | 7 +++++ scripts/wsl2-stop.sh | 9 ++++++ 4 files changed, 91 insertions(+) create mode 100644 scripts/docker-build.sh create mode 100644 scripts/wsl2-deploy.sh create mode 100644 scripts/wsl2-logs.sh create mode 100644 scripts/wsl2-stop.sh diff --git a/scripts/docker-build.sh b/scripts/docker-build.sh new file mode 100644 index 0000000..f0cf8c2 --- /dev/null +++ b/scripts/docker-build.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# 构建 CSSwitch 代理容器镜像。 +set -euo pipefail + +cd "$(dirname "$0")/.." + +echo "构建 CSSwitch 代理容器镜像…" +docker build -t csswitch-proxy:latest -f docker/Dockerfile . +echo "构建完成:csswitch-proxy:latest" +docker images csswitch-proxy:latest diff --git a/scripts/wsl2-deploy.sh b/scripts/wsl2-deploy.sh new file mode 100644 index 0000000..8dbf385 --- /dev/null +++ b/scripts/wsl2-deploy.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# 在 WSL2 / Linux 上启动 CSSwitch 代理容器。 +# 检查依赖与 .env,调用 docker compose up -d,输出连接地址。 +set -euo pipefail + +cd "$(dirname "$0")/.." + +# ── 依赖检查 ── +if ! command -v docker >/dev/null 2>&1; then + echo "✗ 未找到 docker。请先安装 Docker Engine 或 Docker Desktop。" + echo " 参考:https://docs.docker.com/engine/install/" + exit 1 +fi + +if ! docker compose version >/dev/null 2>&1; then + echo "✗ 未找到 docker compose 插件。请安装 Docker Compose v2 插件。" + exit 1 +fi + +# ── .env 检查 ── +ENV_FILE="docker/.env" +if [ ! -f "$ENV_FILE" ]; then + echo "✗ 未找到 $ENV_FILE" + echo " 从模板复制:cp docker/.env.example docker/.env" + echo " 然后编辑 docker/.env,填入你的 API key。" + exit 1 +fi + +# 提示检查 key(只检查变量名是否存在,不读值) +set +u +source <(grep -E '^(DEEPSEEK_API_KEY|DASHSCOPE_API_KEY)=' "$ENV_FILE" 2>/dev/null) +PROVIDER="${CSSWITCH_PROVIDER:-deepseek}" +if [ "$PROVIDER" = "deepseek" ] && [ -z "${DEEPSEEK_API_KEY:-}" ]; then + echo "⚠ CSSWITCH_PROVIDER=deepseek,但 DEEPSEEK_API_KEY 在 .env 中未设置" + echo " 继续启动,但代理会因缺 key 退出。" +elif [ "$PROVIDER" = "qwen" ] && [ -z "${DASHSCOPE_API_KEY:-}" ]; then + echo "⚠ CSSWITCH_PROVIDER=qwen,但 DASHSCOPE_API_KEY 在 .env 中未设置" + echo " 继续启动,但代理会因缺 key 退出。" +fi +set -u + +# ── 启动 ── +echo "启动 CSSwitch 代理容器…" +docker compose -f docker/docker-compose.yml up -d +echo + +# ── 输出连接地址 ── +PORT="${CSSWITCH_PROXY_PORT:-18991}" +SECRET="${CSSWITCH_AUTH_TOKEN:-}" + +# 探测 WSL2 IP(WSL2 内运行) +WSL2_IP="" +if [ -f /proc/version ] && grep -qi microsoft /proc/version; then + WSL2_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" +fi + +echo "=== 代理已启动 ===" +echo " 容器内健康检查:curl http://127.0.0.1:$PORT/${SECRET:+$SECRET/}health" +if [ -n "$WSL2_IP" ]; then + echo " WSL2 IP:$WSL2_IP" + echo " Windows 客户端 ANTHROPIC_BASE_URL:http://$WSL2_IP:$PORT/${SECRET:+$SECRET/}" +fi +echo "" +echo "如需查看日志:bash scripts/wsl2-logs.sh" +echo "如需停止服务:bash scripts/wsl2-stop.sh" diff --git a/scripts/wsl2-logs.sh b/scripts/wsl2-logs.sh new file mode 100644 index 0000000..bce7540 --- /dev/null +++ b/scripts/wsl2-logs.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# 查看 CSSwitch 代理容器日志。 +set -euo pipefail + +cd "$(dirname "$0")/.." + +docker compose -f docker/docker-compose.yml logs -f csswitch-proxy diff --git a/scripts/wsl2-stop.sh b/scripts/wsl2-stop.sh new file mode 100644 index 0000000..d2c64b4 --- /dev/null +++ b/scripts/wsl2-stop.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# 停止 CSSwitch 代理容器。 +set -euo pipefail + +cd "$(dirname "$0")/.." + +echo "停止 CSSwitch 代理容器…" +docker compose -f docker/docker-compose.yml down +echo "已停止。" From 9cf893928238b99ffe9e92034065190129f7db17 Mon Sep 17 00:00:00 2001 From: visail Date: Fri, 3 Jul 2026 10:47:35 +0800 Subject: [PATCH 5/9] feat: add Windows PowerShell deploy scripts --- scripts/wsl2-deploy.ps1 | 107 ++++++++++++++++++++++++++++++++++++++++ scripts/wsl2-logs.ps1 | 52 +++++++++++++++++++ scripts/wsl2-stop.ps1 | 55 +++++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 scripts/wsl2-deploy.ps1 create mode 100644 scripts/wsl2-logs.ps1 create mode 100644 scripts/wsl2-stop.ps1 diff --git a/scripts/wsl2-deploy.ps1 b/scripts/wsl2-deploy.ps1 new file mode 100644 index 0000000..6b5d910 --- /dev/null +++ b/scripts/wsl2-deploy.ps1 @@ -0,0 +1,107 @@ +<# +.SYNOPSIS + 在 WSL2 中部署 CSSwitch 代理容器。 +.DESCRIPTION + 检测 WSL2 环境,在 WSL2 中调用 docker compose up -d, + 自动探测 WSL2 IP 并输出供 Windows 客户端使用的 ANTHROPIC_BASE_URL。 +.PARAMETER WslDistro + WSL2 发行版名称(默认:从 wsl -l -v 自动选择运行中的发行版)。 +.PARAMETER ProjectPath + 项目在 WSL2 中的绝对路径(默认:自动映射当前目录)。 +#> + +param( + [string]$WslDistro = "", + [string]$ProjectPath = "" +) + +$ErrorActionPreference = "Stop" + +# ── 检测 WSL ── +$wslAvailable = Get-Command "wsl.exe" -ErrorAction SilentlyContinue +if (-not $wslAvailable) { + Write-Host "✗ 未找到 wsl.exe。请先安装 WSL2。" -ForegroundColor Red + exit 1 +} + +# 列出运行中的 WSL2 发行版 +$distros = wsl -l -v --quiet 2>$null | Where-Object { $_ -match 'Running' } +if (-not $distros) { + Write-Host "✗ 未找到运行中的 WSL2 发行版。" -ForegroundColor Red + Write-Host " 请先启动一个 WSL2 发行版(例如:wsl -d Ubuntu)" -ForegroundColor Yellow + exit 1 +} + +if (-not $WslDistro) { + # 取第一个运行中的发行版 + $WslDistro = ($distros[0] -split '\s+')[0] + Write-Host " 自动选择 WSL2 发行版:$WslDistro" -ForegroundColor Cyan +} + +# ── 映射项目路径 ── +if (-not $ProjectPath) { + $hostPath = Get-Location + # 尝试将 Windows 路径(如 G:\WorkSpace\CSswitch)映射到 WSL2 挂载点 + if ($hostPath.Path -match '^([A-Z]):\\(.*)') { + $driveLetter = $matches[1].ToLower() + $restPath = $matches[2] -replace '\\', '/' + $ProjectPath = "/mnt/$driveLetter/$restPath" + } else { + Write-Host "✗ 无法自动映射路径:$hostPath" -ForegroundColor Red + Write-Host " 请用 -ProjectPath 参数指定 WSL2 内的项目路径" -ForegroundColor Yellow + exit 1 + } +} + +# ── 检查 .env ── +$checkResult = wsl -d $WslDistro --cd $ProjectPath bash -c 'test -f docker/.env && echo "EXISTS" || echo "MISSING"' +if ($checkResult -eq "MISSING") { + Write-Host "✗ 未找到 docker/.env" -ForegroundColor Red + Write-Host " 在 WSL2 中执行:cp docker/.env.example docker/.env" -ForegroundColor Yellow + Write-Host " 然后编辑 docker/.env,填入你的 API key。" -ForegroundColor Yellow + Write-Host "" + Write-Host " 快速编辑(在 WSL2 中):wsl -d $WslDistro --cd $ProjectPath vi docker/.env" -ForegroundColor Cyan + exit 1 +} + +# ── 部署 ── +Write-Host "部署 CSSwitch 代理容器到 WSL2($WslDistro)…" -ForegroundColor Green +Write-Host " 项目路径:$ProjectPath" -ForegroundColor Cyan + +$deployResult = wsl -d $WslDistro --cd $ProjectPath bash -c 'docker compose -f docker/docker-compose.yml up -d 2>&1' +if ($LASTEXITCODE -ne 0) { + Write-Host "✗ 部署失败:" -ForegroundColor Red + Write-Host $deployResult -ForegroundColor Red + exit 1 +} +Write-Host $deployResult +Write-Host "✓ 容器已启动" -ForegroundColor Green + +# ── 获取 WSL2 IP ── +$wsl2Ip = wsl -d $WslDistro --cd $ProjectPath bash -c "hostname -I 2>/dev/null | awk '{print \$1}'" 2>$null +if (-not $wsl2Ip) { + $wsl2Ip = wsl -d $WslDistro --cd $ProjectPath bash -c "ip route get 1 2>/dev/null | head -1 | awk '{print \$7}'" 2>$null +} + +# ── 读取端口与 secret ── +$proxyPort = wsl -d $WslDistro --cd $ProjectPath bash -c 'grep "^CSSWITCH_PROXY_PORT=" docker/.env 2>/dev/null | cut -d= -f2 || echo "18991"' +$authToken = wsl -d $WslDistro --cd $ProjectPath bash -c 'grep "^CSSWITCH_AUTH_TOKEN=" docker/.env 2>/dev/null | cut -d= -f2 || echo ""' + +$baseUrl = "http://${wsl2Ip}:${proxyPort}/" +if ($authToken) { + $baseUrl += "${authToken}/" +} + +Write-Host "" +Write-Host "=== 部署完成 ===" -ForegroundColor Green +Write-Host " WSL2 IP:$wsl2Ip" -ForegroundColor Cyan +Write-Host " 代理端口:$proxyPort" -ForegroundColor Cyan +Write-Host "" +Write-Host " Windows 客户端配置 ANTHROPIC_BASE_URL:" -ForegroundColor White +Write-Host " $baseUrl" -ForegroundColor Yellow +Write-Host "" +Write-Host " 健康检查命令(在 Windows PowerShell 中):" -ForegroundColor White +Write-Host " curl.exe -s -m 5 $($baseUrl)health" -ForegroundColor Gray +Write-Host "" +Write-Host " 查看日志:.\scripts\wsl2-logs.ps1" -ForegroundColor Gray +Write-Host " 停止服务:.\scripts\wsl2-stop.ps1" -ForegroundColor Gray diff --git a/scripts/wsl2-logs.ps1 b/scripts/wsl2-logs.ps1 new file mode 100644 index 0000000..835ca2c --- /dev/null +++ b/scripts/wsl2-logs.ps1 @@ -0,0 +1,52 @@ +<# +.SYNOPSIS + 查看 WSL2 中 CSSwitch 代理容器的实时日志。 +.PARAMETER WslDistro + WSL2 发行版名称(默认:自动选择运行中的发行版)。 +.PARAMETER ProjectPath + 项目在 WSL2 中的绝对路径(默认:自动映射当前目录)。 +.PARAMETER Lines + 显示最近 N 行日志(默认:显示所有历史并 follow)。 +#> + +param( + [string]$WslDistro = "", + [string]$ProjectPath = "", + [int]$Lines = 0 +) + +$ErrorActionPreference = "Stop" + +# ── 检测 WSL ── +$wslAvailable = Get-Command "wsl.exe" -ErrorAction SilentlyContinue +if (-not $wslAvailable) { + Write-Host "✗ 未找到 wsl.exe。" -ForegroundColor Red + exit 1 +} + +$distros = wsl -l -v --quiet 2>$null | Where-Object { $_ -match 'Running' } +if (-not $distros) { + Write-Host "✗ 未找到运行中的 WSL2 发行版。" -ForegroundColor Red + exit 1 +} + +if (-not $WslDistro) { + $WslDistro = ($distros[0] -split '\s+')[0] +} + +if (-not $ProjectPath) { + $hostPath = Get-Location + if ($hostPath.Path -match '^([A-Z]):\\(.*)') { + $driveLetter = $matches[1].ToLower() + $restPath = $matches[2] -replace '\\', '/' + $ProjectPath = "/mnt/$driveLetter/$restPath" + } else { + Write-Host "✗ 无法自动映射路径:$hostPath" -ForegroundColor Red + exit 1 + } +} + +$tailFlag = if ($Lines -gt 0) { "--tail=$Lines" } else { "" } + +Write-Host "查看 WSL2($WslDistro)CSSwitch 代理日志(按 Ctrl+C 退出)…" -ForegroundColor Cyan +wsl -d $WslDistro --cd $ProjectPath bash -c "docker compose -f docker/docker-compose.yml logs -f $tailFlag csswitch-proxy" diff --git a/scripts/wsl2-stop.ps1 b/scripts/wsl2-stop.ps1 new file mode 100644 index 0000000..23fda35 --- /dev/null +++ b/scripts/wsl2-stop.ps1 @@ -0,0 +1,55 @@ +<# +.SYNOPSIS + 停止 WSL2 中的 CSSwitch 代理容器。 +.PARAMETER WslDistro + WSL2 发行版名称(默认:自动选择运行中的发行版)。 +.PARAMETER ProjectPath + 项目在 WSL2 中的绝对路径(默认:自动映射当前目录)。 +#> + +param( + [string]$WslDistro = "", + [string]$ProjectPath = "" +) + +$ErrorActionPreference = "Stop" + +# ── 检测 WSL ── +$wslAvailable = Get-Command "wsl.exe" -ErrorAction SilentlyContinue +if (-not $wslAvailable) { + Write-Host "✗ 未找到 wsl.exe。" -ForegroundColor Red + exit 1 +} + +$distros = wsl -l -v --quiet 2>$null | Where-Object { $_ -match 'Running' } +if (-not $distros) { + Write-Host "✗ 未找到运行中的 WSL2 发行版。" -ForegroundColor Red + exit 1 +} + +if (-not $WslDistro) { + $WslDistro = ($distros[0] -split '\s+')[0] +} + +# ── 映射路径 ── +if (-not $ProjectPath) { + $hostPath = Get-Location + if ($hostPath.Path -match '^([A-Z]):\\(.*)') { + $driveLetter = $matches[1].ToLower() + $restPath = $matches[2] -replace '\\', '/' + $ProjectPath = "/mnt/$driveLetter/$restPath" + } else { + Write-Host "✗ 无法自动映射路径:$hostPath" -ForegroundColor Red + exit 1 + } +} + +Write-Host "停止 WSL2($WslDistro)中的 CSSwitch 代理容器…" -ForegroundColor Yellow +$result = wsl -d $WslDistro --cd $ProjectPath bash -c 'docker compose -f docker/docker-compose.yml down 2>&1' +if ($LASTEXITCODE -ne 0) { + Write-Host "✗ 停止失败:" -ForegroundColor Red + Write-Host $result -ForegroundColor Red + exit 1 +} +Write-Host $result +Write-Host "✓ 容器已停止" -ForegroundColor Green From c962fcfe075600fcb24496827593d89aecee2638 Mon Sep 17 00:00:00 2001 From: visail Date: Fri, 3 Jul 2026 10:48:19 +0800 Subject: [PATCH 6/9] feat(docs): add WSL2 container deployment guide and update README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 docs/wsl2-container-deployment.md:前置条件、快速开始 (PowerShell 与 WSL2 内)、配置、网络说明、可选 Caddy TLS、 故障排查、安全提示、限制。 README.md 增加 WSL2/Docker 徽章与入口链接。 Co-Authored-By: Claude --- README.md | 3 + docs/wsl2-container-deployment.md | 294 ++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 docs/wsl2-container-deployment.md diff --git a/README.md b/README.md index e81ff75..437f857 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ MIT License macOS Tauri 2 + WSL2 / Docker

# CSSwitch @@ -56,6 +57,8 @@ DeepSeek 原生 Anthropic 端点 / 通义千问等 OpenAI 兼容端点 开发者的命令行用法(手动起代理与沙箱)、构建与测试,见 [`docs/DEVELOPMENT.md`](./docs/DEVELOPMENT.md) 与 [`desktop/README.md`](./desktop/README.md)。 +**Windows / WSL2 用户**:可将 CSSwitch 翻译代理以 Docker 容器方式部署在 WSL2 中,供 Windows 上的 OpenAI/Anthropic 兼容客户端使用。见 [`docs/wsl2-container-deployment.md`](./docs/wsl2-container-deployment.md)(实验性)。 + ## 更新计划(Roadmap) 以下为规划方向,不代表时间承诺。欢迎以 issue / PR 参与共建。 diff --git a/docs/wsl2-container-deployment.md b/docs/wsl2-container-deployment.md new file mode 100644 index 0000000..3e998c1 --- /dev/null +++ b/docs/wsl2-container-deployment.md @@ -0,0 +1,294 @@ +# CSSwitch WSL2 容器化部署指南 + +> **实验性支持**:本方案仅容器化 CSSwitch 翻译代理(`csswitch_proxy.py`), +> 在 WSL2 中以 Docker 容器形式运行代理服务。 +> Claude Science 本身仍是 macOS 应用,不在此部署范围内。 +> 容器化代理适合 Windows 上的 **OpenAI/Anthropic 兼容客户端** 使用, +> 或作为局域网内的代理服务。 + +## 目录 + +- [前置条件](#前置条件) +- [快速开始(PowerShell)](#快速开始powershell) +- [快速开始(WSL2 内)](#快速开始wsl2-内) +- [配置说明](#配置说明) +- [网络说明](#网络说明) +- [可选:Caddy TLS 反代](#可选caddy-tls-反代) +- [手动构建镜像](#手动构建镜像) +- [故障排查](#故障排查) +- [安全提示](#安全提示) +- [限制](#限制) + +## 前置条件 + +- **Windows 10/11** 已安装 **WSL2** 和 **Docker**(以下任选其一): + - [Docker Desktop for Windows](https://docs.docker.com/desktop/wsl/)(推荐,集成 WSL2 后端) + - 或 WSL2 发行版内安装 [Docker Engine](https://docs.docker.com/engine/install/ubuntu/) +- WSL2 发行版(推荐 Ubuntu 22.04+) +- 一个有效的 **第三方 API key**([DeepSeek](https://platform.deepseek.com/api_keys) 或 [阿里通义千问(DashScope)](https://bailian.console.aliyun.com/)) +- PowerShell 5.1+(Windows 自带)或 PowerShell 7+ + +## 快速开始(PowerShell) + +从 Windows 一侧一键部署,适合不常进 WSL2 终端的用户。 + +### 步骤 1:克隆项目到 Windows + +```powershell +git clone https://github.com/SuperJJ007/CSswitch.git +cd CSswitch +``` + +### 步骤 2:配置环境变量 + +从模板复制 `.env` 文件: + +```powershell +# 在 PowerShell 中: +wsl -d <你的发行版> --cd /mnt/g/WorkSpace/CSswitch cp docker/.env.example docker/.env +``` + +或用 WSL2 终端编辑: + +```bash +cd /mnt/g/WorkSpace/CSswitch # 替换为你实际的挂载路径 +cp docker/.env.example docker/.env +vi docker/.env +``` + +填入你的 key: + +```ini +# 选 deepseek 时填写 +DEEPSEEK_API_KEY=sk-your-deepseek-key-here + +# 强烈建议设置随机 path secret +CSSWITCH_AUTH_TOKEN=your-random-secret-here +``` + +### 步骤 3:一键部署 + +```powershell +.\scripts\wsl2-deploy.ps1 +``` + +脚本会自动: +1. 检测 WSL2 运行中的发行版 +2. 在 WSL2 内执行 `docker compose up -d` +3. 探测 WSL2 IP 地址 +4. 输出供 Windows 客户端使用的 `ANTHROPIC_BASE_URL` + +### 步骤 4:验证代理 + +```powershell +# 用 .\scripts\wsl2-deploy.ps1 输出的地址测试健康检查 +curl.exe -s -m 5 http://:18991//health +# 预期:{"status":"ok","provider":"deepseek",...} +``` + +### 步骤 5:停止服务 + +```powershell +.\scripts\wsl2-stop.ps1 +``` + +## 快速开始(WSL2 内) + +### 步骤 1:进入项目目录 + +```bash +cd /path/to/CSswitch +``` + +### 步骤 2:配置环境变量 + +```bash +cp docker/.env.example docker/.env +vi docker/.env +``` + +### 步骤 3:构建并启动 + +```bash +# 构建镜像 +bash scripts/docker-build.sh + +# 启动容器 +bash scripts/wsl2-deploy.sh +``` + +### 步骤 4:验证 + +```bash +# 健康检查 +curl http://localhost:18991//health + +# 查看日志 +bash scripts/wsl2-logs.sh +``` + +### 步骤 5:停止 + +```bash +bash scripts/wsl2-stop.sh +``` + +## 配置说明 + +全部配置通过 `docker/.env` 文件设定(参见 `docker/.env.example` 模板): + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| `DEEPSEEK_API_KEY` | DeepSeek API Key(`deepseek` provider 时必填) | — | +| `DASHSCOPE_API_KEY` | 阿里 DashScope API Key(`qwen` provider 时必填) | — | +| `CSSWITCH_PROVIDER` | 模型提供商:`deepseek` 或 `qwen` | `deepseek` | +| `CSSWITCH_PROXY_PORT` | 代理监听端口 | `18991` | +| `CSSWITCH_AUTH_TOKEN` | 路径 secret 鉴权(**强烈建议设置**) | — | +| `CSSWITCH_UPSTREAM_URL` | 上游 API 地址覆盖(高级用法) | — | + +> **安全**:`DEEPSEEK_API_KEY` 与 `DASHSCOPE_API_KEY` 只通过 `.env` → 环境变量注入容器, +> 不会出现在镜像层、git 历史或命令行参数中。 + +## 网络说明 + +### WSL2 IP 地址 + +WSL2 使用虚拟化网络,其 IP 地址可能随重启变化。`wsl2-deploy.ps1` 会自动探测当前 IP 并输出。 + +```powershell +# 手动获取 WSL2 IP: +wsl hostname -I +``` + +### Docker Desktop 模式 + +使用 Docker Desktop with WSL2 backend 时,容器可通过 `localhost` 直接访问(Docker Desktop 自动端口转发): + +```powershell +curl.exe http://localhost:18991//health +``` + +### 传统 WSL2 模式 + +在 WSL2 发行版内直接安装 Docker Engine 时,需要 **通过 WSL2 IP 访问**: + +```powershell +curl.exe http://:18991//health +``` + +### Windows 防火墙 + +如果从局域网其他机器访问,需要在 Windows 防火墙中放行 WSL2 代理端口: + +```powershell +# 以管理员身份运行 +New-NetFirewallRule -DisplayName "CSSwitch Proxy" -Direction Inbound -LocalPort 18991 -Protocol TCP -Action Allow +``` + +### 客户端配置 + +Windows 上的 OpenAI/Anthropic 兼容客户端需设置: + +```ini +ANTHROPIC_BASE_URL=http://:18991// +# 或(使用 Docker Desktop 时) +ANTHROPIC_BASE_URL=http://localhost:18991// +``` + +## 可选:Caddy TLS 反代 + +容器编排支持可选 Caddy 服务,在 WSL2 内部提供 **自签名 HTTPS**,保护 path secret 不在明文 HTTP 上传输。 + +启用方式: + +```bash +# 构建并启动(包含 Caddy) +docker compose -f docker/docker-compose.yml --profile tls up -d +``` + +访问地址变为: + +``` +https://:8443//health +``` + +> **注意**:自签名证书首次访问时浏览器会提示安全风险,确认即可。 +> `curl` 需加 `-k` 或 `--insecure` 参数。 + +## 构建镜像 + +```bash +bash scripts/docker-build.sh +``` + +或手动: + +```bash +docker build -t csswitch-proxy:latest -f docker/Dockerfile . +``` + +也可通过 Compose 自动构建(首次 `up -d` 时自动完成): + +```bash +docker compose -f docker/docker-compose.yml up -d +``` + +## 故障排查 + +### 代理启动后立即退出 + +最常见原因——**缺少 API key**: + +```bash +# 检查 .env 是否正确 +cat docker/.env | grep -E '^(DEEPSEEK|DASHSCOPE)_API_KEY=' + +# 查看容器日志 +bash scripts/wsl2-logs.sh +``` + +### 端口已被占用 + +```bash +# 修改 docker/.env 中的端口 +CSSWITCH_PROXY_PORT=18992 + +# 重写 docker-compose.yml 映射(或在 docker-compose.yml 中改 ports) +``` + +### WSL2 IP 变化 + +重新运行 `bash scripts/wsl2-deploy.sh` 或 `.\scripts\wsl2-deploy.ps1` 获取新的 IP 地址。 + +### curl 连接被拒绝 + +1. 确认容器在运行:`docker ps | grep csswitch-proxy` +2. 确认端口映射正确:`docker port csswitch-proxy` +3. 确认 Windows 防火墙未拦截:临时关闭防火墙测试 +4. 在 WSL2 内测试回环地址:`curl http://127.0.0.1:18991//health` + +### Docker Desktop 无法启动 WSL2 后端 + +- 确认 WSL2 已安装:`wsl --set-default-version 2` +- 确认发行版版本为 2:`wsl -l -v` +- 升级:`wsl --set-version <发行版名> 2` +- 在 Docker Desktop Settings → Resources → WSL Integration 中启用集成 + +## 安全提示 + +- **path secret 保护**:容器到客户端的 HTTP 连接在本地 WSL2 桥上为明文。 + 在不可信网络(如公司内网、跨子网)上使用时,强烈建议启用 [Caddy TLS](#可选caddy-tls-反代)。 +- **API key 保护**:`docker/.env` 包含你的第三方 API key,请勿提交到 git。 + `.env` 已在 `.gitignore` 中(被 `.dockerignore` 排除),但仍需注意不要手动 `git add` 它。 +- **容器权限**:容器内以非 root 用户(`csswitch`)运行,最小权限原则。 +- **监听地址**:容器内默认监听 `0.0.0.0`(由 Dockerfile 的 `ENTRYPOINT` 指定 `--host 0.0.0.0`); + 在本机使用 `127.0.0.1:18991`(通过 Docker Desktop 的端口转发),局域网可用 WSL2 IP 访问。 +- **端口 8765 禁止**:脚本与 compose 配置均拒绝使用 8765 端口(真实 Claude Science 保留端口)。 + +## 限制 + +- **Claude Science 不在此范围内**:容器化的是翻译代理,Claude Science 仍需运行在 macOS 上。 +- **Windows 上无 Claude Science**:目前 Anthropic 未提供 Windows 版 Claude Science, + 因此本容器方案面向的是其他 OpenAI/Anthropic 兼容客户端的用户。 +- **实验性**:WSL2 容器化部署为实验性功能,未经过生产环境的充分验证。 +- **网络依赖**:WSL2 网络架构(NAT)可能在某些 VPN/代理环境下需要额外配置。 From c90dbb5511169ddb1625fd21d715414ace6e67b7 Mon Sep 17 00:00:00 2001 From: visail Date: Fri, 3 Jul 2026 10:48:31 +0800 Subject: [PATCH 7/9] feat(doctor): add Docker / WSL2 detection to diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 [容器 / WSL2] 诊断段:检测 docker CLI 与 compose 插件、 检测 WSL2 环境(/proc/version 或 WSL_DISTRO_NAME)、 只读检查 csswitch-proxy:latest 镜像是否存在。 Co-Authored-By: Claude --- scripts/doctor.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/scripts/doctor.sh b/scripts/doctor.sh index 23bb875..76f6ffe 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -67,6 +67,32 @@ else warn "config 不存在(首次运行 GUI 会创建):$CONFIG" fi +echo "[容器 / WSL2]" +# Docker CLI +if command -v docker >/dev/null 2>&1; then + pass "docker $(docker --version 2>&1 | awk '{print $3}' | sed 's/,//')" + # docker compose 插件 + if docker compose version >/dev/null 2>&1; then + pass "docker compose 插件可用" + else + warn "未检测到 docker compose 插件(容器编排需要)" + fi +else + warn "未找到 docker(容器化部署需要)" +fi +# WSL2 检测(/proc/version 含 Microsoft 或 WSL_DISTRO_NAME 环境变量) +if [ -f /proc/version ] && grep -qi microsoft /proc/version; then + pass "WSL2 环境(/proc/version 含 Microsoft)" +elif [ -n "${WSL_DISTRO_NAME:-}" ]; then + pass "WSL2 环境(WSL_DISTRO_NAME=$WSL_DISTRO_NAME)" +fi +# csswitch-proxy 镜像检测(只读,不构建) +if docker image inspect csswitch-proxy:latest >/dev/null 2>&1; then + pass "csswitch-proxy:latest 镜像存在" +else + warn "csswitch-proxy:latest 镜像不存在(执行 bash scripts/docker-build.sh 构建)" +fi + echo "[铁律]" if [ -d "$REAL_DIR" ]; then pass "真实目录存在(本工具只读诊断,绝不写/删):$REAL_DIR"; else warn "未见真实 Science 目录:$REAL_DIR"; fi From b277e6d5aa876c9dac9fa7c4d7f8b4e8d7ae3e7d Mon Sep 17 00:00:00 2001 From: visail Date: Fri, 3 Jul 2026 10:50:14 +0800 Subject: [PATCH 8/9] test(container): add optional container smoke test and update runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 test/test_container.sh(RUN_CONTAINER_TESTS=1 时启用): 构建镜像、创建临时 .env、启动容器、健康检查、/v1/models 验证、清理。 test/run_all.sh 新增 container test 调用。 Co-Authored-By: Claude --- test/run_all.sh | 2 + test/test_container.sh | 96 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 test/test_container.sh diff --git a/test/run_all.sh b/test/run_all.sh index a396c5f..bafa6c1 100644 --- a/test/run_all.sh +++ b/test/run_all.sh @@ -10,4 +10,6 @@ echo "== bash scripts ==" bash test/test_scripts.sh echo "== bash ops scripts (doctor/verify-proxy/self-test) ==" bash test/test_ops_scripts.sh +echo "== container smoke test (RUN_CONTAINER_TESTS=1) ==" +bash test/test_container.sh echo "ALL GREEN" diff --git a/test/test_container.sh b/test/test_container.sh new file mode 100644 index 0000000..efd356d --- /dev/null +++ b/test/test_container.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# CSSwitch 容器冒烟测试。 +# 受 RUN_CONTAINER_TESTS=1 环境变量控制,默认跳过。 +# 依赖:docker 与 docker compose。 +set -euo pipefail + +if [ "${RUN_CONTAINER_TESTS:-0}" != "1" ]; then + echo " 跳过容器测试(RUN_CONTAINER_TESTS=1 时启用)" + exit 0 +fi + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" + +# ── 依赖检查 ── +if ! command -v docker >/dev/null 2>&1; then + echo " SKIP: 无 docker 命令" + exit 0 +fi +if ! docker compose version >/dev/null 2>&1; then + echo " SKIP: 无 docker compose 插件" + exit 0 +fi + +PASS=0; FAIL=0 +pass() { echo " ✓ $1"; PASS=$((PASS + 1)); } +fail() { echo " ✗ $1"; FAIL=$((FAIL + 1)); } + +# ── 清理函数 ── +cleanup() { + echo " 清理容器…" + docker compose -f docker/docker-compose.yml down 2>/dev/null || true +} +trap cleanup EXIT + +echo "== container smoke test ==" + +# ── 1. 构建镜像 ── +echo " [构建]" +if docker build -t csswitch-proxy:test -f docker/Dockerfile . >/dev/null 2>&1; then + pass "镜像构建成功" +else + fail "镜像构建失败" + echo " FAIL: 构建失败,跳过后续测试" + exit 1 +fi + +# ── 2. 准备临时 .env ── +echo " [配置]" +cp docker/.env.example docker/.env +# 写入 dummy key(不会真正调用上游,只测代理启动与健康检查) +cat >> docker/.env <<'EOF' +DEEPSEEK_API_KEY=sk-test-dummy-key +CSSWITCH_AUTH_TOKEN=test-secret-123 +CSSWITCH_PROXY_PORT=18991 +EOF +pass "临时 .env 就绪(dummy key)" + +# ── 3. 启动容器 ── +echo " [启动]" +if docker compose -f docker/docker-compose.yml up -d 2>/dev/null; then + pass "容器启动成功" +else + fail "容器启动失败" + exit 1 +fi + +# ── 4. 等待健康检查 ── +echo " [健康检查]" +HEALTH_OK=0 +for i in $(seq 1 10); do + if curl -s -m 3 http://127.0.0.1:18991/test-secret-123/health >/dev/null 2>&1; then + HEALTH_OK=1 + break + fi + sleep 2 +done +if [ "$HEALTH_OK" = "1" ]; then + pass "健康检查通过(/health 200)" +else + fail "健康检查超时" + docker compose -f docker/docker-compose.yml logs --tail=20 csswitch-proxy 2>/dev/null || true +fi + +# ── 5. 验证 /v1/models ── +if curl -s -m 5 http://127.0.0.1:18991/test-secret-123/v1/models | grep -q '"data"' 2>/dev/null; then + pass "/v1/models 返回模型列表" +else + fail "/v1/models 未返回预期数据" +fi + +# ── 汇总 ── +echo "----" +echo "容器测试:通过 ${PASS},失败 ${FAIL}" +[ "$FAIL" -eq 0 ] || exit 1 +echo "ALL CONTAINER TESTS GREEN" From 6af2a4166dd6e8b822f03398bfc710da0510fa36 Mon Sep 17 00:00:00 2001 From: visail Date: Fri, 3 Jul 2026 10:50:51 +0800 Subject: [PATCH 9/9] docs(changelog): add WSL2 containerized deployment to Unreleased MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 记录本次 feature/wsl2-containerized-deployment 分支的全部变更: proxy --host 参数、Dockerfile/compose、WSL2 脚本与 PowerShell 包装、 部署文档、doctor 诊断增强、容器冒烟测试。 Co-Authored-By: Claude --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f35eb27..df5933a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ > **约定**:已修问题从 [`docs/known-issues.md`](docs/known-issues.md)「毕业」到这里(发布即定稿);未修/进行中留在 known-issues;硬 bug 的根因证据链存在 [`findings/`](findings/)。 +## Unreleased — WSL2 容器化部署 + +### 新增 Added + +- **WSL2 容器化代理部署**(实验性):将 CSSwitch 翻译代理以 Docker 容器方式部署在 WSL2 中,供 Windows 上的 OpenAI/Anthropic 兼容客户端使用。 + - `proxy/csswitch_proxy.py` 新增 `--host` / `CSSWITCH_BIND_HOST` 参数,支持绑定 `0.0.0.0` 监听任意地址(默认 `127.0.0.1`)。 + - `docker/Dockerfile`:基于 `python:3.11-slim`,非 root 运行,零 pip 依赖。 + - `docker/docker-compose.yml`:proxy 服务编排(env_file、端口映射、健康检查、自动重启、日志卷),可选 Caddy TLS 反代(`--profile tls`)。 + - `scripts/docker-build.sh` / `scripts/wsl2-deploy.sh` / `scripts/wsl2-stop.sh` / `scripts/wsl2-logs.sh`:WSL2/Linux 容器管理脚本。 + - `scripts/wsl2-deploy.ps1` / `scripts/wsl2-stop.ps1` / `scripts/wsl2-logs.ps1`:Windows PowerShell 包装,自动探测 WSL2 IP 并输出连接地址。 + - `docs/wsl2-container-deployment.md`:完整部署指南(前置条件、网络说明、安全提示、故障排查)。 + - `scripts/doctor.sh` 新增 Docker / docker compose / WSL2 环境检测。 + - `test/test_container.sh`:可选容器冒烟测试(`RUN_CONTAINER_TESTS=1`)。 +- README 增加 WSL2/Docker 徽章与入口链接。 + ## [0.2.0] — 2026-07-03 > 主题:一键**幂等化**——修 #3(运行中再点的分派)与 #6 的「复发」部分(更新后对话「不见」),并随三轮外部复审全面加固代理与进程路径。