feat(roundtable): taskId 会商记录独立存储(不分权) + 第三步生成过程对话/步骤条入库
taskId 深链会商存储独立化: - 新增独立表 roundtable_task_drafts + 路由 /api/roundtable-task-drafts,只按 task_id 归属、完全不按 user 分权(created_by 仅审计),与个人 roundtable_drafts 物理分离; 个人草稿表恢复严格按用户隔离(去掉原 task 放权)。 - 后台研讨作业按 taskId 共享:roundtable_jobs 加 task_id,get/get_by_draft/stream/ resume/cancel 支持 ?task_id= 跨用户读取与控制,作业报告写回 task-draft 存储。 - 前端:新增 api/task-drafts.ts 客户端,useDraftPersistence / useRoundtableDrafts / roundtable-jobs / DispatchChainJobModal / RoundtablePlanningPage 按 taskId 路由读写。 - 迁移 20260618_01 + column_additions_mysql.sql(jobs.task_id) + 数据搬迁脚本 scripts/migrate_roundtable_task_drafts.py(把历史 task_id 行迁出个人表)。 第三步聊天记录像第二步一样存全: - 总结报告 / 经典看板 / 大屏 三条流的生成过程对话(中间文字 + 步骤条 + stub)落库, 加载草稿整段回显,而非只剩最终 md / 流程图 / HTML。DraftStep3Snapshot 扩展 dialogues/seatStubMessages 槽位;存稿走页面 render 期 ref,恢复走各 hook hydrateSaved (旧草稿无字段则回退单条「历史记录」气泡)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
500a562bb6
commit
a8da6c2632
29 files changed
+1191
-263
No files matched your search
@@ -0,0 +1,127 @@
|
||||
"""一次性迁移:把已有的 taskId 会商记录从个人草稿表搬进独立的 task 存储。
|
||||
|
||||
背景
|
||||
====
|
||||
早期 taskId 深链会商记录混存在按用户分权的 ``roundtable_drafts`` 表里(带一个
|
||||
``task_id`` 列 + 放开归属校验)。现在 taskId 聊天记录有了独立、不分权的存储
|
||||
``roundtable_task_drafts``(只按 ``task_id`` 区分,跨用户共享)。本脚本把历史数据搬过去:
|
||||
|
||||
1. ``roundtable_drafts`` 中 ``task_id`` 非空的每一行 → 在 ``roundtable_task_drafts``
|
||||
建同 id 的记录(``created_by`` = 原 ``user_id``,审计用);
|
||||
2. 这些草稿对应的 ``roundtable_jobs`` 行(``draft_id`` 匹配且 ``task_id`` 为空)补上
|
||||
``task_id``,使其转为「task 作业」(按 task 共享);
|
||||
3. 从 ``roundtable_drafts`` 删除已迁走的行,使个人历史不再混入 task 记录。
|
||||
|
||||
幂等:目标表已存在同 id 记录则跳过;可重复运行。alembic 自动升级在运行时是关闭的
|
||||
(见 ``persistence/engine.py``),所以这步数据搬迁用本脚本手动执行。
|
||||
|
||||
用法(在 backend 目录)
|
||||
======================
|
||||
::
|
||||
|
||||
# 预览(不写盘)
|
||||
PYTHONPATH=. uv run python scripts/migrate_roundtable_task_drafts.py --dry-run
|
||||
|
||||
# 执行
|
||||
PYTHONPATH=. uv run python scripts/migrate_roundtable_task_drafts.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
logger = logging.getLogger("migrate_roundtable_task_drafts")
|
||||
|
||||
|
||||
async def _run(dry_run: bool) -> int:
|
||||
from sqlalchemy import select, update
|
||||
|
||||
from deerflow.config import get_app_config
|
||||
from deerflow.persistence.engine import close_engine, get_session_factory, init_engine_from_config
|
||||
from deerflow.persistence.roundtable_drafts.model import RoundtableDraftRow
|
||||
from deerflow.persistence.roundtable_jobs.model import RoundtableJobRow
|
||||
from deerflow.persistence.roundtable_task_drafts.model import RoundtableTaskDraftRow
|
||||
|
||||
config = get_app_config()
|
||||
await init_engine_from_config(config.database)
|
||||
sf = get_session_factory()
|
||||
|
||||
moved = 0
|
||||
skipped = 0
|
||||
jobs_tagged = 0
|
||||
try:
|
||||
async with sf() as session:
|
||||
rows = (
|
||||
await session.execute(
|
||||
select(RoundtableDraftRow).where(RoundtableDraftRow.task_id.is_not(None))
|
||||
)
|
||||
).scalars().all()
|
||||
logger.info("发现 %d 条带 task_id 的个人草稿待迁移。", len(rows))
|
||||
|
||||
for row in rows:
|
||||
# 幂等:目标已存在同 id → 跳过(仍尝试给作业补 task_id + 删源行)。
|
||||
exists = await session.get(RoundtableTaskDraftRow, row.id)
|
||||
if exists is None:
|
||||
if not dry_run:
|
||||
session.add(
|
||||
RoundtableTaskDraftRow(
|
||||
id=row.id,
|
||||
task_id=row.task_id,
|
||||
created_by=row.user_id,
|
||||
title=row.title or "",
|
||||
furthest_step=row.furthest_step or 1,
|
||||
step1=row.step1,
|
||||
step2=row.step2,
|
||||
step3=row.step3,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
)
|
||||
)
|
||||
moved += 1
|
||||
logger.info(" 迁移 draft %s (task_id=%s)", row.id, row.task_id)
|
||||
else:
|
||||
skipped += 1
|
||||
logger.info(" 跳过 draft %s (目标已存在)", row.id)
|
||||
|
||||
# 关联后台作业补 task_id(未标记的才动)。
|
||||
if not dry_run:
|
||||
res = await session.execute(
|
||||
update(RoundtableJobRow)
|
||||
.where(RoundtableJobRow.draft_id == row.id)
|
||||
.where(RoundtableJobRow.task_id.is_(None))
|
||||
.values(task_id=row.task_id)
|
||||
)
|
||||
jobs_tagged += int(res.rowcount or 0)
|
||||
|
||||
# 删除源行(task 记录不再留在个人表)。
|
||||
if not dry_run:
|
||||
await session.delete(row)
|
||||
|
||||
if not dry_run:
|
||||
await session.commit()
|
||||
|
||||
logger.info(
|
||||
"完成%s:迁移 %d 条,跳过 %d 条,关联作业打标 %d 条。",
|
||||
"(dry-run,未写盘)" if dry_run else "",
|
||||
moved,
|
||||
skipped,
|
||||
jobs_tagged,
|
||||
)
|
||||
return 0
|
||||
finally:
|
||||
await close_engine()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="迁移 taskId 会商记录到独立 task 存储")
|
||||
parser.add_argument("--dry-run", action="store_true", help="只预览,不写盘")
|
||||
args = parser.parse_args()
|
||||
return asyncio.run(_run(args.dry_run))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user