Files
deerflow-code/offline-backend-20260512/backend/app/gateway/routers/task_buttons.py
T
AdministratorandClaude Opus 4.8 16afa9bcbb feat: 任务工作区可配置跳转按钮(按钮管理) + 底部快捷跳转栏 + 目的树收起/空自动问答
- 新增「按钮管理」admin 页(/admin/task-buttons):按业务(rwfx/3qfx/xdfx)配置跳转按钮,
  支持改名称/类型(外链·切换业务·目的树弹框)/目标/追加参数名(id·task_id)/任务id·行动id/启停/排序
- 跳转按钮从左侧栏移到工作区底部「快捷跳转」栏:红底白字、靠右、变大;rwfx 历史 4 按钮纳入配置
- 后端新增全局共享 store /api/task-buttons (table task_buttons,admin 整体替换)
- xdfx 行动id/任务id:登录暂存原始行动id,按钮可选追加任一;id 参数名可自定义
- rwfx 清除记录移到「新建对话」下、去掉任务操作框;查询任务目的为空时自动发第一次问答
- 目的树面板默认收起;「弹框查看」抽成可复用 TaskPurposeDialog,可作为按钮配置到任意页面
- 文档(根/后端 CLAUDE.md) + 后端测试 tests/test_task_buttons.py

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:41:05 +08:00

105 lines
4.1 KiB
Python

"""Task-workspace configurable jump-button API (按钮管理).
The task deep-link workspaces (rwfx / 3qfx / xdfx, see the frontend
``TaskFxWorkspace``) render configurable jump buttons in their left task panel.
Their default targets historically lived in ``config.yaml`` ``task_deeplink`` and
their names were hardcoded; this router persists a **global / shared** button
set the frontend renders instead, so an admin can rename / retarget / add /
remove jump buttons per business.
When the table is empty (fresh deployment, never saved) the frontend falls back
to code-defined seed defaults built from the live task-deeplink config; once
saved, this table is the source of truth.
Reads are open to any authenticated user (the workspace needs them); the whole
button set is replaced atomically and only by an admin.
Routes (prefix ``/api/task-buttons``):
GET "" list all buttons
PUT "" replace the entire button set (admin)
"""
from __future__ import annotations
from datetime import datetime
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field
from app.gateway.deps import get_current_user, get_optional_user_from_request
from deerflow.persistence.task_buttons import TaskButtonStore
router = APIRouter(prefix="/api/task-buttons", tags=["task-buttons"])
# ── schemas ──────────────────────────────────────────────────────────────
class TaskButton(BaseModel):
id: str = Field(..., min_length=1, max_length=128)
business: str = Field(..., min_length=1, max_length=32)
label: str = Field(default="", max_length=255)
link_type: str = Field(default="url", max_length=16)
target: str = Field(default="", max_length=1024)
append_task_id: bool = True
# 追加的查询参数名(如 id / task_id)。
id_param: str = Field(default="id", max_length=64)
# task=任务id(默认)/ action=行动id(仅 xdfx 二者不同)。
id_kind: str = Field(default="task", max_length=16)
enabled: bool = True
sort_order: int = 0
class TaskButtonResponse(TaskButton):
updated_by: str | None = None
updated_at: datetime | str | None = None
class TaskButtonListResponse(BaseModel):
buttons: list[TaskButtonResponse]
class TaskButtonReplaceRequest(BaseModel):
buttons: list[TaskButton] = Field(default_factory=list)
# ── helpers ──────────────────────────────────────────────────────────────
def _get_store(request: Request) -> TaskButtonStore:
store = getattr(request.app.state, "task_button_store", None)
if store is None:
raise HTTPException(status_code=503, detail="Task button store not available")
return store
async def _require_admin(request: Request) -> None:
"""Reject non-admins. Lets the call through when auth is disabled."""
user = await get_optional_user_from_request(request)
if user is None:
return
if getattr(user, "system_role", None) != "admin":
raise HTTPException(status_code=403, detail="按钮管理仅限管理员")
# ── routes ───────────────────────────────────────────────────────────────
@router.get("", response_model=TaskButtonListResponse)
async def list_task_buttons(request: Request) -> TaskButtonListResponse:
store = _get_store(request)
rows = await store.list_buttons()
return TaskButtonListResponse(buttons=[TaskButtonResponse(**r) for r in rows])
@router.put("", response_model=TaskButtonListResponse)
async def replace_task_buttons(request: Request, body: TaskButtonReplaceRequest) -> TaskButtonListResponse:
await _require_admin(request)
store = _get_store(request)
user_id = await get_current_user(request)
rows = await store.replace_all(
[b.model_dump() for b in body.buttons],
updated_by=user_id,
)
return TaskButtonListResponse(buttons=[TaskButtonResponse(**r) for r in rows])