Files
董宏凯andClaude Opus 4.8 32772b7448 feat(dashboard): 大屏绘制智能体独立页面(按 taskId 存取,不分权)
- 新增带导航+侧边栏的独立页面 DashboardAgentPage:按 taskId 取/存会话,
  有有效 report-json 直显大屏(DashboardApp),否则进生成过程(不自动跳)+输入框可改
- 后端 dashboard_sessions 存储(model/sql/__init__)+路由/by-task(GET/PUT 不分权)
  + deps/app/models 接线;新表靠 create_all 建,无迁移
- 扩展 useStep3Dashboard:buildInitialPrompt(输入任务JSON) + hydrateTranscript(恢复聊天记录)
- 抽出共享 DashboardChatPanel;report-template 增 buildDashboardPromptFromJson
- 输入 seam core/auth/dashboard-input:JSON 接口未就绪先用占位/演示数据兜底
- 路由 StrategyRoutes dashboard-agent + 侧边栏「问答管理→大屏绘制」
- 圆桌业务链卡片「关系图」改横向布局
- 顺带提交:AIWritingContext 登录切换处理、thread-access-error helper

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:26:27 +08:00

105 lines
3.9 KiB
Python

"""SQLAlchemy-backed 大屏绘制智能体 session store (按 taskId,不分权).
``transcript`` / ``report_json`` are JSON serialized into ``PortableLongText``
and round-tripped here with ``json.dumps`` / ``json.loads`` — same approach as
``roundtable_drafts``. All access keys on ``task_id`` and ignores ``user_id``
(open read + shared writes); ``user_id`` is recorded for audit only.
"""
from __future__ import annotations
import json
from datetime import UTC, datetime
from typing import Any
from uuid import uuid4
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from deerflow.persistence.dashboard_sessions.model import DashboardSessionRow
def _loads(value: Any) -> Any:
if not isinstance(value, str):
return value
if not value:
return None
try:
return json.loads(value)
except Exception:
return None
def _dumps(value: Any) -> str | None:
if value is None:
return None
if isinstance(value, str):
return value # already a JSON string (report_json) — store verbatim
return json.dumps(value, ensure_ascii=False)
class DashboardSessionRepository:
def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
self._sf = session_factory
@staticmethod
def _to_dict(row: DashboardSessionRow) -> dict[str, Any]:
return {
"id": row.id,
"task_id": row.task_id,
"user_id": row.user_id,
"transcript": _loads(row.transcript),
# report_json is a structured-data JSON string; return it as a string
# so the frontend parses it with the same robust parser it uses live.
"report_json": row.report_json,
"created_at": row.created_at.isoformat() if isinstance(row.created_at, datetime) else row.created_at,
"updated_at": row.updated_at.isoformat() if isinstance(row.updated_at, datetime) else row.updated_at,
}
async def get_by_task(self, task_id: str) -> dict[str, Any] | None:
"""取该 task 的会话(**忽略 user_id**,不分权)。无则 None。"""
stmt = select(DashboardSessionRow).where(DashboardSessionRow.task_id == task_id).limit(1)
async with self._sf() as session:
result = await session.execute(stmt)
row = result.scalars().first()
return self._to_dict(row) if row is not None else None
async def upsert_by_task(
self,
task_id: str,
*,
transcript: Any = None,
report_json: Any = None,
user_id: str | None = None,
) -> dict[str, Any]:
"""按 task_id upsert:无则建、有则更新(**不分权**)。
``transcript`` / ``report_json`` 仅在传入非 None 时覆盖,便于只更新其中一项。
"""
now = datetime.now(UTC)
async with self._sf() as session:
stmt = select(DashboardSessionRow).where(DashboardSessionRow.task_id == task_id).limit(1)
row = (await session.execute(stmt)).scalars().first()
if row is None:
row = DashboardSessionRow(
id=uuid4().hex,
task_id=task_id,
user_id=user_id,
transcript=_dumps(transcript),
report_json=_dumps(report_json),
created_at=now,
updated_at=now,
)
session.add(row)
else:
if transcript is not None:
row.transcript = _dumps(transcript)
if report_json is not None:
row.report_json = _dumps(report_json)
if user_id is not None:
row.user_id = user_id
row.updated_at = now
await session.commit()
await session.refresh(row)
return self._to_dict(row)