Merge remote-tracking branch 'origin/master' into xuhd

This commit is contained in:
xuhd committed 2026-06-10 17:15:51 +08:00
commit 06508cfb7c
27 files changed
+1054 -72

No files matched your search

@@ -30,6 +30,7 @@ from app.gateway.routers import (
artifacts,
assistants_compat,
auth,
business_mapping,
channels,
checkpoint_migration,
feedback,
@@ -766,6 +767,16 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"Roundtable functional agent seeding failed at startup (non-fatal)"
)
# 业务链条管理:幂等补建固定业务行(3Q/6BF/7BF/8BF),agent/chain 初始为空。
try:
business_mapping_store = getattr(app.state, "business_mapping_store", None)
if business_mapping_store is not None:
inserted = await business_mapping_store.ensure_seed_defaults()
if inserted:
logger.info("Seeded %d business mapping rows", inserted)
except Exception:
logger.exception("Business mapping seeding failed at startup (non-fatal)")
# 一次性把 .deer-flow/agents/{id}/ 目录里历史遗留的 agent 导入 agents 表。
# 路由层原本每次请求都跑一遍这个 sync(扫文件系统 + N 个 agent 各一次 DB
# SELECT/UPDATE),把它挪到启动期 + 路由层只保留 TTL 兜底,显著降低
@@ -1124,6 +1135,9 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an
# Roundtable-planning business chains at /api/roundtable-chains
app.include_router(roundtable_chains.router)
# Global business → agent / chain mapping at /api/business-mapping
app.include_router(business_mapping.router)
# Standalone parallel multi-agent panel: fan-out streaming + saved tasks
app.include_router(parallel_agents.router)
app.include_router(parallel_agent_tasks.router)
@@ -179,6 +179,10 @@ async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]:
app.state.roundtable_chain_store = make_roundtable_chain_store(sf)
from deerflow.persistence.business_mapping import make_business_mapping_store
app.state.business_mapping_store = make_business_mapping_store(sf)
from deerflow.persistence.parallel_agent_tasks import make_parallel_agent_task_store
app.state.parallel_agent_task_store = make_parallel_agent_task_store(sf)
@@ -0,0 +1,87 @@
"""CRUD API for the global business → agent / chain mapping (业务链条管理).
Binds a fixed business code (3Q / 6BF / 7BF / 8BF) to an optional 智能体 and an
optional 业务链条 (a **public** ``roundtable_chains`` row). Used to auto-pick the
business chain when a task page launches the multi-agent roundtable.
Global / shared — there is no user scoping; all accounts see and edit the same
rows. The four business rows are seeded on startup; this API only lists them and
updates the agent / chain columns (no create/delete).
Routes (prefix ``/api/business-mapping``):
GET "" list all business mappings (display order)
PUT "/{business_code}" update one row's agent / chain selection
"""
from __future__ import annotations
import logging
from datetime import datetime
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/business-mapping", tags=["business-mapping"])
# ── schemas ────────────────────────────────────────────────────────────────
class BusinessMappingResponse(BaseModel):
business_code: str
label: str = ""
agent_id: str | None = None
agent_name: str | None = None
chain_id: str | None = None
chain_title: str | None = None
sort_order: int = 0
created_at: datetime | str | None = None
updated_at: datetime | str | None = None
class BusinessMappingListResponse(BaseModel):
mappings: list[BusinessMappingResponse]
class BusinessMappingUpdateRequest(BaseModel):
# All optional → callers send only what changed; explicit null clears a field.
agent_id: str | None = Field(default=None, max_length=64)
agent_name: str | None = Field(default=None, max_length=191)
chain_id: str | None = Field(default=None, max_length=64)
chain_title: str | None = Field(default=None, max_length=512)
# ── helpers ──────────────────────────────────────────────────────────────
def _get_store(request: Request):
store = getattr(request.app.state, "business_mapping_store", None)
if store is None:
raise HTTPException(status_code=503, detail="Business mapping store not available")
return store
# ── routes ───────────────────────────────────────────────────────────────
@router.get("", response_model=BusinessMappingListResponse)
async def list_mappings(request: Request) -> BusinessMappingListResponse:
store = _get_store(request)
rows = await store.list_mappings()
return BusinessMappingListResponse(
mappings=[BusinessMappingResponse(**r) for r in rows]
)
@router.put("/{business_code}", response_model=BusinessMappingResponse)
async def update_mapping(
request: Request, business_code: str, body: BusinessMappingUpdateRequest
) -> BusinessMappingResponse:
store = _get_store(request)
# Only forward fields the caller explicitly sent (distinguish omitted vs null).
fields = body.model_dump(exclude_unset=True)
row = await store.update_mapping(business_code, **fields)
if row is None:
raise HTTPException(status_code=404, detail="Business mapping not found")
return BusinessMappingResponse(**row)
+2 -2
View File
@@ -14,7 +14,7 @@ models:
display_name: DeepSeek Chat
use: langchain_openai:ChatOpenAI
model: deepseek-chat
api_key: sk-e1c22b7931dc446f8b67e57878edd105
api_key: sk-3fee562f834a4c08bb0d471ef44cb445
base_url: https://api.deepseek.com
request_timeout: 600.0
max_retries: 2
@@ -41,7 +41,7 @@ models:
display_name: DeepSeek V4 Flash
use: langchain_openai:ChatOpenAI
model: deepseek-v4-flash
api_key: sk-e1c22b7931dc446f8b67e57878edd105
api_key: sk-3fee562f834a4c08bb0d471ef44cb445
base_url: https://api.deepseek.com
request_timeout: 600.0
max_retries: 2
@@ -0,0 +1,18 @@
"""Global business → agent / chain mapping persistence (业务链条管理)."""
from __future__ import annotations
from deerflow.persistence.business_mapping.model import BusinessMappingRow
from deerflow.persistence.business_mapping.sql import BusinessMappingRepository
__all__ = [
"BusinessMappingRepository",
"BusinessMappingRow",
"make_business_mapping_store",
]
def make_business_mapping_store(session_factory):
if session_factory is None:
return None
return BusinessMappingRepository(session_factory)
@@ -0,0 +1,53 @@
"""ORM model for the global business → agent / chain mapping (业务链条管理).
A "business mapping" row binds a fixed business code (3Q / 6BF / 7BF / 8BF) to an
optional 智能体 (``agent_id``) and an optional 业务链条 (``chain_id`` → a
``roundtable_chains`` row, which must be **public** so every account can resolve
it). Used to auto-pick the business chain when a task page launches the
multi-agent roundtable.
Unlike ``roundtable_chains`` (per-user), this mapping is **global / shared** —
there is no ``user_id`` column; all accounts see and use the same configuration.
The fixed business rows are seeded on startup; users only fill in the
agent / chain columns from the management page.
``agent_name`` / ``chain_title`` are denormalized display snapshots so the table
renders without cross-referencing and survives a later agent/chain rename or
delete (the picked id still drives the auto-flow).
"""
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
class BusinessMappingRow(Base):
__tablename__ = "business_mapping"
# Business code is the natural primary key (e.g. "3Q", "6BF"). Global, no user scope.
business_code: Mapped[str] = mapped_column(String(32), primary_key=True)
# Display label shown in the 业务 column (e.g. "任务FX").
label: Mapped[str] = mapped_column(String(128), nullable=False, default="")
# Picked 智能体 (暂存不用,自动流程当前不消费此字段). NULL = 未配置.
agent_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
agent_name: Mapped[str | None] = mapped_column(String(191), nullable=True)
# Picked 业务链条 → roundtable_chains.id (须为 public). NULL = 未配置.
chain_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
chain_title: Mapped[str | None] = mapped_column(String(512), nullable=True)
# Fixed display order for the management table.
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
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),
)
@@ -0,0 +1,120 @@
"""SQLAlchemy-backed global business → agent / chain mapping store.
Global / shared (no ``user_id`` scoping): every account reads and writes the same
rows. The four business rows are seeded once on startup (``ensure_seed_defaults``)
with empty agent/chain; users fill those in from the management page via
``update_mapping``.
"""
from __future__ import annotations
from datetime import UTC, datetime
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from deerflow.persistence.business_mapping.model import BusinessMappingRow
_UNSET = object()
# Fixed business rows (业务码 → 展示名). Seeded on first boot; agent/chain left empty.
# 8BF 页面尚未建好,先占位保留行。顺序即管理表的展示顺序。
DEFAULT_BUSINESS_ROWS: list[dict[str, Any]] = [
{"business_code": "3Q", "label": "3QFX"},
{"business_code": "6BF", "label": "任务FX"},
{"business_code": "7BF", "label": "XDFX"},
{"business_code": "8BF", "label": "8BF"},
]
class BusinessMappingRepository:
def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
self._sf = session_factory
@staticmethod
def _to_dict(row: BusinessMappingRow) -> dict[str, Any]:
return {
"business_code": row.business_code,
"label": row.label,
"agent_id": row.agent_id,
"agent_name": row.agent_name,
"chain_id": row.chain_id,
"chain_title": row.chain_title,
"sort_order": row.sort_order,
"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 list_mappings(self) -> list[dict[str, Any]]:
stmt = select(BusinessMappingRow).order_by(
BusinessMappingRow.sort_order.asc(), BusinessMappingRow.business_code.asc()
)
async with self._sf() as session:
result = await session.execute(stmt)
return [self._to_dict(row) for row in result.scalars()]
async def get_mapping(self, business_code: str) -> dict[str, Any] | None:
async with self._sf() as session:
row = await session.get(BusinessMappingRow, business_code)
return self._to_dict(row) if row is not None else None
async def update_mapping(
self,
business_code: str,
*,
agent_id=_UNSET,
agent_name=_UNSET,
chain_id=_UNSET,
chain_title=_UNSET,
) -> dict[str, Any] | None:
"""Update an existing (seeded) business row. Returns None if the code is unknown.
Only forwarded fields are touched; explicit ``None`` clears that field.
"""
async with self._sf() as session:
row = await session.get(BusinessMappingRow, business_code)
if row is None:
return None
if agent_id is not _UNSET:
row.agent_id = agent_id or None
if agent_name is not _UNSET:
row.agent_name = agent_name or None
if chain_id is not _UNSET:
row.chain_id = chain_id or None
if chain_title is not _UNSET:
row.chain_title = chain_title or None
row.updated_at = datetime.now(UTC)
await session.commit()
await session.refresh(row)
return self._to_dict(row)
async def ensure_seed_defaults(self) -> int:
"""Idempotently insert the fixed business rows if missing. Returns inserted count.
Never overwrites an existing row (respects user-configured agent/chain).
"""
inserted = 0
now = datetime.now(UTC)
async with self._sf() as session:
for idx, seed in enumerate(DEFAULT_BUSINESS_ROWS):
existing = await session.get(BusinessMappingRow, seed["business_code"])
if existing is not None:
continue
session.add(
BusinessMappingRow(
business_code=seed["business_code"],
label=seed.get("label", ""),
agent_id=None,
agent_name=None,
chain_id=None,
chain_title=None,
sort_order=idx,
created_at=now,
updated_at=now,
)
)
inserted += 1
if inserted:
await session.commit()
return inserted
@@ -22,6 +22,7 @@ from deerflow.persistence.agents.model import (
AgentSkillRow,
)
from deerflow.persistence.admin_stats.model import AdminLeaderboardDailyStatRow
from deerflow.persistence.business_mapping.model import BusinessMappingRow
from deerflow.persistence.memory_config.model import UserMemoryConfigRow
from deerflow.persistence.feedback.model import FeedbackRow
from deerflow.persistence.llm_metrics.model import LlmCallMetricRow
@@ -61,6 +62,7 @@ __all__ = [
"AgentRow",
"AgentSkillRow",
"AdminLeaderboardDailyStatRow",
"BusinessMappingRow",
"LlmCallMetricRow",
"NotificationRow",
"ParallelAgentTaskRow",