feat(api+app): 对话阶段化、回忆录流水线与客户端会话体验
- DB: segments 用户输入文本(Alembic 0002) - Chat: 阶段检测/阶段提示/回复限制,编排与访谈/画像 prompts 调整 - Memoir: 忠实度检查 agent,叙事与分类等链路更新 - Core: agent 日志、Alembic 启动、LangChain/日志/配置等 - Story: time_hints;Memory 检索与相关测试 - Expo: 助手头像、会话页与消息拆分、实时会话与文案/i18n - Docs/scripts/tests: 迁移脚本、LLM JSON/记忆检索文档、新增单测
This commit is contained in:
100
api/app/core/alembic_startup.py
Normal file
100
api/app/core/alembic_startup.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
启动时执行 Alembic 迁移(与 CLI `uv run alembic upgrade head` 等价)。
|
||||
|
||||
- 对瞬时连接错误重试(数据库尚未就绪、网络抖动)。
|
||||
- 可通过 settings 关闭迁移、或失败时中止进程(生产推荐)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
_API_DIR: Final[Path] = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
|
||||
def _run_alembic_upgrade_once() -> None:
|
||||
from alembic.command import upgrade
|
||||
from alembic.config import Config
|
||||
|
||||
cfg = Config(str(_API_DIR / "alembic.ini"))
|
||||
upgrade(cfg, "head")
|
||||
|
||||
|
||||
def _is_retryable_migration_error(err: BaseException) -> bool:
|
||||
if isinstance(err, OperationalError):
|
||||
return True
|
||||
msg = str(err).lower()
|
||||
needles = (
|
||||
"connection refused",
|
||||
"could not connect",
|
||||
"connection reset",
|
||||
"timeout",
|
||||
"server closed the connection",
|
||||
"the database system is starting",
|
||||
"database system is shutting down",
|
||||
"too many connections",
|
||||
)
|
||||
return any(n in msg for n in needles)
|
||||
|
||||
|
||||
def run_alembic_upgrade_at_startup() -> None:
|
||||
"""
|
||||
同步执行迁移;失败时按 settings 记录日志或抛出。
|
||||
|
||||
在 asyncio 中请使用 ``asyncio.to_thread(run_alembic_upgrade_at_startup)``,
|
||||
避免阻塞事件循环。
|
||||
"""
|
||||
if not settings.alembic_run_on_startup:
|
||||
logger.info("跳过 Alembic 迁移(alembic_run_on_startup=False)")
|
||||
return
|
||||
|
||||
max_tries = max(1, settings.alembic_startup_max_retries)
|
||||
base_delay = float(settings.alembic_startup_retry_base_seconds)
|
||||
last: BaseException | None = None
|
||||
|
||||
for attempt in range(max_tries):
|
||||
try:
|
||||
_run_alembic_upgrade_once()
|
||||
if attempt > 0:
|
||||
logger.info(
|
||||
"Alembic 迁移成功(第 {} 次尝试)",
|
||||
attempt + 1,
|
||||
)
|
||||
else:
|
||||
logger.info("Alembic 迁移已就绪")
|
||||
return
|
||||
except Exception as e:
|
||||
last = e
|
||||
will_retry = attempt < max_tries - 1 and _is_retryable_migration_error(e)
|
||||
if will_retry:
|
||||
delay = base_delay * (2**attempt)
|
||||
logger.warning(
|
||||
"Alembic 迁移失败(将重试): {} — {:.1f}s 后重试 ({}/{})",
|
||||
e,
|
||||
delay,
|
||||
attempt + 1,
|
||||
max_tries,
|
||||
)
|
||||
time.sleep(delay)
|
||||
else:
|
||||
break
|
||||
|
||||
assert last is not None
|
||||
logger.exception("Alembic 迁移失败: {}", last)
|
||||
if settings.alembic_startup_fail_fast:
|
||||
raise RuntimeError(
|
||||
"Alembic migration failed; set ALEMBIC_STARTUP_FAIL_FAST=false "
|
||||
"for dev-only tolerate mode"
|
||||
) from last
|
||||
logger.error(
|
||||
"应用将继续启动,但数据库结构可能未更新;请检查 DATABASE_URL 与 PostgreSQL 是否可达"
|
||||
)
|
||||
Reference in New Issue
Block a user