fix(dev): isolate dev environment ports and fix env loading

- docker-compose.yaml: production mode uses expose (container-only) for
  Redis/PostgreSQL instead of ports (host-mapped)
- docker-compose.dev.yml: dev override maps Redis 6381 and PostgreSQL 5435
  to avoid conflicts with other projects (pms-redis 6379, geo_redis 6380,
  geo_db 5433)
- config.py: fix empty env var handling — only skip .env override when
  os.environ[key] is non-empty; load .env, .env.dev, .env.local in sequence
- scripts/dev-start.sh: manage agentkit-specific Docker containers
- .gitignore: add .env.dev and .env.local (contain API keys)
This commit is contained in:
chiguyong 2026-07-02 21:23:50 +08:00
parent 754d70623c
commit 484b7ddb95
5 changed files with 104 additions and 65 deletions

2
.gitignore vendored
View File

@ -44,6 +44,8 @@ src/agentkit/server/static/
# Env # Env
.env .env
.env.dev
.env.local
# Runtime data (auth DB, conversation DB, etc.) # Runtime data (auth DB, conversation DB, etc.)
data/ data/

27
docker-compose.dev.yml Normal file
View File

@ -0,0 +1,27 @@
version: "3.8"
# =============================================================================
# 开发环境 override
# =============================================================================
# 用法docker compose -f docker-compose.yaml -f docker-compose.dev.yml up -d redis postgres
#
# 仅启动 redis + postgresagentkit 在宿主机运行),映射端口到 6381/5435
# 避免与 pms-redis(6379) / geo_redis(6380) / geo_db(5433) 端口冲突
#
# .env.dev 应包含:
# REDIS_URL=redis://127.0.0.1:6381/0
# DATABASE_URL=postgresql+asyncpg://agentkit:agentkit@127.0.0.1:5435/agentkit
# =============================================================================
services:
# 开发模式不启动 agentkit 容器(在宿主机运行)
agentkit:
profiles: ["never"]
redis:
ports:
- "6381:6379"
postgres:
ports:
- "5435:5432"

View File

@ -1,5 +1,12 @@
version: "3.8" version: "3.8"
# =============================================================================
# 生产部署配置
# =============================================================================
# 启动docker compose up -d
# agentkit 容器内通过 service name (redis/postgres) 连接,不暴露中间件端口
# =============================================================================
services: services:
agentkit: agentkit:
build: . build: .
@ -8,8 +15,12 @@ services:
- "8001:8001" - "8001:8001"
env_file: .env env_file: .env
environment: environment:
# 容器间通信:使用 service name不依赖宿主机端口
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- DATABASE_URL=postgresql+asyncpg://agentkit:agentkit@postgres:5432/agentkit - DATABASE_URL=postgresql+asyncpg://agentkit:agentkit@postgres:5432/agentkit
- AGENTKIT_BUS_BACKEND=redis
- AGENTKIT_SESSION_BACKEND=redis
- AGENTKIT_TASK_STORE_BACKEND=redis
depends_on: depends_on:
redis: redis:
condition: service_healthy condition: service_healthy
@ -25,8 +36,9 @@ services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
ports: # 生产模式不暴露端口到宿主机,仅容器间可达
- "6379:6379" expose:
- "6379"
volumes: volumes:
- redisdata:/data - redisdata:/data
healthcheck: healthcheck:
@ -38,8 +50,9 @@ services:
postgres: postgres:
image: pgvector/pgvector:pg15 image: pgvector/pgvector:pg15
ports: # 生产模式不暴露端口到宿主机,仅容器间可达
- "5432:5432" expose:
- "5432"
environment: environment:
POSTGRES_USER: agentkit POSTGRES_USER: agentkit
POSTGRES_PASSWORD: agentkit POSTGRES_PASSWORD: agentkit

View File

@ -17,6 +17,14 @@
# Python >= 3.11, Node.js >= 18, Redis, PostgreSQL (均自动检查) # Python >= 3.11, Node.js >= 18, Redis, PostgreSQL (均自动检查)
# --tauri 需要Rust 工具链rustup / brew install rust # --tauri 需要Rust 工具链rustup / brew install rust
# #
# 端口(与 .env.dev 保持一致):
# 18001 — AgentKit 后端 API
# 18002 — Web GUI前端 + 内置静态服务)
# 15173 — Vite 开发服务器(--tauri 模式)
# 15174 — Vite HMR websocket
# 6381 — Redisagentkit 专属容器,避免与 pms-redis:6379 / geo_redis:6380 冲突)
# 5435 — PostgreSQL+pgvectoragentkit 专属容器,避免与 geo_db:5433 冲突)
#
# ============================================================================= # =============================================================================
set -euo pipefail set -euo pipefail
@ -49,12 +57,14 @@ Fischer AgentKit — 本地开发环境启动
模式说明: 模式说明:
默认 Web 模式agentkit gui (前后端 + 内置静态服务) 默认 Web 模式agentkit gui (前后端 + 内置静态服务)
--tauri Tauri 模式:后端 API + Vite (:5173) + Tauri 桌面窗口 --tauri Tauri 模式:后端 API + Vite (:15173) + Tauri 桌面窗口
端口映射: 端口映射:
8000 — 后端 API 18001 — 后端 API
8002 — Web GUI / 前端静态服务 18002 — Web GUI / 前端静态服务
5173 — Vite 开发服务器(--tauri 模式) 15173 — Vite 开发服务器(--tauri 模式)
6381 — Redisagentkit 专属容器)
5435 — PostgreSQL+pgvectoragentkit 专属容器)
EOF EOF
} }
@ -112,11 +122,11 @@ print_status() {
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo -e "$([[ $S_DEPS -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_DEPS -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 依赖检查" echo -e "$([[ $S_DEPS -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_DEPS -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 依赖检查"
echo -e "$([[ $S_ENV -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_ENV -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 环境配置" echo -e "$([[ $S_ENV -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_ENV -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 环境配置"
echo -e "$([[ $S_REDIS -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_REDIS -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") Redis" echo -e "$([[ $S_REDIS -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_REDIS -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") Redis (:6381)"
echo -e "$([[ $S_PG -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_PG -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") PostgreSQL" echo -e "$([[ $S_PG -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_PG -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") PostgreSQL (:5435)"
echo -e "$([[ $S_BACKEND -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_BACKEND -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 后端服务 (:8000)" echo -e "$([[ $S_BACKEND -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_BACKEND -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 后端服务 (:18001)"
if [[ $MODE == "gui" || $MODE == "tauri" ]]; then if [[ $MODE == "gui" || $MODE == "tauri" ]]; then
echo -e "$([[ $S_FRONTEND -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_FRONTEND -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 前端服务 (:8002)" echo -e "$([[ $S_FRONTEND -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_FRONTEND -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") 前端服务 (:18002)"
fi fi
if [[ $MODE == "tauri" ]]; then if [[ $MODE == "tauri" ]]; then
echo -e "$([[ $S_TAURI -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_TAURI -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") Tauri 客户端" echo -e "$([[ $S_TAURI -eq 2 ]] && echo " ${GREEN}${NC}" || [[ $S_TAURI -eq 3 ]] && echo " ${RED}${NC}" || echo " ${YELLOW}${NC}") Tauri 客户端"
@ -180,30 +190,23 @@ check_redis() {
section "检查 Redis" section "检查 Redis"
set_status redis 1 set_status redis 1
if command -v redis-cli &>/dev/null && redis-cli ping 2>/dev/null | grep -q PONG; then # agentkit 专属容器映射到 6381避免与 pms-redis(6379) / geo_redis(6380) 冲突
ok "Redis 运行中" if redis-cli -h 127.0.0.1 -p 6381 ping 2>/dev/null | grep -q PONG; then
ok "Redis 运行中 (127.0.0.1:6381, agentkit 专属容器)"
set_status redis 2 set_status redis 2
return 0 return 0
fi fi
# Docker 方式 warn "Redis 未运行 (6381),启动 agentkit 专属 Docker 容器..."
local name="fischer-redis-dev" if docker compose -f docker-compose.yaml -f docker-compose.dev.yml up -d redis &>/dev/null; then
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$"; then
ok "Redis 容器运行中"
set_status redis 2
return 0
fi
warn "Redis 未运行,尝试启动 Docker 容器..."
if docker run -d --name "$name" -p 6379:6379 redis:7-alpine &>/dev/null; then
sleep 2 sleep 2
if docker exec "$name" redis-cli ping 2>/dev/null | grep -q PONG; then if redis-cli -h 127.0.0.1 -p 6381 ping 2>/dev/null | grep -q PONG; then
ok "Redis Docker 容器启动成功 (:6379)" ok "Redis Docker 容器启动成功 (127.0.0.1:6381)"
set_status redis 2 set_status redis 2
return 0 return 0
fi fi
fi fi
fail "Redis 启动失败(请确保 Docker 运行中,或手动启动 Redis" fail "Redis 启动失败(请确保 Docker 运行中)"
set_status redis 3 set_status redis 3
return 1 return 1
} }
@ -212,30 +215,18 @@ check_postgres() {
section "检查 PostgreSQL" section "检查 PostgreSQL"
set_status pg 1 set_status pg 1
if lsof -i :5432 2>/dev/null | grep -q LISTEN; then # agentkit 专属容器映射到 5435避免与 geo_db(5433) 冲突
ok "PostgreSQL 已在 :5432 监听" if PGPASSWORD=agentkit psql -h 127.0.0.1 -p 5435 -U agentkit -d agentkit -c "SELECT 1" &>/dev/null; then
ok "PostgreSQL 运行中 (127.0.0.1:5435, agentkit 专属容器)"
set_status pg 2 set_status pg 2
return 0 return 0
fi fi
# Docker 方式 warn "PostgreSQL 未运行 (5435),启动 agentkit 专属 Docker 容器..."
local name="fischer-pg-dev" if docker compose -f docker-compose.yaml -f docker-compose.dev.yml up -d postgres &>/dev/null; then
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$"; then
ok "PostgreSQL Docker 容器运行中"
set_status pg 2
return 0
fi
warn "PostgreSQL 未在 :5432 运行,尝试启动 Docker 容器..."
if docker run -d --name "$name" \
-p 5432:5432 \
-e POSTGRES_USER=agentkit \
-e POSTGRES_PASSWORD=agentkit \
-e POSTGRES_DB=agentkit \
pgvector/pgvector:pg15 &>/dev/null; then
sleep 3 sleep 3
if docker exec "$name" pg_isready -U agentkit &>/dev/null; then if PGPASSWORD=agentkit psql -h 127.0.0.1 -p 5435 -U agentkit -d agentkit -c "SELECT 1" &>/dev/null; then
ok "PostgreSQL Docker 容器启动成功 (:5432)" ok "PostgreSQL Docker 容器启动成功 (127.0.0.1:5435)"
set_status pg 2 set_status pg 2
return 0 return 0
fi fi
@ -281,17 +272,17 @@ start_backend() {
section "启动后端服务" section "启动后端服务"
set_status backend 1 set_status backend 1
info "启动后端 API (:8000)..." info "启动后端 API (:18001)..."
source .venv/bin/activate source .venv/bin/activate
agentkit serve --port 8000 & agentkit serve --port 18001 &
BACKEND_PID=$! BACKEND_PID=$!
# 等待健康检查就绪(最多 60 秒) # 等待健康检查就绪(最多 60 秒)
info "等待后端就绪..." info "等待后端就绪..."
local attempt=0 local attempt=0
while [[ $attempt -lt 60 ]]; do while [[ $attempt -lt 60 ]]; do
if curl -sf http://127.0.0.1:8000/api/v1/health &>/dev/null; then if curl -sf http://127.0.0.1:18001/api/v1/health &>/dev/null; then
ok "后端 API 就绪 (http://127.0.0.1:8000, PID $BACKEND_PID)" ok "后端 API 就绪 (http://127.0.0.1:18001, PID $BACKEND_PID)"
set_status backend 2 set_status backend 2
return 0 return 0
fi fi
@ -318,17 +309,17 @@ start_gui() {
section "启动 Web GUI" section "启动 Web GUI"
set_status frontend 1 set_status frontend 1
info "启动 Web GUI (:8002)..." info "启动 Web GUI (:18002)..."
source .venv/bin/activate source .venv/bin/activate
agentkit gui --port 8002 & agentkit gui --port 18002 &
GUI_PID=$! GUI_PID=$!
# 等待就绪 # 等待就绪
info "等待 Web GUI 就绪..." info "等待 Web GUI 就绪..."
local attempt=0 local attempt=0
while [[ $attempt -lt 60 ]]; do while [[ $attempt -lt 60 ]]; do
if curl -sf http://127.0.0.1:8002/api/v1/health &>/dev/null; then if curl -sf http://127.0.0.1:18002/api/v1/health &>/dev/null; then
ok "Web GUI 就绪 (http://127.0.0.1:8002, PID $GUI_PID)" ok "Web GUI 就绪 (http://127.0.0.1:18002, PID $GUI_PID)"
set_status frontend 2 set_status frontend 2
return 0 return 0
fi fi
@ -364,7 +355,7 @@ start_tauri() {
fi fi
info "启动 Tauri 桌面客户端..." info "启动 Tauri 桌面客户端..."
info " (Vite → :5173, 后端 API → :8000)" info " (Vite → :15173, 后端 API → :18001)"
cd "$FE_DIR" cd "$FE_DIR"
npm run tauri dev & npm run tauri dev &
TAURI_PID=$! TAURI_PID=$!
@ -400,7 +391,7 @@ stop_services() {
echo "" echo ""
info "正在停止所有服务..." info "正在停止所有服务..."
for port in 8000 8001 8002 5173; do for port in 18001 18002 15173 15174; do
local pid=$(lsof -ti :$port 2>/dev/null || true) local pid=$(lsof -ti :$port 2>/dev/null || true)
if [[ -n "$pid" ]]; then if [[ -n "$pid" ]]; then
kill $pid 2>/dev/null && ok "端口 $port 已停止" || true kill $pid 2>/dev/null && ok "端口 $port 已停止" || true
@ -474,14 +465,14 @@ if [[ $FAILED -eq 0 ]]; then
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo "" echo ""
if [[ $MODE == "gui" ]]; then if [[ $MODE == "gui" ]]; then
echo -e " Web GUI: ${GREEN}http://localhost:8002${NC}" echo -e " Web GUI: ${GREEN}http://localhost:18002${NC}"
echo " (在浏览器中打开,或直接在 http://localhost:8002 访问)" echo " (在浏览器中打开,或直接在 http://localhost:18002 访问)"
elif [[ $MODE == "tauri" ]]; then elif [[ $MODE == "tauri" ]]; then
echo -e " 后端 API: ${GREEN}http://localhost:8000${NC}" echo -e " 后端 API: ${GREEN}http://localhost:18001${NC}"
echo -e " Vite 热重载: ${GREEN}http://localhost:5173${NC}" echo -e " Vite 热重载: ${GREEN}http://localhost:15173${NC}"
echo " Tauri 桌面窗口应已自动打开" echo " Tauri 桌面窗口应已自动打开"
elif [[ $MODE == "serve" ]]; then elif [[ $MODE == "serve" ]]; then
echo -e " 后端 API: ${GREEN}http://localhost:8000${NC}" echo -e " 后端 API: ${GREEN}http://localhost:18001${NC}"
fi fi
echo "" echo ""
echo -e " ${YELLOW}按 Ctrl+C 停止所有服务${NC}" echo -e " ${YELLOW}按 Ctrl+C 停止所有服务${NC}"
@ -491,7 +482,7 @@ else
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo "" echo ""
echo -e " 诊断命令:" echo -e " 诊断命令:"
echo -e " 查看日志: ${CYAN}curl http://127.0.0.1:8000/api/v1/health${NC}" echo -e " 查看日志: ${CYAN}curl http://127.0.0.1:18001/api/v1/health${NC}"
echo -e " 停止服务: ${CYAN}bash scripts/dev-stop.sh${NC}" echo -e " 停止服务: ${CYAN}bash scripts/dev-stop.sh${NC}"
fi fi
echo "" echo ""

View File

@ -555,7 +555,11 @@ def load_dotenv(
key, _, value = line.partition("=") key, _, value = line.partition("=")
key = key.strip() key = key.strip()
value = value.strip().strip("\"'") value = value.strip().strip("\"'")
if not key or key in os.environ: # Skip only if key is set to a non-empty value in the environment.
# An empty/whitespace-only value (e.g. from a shell template like
# `${VAR:-}` that expanded to nothing) is treated as "not set" so
# subsequent .env files can still provide a real value.
if not key or (key in os.environ and os.environ[key].strip()):
continue continue
# Apply allowlist if provided # Apply allowlist if provided
if prefixes is not None or exact is not None: if prefixes is not None or exact is not None:
@ -579,8 +583,10 @@ def load_config_with_dotenv(config_path: str | Path) -> ServerConfig:
This is the canonical way to load config in all CLI commands and app factory. This is the canonical way to load config in all CLI commands and app factory.
""" """
config_path = str(config_path) config_path = str(config_path)
dotenv = Path(config_path).parent / ".env" config_dir = Path(config_path).parent
load_dotenv(dotenv) # Load .env, .env.dev, .env.local in order (first non-empty value wins).
for candidate in (".env", ".env.dev", ".env.local"):
load_dotenv(config_dir / candidate)
return ServerConfig.from_yaml(config_path) return ServerConfig.from_yaml(config_path)