refactor(api): TOML 配置 SSOT、统一错误契约、Auth/事务加固与可观测性 (#33)

配置 SSOT(TOML + .env)
统一错误契约
Auth 与事务边界
Redis / Celery 可靠性:业务 Redis(DB/0)与 Celery broker/backend(DB/1)显式拆分;连接池、sync client
可观测性(OpenTelemetry + LGTM)
This commit is contained in:
Sully
2026-05-22 13:44:50 +08:00
committed by GitHub
parent f09ae248f9
commit 53e0065e3e
298 changed files with 15247 additions and 4344 deletions

View File

@@ -12,13 +12,11 @@ from starlette.websockets import WebSocketState
from app.agents.chat.background_voice import infer_background_voice
from app.agents.chat.prompts_profile import format_user_profile_context
from app.agents.stage_constants import STAGE_TO_ORDER
from app.core.config import settings
from app.core.db import AsyncSessionLocal
from app.core.dependencies import get_asr_provider
from app.core.logging import get_logger
from app.core.security import verify_token
from app.features.conversation.history_store import ConversationHistoryStore
from app.features.conversation.service import ConversationService
from app.features.conversation.ws.connection_manager import manager
from app.features.conversation.ws.message_types import MessageType
@@ -30,7 +28,6 @@ from app.features.conversation.ws.pipeline import (
cleanup_segment_states,
get_or_create_segment_state,
handle_tts_request_on_demand,
memoir_ingest_scheduler,
process_audio_segment,
process_conversation_segments,
process_persisted_user_segment_response,
@@ -40,9 +37,10 @@ from app.features.conversation.ws.pipeline import (
from app.features.conversation.ws.profile_collector import get_missing_profile_fields
from app.features.conversation.ws.quota_guard import check_ws_quota
from app.features.conversation.ws.topic_chips_push import maybe_send_topic_chips_ws
from app.features.memoir.state_service import get_or_create_state
from app.features.memoir.service import MemoirService
from app.features.quota.service import QuotaService
from app.features.user.models import User
from app.features.user.service import UserService
from app.features.conversation.constants import chat
logger = get_logger(__name__)
@@ -92,7 +90,8 @@ async def websocket_endpoint(
return
async with AsyncSessionLocal() as db:
user = await db.get(User, user_id)
user_service = UserService(db)
user = await user_service.get_by_id(user_id)
if not user:
await websocket.close(
code=status.WS_1008_POLICY_VIOLATION, reason="用户不存在"
@@ -108,6 +107,7 @@ async def websocket_endpoint(
quota_service = QuotaService(db=db)
conversation_service = ConversationService(db=db, quota_service=quota_service)
memoir_service = MemoirService(db=db)
try:
await manager.send_message(
@@ -158,15 +158,10 @@ async def websocket_endpoint(
# 冷启动对齐 conversation_stage 与 MemoirState.current_stage
# 若对话行已有更靠前的人生阶段STAGE_TO_ORDER 更大),不覆盖以免回退。
memoir_state = await get_or_create_state(user_id, db)
ms = (memoir_state.current_stage or "").strip()
cs = (conversation.conversation_stage or "").strip()
if ms:
if not cs:
conversation.conversation_stage = ms
elif STAGE_TO_ORDER.get(ms, -1) >= STAGE_TO_ORDER.get(cs, -1):
conversation.conversation_stage = ms
await db.commit()
memoir_state = await memoir_service.get_or_create_memoir_state(user_id)
await conversation_service.align_conversation_stage_from_memoir(
conversation, memoir_state.current_stage or ""
)
await db.refresh(conversation)
history = await conversation_service.ensure_redis_history_from_db(
@@ -184,7 +179,7 @@ async def websocket_endpoint(
"""统一:把一组 AI 消息落库并按 [SPLIT] 分段下发。"""
if not texts:
return
ai_msg_id = await ConversationHistoryStore(db).record_ai_only_turn(
ai_msg_id = await conversation_service.record_ai_only_turn(
conversation_id, texts
)
if not ai_msg_id:
@@ -271,9 +266,9 @@ async def websocket_endpoint(
else:
# 历史非空:判断是否需要回访问候(距上次消息超过阈值)
idle_hours = _idle_hours_since(conversation.last_message_at)
threshold = float(settings.chat_re_greeting_idle_hours)
threshold = float(chat.re_greeting_idle_hours)
if (
settings.chat_re_greeting_enabled
chat.re_greeting_enabled
and not get_missing_profile_fields(user)
and idle_hours is not None
and idle_hours >= threshold
@@ -383,11 +378,6 @@ async def websocket_endpoint(
user_id,
text_message,
)
await memoir_ingest_scheduler.queue_segment(
conversation.user_id,
segment.id,
text_char_count=len(text_message.strip()),
)
task = asyncio.create_task(
process_persisted_user_segment_response(
@@ -645,11 +635,6 @@ async def websocket_endpoint(
audio_duration_seconds=ads if ads > 0 else None,
)
)
await memoir_ingest_scheduler.queue_segment(
conversation.user_id,
segment.id,
text_char_count=len((asr_text or "").strip()),
)
if asr_text and not asr_text.startswith("转写失败"):
task = asyncio.create_task(