deerflow-code/offline-backend-20260512/backend/app/gateway/routers/task_buttons.py
2026-09-07 18:24:55 +08:00

127 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 TaskButtonLoginParam(BaseModel):
"""url 外链追加的登录参数(与应用管理一致)。
运行时由前端从 ``userInfo`` 解析:``source`` 为 ``userInfo`` 字段名
(如 ``accessToken`` / ``yUserId``),``other`` 时取 ``custom_value`` 字面值。
"""
key: str = Field(default="", max_length=64)
source: str = Field(default="accessToken", max_length=64)
custom_value: str = Field(default="", max_length=512)
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)
# url=外链 / business=切换业务 / purpose=目的树弹框 / host=宿主路由(iframe postMessage)。
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)
# url 外链打开方式:blank=新窗口(默认)/ self=当前窗口。
open_mode: str = Field(default="blank", max_length=16)
# url 外链追加的登录参数列表(key + 取值来源)。
login_params: list[TaskButtonLoginParam] = Field(default_factory=list)
# url 跳转是否追加登录态(token 登录的上游 token + 登录用户名)。仅 token 登录时实际拼上。
append_auth: bool = False
# 追加 token / 用户名的查询参数名。
auth_token_param: str = Field(default="token", max_length=64)
auth_name_param: str = Field(default="username", max_length=64)
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])