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

View File

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

View File

@ -555,7 +555,11 @@ def load_dotenv(
key, _, value = line.partition("=")
key = key.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
# Apply allowlist if provided
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.
"""
config_path = str(config_path)
dotenv = Path(config_path).parent / ".env"
load_dotenv(dotenv)
config_dir = Path(config_path).parent
# 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)