fix(config): 修复 .env 加载与密码初始化

- 统一开发脚本、Vite 代理与 Docker Compose 的 HOST/PORT 配置
- 首次启动按字面值读取 AUTH_PASSWORD,避免 ${VAR} 被环境变量插值
- 已有 auth.json 时继续以 Web 端密码为准,不受 .env 覆盖
This commit is contained in:
Arepeater
2026-08-05 17:56:27 +08:00
parent c278dd3b02
commit b43e2fe44a
15 changed files with 237 additions and 49 deletions
+1 -1
View File
@@ -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.
+2 -1
View File
@@ -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
+8 -1
View File
@@ -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",
)
+9 -1
View File
@@ -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():
+66
View File
@@ -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
+30
View File
@@ -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"
+48 -23
View File
@@ -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) {
+43 -6
View File
@@ -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+=("$!")
+3 -1
View File
@@ -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
+5 -4
View File
@@ -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#访问密码设置公网部署必读)。
+2 -1
View File
@@ -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`
+2 -1
View File
@@ -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`
+1 -1
View File
@@ -1,6 +1,6 @@
// 后端 API 客户端 — 全项目统一入口
//
// Dev:Vite 代理 /api 到 :3018
// Dev: Vite 按启动脚本解析出的 BACKEND_HOST/BACKEND_PORT 代理 /api
// Prod:同源(FastAPI 托管前端 dist)
import { toast } from '@/components/Toast'
+8 -4
View File
@@ -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: {
+9 -4
View File
@@ -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: {