diff --git a/frontend-web/src/eightbf-roundtable/pages/EightBfRoundtablePage.tsx b/frontend-web/src/eightbf-roundtable/pages/EightBfRoundtablePage.tsx index 6922a0a0..4bd4f5ed 100644 --- a/frontend-web/src/eightbf-roundtable/pages/EightBfRoundtablePage.tsx +++ b/frontend-web/src/eightbf-roundtable/pages/EightBfRoundtablePage.tsx @@ -257,6 +257,8 @@ export default function EightBfRoundtablePage({ compactChrome = false }: { compa lockedChainId={mapping?.chainId ?? null} lockBusinessChain storageTaskId={storageTaskId} + // 无 taskId 进入时每次都开全新会话(不回显 anonymous 键下的最新草稿);带 taskId 保持回显现状。 + skipInitialDraftRestore={!taskId} taskContext={taskContext as Record | null} entryRailTaskId={taskId || null} entryRailTaskCardSlot={taskId ? ( diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx index 56568b4a..d145291b 100644 --- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx @@ -310,6 +310,11 @@ export default function RoundtablePlanningPage({ lockBusinessChain = false, /** 独立业务页的草稿/后台作业存储键。 */ storageTaskId, + /** + * 独立业务页要求「每次进入都是全新会话」(8BF 无 taskId 模式):挂载时跳过按存储键 + * 回显最新草稿,直接展示空白新会话;历史会话仍留在历史下拉/分析记录里,可手动打开。 + */ + skipInitialDraftRestore = false, /** 供独立业务页在原页头部插入自己的操作入口。 */ toolbarSlot, /** 独立业务页在第一步左上角替换默认业务链选择区的内容。 */ @@ -351,6 +356,7 @@ export default function RoundtablePlanningPage({ lockedChainId?: string | null; lockBusinessChain?: boolean; storageTaskId?: string | null; + skipInitialDraftRestore?: boolean; toolbarSlot?: ReactNode; entryRailBusinessChainSlot?: ReactNode; entryRailTaskCardSlot?: ReactNode; @@ -1368,6 +1374,12 @@ export default function RoundtablePlanningPage({ // 回显聊天记录按存储键:8BF 用独立 `storageTaskId`;xdfx-A 按行动id;其它深链按真实 taskId。 const tid = draftStorageTaskId; if (!tid) return; + // 8BF 无 taskId 模式:每次进入开全新会话,跳过回显(历史下拉仍可手动打开旧会话)。 + // 仍要解除 taskRestoreResolved,别把依赖它的 autoStart 闸门永久挂起。 + if (skipInitialDraftRestore) { + setTaskRestoreResolved(true); + return; + } let cancelled = false; void (async () => { try { diff --git a/offline-backend-20260512/backend/app/gateway/routers/roundtable_chains.py b/offline-backend-20260512/backend/app/gateway/routers/roundtable_chains.py index 321c5a2c..e847830a 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/roundtable_chains.py +++ b/offline-backend-20260512/backend/app/gateway/routers/roundtable_chains.py @@ -28,7 +28,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request from langchain_core.messages import HumanMessage, SystemMessage from pydantic import BaseModel, Field -from app.gateway.deps import get_config, get_local_provider +from app.gateway.deps import get_agent_store, get_config, get_local_provider from deerflow.config.app_config import AppConfig from deerflow.models import create_chat_model from deerflow.runtime.user_context import get_effective_user_id @@ -202,6 +202,65 @@ def _get_store(request: Request): return store +async def _publish_seat_agents(request: Request, row: dict, user_id: str, *, is_admin: bool) -> list[str]: + """把链条席位里尚未公开的自定义智能体一并发布(best-effort)。 + + 公共链条对其他账号可用的前提是链上每个席位智能体本身已发布(见 + business_mapping 的 ``_validate_public_chain_mapping`` 联动校验——只发布 + 链条、不发布其中的自定义智能体,别的账号会解析不到席位)。故链条处于 + 公共态时自动补发: + - 内置智能体(``user_id IS NULL``)全局可见,跳过; + - 已公开(``published`` 且非 ``is_private``)跳过; + - 其余置 ``published=True`` 并清 ``is_private=False``(两者互斥,私密 + 广场智能体要变公共必须同时清私密标记)。 + 非管理员只能发布本人的智能体(store.update 的 owner 限定);他人的智能体 + 正常流程不会出现在自己链条里,跳过并记日志。单个失败不阻断链条发布。 + 返回本次实际发布成功的智能体名列表(仅供日志/排查)。 + """ + agent_ids: list[str] = [] + for seat in row.get("seats") or []: + if isinstance(seat, dict) and seat.get("agent_id"): + agent_ids.append(str(seat["agent_id"])) + if not agent_ids: + return [] + try: + agent_store = get_agent_store(request) + except HTTPException: + return [] + published: list[str] = [] + for agent_id in dict.fromkeys(agent_ids): + try: + agent = await agent_store.get_any(agent_id) + except Exception: + logger.warning("chain seat agent lookup failed: %s", agent_id, exc_info=True) + continue + if agent is None or agent.get("user_id") is None: + continue # 已删除 / 内置智能体:内置全局可见,无需发布 + if agent.get("published") and not agent.get("is_private"): + continue # 已公开 + updates = {"published": True, "is_private": False} + try: + updated = await agent_store.update_any(agent_id, updates) if is_admin else await agent_store.update(agent_id, user_id, updates) + except Exception: + logger.warning("chain seat agent publish failed: %s", agent_id, exc_info=True) + continue + if updated is None: + logger.info( + "chain seat agent %s not publishable by user %s (not owner); skipped", + agent_id, + user_id, + ) + continue + published.append(agent.get("name") or agent_id) + logger.info( + "auto-published chain seat agent %s (%s) with chain %s", + agent_id, + agent.get("name"), + row.get("id"), + ) + return published + + def _validate_seats(seats: list[ChainSeatSchema]) -> None: """Enforce 2 ≤ N ≤ 20 and reject duplicate agent ids within a chain.""" if len(seats) < MIN_SEATS or len(seats) > MAX_SEATS: @@ -259,9 +318,7 @@ def _validate_stages(seats: list[ChainSeatSchema], stages: list[list[str]] | Non MAX_STAGE_GOAL_CHARS = 1000 -def _validate_stage_goals( - stages: list[list[str]] | None, stage_goals: list[str | None] | None -) -> None: +def _validate_stage_goals(stages: list[list[str]] | None, stage_goals: list[str | None] | None) -> None: """Validate the optional per-stage goals (None = no goals, always valid). Rules: @@ -379,11 +436,16 @@ async def update_chain(request: Request, chain_id: str, body: ChainUpdateRequest _validate_seat_mode(body.seat_mode) store = _get_store(request) user_id = _current_user_id(request) + is_admin = _is_admin(request) # Distinguish "omitted" from "explicitly sent"; only forward provided fields. fields = body.model_dump(exclude_unset=True) - row = await store.update_chain(chain_id, user_id, is_admin=_is_admin(request), **fields) + row = await store.update_chain(chain_id, user_id, is_admin=is_admin, **fields) if row is None: raise HTTPException(status_code=404, detail="Chain not found") + # 发布联动:链条更新后处于公共态时(点发布,或已发布链再改席位),把链上 + # 尚未发布的自定义智能体一并发布,保证其他账号解析公共链条时席位可用。 + if row.get("is_public"): + await _publish_seat_agents(request, row, user_id, is_admin=is_admin) await _enrich_owner_names([row]) return ChainResponse(**row)