diff --git a/.env.example b/.env.example index 0fac35d..59db6f1 100644 --- a/.env.example +++ b/.env.example @@ -18,7 +18,7 @@ LOG_LEVEL=INFO # 首次启动时预置访问密码(可选)。公网服务器部署时填入,免去 SSH 端口转发设密码。 # 仅在尚未设置密码时生效(一次性初始化);设过后改密码请用页面 UI, 此处不再读取。 # 建议至少 6 位。.env 文件权限保持 600 且不要提交到 Git。 -AUTH_PASSWORD= +AUTH_PASSWORD='' # Optional backend dependency extras for Docker and ./dev.sh / .\dev.ps1. # Set to legacy-cpu on older CPUs without AVX2/FMA support. diff --git a/Dockerfile b/Dockerfile index cb295bf..dd0d6d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -125,7 +125,8 @@ COPY --from=stocksdk-builder /build/node_modules ./app/plugins/stocksdk/node_mod COPY tiers.yaml /app/tiers.yaml ENV STATIC_DIR=/app/static \ TIERS_YAML=/app/tiers.yaml \ - DATA_DIR=/app/data + DATA_DIR=/app/data \ + TICKFLOW_ENV_FILE=/app/.env # Frontend 静态产物 COPY --from=frontend-builder /build/dist ./static diff --git a/backend/app/config.py b/backend/app/config.py index b57f305..38be513 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -1,6 +1,7 @@ """全局配置 — 从环境变量 / .env 读取。""" from __future__ import annotations +import os import sys from pathlib import Path @@ -63,11 +64,17 @@ def _project_root() -> Path: _PROJECT_ROOT = _project_root() _RESOURCE_ROOT = _resource_root() +_ENV_FILE = Path( + os.environ.get( + "TICKFLOW_ENV_FILE", + str(_RESOURCE_ROOT / ".env") if not _IS_FROZEN else ".env", + ) +) class Settings(BaseSettings): model_config = SettingsConfigDict( - env_file=str(_RESOURCE_ROOT / ".env") if not _IS_FROZEN else ".env", + env_file=str(_ENV_FILE), env_file_encoding="utf-8", extra="ignore", ) diff --git a/backend/app/services/auth.py b/backend/app/services/auth.py index dbfcbad..951956a 100644 --- a/backend/app/services/auth.py +++ b/backend/app/services/auth.py @@ -131,9 +131,17 @@ def bootstrap_from_env() -> bool: Returns: True 表示本次用环境变量初始化了密码; False 表示无需初始化。 """ - from app.config import settings + from app.config import _ENV_FILE, settings pwd = (settings.auth_password or "").strip() + # Compose 会对 env_file 中未加单引号的 $VAR 做插值。Docker 部署时同时 + # 只读挂载原始 .env,首次初始化密码直接按 dotenv 语义读取,避免特殊字符被截断。 + if _ENV_FILE.is_file(): + from dotenv import dotenv_values + + raw_pwd = dotenv_values(_ENV_FILE, encoding="utf-8", interpolate=False).get("AUTH_PASSWORD") + if isinstance(raw_pwd, str) and raw_pwd.strip(): + pwd = raw_pwd.strip() if not pwd: return False if is_configured(): diff --git a/backend/tests/test_auth_env_bootstrap.py b/backend/tests/test_auth_env_bootstrap.py new file mode 100644 index 0000000..efbb41c --- /dev/null +++ b/backend/tests/test_auth_env_bootstrap.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from collections.abc import Iterator +from pathlib import Path +from types import ModuleType + +import pytest + +from app import config as app_config +from app.config import Settings + + +@pytest.fixture(autouse=True) +def isolated_auth_store( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> Iterator[tuple[ModuleType, Path, Path]]: + monkeypatch.setattr(app_config.settings, "data_dir", tmp_path) + from app.services import auth + + auth_path = tmp_path / "user_data" / "auth.json" + env_path = tmp_path / ".env" + monkeypatch.setattr(app_config, "_ENV_FILE", env_path) + monkeypatch.setattr(app_config.settings, "auth_password", "") + auth._sessions.clear() + auth._configured_cache = None + yield auth, auth_path, env_path + auth._sessions.clear() + auth._configured_cache = None + + +def test_bootstrap_recovers_compose_interpolated_password_from_raw_env( + isolated_auth_store: tuple[ModuleType, Path, Path], + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + auth, auth_path, env_path = isolated_auth_store + password = "pw${special}-secret" + env_path.write_text( + f"AUTH_PASSWORD={password}\nDATA_DIR={tmp_path}\n", + encoding="utf-8", + ) + configured = Settings(_env_file=env_path) + configured.auth_password = "pw-secret" # 模拟 Compose 将未定义的 ${special} 插值为空串 + monkeypatch.setattr(app_config, "settings", configured) + + assert auth.bootstrap_from_env() is True + assert auth_path.exists() + assert password not in auth_path.read_text(encoding="utf-8") + assert auth.verify_and_create_session(password) is not None + assert auth.verify_and_create_session("pw-secret") is None + + +def test_bootstrap_from_env_does_not_override_existing_password( + isolated_auth_store: tuple[ModuleType, Path, Path], +) -> None: + auth, auth_path, env_path = isolated_auth_store + auth.set_password("web-managed-secret") + before = auth_path.read_bytes() + env_path.write_text("AUTH_PASSWORD=replacement-secret\n", encoding="utf-8") + app_config.settings.auth_password = "replacement-secret" + + assert auth.bootstrap_from_env() is False + assert auth_path.read_bytes() == before + assert auth.verify_and_create_session("web-managed-secret") is not None + assert auth.verify_and_create_session("replacement-secret") is None diff --git a/backend/tests/test_config_env.py b/backend/tests/test_config_env.py new file mode 100644 index 0000000..1239d6e --- /dev/null +++ b/backend/tests/test_config_env.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from app.config import Settings + + +def test_settings_reads_server_and_auth_values_from_env( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + for name in ("HOST", "PORT", "LOG_LEVEL", "AUTH_PASSWORD"): + monkeypatch.delenv(name, raising=False) + env_path = tmp_path / ".env" + env_path.write_text( + "HOST=127.0.0.1\n" + "PORT=4318\n" + "LOG_LEVEL=DEBUG\n" + "AUTH_PASSWORD=config-secret\n", + encoding="utf-8", + ) + + configured = Settings(_env_file=env_path) + + assert configured.host == "127.0.0.1" + assert configured.port == 4318 + assert configured.log_level == "DEBUG" + assert configured.auth_password == "config-secret" diff --git a/dev.ps1 b/dev.ps1 index a89464f..1f9046c 100644 --- a/dev.ps1 +++ b/dev.ps1 @@ -18,8 +18,42 @@ param( $ErrorActionPreference = 'Stop' -# Port precedence: CLI arg > env var > default -if ($BackendPort -le 0) { $BackendPort = if ($env:BACKEND_PORT) { [int]$env:BACKEND_PORT } else { 3018 } } +$Root = Split-Path -Parent $MyInvocation.MyCommand.Path +$BackendDir = Join-Path $Root 'backend' +$FrontendDir = Join-Path $Root 'frontend' +$EnvFile = Join-Path $Root '.env' + +# Read only launcher-owned keys. Do not execute .env as PowerShell code. +function Read-DotEnvValue($Path, $Name) { + if (-not (Test-Path $Path)) { return $null } + $escaped = [Regex]::Escape($Name) + foreach ($line in Get-Content $Path) { + if ($line -match "^\s*$escaped\s*=\s*(.*?)\s*$") { + $value = $Matches[1].Trim() + $value = ($value -replace '\s+#.*$', '').Trim() + if ($value.Length -ge 2 -and + (($value.StartsWith('"') -and $value.EndsWith('"')) -or + ($value.StartsWith("'") -and $value.EndsWith("'")))) { + return $value.Substring(1, $value.Length - 2) + } + return $value + } + } + return $null +} + +$DotEnvHost = Read-DotEnvValue $EnvFile 'HOST' +$DotEnvPort = Read-DotEnvValue $EnvFile 'PORT' +$BindAddress = if ($env:HOST) { $env:HOST } elseif ($DotEnvHost) { $DotEnvHost } else { '0.0.0.0' } +$DisplayHost = if ($BindAddress -in @('0.0.0.0', '::')) { 'localhost' } else { $BindAddress } + +# Port precedence: CLI arg > BACKEND_PORT env > PORT env > .env PORT > default +if ($BackendPort -le 0) { + if ($env:BACKEND_PORT) { $BackendPort = [int]$env:BACKEND_PORT } + elseif ($env:PORT) { $BackendPort = [int]$env:PORT } + elseif ($DotEnvPort) { $BackendPort = [int]$DotEnvPort } + else { $BackendPort = 3018 } +} if ($FrontendPort -le 0) { $FrontendPort = if ($env:FRONTEND_PORT) { [int]$env:FRONTEND_PORT } else { 3011 } } # Force UTF-8 console output so child process logs aren't garbled @@ -28,10 +62,6 @@ try { $OutputEncoding = New-Object System.Text.UTF8Encoding $false } catch {} -$Root = Split-Path -Parent $MyInvocation.MyCommand.Path -$BackendDir = Join-Path $Root 'backend' -$FrontendDir = Join-Path $Root 'frontend' - function Log-Info($m) { Write-Host "[dev] $m" -ForegroundColor DarkGray } function Log-Ok ($m) { Write-Host "[dev] $m" -ForegroundColor Green } function Log-Warn($m) { Write-Host "[dev] $m" -ForegroundColor Yellow } @@ -113,15 +143,7 @@ Free-Port 'frontend' $FrontendPort # select Polars' rtcompat runtime before the backend starts. $BackendExtras = $env:BACKEND_EXTRAS if (-not (Test-Path Env:BACKEND_EXTRAS)) { - $envFile = Join-Path $Root '.env' - if (Test-Path $envFile) { - foreach ($line in Get-Content $envFile) { - if ($line -match '^\s*BACKEND_EXTRAS\s*=\s*(.*?)\s*$') { - $BackendExtras = $Matches[1] - break - } - } - } + $BackendExtras = Read-DotEnvValue $EnvFile 'BACKEND_EXTRAS' } $BackendExtraArgs = @() @@ -156,8 +178,8 @@ Write-Host '' Write-Host '+----------------------------------------------+' -ForegroundColor Blue Write-Host '| tickflow-stock-panel |' -ForegroundColor Blue Write-Host '| |' -ForegroundColor Blue -Write-Host "| backend http://localhost:$BackendPort" -ForegroundColor Blue -Write-Host "| frontend http://localhost:$FrontendPort" -ForegroundColor Blue +Write-Host "| backend http://${DisplayHost}:$BackendPort" -ForegroundColor Blue +Write-Host "| frontend http://${DisplayHost}:$FrontendPort" -ForegroundColor Blue Write-Host '| |' -ForegroundColor Blue Write-Host '| Ctrl-C closes both |' -ForegroundColor Blue Write-Host '+----------------------------------------------+' -ForegroundColor Blue @@ -170,7 +192,7 @@ $backendPidFile = [System.IO.Path]::GetTempFileName() $frontendPidFile = [System.IO.Path]::GetTempFileName() $backendJob = Start-Job -Name 'backend' -ScriptBlock { - param($pidFile, $dir, $port) + param($pidFile, $dir, $envFile, $bindAddress, $port) # Start-Job 开的是全新 powershell.exe 子进程, 不继承主进程的 UTF-8 设置, # 默认用系统 ANSI (中文 Windows = GBK/cp936) 解码后端 UTF-8 输出 → 中文乱码。 # 这里强制子进程用 UTF-8, 与 app/__init__.py 的 stdout/stderr 编码对齐。 @@ -179,18 +201,21 @@ $backendJob = Start-Job -Name 'backend' -ScriptBlock { $PID | Out-File -FilePath $pidFile -Encoding ascii -Force $env:PYTHONUNBUFFERED = '1' Set-Location $dir - & .\.venv\Scripts\python.exe -m uvicorn app.main:app --reload --host 0.0.0.0 --port $port 2>&1 -} -ArgumentList $backendPidFile, $BackendDir, $BackendPort + $envArgs = if (Test-Path $envFile) { @('--env-file', $envFile) } else { @() } + & .\.venv\Scripts\python.exe -m uvicorn app.main:app @envArgs --reload --host $bindAddress --port $port 2>&1 +} -ArgumentList $backendPidFile, $BackendDir, $EnvFile, $BindAddress, $BackendPort $frontendJob = Start-Job -Name 'frontend' -ScriptBlock { - param($pidFile, $dir, $port) + param($pidFile, $dir, $bindAddress, $backendPort, $port) # 同上: job 子进程默认 GBK, pnpm/前端工具链也是 UTF-8 输出, 需对齐。 [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false $OutputEncoding = New-Object System.Text.UTF8Encoding $false $PID | Out-File -FilePath $pidFile -Encoding ascii -Force Set-Location $dir - & pnpm dev --host 0.0.0.0 --port $port 2>&1 -} -ArgumentList $frontendPidFile, $FrontendDir, $FrontendPort + $env:BACKEND_HOST = $bindAddress + $env:BACKEND_PORT = [string]$backendPort + & pnpm dev --host $bindAddress --port $port 2>&1 +} -ArgumentList $frontendPidFile, $FrontendDir, $BindAddress, $BackendPort, $FrontendPort # Wait up to 5 seconds for the PID files to materialise function Read-JobPid($file) { diff --git a/dev.sh b/dev.sh index b8deca7..09f8a67 100755 --- a/dev.sh +++ b/dev.sh @@ -13,13 +13,48 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BACKEND_DIR="$ROOT/backend" FRONTEND_DIR="$ROOT/frontend" -BACKEND_PORT="${BACKEND_PORT:-3018}" + +# Read only the launcher-owned keys from .env. Do not source the whole file: +# .env is data, not a shell script, and may contain values that are unsafe or +# invalid as Bash syntax. Exported environment variables keep highest priority. +read_dotenv_value() { + local key="$1" + if [[ ! -f "$ROOT/.env" ]]; then + return 0 + fi + awk -v wanted="$key" ' + $0 ~ "^[[:space:]]*" wanted "[[:space:]]*=" { + sub(/^[^=]*=/, "") + sub(/[[:space:]]+#.*$/, "") + gsub(/^[[:space:]]+|[[:space:]]+$/, "") + if (($0 ~ /^".*"$/) || ($0 ~ /^\047.*\047$/)) { + $0 = substr($0, 2, length($0) - 2) + } + print + exit + } + ' "$ROOT/.env" +} + +ENV_HOST="$(read_dotenv_value HOST)" +ENV_PORT="$(read_dotenv_value PORT)" +BACKEND_HOST="${HOST:-${ENV_HOST:-0.0.0.0}}" +# Keep BACKEND_PORT as a backwards-compatible explicit override. +BACKEND_PORT="${BACKEND_PORT:-${PORT:-${ENV_PORT:-3018}}}" FRONTEND_PORT="${FRONTEND_PORT:-3011}" +UVICORN_ENV_ARGS=() +if [[ -f "$ROOT/.env" ]]; then + UVICORN_ENV_ARGS=(--env-file "$ROOT/.env") +fi +DISPLAY_HOST="$BACKEND_HOST" +if [[ "$DISPLAY_HOST" == "0.0.0.0" || "$DISPLAY_HOST" == "::" ]]; then + DISPLAY_HOST="localhost" +fi # Match Docker's BACKEND_EXTRAS behavior so old CPUs can select Polars' # rtcompat runtime before the backend starts. An exported value wins over .env. if [[ -z "${BACKEND_EXTRAS+x}" && -f "$ROOT/.env" ]]; then - BACKEND_EXTRAS="$(awk '/^[[:space:]]*BACKEND_EXTRAS[[:space:]]*=/ {sub(/^[^=]*=/, ""); gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print; exit}' "$ROOT/.env")" + BACKEND_EXTRAS="$(read_dotenv_value BACKEND_EXTRAS)" fi BACKEND_EXTRAS="${BACKEND_EXTRAS:-}" BACKEND_EXTRA_ARGS=() @@ -129,8 +164,8 @@ echo echo -e "${BLUE}╭──────────────────────────────────────────────╮${NC}" echo -e "${BLUE}│${NC} ${GREEN}tickflow-stock-panel${NC} ${BLUE}│${NC}" echo -e "${BLUE}│${NC} ${BLUE}│${NC}" -echo -e "${BLUE}│${NC} backend ${YELLOW}http://localhost:$BACKEND_PORT${NC} ${BLUE}│${NC}" -echo -e "${BLUE}│${NC} frontend ${YELLOW}http://localhost:$FRONTEND_PORT${NC} ${BLUE}│${NC}" +echo -e "${BLUE}│${NC} backend ${YELLOW}http://$DISPLAY_HOST:$BACKEND_PORT${NC} ${BLUE}│${NC}" +echo -e "${BLUE}│${NC} frontend ${YELLOW}http://$DISPLAY_HOST:$FRONTEND_PORT${NC} ${BLUE}│${NC}" echo -e "${BLUE}│${NC} ${BLUE}│${NC}" echo -e "${BLUE}│${NC} Ctrl-C 同时关闭两端 ${BLUE}│${NC}" echo -e "${BLUE}╰──────────────────────────────────────────────╯${NC}" @@ -138,14 +173,16 @@ echo ( cd "$BACKEND_DIR" - uv run uvicorn app.main:app --reload --host 0.0.0.0 --port "$BACKEND_PORT" 2>&1 \ + uv run uvicorn app.main:app "${UVICORN_ENV_ARGS[@]}" --reload \ + --host "$BACKEND_HOST" --port "$BACKEND_PORT" 2>&1 \ | prefix_awk "$(printf "${BLUE}[backend ]${NC} ")" ) & PIDS+=("$!") ( cd "$FRONTEND_DIR" - pnpm dev --host 0.0.0.0 --port "$FRONTEND_PORT" 2>&1 \ + BACKEND_HOST="$BACKEND_HOST" BACKEND_PORT="$BACKEND_PORT" \ + pnpm dev --host "$BACKEND_HOST" --port "$FRONTEND_PORT" 2>&1 \ | prefix_awk "$(printf "${GREEN}[frontend]${NC} ")" ) & PIDS+=("$!") diff --git a/docker-compose.yml b/docker-compose.yml index f705549..c4ed028 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: CODEX_CLI_VERSION: ${CODEX_CLI_VERSION:-0.144.3} container_name: TickFlow_Stock_Panel ports: - - "${PORT:-3018}:3018" + - "${HOST:-0.0.0.0}:${PORT:-3018}:3018" extra_hosts: - "host.docker.internal:host-gateway" env_file: @@ -26,6 +26,8 @@ services: volumes: - ./data:/app/data - ./tiers.yaml:/app/tiers.yaml:ro + # 保留原始 dotenv 值供首次密码初始化读取,避免 Compose 展开密码中的 $VAR。 + - ./.env:/app/.env:ro # 复用主机 Codex 登录态;后端只读后复制到单次请求的临时 CODEX_HOME。 # Windows PowerShell/CMD 下 HOME 常未设置, 可通过 .env 里 CODEX_HOME_HOST 覆盖。 - ${CODEX_HOME_HOST:-${HOME}/.codex}:/root/.codex:ro diff --git a/docs/configuration.md b/docs/configuration.md index ca774cf..21c38ad 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -58,13 +58,13 @@ AI_DAILY_TOKEN_BUDGET=500000 # 每日 token 预算上限 ## 服务 ```ini -HOST=0.0.0.0 # 监听地址 -PORT=3018 # 服务端口 +HOST=0.0.0.0 # 开发服务监听地址 / Docker 主机绑定地址 +PORT=3018 # 开发后端端口 / Docker 主机映射端口 LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERROR ``` - `HOST`:`0.0.0.0` 监听所有网卡(容器/公网部署需要);仅本机用可设 `127.0.0.1` -- `PORT`:默认 `3018`,改端口后 Docker 映射、SSH 转发命令里的端口也要同步改 +- `PORT`:默认 `3018`;开发模式兼容显式的 `BACKEND_PORT` 覆盖,改端口后 SSH 转发命令也要同步改 - `LOG_LEVEL`:排查问题时改 `DEBUG` --- @@ -84,10 +84,11 @@ DATA_DIR=./data # Parquet / DuckDB 数据存储目录 ## 访问密码(公网部署) ```ini -AUTH_PASSWORD=你的密码 # 至少 6 位;仅首次生效,已设过则不覆盖 +AUTH_PASSWORD='你的密码' # 至少 6 位;仅首次生效,已设过则不覆盖 ``` 面板首次设置访问密码时,出于安全考虑**仅允许本机或内网访问**(防公网陌生人抢先设置锁死面板)。公网服务器部署可通过此环境变量预置首个密码。 +密码建议使用单引号包裹,避免 Docker Compose 插值 `$VAR`;Docker 启动时也会只读挂载原始 `.env`,兼容已有的未加引号配置。 详细步骤、SSH 转发方案、重置密码方法见 [deployment.md → 访问密码设置](./deployment.md#访问密码设置公网部署必读)。 diff --git a/docs/deploy-password.md b/docs/deploy-password.md index c828b31..9a1c053 100644 --- a/docs/deploy-password.md +++ b/docs/deploy-password.md @@ -16,7 +16,7 @@ ```bash # 编辑服务器上的 .env (通常在项目根目录或 backend/ 下) -AUTH_PASSWORD=你的密码 +AUTH_PASSWORD='你的密码' ``` 然后重启服务。启动时会自动: @@ -30,6 +30,7 @@ AUTH_PASSWORD=你的密码 - **密码至少 6 位**,否则会被跳过并记一条 warning 日志 - **仅在未设过密码时生效**。已设过密码后,改这里不会覆盖(避免重启时重置你在 UI 改的密码) +- 密码建议使用单引号包裹,避免 Docker Compose 插值 `$VAR`;启动时也会从只读挂载的原始 `.env` 初始化,兼容已有的未加引号配置 - `.env` 文件权限保持 `600`,**不要提交到 Git** - 明文密码只存在于 `.env` / 环境变量中,落盘的是哈希,安全性等同 `auth.json` diff --git a/docs/deployment.md b/docs/deployment.md index 8e006a5..af16e40 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -123,7 +123,7 @@ git pull 在 `.env` 文件(或 Docker / 系统环境变量)里设置 `AUTH_PASSWORD`: ```bash -AUTH_PASSWORD=你的密码 +AUTH_PASSWORD='你的密码' ``` 然后重启服务。启动时会自动: @@ -138,6 +138,7 @@ AUTH_PASSWORD=你的密码 - **密码至少 6 位**,否则会被跳过并记一条 warning 日志 - **仅在未设过密码时生效**。已设过密码后,改这里不会覆盖(避免重启时重置你在 UI 改的密码) +- 密码建议使用单引号包裹,避免 Docker Compose 插值 `$VAR`;启动时也会从只读挂载的原始 `.env` 初始化,兼容已有的未加引号配置 - `.env` 文件权限保持 `600`,**不要提交到 Git** - 明文密码只存在于 `.env` / 环境变量中,落盘的是哈希,安全性等同 `auth.json` diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 7b6f8d9..0d03ab9 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,6 +1,6 @@ // 后端 API 客户端 — 全项目统一入口 // -// Dev:Vite 代理 /api 到 :3018 +// Dev: Vite 按启动脚本解析出的 BACKEND_HOST/BACKEND_PORT 代理 /api // Prod:同源(FastAPI 托管前端 dist) import { toast } from '@/components/Toast' diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 50f463f..239f479 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,6 +1,10 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'node:path'; +const backendHost = process.env.BACKEND_HOST || '127.0.0.1'; +const proxyHost = ['0.0.0.0', '::'].includes(backendHost) ? '127.0.0.1' : backendHost; +const backendPort = process.env.BACKEND_PORT || '3018'; +const backendTarget = `http://${proxyHost}:${backendPort}`; export default defineConfig({ plugins: [react()], resolve: { @@ -9,12 +13,12 @@ export default defineConfig({ }, }, server: { - host: '0.0.0.0', // 允许局域网访问 + host: '0.0.0.0', // dev.sh / dev.ps1 会用 CLI --host 覆盖 port: 3011, proxy: { - // dev 时 /api 转发到 FastAPI + // dev 时 /api 转发到与启动脚本相同的 FastAPI 地址 '/api': { - target: 'http://localhost:3018', + target: backendTarget, // SSE 端点需要禁用缓冲 configure: (proxy) => { proxy.on('proxyReq', (_proxyReq, req) => { @@ -26,7 +30,7 @@ export default defineConfig({ }); }, }, - '/health': 'http://localhost:3018', + '/health': backendTarget, }, }, build: { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 554d93d..51d5a55 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -2,6 +2,11 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'node:path' +const backendHost = process.env.BACKEND_HOST || '127.0.0.1' +const proxyHost = ['0.0.0.0', '::'].includes(backendHost) ? '127.0.0.1' : backendHost +const backendPort = process.env.BACKEND_PORT || '3018' +const backendTarget = `http://${proxyHost}:${backendPort}` + export default defineConfig({ plugins: [react()], resolve: { @@ -10,12 +15,12 @@ export default defineConfig({ }, }, server: { - host: '0.0.0.0', // 允许局域网访问 + host: '0.0.0.0', // dev.sh / dev.ps1 会用 CLI --host 覆盖 port: 3011, proxy: { - // dev 时 /api 转发到 FastAPI + // dev 时 /api 转发到与启动脚本相同的 FastAPI 地址 '/api': { - target: 'http://localhost:3018', + target: backendTarget, // SSE 端点需要禁用缓冲 configure: (proxy) => { proxy.on('proxyReq', (_proxyReq, req) => { @@ -27,7 +32,7 @@ export default defineConfig({ }) }, }, - '/health': 'http://localhost:3018', + '/health': backendTarget, }, }, build: {