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 端口转发设密码。 # 首次启动时预置访问密码(可选)。公网服务器部署时填入,免去 SSH 端口转发设密码。
# 仅在尚未设置密码时生效(一次性初始化);设过后改密码请用页面 UI, 此处不再读取。 # 仅在尚未设置密码时生效(一次性初始化);设过后改密码请用页面 UI, 此处不再读取。
# 建议至少 6 位。.env 文件权限保持 600 且不要提交到 Git。 # 建议至少 6 位。.env 文件权限保持 600 且不要提交到 Git。
AUTH_PASSWORD= AUTH_PASSWORD=''
# Optional backend dependency extras for Docker and ./dev.sh / .\dev.ps1. # Optional backend dependency extras for Docker and ./dev.sh / .\dev.ps1.
# Set to legacy-cpu on older CPUs without AVX2/FMA support. # 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 COPY tiers.yaml /app/tiers.yaml
ENV STATIC_DIR=/app/static \ ENV STATIC_DIR=/app/static \
TIERS_YAML=/app/tiers.yaml \ TIERS_YAML=/app/tiers.yaml \
DATA_DIR=/app/data DATA_DIR=/app/data \
TICKFLOW_ENV_FILE=/app/.env
# Frontend 静态产物 # Frontend 静态产物
COPY --from=frontend-builder /build/dist ./static COPY --from=frontend-builder /build/dist ./static
+8 -1
View File
@@ -1,6 +1,7 @@
"""全局配置 — 从环境变量 / .env 读取。""" """全局配置 — 从环境变量 / .env 读取。"""
from __future__ import annotations from __future__ import annotations
import os
import sys import sys
from pathlib import Path from pathlib import Path
@@ -63,11 +64,17 @@ def _project_root() -> Path:
_PROJECT_ROOT = _project_root() _PROJECT_ROOT = _project_root()
_RESOURCE_ROOT = _resource_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): class Settings(BaseSettings):
model_config = SettingsConfigDict( 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", env_file_encoding="utf-8",
extra="ignore", extra="ignore",
) )
+9 -1
View File
@@ -131,9 +131,17 @@ def bootstrap_from_env() -> bool:
Returns: Returns:
True 表示本次用环境变量初始化了密码; False 表示无需初始化。 True 表示本次用环境变量初始化了密码; False 表示无需初始化。
""" """
from app.config import settings from app.config import _ENV_FILE, settings
pwd = (settings.auth_password or "").strip() 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: if not pwd:
return False return False
if is_configured(): 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' $ErrorActionPreference = 'Stop'
# Port precedence: CLI arg > env var > default $Root = Split-Path -Parent $MyInvocation.MyCommand.Path
if ($BackendPort -le 0) { $BackendPort = if ($env:BACKEND_PORT) { [int]$env:BACKEND_PORT } else { 3018 } } $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 } } 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 # Force UTF-8 console output so child process logs aren't garbled
@@ -28,10 +62,6 @@ try {
$OutputEncoding = New-Object System.Text.UTF8Encoding $false $OutputEncoding = New-Object System.Text.UTF8Encoding $false
} catch {} } 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-Info($m) { Write-Host "[dev] $m" -ForegroundColor DarkGray }
function Log-Ok ($m) { Write-Host "[dev] $m" -ForegroundColor Green } function Log-Ok ($m) { Write-Host "[dev] $m" -ForegroundColor Green }
function Log-Warn($m) { Write-Host "[dev] $m" -ForegroundColor Yellow } 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. # select Polars' rtcompat runtime before the backend starts.
$BackendExtras = $env:BACKEND_EXTRAS $BackendExtras = $env:BACKEND_EXTRAS
if (-not (Test-Path Env:BACKEND_EXTRAS)) { if (-not (Test-Path Env:BACKEND_EXTRAS)) {
$envFile = Join-Path $Root '.env' $BackendExtras = Read-DotEnvValue $EnvFile 'BACKEND_EXTRAS'
if (Test-Path $envFile) {
foreach ($line in Get-Content $envFile) {
if ($line -match '^\s*BACKEND_EXTRAS\s*=\s*(.*?)\s*$') {
$BackendExtras = $Matches[1]
break
}
}
}
} }
$BackendExtraArgs = @() $BackendExtraArgs = @()
@@ -156,8 +178,8 @@ Write-Host ''
Write-Host '+----------------------------------------------+' -ForegroundColor Blue Write-Host '+----------------------------------------------+' -ForegroundColor Blue
Write-Host '| tickflow-stock-panel |' -ForegroundColor Blue Write-Host '| tickflow-stock-panel |' -ForegroundColor Blue
Write-Host '| |' -ForegroundColor Blue Write-Host '| |' -ForegroundColor Blue
Write-Host "| backend http://localhost:$BackendPort" -ForegroundColor Blue Write-Host "| backend http://${DisplayHost}:$BackendPort" -ForegroundColor Blue
Write-Host "| frontend http://localhost:$FrontendPort" -ForegroundColor Blue Write-Host "| frontend http://${DisplayHost}:$FrontendPort" -ForegroundColor Blue
Write-Host '| |' -ForegroundColor Blue Write-Host '| |' -ForegroundColor Blue
Write-Host '| Ctrl-C closes both |' -ForegroundColor Blue Write-Host '| Ctrl-C closes both |' -ForegroundColor Blue
Write-Host '+----------------------------------------------+' -ForegroundColor Blue Write-Host '+----------------------------------------------+' -ForegroundColor Blue
@@ -170,7 +192,7 @@ $backendPidFile = [System.IO.Path]::GetTempFileName()
$frontendPidFile = [System.IO.Path]::GetTempFileName() $frontendPidFile = [System.IO.Path]::GetTempFileName()
$backendJob = Start-Job -Name 'backend' -ScriptBlock { $backendJob = Start-Job -Name 'backend' -ScriptBlock {
param($pidFile, $dir, $port) param($pidFile, $dir, $envFile, $bindAddress, $port)
# Start-Job 开的是全新 powershell.exe 子进程, 不继承主进程的 UTF-8 设置, # Start-Job 开的是全新 powershell.exe 子进程, 不继承主进程的 UTF-8 设置,
# 默认用系统 ANSI (中文 Windows = GBK/cp936) 解码后端 UTF-8 输出 → 中文乱码。 # 默认用系统 ANSI (中文 Windows = GBK/cp936) 解码后端 UTF-8 输出 → 中文乱码。
# 这里强制子进程用 UTF-8, 与 app/__init__.py 的 stdout/stderr 编码对齐。 # 这里强制子进程用 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 $PID | Out-File -FilePath $pidFile -Encoding ascii -Force
$env:PYTHONUNBUFFERED = '1' $env:PYTHONUNBUFFERED = '1'
Set-Location $dir Set-Location $dir
& .\.venv\Scripts\python.exe -m uvicorn app.main:app --reload --host 0.0.0.0 --port $port 2>&1 $envArgs = if (Test-Path $envFile) { @('--env-file', $envFile) } else { @() }
} -ArgumentList $backendPidFile, $BackendDir, $BackendPort & .\.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 { $frontendJob = Start-Job -Name 'frontend' -ScriptBlock {
param($pidFile, $dir, $port) param($pidFile, $dir, $bindAddress, $backendPort, $port)
# 同上: job 子进程默认 GBK, pnpm/前端工具链也是 UTF-8 输出, 需对齐。 # 同上: job 子进程默认 GBK, pnpm/前端工具链也是 UTF-8 输出, 需对齐。
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false
$OutputEncoding = New-Object System.Text.UTF8Encoding $false $OutputEncoding = New-Object System.Text.UTF8Encoding $false
$PID | Out-File -FilePath $pidFile -Encoding ascii -Force $PID | Out-File -FilePath $pidFile -Encoding ascii -Force
Set-Location $dir Set-Location $dir
& pnpm dev --host 0.0.0.0 --port $port 2>&1 $env:BACKEND_HOST = $bindAddress
} -ArgumentList $frontendPidFile, $FrontendDir, $FrontendPort $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 # Wait up to 5 seconds for the PID files to materialise
function Read-JobPid($file) { function Read-JobPid($file) {
+43 -6
View File
@@ -13,13 +13,48 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="$ROOT/backend" BACKEND_DIR="$ROOT/backend"
FRONTEND_DIR="$ROOT/frontend" 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}" 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' # Match Docker's BACKEND_EXTRAS behavior so old CPUs can select Polars'
# rtcompat runtime before the backend starts. An exported value wins over .env. # rtcompat runtime before the backend starts. An exported value wins over .env.
if [[ -z "${BACKEND_EXTRAS+x}" && -f "$ROOT/.env" ]]; then 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 fi
BACKEND_EXTRAS="${BACKEND_EXTRAS:-}" BACKEND_EXTRAS="${BACKEND_EXTRAS:-}"
BACKEND_EXTRA_ARGS=() BACKEND_EXTRA_ARGS=()
@@ -129,8 +164,8 @@ echo
echo -e "${BLUE}╭──────────────────────────────────────────────╮${NC}" echo -e "${BLUE}╭──────────────────────────────────────────────╮${NC}"
echo -e "${BLUE}${NC} ${GREEN}tickflow-stock-panel${NC} ${BLUE}${NC}" echo -e "${BLUE}${NC} ${GREEN}tickflow-stock-panel${NC} ${BLUE}${NC}"
echo -e "${BLUE}${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} backend ${YELLOW}http://$DISPLAY_HOST:$BACKEND_PORT${NC} ${BLUE}${NC}"
echo -e "${BLUE}${NC} frontend ${YELLOW}http://localhost:$FRONTEND_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} ${BLUE}${NC}"
echo -e "${BLUE}${NC} Ctrl-C 同时关闭两端 ${BLUE}${NC}" echo -e "${BLUE}${NC} Ctrl-C 同时关闭两端 ${BLUE}${NC}"
echo -e "${BLUE}╰──────────────────────────────────────────────╯${NC}" echo -e "${BLUE}╰──────────────────────────────────────────────╯${NC}"
@@ -138,14 +173,16 @@ echo
( (
cd "$BACKEND_DIR" 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} ")" | prefix_awk "$(printf "${BLUE}[backend ]${NC} ")"
) & ) &
PIDS+=("$!") PIDS+=("$!")
( (
cd "$FRONTEND_DIR" 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} ")" | prefix_awk "$(printf "${GREEN}[frontend]${NC} ")"
) & ) &
PIDS+=("$!") PIDS+=("$!")
+3 -1
View File
@@ -10,7 +10,7 @@ services:
CODEX_CLI_VERSION: ${CODEX_CLI_VERSION:-0.144.3} CODEX_CLI_VERSION: ${CODEX_CLI_VERSION:-0.144.3}
container_name: TickFlow_Stock_Panel container_name: TickFlow_Stock_Panel
ports: ports:
- "${PORT:-3018}:3018" - "${HOST:-0.0.0.0}:${PORT:-3018}:3018"
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
env_file: env_file:
@@ -26,6 +26,8 @@ services:
volumes: volumes:
- ./data:/app/data - ./data:/app/data
- ./tiers.yaml:/app/tiers.yaml:ro - ./tiers.yaml:/app/tiers.yaml:ro
# 保留原始 dotenv 值供首次密码初始化读取,避免 Compose 展开密码中的 $VAR。
- ./.env:/app/.env:ro
# 复用主机 Codex 登录态;后端只读后复制到单次请求的临时 CODEX_HOME。 # 复用主机 Codex 登录态;后端只读后复制到单次请求的临时 CODEX_HOME。
# Windows PowerShell/CMD 下 HOME 常未设置, 可通过 .env 里 CODEX_HOME_HOST 覆盖。 # Windows PowerShell/CMD 下 HOME 常未设置, 可通过 .env 里 CODEX_HOME_HOST 覆盖。
- ${CODEX_HOME_HOST:-${HOME}/.codex}:/root/.codex:ro - ${CODEX_HOME_HOST:-${HOME}/.codex}:/root/.codex:ro
+5 -4
View File
@@ -58,13 +58,13 @@ AI_DAILY_TOKEN_BUDGET=500000 # 每日 token 预算上限
## 服务 ## 服务
```ini ```ini
HOST=0.0.0.0 # 监听地址 HOST=0.0.0.0 # 开发服务监听地址 / Docker 主机绑定地址
PORT=3018 # 服务端口 PORT=3018 # 开发后端端口 / Docker 主机映射端口
LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERROR LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERROR
``` ```
- `HOST`:`0.0.0.0` 监听所有网卡(容器/公网部署需要);仅本机用可设 `127.0.0.1` - `HOST`:`0.0.0.0` 监听所有网卡(容器/公网部署需要);仅本机用可设 `127.0.0.1`
- `PORT`:默认 `3018`,改端口后 Docker 映射、SSH 转发命令里的端口也要同步改 - `PORT`:默认 `3018`;开发模式兼容显式的 `BACKEND_PORT` 覆盖,改端口后 SSH 转发命令也要同步改
- `LOG_LEVEL`:排查问题时改 `DEBUG` - `LOG_LEVEL`:排查问题时改 `DEBUG`
--- ---
@@ -84,10 +84,11 @@ DATA_DIR=./data # Parquet / DuckDB 数据存储目录
## 访问密码(公网部署) ## 访问密码(公网部署)
```ini ```ini
AUTH_PASSWORD=你的密码 # 至少 6 位;仅首次生效,已设过则不覆盖 AUTH_PASSWORD='你的密码' # 至少 6 位;仅首次生效,已设过则不覆盖
``` ```
面板首次设置访问密码时,出于安全考虑**仅允许本机或内网访问**(防公网陌生人抢先设置锁死面板)。公网服务器部署可通过此环境变量预置首个密码。 面板首次设置访问密码时,出于安全考虑**仅允许本机或内网访问**(防公网陌生人抢先设置锁死面板)。公网服务器部署可通过此环境变量预置首个密码。
密码建议使用单引号包裹,避免 Docker Compose 插值 `$VAR`;Docker 启动时也会只读挂载原始 `.env`,兼容已有的未加引号配置。
详细步骤、SSH 转发方案、重置密码方法见 [deployment.md → 访问密码设置](./deployment.md#访问密码设置公网部署必读)。 详细步骤、SSH 转发方案、重置密码方法见 [deployment.md → 访问密码设置](./deployment.md#访问密码设置公网部署必读)。
+2 -1
View File
@@ -16,7 +16,7 @@
```bash ```bash
# 编辑服务器上的 .env (通常在项目根目录或 backend/ 下) # 编辑服务器上的 .env (通常在项目根目录或 backend/ 下)
AUTH_PASSWORD=你的密码 AUTH_PASSWORD='你的密码'
``` ```
然后重启服务。启动时会自动: 然后重启服务。启动时会自动:
@@ -30,6 +30,7 @@ AUTH_PASSWORD=你的密码
- **密码至少 6 位**,否则会被跳过并记一条 warning 日志 - **密码至少 6 位**,否则会被跳过并记一条 warning 日志
- **仅在未设过密码时生效**。已设过密码后,改这里不会覆盖(避免重启时重置你在 UI 改的密码) - **仅在未设过密码时生效**。已设过密码后,改这里不会覆盖(避免重启时重置你在 UI 改的密码)
- 密码建议使用单引号包裹,避免 Docker Compose 插值 `$VAR`;启动时也会从只读挂载的原始 `.env` 初始化,兼容已有的未加引号配置
- `.env` 文件权限保持 `600`,**不要提交到 Git** - `.env` 文件权限保持 `600`,**不要提交到 Git**
- 明文密码只存在于 `.env` / 环境变量中,落盘的是哈希,安全性等同 `auth.json` - 明文密码只存在于 `.env` / 环境变量中,落盘的是哈希,安全性等同 `auth.json`
+2 -1
View File
@@ -123,7 +123,7 @@ git pull
`.env` 文件(或 Docker / 系统环境变量)里设置 `AUTH_PASSWORD`: `.env` 文件(或 Docker / 系统环境变量)里设置 `AUTH_PASSWORD`:
```bash ```bash
AUTH_PASSWORD=你的密码 AUTH_PASSWORD='你的密码'
``` ```
然后重启服务。启动时会自动: 然后重启服务。启动时会自动:
@@ -138,6 +138,7 @@ AUTH_PASSWORD=你的密码
- **密码至少 6 位**,否则会被跳过并记一条 warning 日志 - **密码至少 6 位**,否则会被跳过并记一条 warning 日志
- **仅在未设过密码时生效**。已设过密码后,改这里不会覆盖(避免重启时重置你在 UI 改的密码) - **仅在未设过密码时生效**。已设过密码后,改这里不会覆盖(避免重启时重置你在 UI 改的密码)
- 密码建议使用单引号包裹,避免 Docker Compose 插值 `$VAR`;启动时也会从只读挂载的原始 `.env` 初始化,兼容已有的未加引号配置
- `.env` 文件权限保持 `600`,**不要提交到 Git** - `.env` 文件权限保持 `600`,**不要提交到 Git**
- 明文密码只存在于 `.env` / 环境变量中,落盘的是哈希,安全性等同 `auth.json` - 明文密码只存在于 `.env` / 环境变量中,落盘的是哈希,安全性等同 `auth.json`
+1 -1
View File
@@ -1,6 +1,6 @@
// 后端 API 客户端 — 全项目统一入口 // 后端 API 客户端 — 全项目统一入口
// //
// Dev:Vite 代理 /api 到 :3018 // Dev: Vite 按启动脚本解析出的 BACKEND_HOST/BACKEND_PORT 代理 /api
// Prod:同源(FastAPI 托管前端 dist) // Prod:同源(FastAPI 托管前端 dist)
import { toast } from '@/components/Toast' import { toast } from '@/components/Toast'
+8 -4
View File
@@ -1,6 +1,10 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import path from 'node:path'; 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({ export default defineConfig({
plugins: [react()], plugins: [react()],
resolve: { resolve: {
@@ -9,12 +13,12 @@ export default defineConfig({
}, },
}, },
server: { server: {
host: '0.0.0.0', // 允许局域网访问 host: '0.0.0.0', // dev.sh / dev.ps1 会用 CLI --host 覆盖
port: 3011, port: 3011,
proxy: { proxy: {
// dev 时 /api 转发到 FastAPI // dev 时 /api 转发到与启动脚本相同的 FastAPI 地址
'/api': { '/api': {
target: 'http://localhost:3018', target: backendTarget,
// SSE 端点需要禁用缓冲 // SSE 端点需要禁用缓冲
configure: (proxy) => { configure: (proxy) => {
proxy.on('proxyReq', (_proxyReq, req) => { proxy.on('proxyReq', (_proxyReq, req) => {
@@ -26,7 +30,7 @@ export default defineConfig({
}); });
}, },
}, },
'/health': 'http://localhost:3018', '/health': backendTarget,
}, },
}, },
build: { build: {
+9 -4
View File
@@ -2,6 +2,11 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import path from 'node:path' 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({ export default defineConfig({
plugins: [react()], plugins: [react()],
resolve: { resolve: {
@@ -10,12 +15,12 @@ export default defineConfig({
}, },
}, },
server: { server: {
host: '0.0.0.0', // 允许局域网访问 host: '0.0.0.0', // dev.sh / dev.ps1 会用 CLI --host 覆盖
port: 3011, port: 3011,
proxy: { proxy: {
// dev 时 /api 转发到 FastAPI // dev 时 /api 转发到与启动脚本相同的 FastAPI 地址
'/api': { '/api': {
target: 'http://localhost:3018', target: backendTarget,
// SSE 端点需要禁用缓冲 // SSE 端点需要禁用缓冲
configure: (proxy) => { configure: (proxy) => {
proxy.on('proxyReq', (_proxyReq, req) => { proxy.on('proxyReq', (_proxyReq, req) => {
@@ -27,7 +32,7 @@ export default defineConfig({
}) })
}, },
}, },
'/health': 'http://localhost:3018', '/health': backendTarget,
}, },
}, },
build: { build: {