feat(qa-center): 问答中心左侧栏改造(0616 需求 2-5)
- 多智能体会商:Step1 落地左栏「业务链(public 前3) + 分析记录」;点链=勾选(autoChainResult),第一步澄清后点「进入研讨」才按链执行
- zzJC/zzXD:独立模块路由 /page/strategy/qa/{zzjc,zzxd},常驻左栏(按 agent 标签分组 + 仅本模块会话的聊天记录),点智能体模块内切换不跳页,默认落地配置类型下首个智能体
- zzJC/zzXD admin 设置:左栏齿轮→单选类型(TDesign)配置面板+智能体预览;后端 system_settings.agent_modules 持久化(GET 开放/PUT admin)+ test_agent_modules
- 课题研究:左栏「研究模板(文章类型) + 笔记本」;模板点击预填表单,笔记本点击设为素材来源(selectedNotebookId 上提 WritingFormContext)
- 通用问答:全量模式历史侧栏默认展开
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8794c51f88
commit
f39cffcb9a
18 files changed
+1113
-41
No files matched your search
@@ -330,6 +330,10 @@ Admin-configurable, **access-control-free** categories that published agents/ski
|
||||
|
||||
Each agent/skill carries a single `square_id` column (`AgentRow` / `SkillRow`, `String(64)` NOT NULL default `''`, migration `20260611_01`, idempotent SQL in `column_additions_mysql.sql`). One column = at most one square, so an item can never be "published twice". `''` = unassigned ? resolved to the default square at read/filter time. Writes normalize via `normalize_square_id`: agents on create/update (`square_id` field), skills on publish (`PUT /api/skills/custom/{name}/published` accepts optional `square_id`). Listing filters: `GET /api/agents?square=` and `GET /api/skills?square=` keep only that square's items; the **default** square additionally sweeps in legacy `''` rows (`square_is_default`). Tests: `tests/test_publish_squares.py`. Frontend: `core/system-settings` (`usePublishSquares`/`useUpdatePublishSquares`), admin `PublishSquaresSettingsPage` (embedded in the appearance/settings hub), square picker in the agent edit dialog + per-skill card, and a ??-tab square sub-filter in both galleries.
|
||||
|
||||
### Agent Module Tag Config (zzJC / zzXD 按类型聚合)
|
||||
|
||||
Backs the zzJC / zzXD agent-chat left sidebar (「按类型聚合智能体 + 聊天记录」). An admin picks which agent **tags** belong to a module; the module's left sidebar then shows only agents under those tags (empty/unconfigured = show all visible agents). Stored in `system_settings.json` → `agent_modules` (`AgentModuleSettings`: `modules: {module_key → [tag_id, ...]}`); pure helpers `get_module_tag_ids` / `set_module_tag_ids` (de-dupe + strip). Endpoints (in `app/gateway/routers/system_settings.py`): `GET /api/system-settings/agent-modules/{key}` (readable by any user — the sidebar needs it to filter), `PUT /api/system-settings/agent-modules/{key}` (admin only, full replacement of the tag-id list). Module keys are frontend-defined (`AGENT_MODULES` in `components/workspace/agents/agent-module-sidebar.tsx`, keyed by the module's landing agent id: `6bf`→`zzjc`, `10d69…`→`zzxd`). Tests: `tests/test_agent_modules.py`. Frontend: `core/system-settings` (`useAgentModuleConfig`/`useUpdateAgentModuleConfig`), admin `AgentModuleSettingsDialog` (gear button in the agent-chat header).
|
||||
|
||||
### Portable Column Types (`deerflow/persistence/types.py`)
|
||||
|
||||
Two `TypeDecorator`s keep ORM columns correct across the `sqlite` / `postgres` / `mysql` backends. **All ORM timestamp/JSON columns must use these, not the raw SQLAlchemy types.**
|
||||
|
||||
@@ -16,11 +16,12 @@ from deerflow.config.system_settings import (
|
||||
PromptPrefixSettings,
|
||||
PublishSquare,
|
||||
PublishSquaresSettings,
|
||||
SystemSettings,
|
||||
get_module_tag_ids,
|
||||
list_publish_squares,
|
||||
load_system_settings,
|
||||
resolve_default_square_id,
|
||||
save_system_settings,
|
||||
set_module_tag_ids,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -377,3 +378,35 @@ async def get_log_system(request: Request) -> LogSystemResponse:
|
||||
await _require_admin(request)
|
||||
cfg = get_app_config().log_system
|
||||
return LogSystemResponse(enabled=cfg.enabled, url=cfg.url, label=cfg.label)
|
||||
|
||||
|
||||
class AgentModuleConfigResponse(BaseModel):
|
||||
"""Selected agent-tag ids for a「按类型聚合」module (zzJC / zzXD)."""
|
||||
|
||||
key: str = Field(..., description="Module key, e.g. zzjc / zzxd")
|
||||
tag_ids: list[str] = Field(default_factory=list, description="Selected agent tag ids")
|
||||
|
||||
|
||||
class AgentModuleConfigUpdateRequest(BaseModel):
|
||||
tag_ids: list[str] = Field(default_factory=list, description="Full replacement list of selected agent tag ids")
|
||||
|
||||
|
||||
@router.get("/agent-modules/{key}", response_model=AgentModuleConfigResponse)
|
||||
async def get_agent_module_config(key: str) -> AgentModuleConfigResponse:
|
||||
"""Return the agent tag ids configured for ``key`` (readable by any user —
|
||||
the module's left sidebar needs it to filter the agent list)."""
|
||||
settings = load_system_settings().agent_modules
|
||||
return AgentModuleConfigResponse(key=key, tag_ids=get_module_tag_ids(settings, key))
|
||||
|
||||
|
||||
@router.put("/agent-modules/{key}", response_model=AgentModuleConfigResponse)
|
||||
async def update_agent_module_config(
|
||||
key: str, body: AgentModuleConfigUpdateRequest, request: Request
|
||||
) -> AgentModuleConfigResponse:
|
||||
"""Replace the agent tag ids configured for ``key``. Admin only."""
|
||||
await _require_admin(request)
|
||||
|
||||
current = load_system_settings()
|
||||
updated = set_module_tag_ids(current.agent_modules, key, body.tag_ids)
|
||||
saved = save_system_settings(current.model_copy(update={"agent_modules": updated}))
|
||||
return AgentModuleConfigResponse(key=key, tag_ids=get_module_tag_ids(saved.agent_modules, key))
|
||||
@@ -170,6 +170,37 @@ class LeaderboardSettings(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
|
||||
class AgentModuleSettings(BaseModel):
|
||||
"""Admin-configured「按类型聚合」module → selected agent-tag ids.
|
||||
|
||||
Keyed by module key (e.g. ``"zzjc"`` / ``"zzxd"``). Each value is the list
|
||||
of agent **tag ids** whose agents that module's left sidebar shows. A missing
|
||||
key / empty list means "not configured" — the frontend falls back to showing
|
||||
all visible agents grouped by tag.
|
||||
"""
|
||||
|
||||
modules: dict[str, list[str]] = Field(
|
||||
default_factory=dict,
|
||||
description="module key → selected agent tag ids",
|
||||
)
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
|
||||
def get_module_tag_ids(settings: AgentModuleSettings, key: str) -> list[str]:
|
||||
"""Return the configured agent-tag ids for ``key`` (empty when unset)."""
|
||||
return list(settings.modules.get(key, []))
|
||||
|
||||
|
||||
def set_module_tag_ids(
|
||||
settings: AgentModuleSettings, key: str, tag_ids: list[str]
|
||||
) -> AgentModuleSettings:
|
||||
"""Return a copy with ``key`` set to a de-duplicated, stripped tag-id list."""
|
||||
cleaned = list(dict.fromkeys([str(t).strip() for t in tag_ids if str(t).strip()]))
|
||||
modules = dict(settings.modules)
|
||||
modules[key] = cleaned
|
||||
return settings.model_copy(update={"modules": modules})
|
||||
|
||||
|
||||
class SystemSettings(BaseModel):
|
||||
"""Top-level system settings document."""
|
||||
|
||||
@@ -179,6 +210,7 @@ class SystemSettings(BaseModel):
|
||||
appearance: AppearanceSettings = Field(default_factory=AppearanceSettings)
|
||||
citation_display: CitationDisplaySettings = Field(default_factory=CitationDisplaySettings)
|
||||
leaderboard: LeaderboardSettings = Field(default_factory=LeaderboardSettings)
|
||||
agent_modules: AgentModuleSettings = Field(default_factory=AgentModuleSettings)
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Tests for the admin-configured「按类型聚合」module → agent-tag config.
|
||||
|
||||
Backs the zzJC / zzXD module left sidebar (需求 0616 #3/#4): an admin picks
|
||||
which agent tags belong to the module, persisted in ``system_settings.json``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from deerflow.config import system_settings as ss
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_runtime_home(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
|
||||
"""Redirect ``runtime_home()`` to a per-test tmp dir."""
|
||||
|
||||
def fake_home() -> Path:
|
||||
return tmp_path
|
||||
|
||||
monkeypatch.setattr(ss, "runtime_home", fake_home)
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_load_returns_empty_modules_when_missing(tmp_runtime_home: Path) -> None:
|
||||
settings = ss.load_system_settings()
|
||||
assert settings.agent_modules.modules == {}
|
||||
assert ss.get_module_tag_ids(settings.agent_modules, "zzjc") == []
|
||||
|
||||
|
||||
def test_set_module_tag_ids_dedupes_and_strips() -> None:
|
||||
base = ss.AgentModuleSettings()
|
||||
updated = ss.set_module_tag_ids(base, "zzjc", [" t1 ", "t2", "t1", "", " "])
|
||||
assert updated.modules["zzjc"] == ["t1", "t2"]
|
||||
# Original is untouched (returns a copy).
|
||||
assert base.modules == {}
|
||||
|
||||
|
||||
def test_set_module_tag_ids_keeps_other_modules() -> None:
|
||||
base = ss.set_module_tag_ids(ss.AgentModuleSettings(), "zzjc", ["t1"])
|
||||
both = ss.set_module_tag_ids(base, "zzxd", ["t2", "t3"])
|
||||
assert ss.get_module_tag_ids(both, "zzjc") == ["t1"]
|
||||
assert ss.get_module_tag_ids(both, "zzxd") == ["t2", "t3"]
|
||||
|
||||
|
||||
def test_save_then_load_roundtrips(tmp_runtime_home: Path) -> None:
|
||||
current = ss.load_system_settings()
|
||||
updated = ss.set_module_tag_ids(current.agent_modules, "zzjc", ["tag-a", "tag-b"])
|
||||
ss.save_system_settings(current.model_copy(update={"agent_modules": updated}))
|
||||
|
||||
file_path = tmp_runtime_home / "system_settings.json"
|
||||
assert file_path.is_file()
|
||||
saved = json.loads(file_path.read_text(encoding="utf-8"))
|
||||
assert saved["agent_modules"]["modules"]["zzjc"] == ["tag-a", "tag-b"]
|
||||
|
||||
reloaded = ss.load_system_settings()
|
||||
assert ss.get_module_tag_ids(reloaded.agent_modules, "zzjc") == ["tag-a", "tag-b"]
|
||||
Reference in new issue
Block a user