"""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])