"""ORM model for task-workspace configurable jump buttons (按钮管理). Global / shared — there is no ``user_id`` scoping; every account sees the same buttons. The task deep-link workspaces (rwfx / 3qfx / xdfx, see ``TaskFxWorkspace``) render the enabled buttons for their ``business`` in the left task panel. Each row is one button: - ``business`` — which workspace the button belongs to (``rwfx`` / ``3qfx`` / ``xdfx``). - ``label`` — display name. - ``link_type`` — ``url`` (external / in-app route), ``business`` (internal switch to another deep-link business), ``purpose`` (task-purpose dialog), or ``host`` (iframe ``postMessage`` ``HOST_NAVIGATE`` to the embedding host). - ``target`` — the full URL (``url``) or the target business code (``business``). - ``append_task_id`` — for ``url`` buttons, whether to append ``?id={taskId}``. - ``enabled`` — 停用 parks a button without deleting it. - ``sort_order`` — position among the business's buttons. When the whole 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. ``updated_by`` is audit-only. """ from __future__ import annotations from datetime import UTC, datetime from sqlalchemy import Boolean, Integer, String from sqlalchemy.orm import Mapped, mapped_column from deerflow.persistence.base import Base from deerflow.persistence.types import BeijingDateTime, PortableJSON class TaskButtonRow(Base): __tablename__ = "task_buttons" id: Mapped[str] = mapped_column(String(128), primary_key=True) business: Mapped[str] = mapped_column(String(32), index=True) label: Mapped[str] = mapped_column(String(255), default="") link_type: Mapped[str] = mapped_column(String(16), default="url") target: Mapped[str] = mapped_column(String(1024), default="") append_task_id: Mapped[bool] = mapped_column(Boolean, default=True) # 追加的查询参数名(如 ``id`` / ``task_id``)。可空,读时归一化为 ``id``。 id_param: Mapped[str | None] = mapped_column(String(64), default="id") # 追加的 id 取哪种:``task``=任务id(默认)/ ``action``=行动id(仅 xdfx 二者不同:深链原始 # ?taskId= 实为行动id,任务id 由 action 接口解析)。可空,读时归一化为 ``task``。 id_kind: Mapped[str | None] = mapped_column(String(16), default="task") # ``url`` 外链打开方式:``blank``=新窗口(默认)/ ``self``=当前窗口。可空,读时归一化为 ``blank``。 open_mode: Mapped[str | None] = mapped_column(String(16), default="blank") # ``url`` 外链追加的登录参数(与应用管理一致):在拼好的地址后再依次追加 # ``?{key}={value}``,``value`` 由前端运行时从 ``userInfo`` 按 ``source`` 解析 # (``other`` 时取 ``custom_value`` 字面值)。每项 ``{key, source, custom_value}``。 # 旧行 / NULL 读时归一化为 ``[]``。必须 ``nullable=True``:否则在已存在 # ``task_buttons`` 表的旧库上,启动期 ``_ensure_orm_columns_sync`` 会跳过 # 「NOT NULL 且无 server_default」的列(见 engine.py),该列永远补不上。 login_params_json: Mapped[list | None] = mapped_column(PortableJSON(), default=list, nullable=True) # ``url`` 跳转是否追加登录态(token 登录的上游 token + 登录用户名),供外链所在的外部系统 # 校验当前登录用户。仅 token 登录(前端存有 token1)时实际拼上;非 token 登录自动跳过。 # ``server_default`` 必须显式声明:否则在已存在 ``task_buttons`` 表的旧库上, # 启动期自动补列 ``_ensure_orm_columns_sync`` 会跳过「NOT NULL 且无 server_default」 # 的列(见 engine.py),导致该列永远补不上 → GET 查询报 Unknown column → 500。 append_auth: Mapped[bool] = mapped_column(Boolean, default=False, server_default="0") # 追加 token / 用户名的查询参数名。可空,读时归一化为 ``token`` / ``username``。 auth_token_param: Mapped[str | None] = mapped_column(String(64), default="token") auth_name_param: Mapped[str | None] = mapped_column(String(64), default="username") enabled: Mapped[bool] = mapped_column(Boolean, default=True) sort_order: Mapped[int] = mapped_column(Integer, default=0) updated_by: Mapped[str | None] = mapped_column(String(64)) updated_at: Mapped[datetime] = mapped_column( BeijingDateTime(), default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC) )