按钮管理「登录参数」两套实现冲突解决——两分支各自独立给跳转按钮加了登录态追加能力, 二者正交、保留两套,互不影响: - master: login_params (JSON 列 login_params_json,从 userInfo 解析,类似轻应用登录参数) - ji: append_auth + auth_token_param + auth_name_param (token 登录时追加 token1+用户名) 迁移 revision 碰撞修复(两分支都用了 20260625_01):重编号为线性链 20260620_02 → 20260625_01 (login_params) → 20260625_02 (auth_params) → 20260625_03 (users_approved) 并同步 column_additions_mysql.sql 注释里的 migration 编号。 前后端 task_buttons(model/sql/memory/router/前端 type·api·admin 页)均保留两套字段; 两份 CLAUDE.md 文档合并为同时记录两种机制。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
126 lines
5.2 KiB
Python
126 lines
5.2 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 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)
|
|
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])
|