Merge branch 'master-nj' into 'master'

fix: 8BF 无 taskId 开全新会话,公共链条发布联动席位智能体

See merge request knowledge-base-bj/deerflow-code!35
This commit is contained in:
董宏凯 committed 2026-08-27 13:48:17 +00:00
commit 1764d0d5f2
3 files changed
+81 -5

No files matched your search

@@ -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<string, unknown> | null}
entryRailTaskId={taskId || null}
entryRailTaskCardSlot={taskId ? (
@@ -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 {
@@ -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)