Files
董宏凯andCursor 838cae0d2d 圆桌草稿持久化:localStorage 迁至后端 MySQL
新增 /api/roundtable-drafts 草稿 CRUD 与会话级推荐历史;前端 useDraftPersistence 改走 api/drafts,删除本地 drafts.ts。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 00:23:25 +08:00

70 lines
3.5 KiB
Python

"""ORM models for roundtable-planning drafts and recommendation history.
A "draft" is one full roundtable-planning session (Step 1 intent + Step 2
multi-agent roundtable), persisted per user so it follows the account across
devices/sessions — replacing the old browser-only ``localStorage`` storage.
``step1`` / ``step2`` are stored as JSON serialized into ``PortableLongText``
(MySQL ``LONGTEXT``) rather than ``PortableJSON`` (MySQL ``TEXT``, 64 KB cap),
because the Step 2 snapshot can carry a long roundtable transcript plus per-seat
stub messages that easily exceed 64 KB — mirroring ``ai_writing_sessions``.
"""
from __future__ import annotations
from datetime import UTC, datetime
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from deerflow.persistence.base import Base
from deerflow.persistence.types import BeijingDateTime, PortableLongText
class RoundtableDraftRow(Base):
__tablename__ = "roundtable_drafts"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
user_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
# Auto-derived task summary (objective → first user message → timestamp).
title: Mapped[str] = mapped_column(String(512), nullable=False, default="")
# Furthest step reached when this snapshot was taken (1 | 2 | 3).
furthest_step: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
# JSON blobs matching the frontend Step1Snapshot / Step2Snapshot shapes.
step1: Mapped[str | None] = mapped_column(PortableLongText, nullable=True) # JSON object
step2: Mapped[str | None] = mapped_column(PortableLongText, nullable=True) # JSON object
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC))
updated_at: Mapped[datetime] = mapped_column(
BeijingDateTime(),
nullable=False,
default=lambda: datetime.now(UTC),
onupdate=lambda: datetime.now(UTC),
)
class RoundtableRecommendHistoryRow(Base):
"""One agent-recommendation run, bound to a draft/session.
Lets the recommend dialog show "last recommendation for this session" on
open, while still letting the user re-analyze (which appends a new row).
"""
__tablename__ = "roundtable_recommend_history"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
user_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
draft_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
# Denormalized intent objective at recommend time (list display / context).
objective: Mapped[str] = mapped_column(String(512), nullable=False, default="")
# How the run ended on the backend: "done" | "fallback" | "asking" | "error".
status: Mapped[str] = mapped_column(String(32), nullable=False, default="done")
model: Mapped[str | None] = mapped_column(String(128), nullable=True)
# Natural-language rationale (may be long → LONGTEXT).
rationale: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
# JSON: [{"agent_id": str, "reason": str}, ...]
picks: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
# JSON snapshot of the candidate pool at recommend time:
# [{"agent_id": str, "name": str, "description": str}, ...]
candidates: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC))