feat: add class share drafts

This commit is contained in:
2026-07-31 16:07:18 +08:00
parent d8fde93b69
commit 429636f3e8
15 changed files with 410 additions and 5 deletions

View File

@@ -16,13 +16,14 @@ from app.core.dependencies import get_current_admin
from app.core.responses import api_success
from app.models.admin import Admin
from app.models.chat import ChatMessage, ChatSession, TopicSession
from app.models.growth import TeacherHelpCard, TopicSummary
from app.models.growth import ShareDraft, TeacherHelpCard, TopicSummary
from app.models.logs import AiRequestLog, OperationLog
from app.models.user import User
from app.api.pagination import page_result
from app.services.question_insight_service import QuestionInsightService
from app.services.growth_profile_service import topic_dict, topic_summary_dict
from app.services.help_card_service import help_card_dict
from app.services.share_draft_service import share_draft_dict
router = APIRouter()
@@ -141,6 +142,7 @@ def chat_detail(
topics = _topic_rows(db, session_id)
topic_ids = [item["id"] for item in topics]
help_cards = []
share_drafts = []
if topic_ids:
help_cards = list(
db.scalars(
@@ -149,6 +151,13 @@ def chat_detail(
.order_by(TeacherHelpCard.created_at.desc(), TeacherHelpCard.id.desc())
)
)
share_drafts = list(
db.scalars(
select(ShareDraft)
.where(ShareDraft.topic_session_id.in_(topic_ids))
.order_by(ShareDraft.created_at.desc(), ShareDraft.id.desc())
)
)
return api_success(
{
"session": _chat_row_dict(session, user),
@@ -162,6 +171,7 @@ def chat_detail(
"aiLogs": [_ai_log_dict(item, include_prompt=True) for item in ai_logs],
"topics": topics,
"helpCards": [help_card_dict(item) for item in help_cards],
"shareDrafts": [share_draft_dict(item) for item in share_drafts],
}
)

View File

@@ -33,6 +33,7 @@ from app.services.chat_stream_service import ChatStreamService
from app.services.growth_profile_service import GrowthProfileService
from app.services.help_card_service import HelpCardService, help_card_dict
from app.services.reasoning_policy_service import ReasoningPolicyService
from app.services.share_draft_service import ShareDraftService, share_draft_dict
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -126,6 +127,35 @@ def mark_help_card_copied(
return api_success(help_card_dict(HelpCardService.mark_copied(db, user=current_user, card_id=card_id)))
@router.post("/session/{session_id}/share-draft")
def generate_share_draft(
session_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> dict:
session = ChatService._get_user_session(db, current_user, session_id)
draft = ShareDraftService.generate_for_session(db, user=current_user, session=session)
return api_success(share_draft_dict(draft))
@router.get("/share-draft/list")
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),
) -> dict:
return api_success([share_draft_dict(draft) for draft in ShareDraftService.list_user_drafts(db, user=current_user, limit=limit)])
@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),
) -> dict:
return api_success(share_draft_dict(ShareDraftService.mark_copied(db, user=current_user, draft_id=draft_id)))
@router.post("/completions")
def completions(
payload: ChatCompletionRequest,

View File

@@ -3,7 +3,7 @@ from app.models.ai_config import ModelConfig, Prompt, SystemConfig
from app.models.base import Base
from app.models.chat import ChatMessage, ChatSession, TopicSession
from app.models.entitlement import EntitlementPlan, UserEntitlement, UserEntitlementLog
from app.models.growth import GrowthProfileRevision, TeacherHelpCard, TopicSummary, UserGrowthProfile
from app.models.growth import GrowthProfileRevision, ShareDraft, TeacherHelpCard, TopicSummary, UserGrowthProfile
from app.models.knowledge import (
HumanAttentionHistory,
HumanAttentionRecord,
@@ -55,6 +55,7 @@ __all__ = [
"Prompt",
"Role",
"SystemConfig",
"ShareDraft",
"TeacherHelpCard",
"User",
"UserEntitlement",

View File

@@ -82,3 +82,21 @@ class TeacherHelpCard(Base):
copied: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
copied_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
class ShareDraft(Base):
__tablename__ = "sys_share_draft"
__table_args__ = (
Index("ix_sys_share_draft_user_created", "user_id", "created_at"),
Index("ix_sys_share_draft_topic_created", "topic_session_id", "created_at"),
)
id: Mapped[int] = mapped_column(PRIMARY_KEY_TYPE, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("sys_user.id"), index=True, nullable=False)
topic_session_id: Mapped[int] = mapped_column(ForeignKey("sys_topic_session.id"), index=True, nullable=False)
summary_id: Mapped[int | None] = mapped_column(BigInteger, index=True, nullable=True)
content: Mapped[str] = mapped_column(Text, nullable=False)
source: Mapped[str] = mapped_column(String(30), default="topic_summary", nullable=False)
copied: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
copied_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)

View File

@@ -0,0 +1,113 @@
from __future__ import annotations
from datetime import UTC, datetime
from fastapi import HTTPException, status
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.models.chat import ChatSession, TopicSession
from app.models.growth import ShareDraft, TopicSummary
from app.models.user import User
from app.services.entitlement_service import EntitlementService
from app.services.growth_profile_service import GrowthProfileService, topic_summary_dict
class ShareDraftService:
@staticmethod
def generate_for_session(db: Session, *, user: User, session: ChatSession) -> ShareDraft:
entitlement = EntitlementService.active_entitlement(db, user)
if not entitlement.allow_share_draft:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="当前权益暂不支持生成班级分享稿")
topic = _latest_topic(db, user=user, session=session)
if topic is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="当前会话还没有可生成分享稿的主题")
summary = GrowthProfileService.generate_topic_summary(db, user=user, topic=topic)
draft = ShareDraft(
user_id=user.id,
topic_session_id=topic.id,
summary_id=summary.id,
content=_render_share_draft(topic=topic, summary=summary),
source="topic_summary",
)
topic.share_draft_generated = 1
db.add_all([topic, draft])
db.commit()
db.refresh(draft)
return draft
@staticmethod
def list_user_drafts(db: Session, *, user: User, limit: int = 20) -> list[ShareDraft]:
return list(
db.scalars(
select(ShareDraft)
.where(ShareDraft.user_id == user.id)
.order_by(ShareDraft.created_at.desc(), ShareDraft.id.desc())
.limit(limit)
)
)
@staticmethod
def mark_copied(db: Session, *, user: User, draft_id: int) -> ShareDraft:
draft = db.scalar(select(ShareDraft).where(ShareDraft.id == draft_id, ShareDraft.user_id == user.id))
if draft is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="分享稿不存在")
draft.copied = 1
draft.copied_at = _now()
db.add(draft)
db.commit()
db.refresh(draft)
return draft
def share_draft_dict(draft: ShareDraft) -> dict:
return {
"id": draft.id,
"userId": draft.user_id,
"topicSessionId": draft.topic_session_id,
"summaryId": draft.summary_id,
"content": draft.content,
"source": draft.source,
"copied": bool(draft.copied),
"copiedAt": draft.copied_at,
"createdAt": draft.created_at,
}
def _latest_topic(db: Session, *, user: User, session: ChatSession) -> TopicSession | None:
return db.scalar(
select(TopicSession)
.where(TopicSession.user_id == user.id, TopicSession.chat_session_id == session.id)
.order_by(TopicSession.updated_at.desc(), TopicSession.id.desc())
.limit(1)
)
def _render_share_draft(*, topic: TopicSession, summary: TopicSummary) -> str:
data = topic_summary_dict(summary)
return (
"【实修分享稿草稿】\n"
"说明:这是根据我本次对话整理出的分享草稿,系统不会自动发送到任何群,"
"我会按真实情况删改后再决定是否发到班级群。\n\n"
"大家好,我想分享一下这次实修里看到的一点东西。\n\n"
"1. 我这次观察到的议题\n"
f"{topic.core_question or topic.title}\n\n"
"2. 我看见了什么\n"
f"{data.get('summary') or '(请用自己的话补充真实看见)'}\n\n"
"3. 我感受到的情绪和身体反应\n"
f"情绪:{data.get('emotions') or '(请补充)'}\n"
f"身体:{data.get('bodyFeelings') or '(请补充)'}\n\n"
"4. 我做了什么功课 / 准备继续做什么\n"
f"{data.get('recommendedHomework') or '(请补充)'}\n\n"
"5. 当下的一点变化\n"
f"{data.get('insights') or '(请补充真实变化,不需要夸大)'}\n\n"
"6. 我还在继续观察的方向\n"
f"{data.get('nextObservation') or '(请补充)'}\n\n"
"备注:这只是我的阶段性观察,不代表已经彻底解决,也不是建议别人照搬。"
)
def _now() -> datetime:
return datetime.now(UTC).replace(tzinfo=None)