Files
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

67 lines
2.5 KiB
Python

"""SQLAlchemy-backed task-button store (global / shared)."""
from __future__ import annotations
from datetime import datetime
from typing import Any
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from deerflow.persistence.task_buttons.base import TaskButtonStore
from deerflow.persistence.task_buttons.model import TaskButtonRow
def _to_dict(row: TaskButtonRow) -> dict[str, Any]:
data = row.to_dict()
value = data.get("updated_at")
if isinstance(value, datetime):
data["updated_at"] = value.isoformat()
# 旧行 / NULL 归一化为默认值。
data["id_param"] = data.get("id_param") or "id"
data["id_kind"] = data.get("id_kind") or "task"
return data
def _row_from(data: dict[str, Any], updated_by: str | None) -> TaskButtonRow:
return TaskButtonRow(
id=str(data["id"]),
business=str(data.get("business") or ""),
label=str(data.get("label") or ""),
link_type=str(data.get("link_type") or "url"),
target=str(data.get("target") or ""),
append_task_id=bool(data.get("append_task_id", True)),
id_param=str(data.get("id_param") or "id"),
id_kind=str(data.get("id_kind") or "task"),
enabled=bool(data.get("enabled", True)),
sort_order=int(data.get("sort_order", 0)),
updated_by=updated_by,
)
class TaskButtonRepository(TaskButtonStore):
def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
self._sf = session_factory
async def list_buttons(self) -> list[dict[str, Any]]:
stmt = select(TaskButtonRow).order_by(TaskButtonRow.sort_order.asc())
async with self._sf() as session:
result = await session.execute(stmt)
return [_to_dict(row) for row in result.scalars()]
async def replace_all(self, rows: list[dict[str, Any]], *, updated_by: str | None = None) -> list[dict[str, Any]]:
# De-dupe by id (last wins) so a malformed payload can't violate the PK.
by_id: dict[str, dict[str, Any]] = {}
for r in rows:
row_id = r.get("id")
if row_id:
by_id[str(row_id)] = r
async with self._sf() as session:
await session.execute(delete(TaskButtonRow))
new_rows = [_row_from(r, updated_by) for r in by_id.values()]
session.add_all(new_rows)
await session.commit()
for row in new_rows:
await session.refresh(row)
return [_to_dict(row) for row in new_rows]