feat: isolate external application conversations

This commit is contained in:
2026-08-03 18:57:19 +08:00
parent 26109648af
commit cd3c875106
26 changed files with 843 additions and 110 deletions

View File

@@ -7,12 +7,15 @@ from collections.abc import AsyncIterator
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.dependencies import get_current_user
from app.core.auth_context import ChatAccessScope, UserAuthContext
from app.core.dependencies import get_current_user_context
from app.core.responses import api_success
from app.models.user import User
from app.models.chat import TopicSession
from app.models.growth import TopicSummary
from app.schemas.chat import (
ChatCompletionRequest,
ChatMessageRead,
@@ -40,14 +43,14 @@ logger = logging.getLogger(__name__)
@router.post("/session")
def create_session(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)) -> dict:
session = ChatService.create_session(db, current_user)
def create_session(db: Session = Depends(get_db), current: UserAuthContext = Depends(get_current_user_context)) -> dict:
session = ChatService.create_session(db, current.user, current.chat_scope)
return api_success(CreateSessionResponse(sessionId=session.id).model_dump())
@router.get("/session/list")
def list_sessions(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)) -> dict:
sessions = ChatService.list_sessions(db, current_user)
def list_sessions(db: Session = Depends(get_db), current: UserAuthContext = Depends(get_current_user_context)) -> dict:
sessions = ChatService.list_sessions(db, current.user, current.chat_scope)
return api_success([ChatSessionRead.model_validate(session).model_dump() for session in sessions])
@@ -55,9 +58,9 @@ def list_sessions(db: Session = Depends(get_db), current_user: User = Depends(ge
def history(
sessionId: int = Query(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
messages = ChatService.get_history(db, current_user, sessionId)
messages = ChatService.get_history(db, current.user, sessionId, current.chat_scope)
reasoning_visible = ReasoningPolicyService.is_visible(db)
result = []
for message in messages:
@@ -72,9 +75,9 @@ def history(
def update_title(
payload: UpdateSessionTitleRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
session = ChatService.update_title(db, current_user, payload.sessionId, payload.title)
session = ChatService.update_title(db, current.user, payload.sessionId, payload.title, current.chat_scope)
return api_success(ChatSessionRead.model_validate(session).model_dump())
@@ -82,9 +85,9 @@ def update_title(
def delete_session(
session_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
ChatService.delete_session(db, current_user, session_id)
ChatService.delete_session(db, current.user, session_id, current.chat_scope)
return api_success()
@@ -92,29 +95,37 @@ def delete_session(
def finish_topic(
session_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
session = ChatService._get_user_session(db, current_user, session_id)
return api_success(GrowthProfileService.finish_active_topic(db, user=current_user, session=session))
session = ChatService._get_user_session(db, current.user, session_id, current.chat_scope)
return api_success(GrowthProfileService.finish_active_topic(db, user=current.user, session=session))
@router.get("/topic/settlement/{summary_id}")
def topic_settlement(
summary_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
return api_success(GrowthProfileService.topic_settlement_result(db, user=current_user, summary_id=summary_id))
topic_session_id = db.scalar(
select(TopicSession.chat_session_id)
.join(TopicSummary, TopicSummary.topic_session_id == TopicSession.id)
.where(TopicSummary.id == summary_id, TopicSummary.user_id == current.user.id)
)
if topic_session_id is None:
raise HTTPException(status_code=404, detail="主题沉淀任务不存在")
ChatService._get_user_session(db, current.user, topic_session_id, current.chat_scope)
return api_success(GrowthProfileService.topic_settlement_result(db, user=current.user, summary_id=summary_id))
@router.post("/session/{session_id}/help-card")
def generate_help_card(
session_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
session = ChatService._get_user_session(db, current_user, session_id)
card = HelpCardService.generate_for_session(db, user=current_user, session=session)
session = ChatService._get_user_session(db, current.user, session_id, current.chat_scope)
card = HelpCardService.generate_for_session(db, user=current.user, session=session)
return api_success(help_card_dict(card))
@@ -122,27 +133,33 @@ def generate_help_card(
def list_help_cards(
limit: int = Query(default=20, ge=1, le=50),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
return api_success([help_card_dict(card) for card in HelpCardService.list_user_cards(db, user=current_user, limit=limit)])
cards = HelpCardService.list_user_cards(
db,
user=current.user,
scope=current.chat_scope,
limit=limit,
)
return api_success([help_card_dict(card) for card in cards])
@router.post("/help-card/{card_id}/copied")
def mark_help_card_copied(
card_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
return api_success(help_card_dict(HelpCardService.mark_copied(db, user=current_user, card_id=card_id)))
return api_success(help_card_dict(HelpCardService.mark_copied(db, user=current.user, scope=current.chat_scope, card_id=card_id)))
@router.delete("/help-card/{card_id}")
def delete_help_card(
card_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
HelpCardService.delete(db, user=current_user, card_id=card_id)
HelpCardService.delete(db, user=current.user, scope=current.chat_scope, card_id=card_id)
return api_success()
@@ -150,10 +167,10 @@ def delete_help_card(
def generate_share_draft(
session_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
session = ChatService._get_user_session(db, current_user, session_id)
draft = ShareDraftService.generate_for_session(db, user=current_user, session=session)
session = ChatService._get_user_session(db, current.user, session_id, current.chat_scope)
draft = ShareDraftService.generate_for_session(db, user=current.user, session=session)
return api_success(share_draft_dict(draft))
@@ -161,27 +178,33 @@ def generate_share_draft(
def list_share_drafts(
limit: int = Query(default=20, ge=1, le=50),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
return api_success([share_draft_dict(draft) for draft in ShareDraftService.list_user_drafts(db, user=current_user, limit=limit)])
drafts = ShareDraftService.list_user_drafts(
db,
user=current.user,
scope=current.chat_scope,
limit=limit,
)
return api_success([share_draft_dict(draft) for draft in drafts])
@router.post("/share-draft/{draft_id}/copied")
def mark_share_draft_copied(
draft_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
return api_success(share_draft_dict(ShareDraftService.mark_copied(db, user=current_user, draft_id=draft_id)))
return api_success(share_draft_dict(ShareDraftService.mark_copied(db, user=current.user, scope=current.chat_scope, draft_id=draft_id)))
@router.delete("/share-draft/{draft_id}")
def delete_share_draft(
draft_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
ShareDraftService.delete(db, user=current_user, draft_id=draft_id)
ShareDraftService.delete(db, user=current.user, scope=current.chat_scope, draft_id=draft_id)
return api_success()
@@ -189,10 +212,10 @@ def delete_share_draft(
def completions(
payload: ChatCompletionRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> StreamingResponse:
return StreamingResponse(
_chat_stream(payload, db, current_user),
_chat_stream(payload, db, current),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache, no-transform",
@@ -206,13 +229,17 @@ def completions(
def stop(
payload: StopChatRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
current: UserAuthContext = Depends(get_current_user_context),
) -> dict:
ChatService.stop_generation(db, current_user, payload.sessionId)
ChatService.stop_generation(db, current.user, payload.sessionId, current.chat_scope)
return api_success()
async def _chat_stream(payload: ChatCompletionRequest, db: Session, current_user: User) -> AsyncIterator[str]:
async def _chat_stream(payload: ChatCompletionRequest, db: Session, current: UserAuthContext) -> AsyncIterator[str]:
# Keep the internal stream helper compatible with direct service/test callers;
# HTTP requests always pass a fully validated UserAuthContext.
current_user = getattr(current, "user", current)
chat_scope = getattr(current, "chat_scope", ChatAccessScope.direct())
config = load_chat_queue_config(db)
queue_lease = await request_chat_slot(config)
queue_request = queue_lease.request
@@ -274,6 +301,7 @@ async def _chat_stream(payload: ChatCompletionRequest, db: Session, current_user
payload.sessionId,
payload.message,
retry_failed_question=payload.retry,
scope=chat_scope,
)
async for segment in ReasoningPolicyService.iter_segments(chunks):
if segment.kind == "content":