feat(api): 叙事 prompt、职业上下文、读路径章节、WS 解耦与错误脱敏

- 回忆录:事实边界补充允许清单;传记文体示例与 JSON 叙事要求对齐
- default 职业提示 occupation_context;cadre/military 退休语境
- GET 章节读路径零写入,prepare_chapter_read_view + markdown_for_response
- 文本归一抽到 core/text_normalize;移除弃用 reply 策略与 recompose_chapters_for_story
- ConversationService:WS 连接/用户段落/结束对话;对外错误固定文案
- 测试:HTTP 脱敏契约、章节读视图、occupation 与 background_voice
This commit is contained in:
Kevin
2026-04-01 11:49:33 +08:00
parent a5473e8fe2
commit 53d9e003af
28 changed files with 598 additions and 397 deletions

View File

@@ -18,13 +18,11 @@ 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.models import Conversation, Segment
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
from app.features.conversation.ws.pipeline import (
_delayed_listening_feedback,
_mark_conversation_active,
_voice_session_id_from_client_segment_id,
background_runner,
bump_tts_cancel_epoch,
@@ -106,49 +104,41 @@ async def websocket_endpoint(
},
)
conversation = await db.get(Conversation, conversation_id)
if not conversation:
conversation = Conversation(
id=conversation_id,
user_id=user_id,
started_at=datetime.now(timezone.utc),
status="active",
conversation, ws_conn_err = await conversation_service.ensure_ws_connection(
conversation_id, user_id
)
if ws_conn_err == "forbidden":
try:
await manager.send_message(
conversation_id,
{
"type": MessageType.ERROR,
"data": {"message": "无权访问此对话"},
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)
except Exception:
pass
await websocket.close(
code=status.WS_1008_POLICY_VIOLATION, reason="无权访问此对话"
)
db.add(conversation)
await db.commit()
else:
if conversation.user_id != user_id:
try:
await manager.send_message(
conversation_id,
{
"type": MessageType.ERROR,
"data": {"message": "无权访问此对话"},
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)
except Exception:
pass
await websocket.close(
code=status.WS_1008_POLICY_VIOLATION, reason="无权访问此对话"
return
if ws_conn_err == "deleted":
try:
await manager.send_message(
conversation_id,
{
"type": MessageType.ERROR,
"data": {"message": "对话已删除"},
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)
return
if conversation.deleted_at is not None:
try:
await manager.send_message(
conversation_id,
{
"type": MessageType.ERROR,
"data": {"message": "对话已删除"},
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)
except Exception:
pass
await websocket.close(
code=status.WS_1008_POLICY_VIOLATION, reason="对话已删除"
)
return
except Exception:
pass
await websocket.close(
code=status.WS_1008_POLICY_VIOLATION, reason="对话已删除"
)
return
history = await conversation_service.ensure_redis_history_from_db(
conversation_id
@@ -205,6 +195,7 @@ async def websocket_endpoint(
background_voice=infer_background_voice(
user.occupation
),
occupation=user.occupation or "",
)
)
ai_msg_id = await ConversationHistoryStore(
@@ -291,18 +282,12 @@ async def websocket_endpoint(
)
continue
segment = Segment(
id=str(uuid.uuid4()),
conversation_id=conversation_id,
user_input_text=text_message,
processed=False,
segment = await conversation_service.create_user_segment(
conversation,
user_id,
text_message,
)
db.add(segment)
user_message_timestamp = _mark_conversation_active(
conversation
)
await db.commit()
await db.refresh(segment)
user_message_timestamp = conversation.last_message_at
await background_runner.queue_message(
conversation.user_id,
segment.id,
@@ -554,20 +539,16 @@ async def websocket_endpoint(
ads = int(audio_duration)
except (TypeError, ValueError):
ads = 0
segment = Segment(
id=str(uuid.uuid4()),
conversation_id=conversation_id,
user_input_text=asr_text,
audio_url=f"audio:{audio_duration}s",
audio_duration_seconds=ads if ads > 0 else None,
processed=False,
segment = (
await conversation_service.create_user_segment(
conversation,
user_id,
asr_text,
audio_url=f"audio:{audio_duration}s",
audio_duration_seconds=ads if ads > 0 else None,
)
)
db.add(segment)
user_message_timestamp = _mark_conversation_active(
conversation
)
await db.commit()
await db.refresh(segment)
user_message_timestamp = conversation.last_message_at
await background_runner.queue_message(
conversation.user_id,
segment.id,
@@ -606,7 +587,7 @@ async def websocket_endpoint(
{
"type": MessageType.ERROR,
"data": {
"message": f"处理音频消息失败: {str(e)}"
"message": "语音处理失败,请重试或使用文字输入"
},
"timestamp": datetime.now(
timezone.utc
@@ -646,7 +627,7 @@ async def websocket_endpoint(
conversation_id,
{
"type": MessageType.ERROR,
"data": {"message": f"转写失败: {str(e)}"},
"data": {"message": "语音转写失败,请重试"},
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)
@@ -655,9 +636,7 @@ async def websocket_endpoint(
bump_tts_cancel_epoch(conversation_id)
elif msg_type == MessageType.END_CONVERSATION:
conversation.status = "ended"
conversation.ended_at = datetime.now(timezone.utc)
await db.commit()
await conversation_service.end(conversation_id, user_id)
await process_conversation_segments(
conversation_id, db, quota_service
@@ -715,7 +694,7 @@ async def websocket_endpoint(
conversation_id,
{
"type": MessageType.ERROR,
"data": {"message": str(e)},
"data": {"message": "处理失败,请重试"},
"timestamp": datetime.now(
timezone.utc
).isoformat(),
@@ -739,7 +718,7 @@ async def websocket_endpoint(
conversation_id,
{
"type": MessageType.ERROR,
"data": {"message": str(e)},
"data": {"message": "处理失败,请重试"},
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)