- 对齐「多智能体收敛」与「回忆录 stories-first / markdown-first」方向:收紧运行时契约、 删除过渡兼容路径与双轨逻辑,并同步更新客户端与文档。 - Chat:以 ChatOrchestrator 为实时编排入口;删除独立 conversation_agent,精简 prompts。 - Memoir:删除 memory_agent;MemoirOrchestrator、classification / story_route 与 prompts 收敛到 prepare_batches + run_story_pipeline_for_category_batch 主链路。 - 将 agents 侧 processor 迁入 feature 层为 background_runner,并移除 features 下重复/过时 processor 封装。 - 新增 history_store,强化「conversation_messages 为 DB 真源、Redis 为缓存」模型。 - 调整 models、repo、service、session_history;精简 WS message_types,重构 pipeline 与 router。 - 移除章节占位、整章再生等旧路径;章节列表与封面逻辑要求 story 关联;收紧 cover 资格与 enqueue。 - helpers、repo、service、router、reading_segment_materialize、story_pipeline_sync、pdf_service 等按 canonical markdown / cover_asset_id 收缩;删除 memoir_images/provider 等冗余。 - tasks:memoir_tasks、chapter_cover_tasks 等大幅瘦身;story_image_tasks 等与当前图片任务对齐。 - core:config、logging、redis、task_tracker 小幅调整。 - auth / user / payment / quota:路由或服务侧删减过时接口或逻辑(如 payment router 行数减少)。 - pyproject.toml、development.sh、.env.example / .env.production、README 等同步说明或变量。 - Alembic 0001_initial_schema 微调(与当前 schema 叙事一致的小改动)。 - 回忆录:types / mappers / api、章节页与 memoir 页与后端契约对齐;markdown-renderer 调整。 - 语音:删除 voice/player,voice-segment-store 相应精简。 - api/tests:删除 conftest 及绝大部分既有测试文件(websocket_baseline、conversation、memoir 图片、PDF、SMS 等),属有意收缩/待按 backend-test-system 重建的信号。 - docs:新增多智能体收敛与移除兼容层计划摘要;更新 story-first 设计、backend-test-system、 multi-agent-refactor-plan、实施总结等。 BREAKING CHANGE: 后端对外契约、回忆录章节字段与若干路由/任务行为已变更;大量 API 测试被移除, CI 若依赖这些用例需按新策略补测或调整流水线。
253 lines
5.9 KiB
Bash
Executable File
253 lines
5.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
|
||
set -euo pipefail
|
||
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m'
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
VENV_DIR="${ROOT_DIR}/.venv"
|
||
PYTHON_BIN="${VENV_DIR}/bin/python"
|
||
UVICORN_BIN="${VENV_DIR}/bin/uvicorn"
|
||
CELERY_BIN="${VENV_DIR}/bin/celery"
|
||
|
||
API_HOST="${API_HOST:-0.0.0.0}"
|
||
API_PORT="${API_PORT:-8000}"
|
||
CELERY_POOL="${CELERY_POOL:-solo}"
|
||
SKIP_INSTALL="${SKIP_INSTALL:-0}"
|
||
SHUTDOWN_TIMEOUT="${SHUTDOWN_TIMEOUT:-12}"
|
||
|
||
API_PID=""
|
||
CELERY_PID=""
|
||
CLEANED_UP=0
|
||
INFRA_STARTED=0
|
||
|
||
print_header() {
|
||
echo -e "\n${BLUE}========================================${NC}"
|
||
echo -e "${BLUE}$1${NC}"
|
||
echo -e "${BLUE}========================================${NC}"
|
||
}
|
||
|
||
print_ok() {
|
||
echo -e "${GREEN}✓ $1${NC}"
|
||
}
|
||
|
||
print_warn() {
|
||
echo -e "${YELLOW}⚠ $1${NC}"
|
||
}
|
||
|
||
print_err() {
|
||
echo -e "${RED}✗ $1${NC}"
|
||
}
|
||
|
||
is_pid_alive() {
|
||
local pid="$1"
|
||
[[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null
|
||
}
|
||
|
||
wait_pid_exit() {
|
||
local pid="$1"
|
||
local timeout="$2"
|
||
local waited=0
|
||
|
||
while is_pid_alive "${pid}"; do
|
||
if (( waited >= timeout )); then
|
||
return 1
|
||
fi
|
||
sleep 1
|
||
waited=$((waited + 1))
|
||
done
|
||
return 0
|
||
}
|
||
|
||
kill_children_term() {
|
||
local pid="$1"
|
||
local children
|
||
|
||
children="$(pgrep -P "${pid}" 2>/dev/null || true)"
|
||
if [[ -n "${children}" ]]; then
|
||
# 先递归处理子进程,避免 reloader/server 残留
|
||
while IFS= read -r child_pid; do
|
||
[[ -z "${child_pid}" ]] && continue
|
||
kill_children_term "${child_pid}"
|
||
kill -TERM "${child_pid}" 2>/dev/null || true
|
||
done <<< "${children}"
|
||
fi
|
||
}
|
||
|
||
stop_process_gracefully() {
|
||
local name="$1"
|
||
local pid="$2"
|
||
local timeout="${3:-10}"
|
||
|
||
if ! is_pid_alive "${pid}"; then
|
||
print_ok "${name} 已退出"
|
||
return 0
|
||
fi
|
||
|
||
print_warn "正在停止 ${name}(PID: ${pid})..."
|
||
kill_children_term "${pid}"
|
||
kill -TERM "${pid}" 2>/dev/null || true
|
||
|
||
if wait_pid_exit "${pid}" "${timeout}"; then
|
||
print_ok "${name} 已停止"
|
||
return 0
|
||
fi
|
||
|
||
print_warn "${name} 在 ${timeout}s 内未退出,准备强制结束"
|
||
kill -KILL "${pid}" 2>/dev/null || true
|
||
wait_pid_exit "${pid}" 3 || true
|
||
print_ok "${name} 已强制结束"
|
||
}
|
||
|
||
cleanup() {
|
||
if [[ "${CLEANED_UP}" == "1" ]]; then
|
||
return 0
|
||
fi
|
||
CLEANED_UP=1
|
||
|
||
print_header "正在关闭开发环境"
|
||
|
||
if is_pid_alive "${API_PID}"; then
|
||
stop_process_gracefully "FastAPI" "${API_PID}" "${SHUTDOWN_TIMEOUT}"
|
||
fi
|
||
|
||
if is_pid_alive "${CELERY_PID}"; then
|
||
stop_process_gracefully "Celery" "${CELERY_PID}" "${SHUTDOWN_TIMEOUT}"
|
||
fi
|
||
|
||
if [[ "${INFRA_STARTED}" == "1" ]]; then
|
||
print_warn "正在停止 PostgreSQL / Redis 容器..."
|
||
(
|
||
cd "${ROOT_DIR}" && docker compose -f docker-compose.dev.yml stop
|
||
) >/dev/null 2>&1 || true
|
||
print_ok "PostgreSQL/Redis 容器已停止"
|
||
fi
|
||
}
|
||
|
||
require_cmd() {
|
||
local cmd="$1"
|
||
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
||
print_err "未找到命令: ${cmd}"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
start_infra() {
|
||
print_header "启动 PostgreSQL 和 Redis"
|
||
cd "${ROOT_DIR}"
|
||
docker compose -f docker-compose.dev.yml up -d
|
||
INFRA_STARTED=1
|
||
print_ok "基础设施已就绪"
|
||
}
|
||
|
||
# Docker 刚启动时 Postgres 可能尚未接受连接,立即跑 Alembic 会误报失败
|
||
wait_postgres_ready() {
|
||
local retries=30
|
||
local i=0
|
||
print_header "等待 PostgreSQL 就绪"
|
||
cd "${ROOT_DIR}"
|
||
while (( i < retries )); do
|
||
if docker compose -f docker-compose.dev.yml exec -T postgres \
|
||
pg_isready -U postgres >/dev/null 2>&1; then
|
||
print_ok "PostgreSQL 已就绪"
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
i=$((i + 1))
|
||
done
|
||
print_warn "PostgreSQL 在 ${retries}s 内未就绪,迁移可能失败"
|
||
return 1
|
||
}
|
||
|
||
ensure_venv() {
|
||
print_header "检查 Python 虚拟环境"
|
||
|
||
if [[ ! -d "${VENV_DIR}" ]]; then
|
||
print_warn ".venv 不存在,正在创建"
|
||
uv venv "${VENV_DIR}"
|
||
fi
|
||
|
||
if [[ "${SKIP_INSTALL}" != "1" ]]; then
|
||
print_header "安装 Python 依赖"
|
||
uv sync
|
||
print_ok "依赖安装完成"
|
||
else
|
||
print_warn "已跳过依赖安装 (SKIP_INSTALL=1)"
|
||
fi
|
||
}
|
||
|
||
check_env_file() {
|
||
print_header "检查环境变量文件"
|
||
if [[ ! -f "${ROOT_DIR}/.env" ]]; then
|
||
print_warn "未找到 .env,应用可能因缺少配置启动失败"
|
||
print_warn "请参考 api/README.md 创建 .env"
|
||
else
|
||
print_ok "检测到 .env"
|
||
fi
|
||
}
|
||
|
||
run_migrations() {
|
||
print_header "执行数据库迁移"
|
||
cd "${ROOT_DIR}"
|
||
if uv run alembic upgrade head 2>/dev/null; then
|
||
print_ok "Alembic 迁移已就绪"
|
||
else
|
||
print_warn "Alembic 迁移失败(可能数据库未启动或 DATABASE_URL 未配置),应用启动可能失败"
|
||
fi
|
||
}
|
||
|
||
start_services() {
|
||
print_header "启动 FastAPI 和 Celery"
|
||
cd "${ROOT_DIR}"
|
||
|
||
if command -v lsof >/dev/null 2>&1; then
|
||
if lsof -nP -iTCP:"${API_PORT}" -sTCP:LISTEN >/dev/null 2>&1; then
|
||
print_err "端口 ${API_PORT} 已被占用,无法启动新的 Uvicorn。"
|
||
print_err "请先结束占用进程,例如: lsof -nP -iTCP:${API_PORT} -sTCP:LISTEN"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# 迁移由 main.py 在启动时执行;排除 alembic 目录与 alembic.ini,避免编辑迁移时触发整进程重载
|
||
"${UVICORN_BIN}" main:app --reload \
|
||
--reload-exclude 'alembic/**' \
|
||
--reload-exclude 'alembic.ini' \
|
||
--host "${API_HOST}" --port "${API_PORT}" &
|
||
API_PID=$!
|
||
print_ok "FastAPI 已启动 (PID: ${API_PID})"
|
||
|
||
"${CELERY_BIN}" -A app.tasks.celery_app worker --loglevel=info --pool="${CELERY_POOL}" &
|
||
CELERY_PID=$!
|
||
print_ok "Celery 已启动 (PID: ${CELERY_PID})"
|
||
|
||
echo
|
||
echo -e "${GREEN}开发环境启动完成${NC}"
|
||
echo "API 文档: http://localhost:${API_PORT}/docs"
|
||
echo "健康检查: http://localhost:${API_PORT}/health"
|
||
echo "按 Ctrl+C 停止所有进程"
|
||
}
|
||
|
||
main() {
|
||
print_header "Life Echo 开发环境一键启动"
|
||
|
||
require_cmd "docker"
|
||
require_cmd "uv"
|
||
|
||
trap cleanup EXIT INT TERM
|
||
|
||
start_infra
|
||
wait_postgres_ready || true
|
||
ensure_venv
|
||
check_env_file
|
||
run_migrations
|
||
start_services
|
||
|
||
wait "${API_PID}" "${CELERY_PID}"
|
||
}
|
||
|
||
main "$@"
|