feat(task-buttons): 跳转按钮可追加登录态 token+username + token 登录离线测试映射
rwfx/3qfx/xdfx 任务工作区的可配置跳转按钮(按钮管理)新增「追加登录态」开关: url 类型按钮可在外链后拼 ?token=<token1>&username=<登录名>(参数名可配,默认 token/username),供跳转目标外部系统识别当前登录用户。 - 仅 token 登录(login/token)生效: token1 = ?authToken=,username = 后端用 token 调 getTokenInfo 解析出的原始登录名(非邮箱前缀/非解 JWT)。二者登录时 经 persistLoginCredentials 存入 login.token1/login.username(scoped-storage 自动隔离嵌入/独立会话);非 token 登录读不到 token1 则跳过追加。 - 后端 UsernameLoginResponse 回传 username(原始名,规避邮箱归一化有损); token_login 加 mock_tokens 离线测试映射(123ewq->admin),命中即跳过外网 getTokenInfo,便于本地全链路联调。 - task_buttons 加列 append_auth/auth_token_param/auth_name_param (migration 20260625_01 + 幂等 SQL)。 Tests: test_task_buttons.py, test_auth_login_modes.py(mock token/username 回传)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6989dd45f1
commit
f8b40353ef
18 files changed
+298
-6
No files matched your search
@@ -258,7 +258,7 @@ FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWA
|
||||
| **Menu Overrides** (`/api/menu-overrides`) | Sidebar-menu overrides (菜单管理), **global/shared** (no user scoping; `updated_by` audit-only). The static menu tree (icons/routes/`adminOnly`) lives in the **frontend** code (`core/page-layout/sidebar-menu.ts`); iframe light-apps are injected at render time. This router persists a per-node **overlay** the frontend merges onto that tree: `parent_id` (re-parent, moves the whole subtree; NULL = top level), `sort_order` (reorder among siblings), `label` (rename; NULL keeps code label), `disabled` (停用 — hides the node **and its subtree**). A node with no row renders exactly as code defines it, so new code menus / newly-registered iframe apps appear automatically and are immediately configurable. `GET /` - list all overrides (open to any authenticated user); `PUT /` - **admin only**, atomically **replaces the whole set** (`{overrides: [...]}`); rejects re-parenting that would exceed **3 levels** or form a cycle (`_validate_depth`, HTTP 400). Backed by `deerflow.persistence.menu_overrides` (table `menu_overrides`, auto-created by `create_all`); store wired in `deps.py` as `app.state.menu_override_store`. Frontend: merge logic `core/page-layout/menu-overrides.ts` (`applyMenuOverrides` in `use-sidebar-menu.ts`), client `strategy-components/api/menu-overrides.ts`, admin page `pages/MenuManagementPage.tsx` (@dnd-kit drag-reorder/re-parent + 「移动到」parent selector + ↑↓ arrows + 改名/停用), entry in the bottom-left settings dropdown (`components/page-sidebar.tsx`, admin-only). Tests: `tests/test_menu_overrides.py`, `frontend-web/src/core/page-layout/menu-overrides.test.ts`. |
|
||||
| **Sensitive Words** (`/api/sensitive-words`) | Sensitive-word / desensitize map (敏感词管理 / 脱敏词), **global/shared** (no user scoping; `updated_by` audit-only). Each row maps an original `word` → `replacement`, with `enabled` (停用 parks a word without deleting) + `sort_order`. The **frontend** ships a seed map in code (`src/lib/desensitize.ts` `SEED_DESENSITIZE_MAP`) used as the default on a fresh deployment; once an admin saves, this table is the source of truth and the frontend loads it at startup (`DesensitizeWordsLoader` → `replaceDesensitizeMap`, **enabled** rows only). `GET /` - list all words (open to any authenticated user — every page desensitizes); `PUT /` - **admin OR the `lqq` account** (`_require_manager`: `system_role == "admin"` or email `@`-prefix `== "lqq"`), atomically **replaces the whole set** (`{words: [...]}`, de-duped by `word`, last wins). Backed by `deerflow.persistence.sensitive_words` (table `sensitive_words`, PK `word` `String(191)`, auto-created by `create_all`); store wired in `deps.py` as `app.state.sensitive_word_store`. Frontend: client `strategy-components/api/sensitive-words.ts`, page `pages/SensitiveWordsPage.tsx` (route `/page/strategy/admin/sensitive-words`, entry in the bottom-left settings dropdown — **gated on username `lqq`, not admin role**, present in both sidebar modes; 「导入种子词」 merges missing seed entries). Tests: `tests/test_sensitive_words.py`. |
|
||||
| **Dashboard Sessions** (`/api/dashboard-sessions`) | 「大屏绘制智能体」独立页面会话,**按外部 `task_id` 存取,不按 user 分权**(open read + shared writes;`user_id` audit-only)。一个 task 一行(upsert):`transcript`(聊天记录 JSON)+ `report_json`(最新结构化数据,可解析 = 「有效结果」)。`GET /by-task/{task_id}` - 取该 task 会话(无则 404);`PUT /by-task/{task_id}` - upsert(`{transcript?, report_json?}`,只覆盖传入项)。Backed by `deerflow.persistence.dashboard_sessions`(table `dashboard_sessions`, auto-created by `create_all`,无迁移);store wired in `deps.py` as `app.state.dashboard_session_store`。前端独立页 `roundtable-planning/pages/DashboardAgentPage.tsx`(路由 `/page/strategy/dashboard-agent?taskId=`,侧边栏「问答管理 → 大屏绘制」)让智能体分析登录拉取的业务 JSON → report-json → 渲染 `DashboardApp`;输入 seam `core/auth/dashboard-input.ts`(JSON 接口未就绪时回退 copTaskDetail 占位)。Tests: `tests/test_dashboard_sessions.py`。 |
|
||||
| **Task Buttons** (`/api/task-buttons`) | Task-workspace configurable jump buttons (按钮管理), **global/shared** (no user scoping; `updated_by` audit-only). Each row = one jump button in a task deep-link workspace's bottom 快捷跳转 bar: `{id, business (rwfx/3qfx/xdfx), label, link_type (url/business), target, append_task_id, id_param (editable query-param name, e.g. id/task_id), id_kind (task/action — only xdfx differs, its deep-link id is an 行动id), enabled, sort_order}`. The **frontend** holds code-seed defaults (rwfx's 4 historical buttons built from the live `task_deeplink` config); only when this table is empty do the workspace + admin page fall back to them, else this table is the sole source of truth. `GET /` - list all (open to any authenticated user — the workspace renders them); `PUT /` - **admin only** (`_require_admin`), atomically **replaces the whole set** (`{buttons: [...]}`, de-duped by `id`, last wins). Backed by `deerflow.persistence.task_buttons` (table `task_buttons`, auto-created by `create_all`); store wired in `deps.py` as `app.state.task_button_store`. Frontend: helpers `core/auth/task-buttons.ts`, client `strategy-components/api/task-buttons.ts`, admin page `pages/TaskButtonsPage.tsx` (route `/page/strategy/admin/task-buttons`, entry「按钮管理」in the bottom-left settings dropdown, admin-only, both sidebar modes; per-business tabs + 「导入默认」). Tests: `tests/test_task_buttons.py`. |
|
||||
| **Task Buttons** (`/api/task-buttons`) | Task-workspace configurable jump buttons (按钮管理), **global/shared** (no user scoping; `updated_by` audit-only). Each row = one jump button in a task deep-link workspace's bottom 快捷跳转 bar: `{id, business (rwfx/3qfx/xdfx), label, link_type (url/business), target, append_task_id, id_param (editable query-param name, e.g. id/task_id), id_kind (task/action — only xdfx differs, its deep-link id is an 行动id), append_auth, auth_token_param, auth_name_param, enabled, sort_order}`. **`append_auth` (追加登录态)**: a `url` button can append `?{auth_token_param}={token1}&{auth_name_param}={username}` (default names `token`/`username`) to its external link so the target system can identify the current user; gated on **token login** — the frontend only stashes `token1` (the `?authToken=`) + the raw `getTokenInfo` username (`login.token1`/`login.username`) when login went through `/login/token`, so a non-token-login session appends neither (the auth columns are added by migration `20260625_01`; default off). The **frontend** holds code-seed defaults (rwfx's 4 historical buttons built from the live `task_deeplink` config); only when this table is empty do the workspace + admin page fall back to them, else this table is the sole source of truth. `GET /` - list all (open to any authenticated user — the workspace renders them); `PUT /` - **admin only** (`_require_admin`), atomically **replaces the whole set** (`{buttons: [...]}`, de-duped by `id`, last wins). Backed by `deerflow.persistence.task_buttons` (table `task_buttons`, auto-created by `create_all`); store wired in `deps.py` as `app.state.task_button_store`. Frontend: helpers `core/auth/task-buttons.ts`, client `strategy-components/api/task-buttons.ts`, admin page `pages/TaskButtonsPage.tsx` (route `/page/strategy/admin/task-buttons`, entry「按钮管理」in the bottom-left settings dropdown, admin-only, both sidebar modes; per-business tabs + 「导入默认」). Tests: `tests/test_task_buttons.py`. |
|
||||
| **Knowledge** (`/api/knowledge`) | Product knowledge base, **global/shared** (no user isolation; writes record `created_by`/`updated_by` for audit). `POST /ingest/thread/{thread_id}` - sediment one thread into a note (reads messages from the checkpointer, `threads:read` owner-checked); `POST /ingest/threads/batch` - batch-sediment the caller's recent threads (`skip_existing` via `.manifest.json` dedup). **Defaults to `background=true`**: the slow per-thread extraction loop is deferred to a FastAPI background task and the endpoint returns `{queued:true, processed:N}` immediately (each thread's transcript is truncated to `max_input_chars` before the LLM call). Running the loop inline for a large batch outlives the nginx gateway timeout (504), which the frontend's `parseResponse` surfaces as a generic "Request failed"; `background=false` keeps the blocking path (full per-thread `created/skipped/failed` tally) for small batches/scripts. Shared loop: `_run_batch_ingest`. **Optional model selection**: both ingest endpoints accept `model_name` (overrides `knowledge.extract_model_name` for that capture; threaded through `KnowledgeService.capture_thread` ? `extract_thread_llm_multi`). **Model-error surfacing**: when the chosen model produces nothing usable (timeout / API error / non-JSON) capture still **falls back to rule-based extraction and lands a draft** (`llm_fallback:true`); the single-thread (non-background) response and the batch response (`llm_fallback` count) carry this so the UI tells the user "???????". The "??????" chat button stays background/fire-and-forget (passes `model_name`, no per-save error toast); the batch-import dialog is synchronous and shows the fallback count. `POST /ingest/file` - **import an uploaded file** (multipart: `file` + optional `title`/`tags`/`status`/`model_name`) into wiki note(s): the file is converted to Markdown (md/txt read directly, pdf/word/ppt/excel via `convert_file_to_markdown`), distilled by the same wiki-capture LLM pipeline as thread capture (`source_type="file"`), and falls back to a verbatim **draft** note when the model returns nothing usable (`llm_fallback:true`); `POST /ingest/search` - sediment search/tool results as a reference note; `POST /notes` - manual note; `GET /notes` (keyword/tag/source_type/status + pagination); `GET/PUT/DELETE /notes/{id}` (DELETE is soft = archive, `?hard=true` removes the vault file); `POST /search` - `mode=keyword\|vector\|hybrid` (vector?keyword fallback when no embedding endpoint); `GET /notes/{id}/versions` - edit history; `GET /duplicates` + `POST /merge` - dedup; `POST /reindex` - rebuild vector index; `GET /graph` - product graph data (notes + entities, graph-blacklist filtered); `GET/POST /graph/blacklist` + `DELETE /graph/blacklist/{id}` - graph node blacklist (labels = entity names / note titles, case-insensitive match; blacklisted nodes and their edges are dropped from `/graph` and the wiki-export artifacts; table `knowledge_graph_blacklist`, auto-created); `POST /export/graph` + `GET /export/files/{graph.json\|graph.html\|graph.graphml\|cypher.txt}` - obsidian-wiki `wiki-export` artifacts (self-contained `graph.html` for iframe); `POST /resolve` `{targets:[?]}` + `GET /notes/{id}/wikilinks` - resolve `[[wikilink]]` targets to a note id / vault page / missing (powers clickable wikilinks); `GET /vault-page?path=` - read a raw vault meta/hub Markdown page (no DB note). `GET/POST /folders` + `PUT/DELETE /folders/{id}` - user-managed directories (notes carry a `folder` path; `GET /notes?folder=` filters a dir + sub-dirs); `GET/POST /extract-templates` + `GET/PUT/DELETE /extract-templates/{id}` - editable extraction-prompt templates (thread/document ingest accept `template_id`; capture/PUT-note accept `folder`). Backed by the `deerflow.knowledge` module (see below). |
|
||||
|
||||
Proxied through nginx: `/api/langgraph/*` ? LangGraph, all other `/api/*` ? Gateway.
|
||||
@@ -799,7 +799,7 @@ Per-user runtime overrides (whitelisted fields) live at `{base_dir}/users/{user_
|
||||
- `memory` - Memory system (enabled, storage_path, debounce_seconds, model_name, max_facts, fact_confidence_threshold, injection_enabled, max_injection_tokens)
|
||||
- `auth_login` - Custom login modes layered on the base auth (`deerflow/config/login_config.py`):
|
||||
- `username_login.{require_password, password}` - Password gate for `POST /api/v1/auth/login/username`. When `require_password=true`, the (otherwise passwordless) username login additionally requires `?password=<shared-secret>`; empty `password` falls back to `123ewq`. Gate only ? it does not change which account logs in.
|
||||
- `token_login.{enabled, token_info_url, service_authorization, timeout_seconds, verify_ssl, ca_cert_path}` - Token-exchange login (`POST /api/v1/auth/login/token`). The gateway POSTs to `token_info_url` with the caller-supplied token placed directly in the `Authorization: Bearer <token>` header (no cookie; the static `service_authorization` field is deprecated/unused, kept only for backward compat), reads `data.username` from a `{"state":"200",...}` body, then signs the matching local user in (auto-registering if absent). `token_info_url` is HTTPS ? `verify_ssl: false` skips cert verification for self-signed/internal UAT hosts, or set `ca_cert_path` to a PEM bundle (takes precedence, keeps verification on). See [docs/AUTH_LOGIN.md](docs/AUTH_LOGIN.md) for the full request/response contract.
|
||||
- `token_login.{enabled, token_info_url, service_authorization, timeout_seconds, verify_ssl, ca_cert_path, mock_tokens}` - Token-exchange login (`POST /api/v1/auth/login/token`). The gateway POSTs to `token_info_url` with the caller-supplied token placed directly in the `Authorization: Bearer <token>` header (no cookie; the static `service_authorization` field is deprecated/unused, kept only for backward compat), reads `data.username` from a `{"state":"200",...}` body, then signs the matching local user in (auto-registering if absent). `token_info_url` is HTTPS ? `verify_ssl: false` skips cert verification for self-signed/internal UAT hosts, or set `ca_cert_path` to a PEM bundle (takes precedence, keeps verification on). **`mock_tokens` (offline test mapping `{token: username}`)**: when an incoming token matches a key, `_fetch_username_from_token` returns the mapped username and **skips the `token_info_url` call entirely** — for environments where the upstream `getTokenInfo` host is unreachable (e.g. external networks). Empty in prod (no effect); seeded `{"123ewq": "admin"}` in this deployment so `/login/任意?authToken=123ewq` signs in as admin. **`UsernameLoginResponse` now also returns `username`** (the raw resolved login name, pre email-normalization) on both `/login/token` and `/login/username`, so the frontend can append a faithful `?username=` on jump links (按钮管理 追加登录态) instead of the lossy email local-part. See [docs/AUTH_LOGIN.md](docs/AUTH_LOGIN.md) for the full request/response contract.
|
||||
|
||||
Both endpoints are whitelisted in `auth_middleware._PUBLIC_EXACT_PATHS`. The shared `_get_or_create_user_by_username()` helper (in `routers/auth.py`) backs both `/login/username` and `/login/token`.
|
||||
|
||||
|
||||
@@ -145,6 +145,15 @@ class UsernameLoginResponse(BaseModel):
|
||||
system_role: Literal["admin", "user"] = "user"
|
||||
needs_setup: bool = False
|
||||
created: bool = Field(default=False, description="True when the user was auto-registered by this call")
|
||||
username: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"The login username this call resolved (token login: the value read "
|
||||
"from getTokenInfo; username login: the supplied username). Used by "
|
||||
"the frontend to append ?username= on task-workspace jump links; the "
|
||||
"stored email local-part is lossy (lowercased/normalized)."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
@@ -277,6 +286,15 @@ async def _fetch_username_from_token(token: str) -> str:
|
||||
Any upstream failure, non-200 ``state``, or missing username raises HTTP 401.
|
||||
"""
|
||||
config = get_app_config().auth_login.token_login
|
||||
|
||||
# Offline test shortcut: a token listed in ``mock_tokens`` resolves to its
|
||||
# mapped username without ever calling the (possibly unreachable) upstream
|
||||
# ``token_info_url``. Lets token login be exercised end-to-end where the real
|
||||
# getTokenInfo host can't be reached (external networks). Empty in prod.
|
||||
mocked = config.mock_tokens.get(token)
|
||||
if mocked and str(mocked).strip():
|
||||
return str(mocked).strip()
|
||||
|
||||
if not config.token_info_url:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="token 登录未配置(token_info_url 为空)")
|
||||
|
||||
@@ -547,6 +565,7 @@ async def login_by_username(
|
||||
system_role=user.system_role,
|
||||
needs_setup=user.needs_setup,
|
||||
created=created,
|
||||
username=body.username,
|
||||
)
|
||||
|
||||
|
||||
@@ -598,6 +617,9 @@ async def login_by_token(
|
||||
system_role=user.system_role,
|
||||
needs_setup=user.needs_setup,
|
||||
created=created,
|
||||
# The raw username resolved from getTokenInfo (pre email-normalization),
|
||||
# so the frontend can append the exact ?username= on jump links.
|
||||
username=username,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -48,6 +48,11 @@ class TaskButton(BaseModel):
|
||||
id_kind: str = Field(default="task", max_length=16)
|
||||
# url 外链打开方式:blank=新窗口(默认)/ self=当前窗口。
|
||||
open_mode: str = Field(default="blank", max_length=16)
|
||||
# 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
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ auth_login:
|
||||
# (把该 token 直接拼成 Authorization: Bearer <token> 头)换出用户名,再登录;
|
||||
# 用户名不在库里时自动注册。
|
||||
token_login:
|
||||
enabled: false
|
||||
enabled: true
|
||||
token_info_url: 'https://ch1.b.uat.4.cn/consumer/login/getTokenInfo'
|
||||
# 已弃用:鉴权头现由前端传入的 token 拼成 "Bearer <token>",此项不再生效,保留仅兼容。
|
||||
service_authorization: 'Bearer admin'
|
||||
@@ -316,6 +316,11 @@ auth_login:
|
||||
# 永远 401。此处 uat 域名按自签处理,先关校验;生产请置 true 或配 ca_cert_path。
|
||||
verify_ssl: false
|
||||
ca_cert_path: '' # 可选:PEM 格式 CA 包路径,设置后优先生效并启用校验
|
||||
# 离线测试映射:token -> 用户名。命中的 token 跳过 token_info_url 调用,直接当成
|
||||
# 该用户登录——外网调不通 getTokenInfo 时用来本地全链路联调。生产留空即可(不生效)。
|
||||
# 例:用 /login/任意?authToken=123ewq 进入,即以 admin 身份登录。
|
||||
mock_tokens:
|
||||
'123ewq': 'admin'
|
||||
|
||||
# 日志系统入口(仅管理员可见):右下角「设置」菜单中会多出一个「日志系统」入口,
|
||||
# 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。
|
||||
|
||||
@@ -74,6 +74,16 @@ class TokenLoginConfig(BaseModel):
|
||||
"certificate. Takes precedence over verify_ssl when set."
|
||||
),
|
||||
)
|
||||
mock_tokens: dict[str, str] = Field(
|
||||
default_factory=dict,
|
||||
description=(
|
||||
"Offline test mapping of token -> username. When an incoming token "
|
||||
"matches a key here, the gateway skips the token_info_url call and "
|
||||
"signs in as the mapped username — useful where the upstream "
|
||||
"getTokenInfo endpoint is unreachable (e.g. external networks). "
|
||||
"Empty in production (no effect). Example: {\"123ewq\": \"admin\"}."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AuthLoginConfig(BaseModel):
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
"""Add append_auth / auth_token_param / auth_name_param to task_buttons.
|
||||
|
||||
These back the「追加登录态」option on task-workspace jump buttons (按钮管理):
|
||||
a ``url`` button can append the login token (``token``) + username (``username``)
|
||||
to its external link so the target system can identify the current user.
|
||||
|
||||
Revision ID: 20260625_01
|
||||
Revises: 20260620_02
|
||||
Create Date: 2026-06-25
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
revision = "20260625_01"
|
||||
down_revision = "20260620_02"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _has_table(table_name: str) -> bool:
|
||||
inspector = inspect(op.get_bind())
|
||||
return table_name in inspector.get_table_names()
|
||||
|
||||
|
||||
def _has_column(table_name: str, column_name: str) -> bool:
|
||||
if not _has_table(table_name):
|
||||
return False
|
||||
inspector = inspect(op.get_bind())
|
||||
return any(col["name"] == column_name for col in inspector.get_columns(table_name))
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ``task_buttons`` may not exist yet on a brand-new DB — create_all handles
|
||||
# that case with the columns already present, so only patch existing tables.
|
||||
if _has_table("task_buttons"):
|
||||
if not _has_column("task_buttons", "append_auth"):
|
||||
op.add_column(
|
||||
"task_buttons",
|
||||
sa.Column("append_auth", sa.Boolean(), nullable=False, server_default="0"),
|
||||
)
|
||||
if not _has_column("task_buttons", "auth_token_param"):
|
||||
op.add_column(
|
||||
"task_buttons",
|
||||
sa.Column("auth_token_param", sa.String(length=64), nullable=True, server_default="token"),
|
||||
)
|
||||
if not _has_column("task_buttons", "auth_name_param"):
|
||||
op.add_column(
|
||||
"task_buttons",
|
||||
sa.Column("auth_name_param", sa.String(length=64), nullable=True, server_default="username"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
for col in ("auth_name_param", "auth_token_param", "append_auth"):
|
||||
if _has_column("task_buttons", col):
|
||||
op.drop_column("task_buttons", col)
|
||||
+3
@@ -19,6 +19,9 @@ def _normalize(data: dict[str, Any], updated_by: str | None) -> dict[str, Any]:
|
||||
"id_param": str(data.get("id_param") or "id"),
|
||||
"id_kind": str(data.get("id_kind") or "task"),
|
||||
"open_mode": str(data.get("open_mode") or "blank"),
|
||||
"append_auth": bool(data.get("append_auth", False)),
|
||||
"auth_token_param": str(data.get("auth_token_param") or "token"),
|
||||
"auth_name_param": str(data.get("auth_name_param") or "username"),
|
||||
"enabled": bool(data.get("enabled", True)),
|
||||
"sort_order": int(data.get("sort_order", 0)),
|
||||
"updated_by": updated_by,
|
||||
|
||||
+6
@@ -48,6 +48,12 @@ class TaskButtonRow(Base):
|
||||
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`` 跳转是否追加登录态(token 登录的上游 token + 登录用户名),供外链所在的外部系统
|
||||
# 校验当前登录用户。仅 token 登录(前端存有 token1)时实际拼上;非 token 登录自动跳过。
|
||||
append_auth: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
# 追加 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))
|
||||
|
||||
+6
@@ -21,6 +21,9 @@ def _to_dict(row: TaskButtonRow) -> dict[str, Any]:
|
||||
data["id_param"] = data.get("id_param") or "id"
|
||||
data["id_kind"] = data.get("id_kind") or "task"
|
||||
data["open_mode"] = data.get("open_mode") or "blank"
|
||||
data["append_auth"] = bool(data.get("append_auth", False))
|
||||
data["auth_token_param"] = data.get("auth_token_param") or "token"
|
||||
data["auth_name_param"] = data.get("auth_name_param") or "username"
|
||||
return data
|
||||
|
||||
|
||||
@@ -35,6 +38,9 @@ def _row_from(data: dict[str, Any], updated_by: str | None) -> TaskButtonRow:
|
||||
id_param=str(data.get("id_param") or "id"),
|
||||
id_kind=str(data.get("id_kind") or "task"),
|
||||
open_mode=str(data.get("open_mode") or "blank"),
|
||||
append_auth=bool(data.get("append_auth", False)),
|
||||
auth_token_param=str(data.get("auth_token_param") or "token"),
|
||||
auth_name_param=str(data.get("auth_name_param") or "username"),
|
||||
enabled=bool(data.get("enabled", True)),
|
||||
sort_order=int(data.get("sort_order", 0)),
|
||||
updated_by=updated_by,
|
||||
|
||||
@@ -162,6 +162,15 @@ PREPARE stmt FROM @idx_ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---- task_buttons ------------------------------------------------------------
|
||||
-- 任务工作区跳转按钮的「追加登录态」选项,对应 migration 20260625_01。url 按钮可在外链
|
||||
-- 后追加 token(token 登录的上游 token)+ username(登录用户名),供外部系统识别当前用户。
|
||||
-- ORM: TaskButtonRow.append_auth = Boolean NOT NULL default False;
|
||||
-- auth_token_param / auth_name_param = String(64), nullable, 读时归一化为 token/username。
|
||||
CALL _add_column_if_missing('task_buttons', 'append_auth', 'TINYINT(1) NOT NULL DEFAULT 0');
|
||||
CALL _add_column_if_missing('task_buttons', 'auth_token_param', "VARCHAR(64) NULL DEFAULT 'token'");
|
||||
CALL _add_column_if_missing('task_buttons', 'auth_name_param', "VARCHAR(64) NULL DEFAULT 'username'");
|
||||
|
||||
-- ---- 执行后自检:确认本批次涉及的列都已存在 ---------------------------------
|
||||
SELECT table_name, column_name
|
||||
FROM information_schema.columns
|
||||
@@ -169,5 +178,6 @@ SELECT table_name, column_name
|
||||
AND ((table_name = 'skills' AND column_name IN ('name_zh', 'detail', 'square_id', 'always_on'))
|
||||
OR (table_name = 'agents' AND column_name IN ('square_id'))
|
||||
OR (table_name = 'roundtable_drafts' AND column_name = 'task_id')
|
||||
OR (table_name = 'roundtable_jobs' AND column_name = 'task_id'))
|
||||
OR (table_name = 'roundtable_jobs' AND column_name = 'task_id')
|
||||
OR (table_name = 'task_buttons' AND column_name IN ('append_auth', 'auth_token_param', 'auth_name_param')))
|
||||
ORDER BY table_name, column_name;
|
||||
@@ -212,6 +212,12 @@ auth_login:
|
||||
# 永远 401。此处 uat 域名按自签处理,先关校验;生产请置 true 或配 ca_cert_path。
|
||||
verify_ssl: false
|
||||
ca_cert_path: "" # 可选:PEM 格式 CA 包路径,设置后优先生效并启用校验
|
||||
# 离线测试映射:token -> 用户名。命中的 token 跳过 token_info_url 调用,直接当成
|
||||
# 该用户登录——外网调不通 getTokenInfo 时用来本地全链路联调。生产留空即可(不生效)。
|
||||
# 例:用 /login/任意?authToken=123ewq 进入,即以 admin 身份登录;token1=123ewq、
|
||||
# username=admin 会被存下,供任务工作区跳转按钮的「追加登录态」拼到外链上。
|
||||
mock_tokens:
|
||||
"123ewq": "admin"
|
||||
# 日志系统入口(仅管理员可见):右下角「设置」菜单中会多出一个「日志系统」入口,
|
||||
# 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。
|
||||
|
||||
|
||||
Reference in new issue
Block a user