From f8b40353efd9b9fcfcde0ea1b7d84ad3970e9815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Thu, 25 Jun 2026 14:52:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(task-buttons):=20=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E5=8F=AF=E8=BF=BD=E5=8A=A0=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=80=81=20token+username=20+=20token=20=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E7=A6=BB=E7=BA=BF=E6=B5=8B=E8=AF=95=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rwfx/3qfx/xdfx 任务工作区的可配置跳转按钮(按钮管理)新增「追加登录态」开关: url 类型按钮可在外链后拼 ?token=&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) --- CLAUDE.md | 2 +- frontend-web/src/core/auth/index.ts | 6 ++ .../src/core/auth/login-credentials.ts | 61 +++++++++++++++++++ frontend-web/src/core/auth/task-buttons.ts | 46 +++++++++++++- frontend-web/src/pages/LoginPage.tsx | 4 ++ frontend-web/src/pages/TaskButtonsPage.tsx | 34 +++++++++++ .../strategy-components/api/task-buttons.ts | 9 +++ offline-backend-20260512/backend/CLAUDE.md | 4 +- .../backend/app/gateway/routers/auth.py | 22 +++++++ .../app/gateway/routers/task_buttons.py | 5 ++ offline-backend-20260512/backend/config.yaml | 7 ++- .../harness/deerflow/config/login_config.py | 10 +++ .../20260625_01_task_buttons_auth_params.py | 61 +++++++++++++++++++ .../persistence/task_buttons/memory.py | 3 + .../persistence/task_buttons/model.py | 6 ++ .../deerflow/persistence/task_buttons/sql.py | 6 ++ .../scripts/sql/column_additions_mysql.sql | 12 +++- offline-backend-20260512/config.yaml | 6 ++ 18 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 frontend-web/src/core/auth/login-credentials.ts create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260625_01_task_buttons_auth_params.py diff --git a/CLAUDE.md b/CLAUDE.md index db045e54..9c3d4419 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,7 +84,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA: - **菜单管理 (Menu Management, admin-only)**: an **overlay** over the static sidebar menu — `MenuManagementPage` (`pages/MenuManagementPage.tsx`, route `/page/strategy/admin/menu-management`, opened from the admin-only 「菜单管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`). Admins rename / 停用·启用 / reorder (↑↓ or @dnd-kit drag) / re-parent (horizontal drag **or** the edit dialog's 「移动到」parent selector) any node across 一级/二级/三级, **including injected iframe light-app nodes** (`light-app-`). The static tree (`core/page-layout/sidebar-menu.ts`) stays the source of icons/routes; only per-node deltas (`label`/`disabled`/`parentId`/`sortOrder`) persist to the backend. Merge logic in `core/page-layout/menu-overrides.ts` (`applyMenuOverrides`, applied in `use-sidebar-menu.ts` after light-app injection, before role filter; disabling a node hides its whole subtree) — strict **3-level cap** (`wouldExceedDepth`). Client `strategy-components/api/menu-overrides.ts`. Backend: `/api/menu-overrides` (global/shared; open read, admin atomic replace-all). - **敏感词管理 (Sensitive Word Management)**: configures the **展示层脱敏映射** (原始词 → 替换词). **Gated on the username `lqq`** (not the admin role) — `getAccountDisplayName().toLowerCase() === "lqq"` (username = email `@` prefix, else `user_id`, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check. `src/lib/desensitize.ts` ships a frozen seed map `SEED_DESENSITIZE_MAP` (the deployment seed) plus a live mutable `DESENSITIZE_MAP` (initialized from the seed so `desensitize()` works before any fetch). `DesensitizeWordsLoader` (mounted in `App.tsx` `AppProviders`) fetches `/api/sensitive-words` on startup and, if non-empty, calls `replaceDesensitizeMap(...)` to make the backend config the source of truth (empty/failed → keep seed). Page `SensitiveWordsPage.tsx` (route `/page/strategy/admin/sensitive-words`, opened from the 「敏感词管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`, present in **both** full + collapsed sidebar modes) lists/adds/edits/启停/deletes words and saves the **whole set atomically** (replace-all); a 「导入种子词」 button merges any missing `SEED_DESENSITIZE_MAP` entries into the editor. Client + hooks in `strategy-components/api/sensitive-words.ts` (`wordsToMap` folds enabled rows into the runtime map). Backend: `/api/sensitive-words` (global/shared; open read, write allowed for admin **or** the `lqq` account), mirroring 菜单管理's store shape. **侧边栏 + 导航栏 menu labels are desensitized** at the data source: `useSidebarMenuItems`/`useActiveThirdLevel` (`core/page-layout/use-sidebar-menu.ts`) map every node's `label` through a desensitizer built **directly from the `/api/sensitive-words` query** (`makeDesensitizer(wordsToMap(words))`, falling back to the global seed `desensitize` while empty) — this sidesteps the `replaceDesensitizeMap` effect-timing race. Only `label` is rewritten; `path`/`id`/`icon` are untouched so routing/highlight are unaffected. All nav surfaces (`page-sidebar.tsx`, `page-sidebar-dark.tsx`, `page-sidebar-v1.tsx`, `strategy-components/Header.tsx`, `PageRoutes.tsx`) consume this hook and are covered; `MenuManagementPage` uses the raw tree so admins still edit real labels. - **技能地址管理 (Skill Address Management, admin-only)**: 扫描运行中的技能(`skills/{public,custom}` 下所有 `.md`/`.py`)里的 **http(s) URL 与 IP/IP:端口**,按唯一地址聚合全部出现位置(跨 .md/.py),支持**批量替换**(如把 UAT 地址一次性改成生产)。主体抽成可复用组件 `SkillAddressesPanel`(`pages/SkillAddressesPage.tsx` 导出,默认导出 `SkillAddressesPage` 仍保留旧路由 `/page/strategy/admin/skill-addresses` 作直链回退),**作为「技能地址管理」标签页嵌入管理员「技能管理」页 `pages/AdminSkillsPage.tsx`**(该页加 Tabs:「技能列表」+「技能地址管理」;入口在「设置和更多」下拉的「技能管理」`workspace-nav-menu.tsx` → `/page/workspace/admin/skills`,不再单列于 `components/page-sidebar.tsx` 左下角设置下拉):打开自动扫描并带**进度条**(扫描逐技能 / 应用逐文件 X/N,应对 150+ 技能),URL/IP 类型过滤 + 搜索 + 「片段批量套用」(如所有 `uat.4.cn` → `prod.4.cn`)、每地址「替换为」输入框(可展开看 `file:line` 片段)、内联**预览**、**应用**、**多关键词检索**(空格分隔全部命中),以及**修改记录**面板——每次应用生成一条记录(显示时间/改了哪些地址 from→to/文件数/处数),可**针对单次记录回退**(二次确认)。client + NDJSON 流式解析在 `strategy-components/api/skill-addresses.ts`。后端 `/api/skill-addresses`(**仅管理员**):所有文件 IO 走 `asyncio.to_thread` 不阻塞事件循环;`GET /scan`(+`/scan/stream`)、`POST /preview`、`POST /apply`(+`/apply/stream`)、`GET /backups`、`POST /rollback/{id}`;**子串安全替换**(基于 token span,不会误伤子串)、`expected_total` 乐观锁(409)、写前自动磁盘备份到 `{skills_root}/.address-edits/`(同时写一份 `{id}.manifest.json` 记录本次替换明细,供修改记录展示)、应用后刷新技能系统提示缓存。纯引擎 `deerflow/skills/address_scan.py`,router `app/gateway/routers/skill_addresses.py`,tests `tests/test_skill_addresses.py`。范围仅运行中技能,不动 `deploy/minimal/skills/`。 -- **按钮管理 (Task Button Management, admin-only)**: configures the **可配置跳转按钮** rendered in the left task panel of the task deep-link workspaces (rwfx / 3qfx / xdfx, see `TaskFxWorkspace`). Each button = `{business, label, linkType, target, appendTaskId, idParam, idKind, enabled, sortOrder}`; `linkType` is `url` (外链 `window.open`, optional append of `?{idParam}={id}` — **`idParam` is the editable query-param name**, e.g. `id`/`task_id`), `business` (内部切换到另一业务, reuses `resolveTaskDeeplinkTarget`, always task id), or `purpose` (弹出当前任务的「任务目的」表格 via the shared `TaskPurposeDialog`, no target — configurable for any page incl. xdfx). **`idKind` (`task`/`action`) only matters for xdfx**: its deep-link `?taskId=` is an 行动id and the 任务id is resolved at login (`fetchTaskIdFromAction`) — `TaskContext` now also stashes the raw `actionId`, so a url button can append either (default `task`). Page `TaskButtonsPage.tsx` (route `/page/strategy/admin/task-buttons`, **admin-gated**, entry「按钮管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both full + collapsed modes) has per-business tabs, edits name/type/target/启停/排序/增删, saves the **whole set atomically** (replace-all), plus 「导入默认」 to merge code-seed defaults. **Seed/fallback** (like 敏感词管理): only when the whole table is empty (fresh deploy, never saved) do the workspace + page fall back to `defaultTaskButtons(cfg)` — rwfx's url buttons (编辑/上一步/下一步/3q详情) built from the **live** `/api/public/config/task-deeplink` plus cross-business switch buttons for **all three** businesses (each switches to the other two — rwfx→三情/行动, 3qfx→任务/行动, xdfx→任务/三情). Once saved, the store is the sole source of truth. **Bottom 快捷跳转 bar groups by prev/next**: `TaskJumpBar` renders only「上一步/下一步」-type buttons in the bottom bar — a `prev` group (id 含 `prev` 或文案「上一步」, 描边次要样式 + ←图标) rendered **左侧**, then a `next` group (id 含 `next` 或文案「下一步」, 实心主色) on the **右侧** (container `justify-end`); everything else goes to the right-edge floating menu. The default 上一步 button (`rwfx-prev`, `url`, target = `DASHBOARD_AGENT_ROUTE` `/page/strategy/dashboard-agent`, `appendTaskId` + `idParam=taskId` → `?taskId=任务id`, `openMode=self`) 默认指向**应用内**「大屏绘制」页,在 **both** 单智能体 (`TaskFxWorkspace`) 与 多智能体会商 (`RoundtablePlanningPage` 深链 rwfx-A) 都显示(共用 `TaskJumpBar`)。**`runTaskButton` 的 `url` 类型支持应用内路由**:target 以单个 `/` 开头时按内部路由处理——`self` 走 react-router 同窗导航(不刷新),`blank` 拼成当前源的 hash 路由新标签打开;外链行为不变。 The old hardcoded jump buttons were migrated out: `TaskFxWorkspace` no longer hardcodes 切换到三情分析, `TaskActionsPanel` keeps only the non-jump 清除记录, and `TaskPurposePanel` dropped its 3q详情 button (now configurable) **and defaults the 目的树 table to collapsed** (chevron toggle). Helpers `core/auth/task-buttons.ts` (`TaskButton`, `defaultTaskButtons`, `resolveTaskButtons`, `buttonsForBusiness`, `runTaskButton`), client+hooks `strategy-components/api/task-buttons.ts`. Backend: `/api/task-buttons` (global/shared; open read, admin replace-all), store `deerflow.persistence.task_buttons` (table `task_buttons`, auto-created by `create_all`), wired `app.state.task_button_store`. Tests: `tests/test_task_buttons.py`. +- **按钮管理 (Task Button Management, admin-only)**: configures the **可配置跳转按钮** rendered in the left task panel of the task deep-link workspaces (rwfx / 3qfx / xdfx, see `TaskFxWorkspace`). Each button = `{business, label, linkType, target, appendTaskId, idParam, idKind, appendAuth, authTokenParam, authNameParam, enabled, sortOrder}`; `linkType` is `url` (外链 `window.open`, optional append of `?{idParam}={id}` — **`idParam` is the editable query-param name**, e.g. `id`/`task_id`), `business` (内部切换到另一业务, reuses `resolveTaskDeeplinkTarget`, always task id), or `purpose` (弹出当前任务的「任务目的」表格 via the shared `TaskPurposeDialog`, no target — configurable for any page incl. xdfx). **`appendAuth` (追加登录态)**: a `url` button can additionally append `?{authTokenParam}={token1}&{authNameParam}={username}` (default param names `token`/`username`) so the跳转目标外部系统 can identify the current user — e.g. `…/action/sentiment?token=&username=lizhaoxing`. **Gated on token login**: `token1` = the `?authToken=` used by token-exchange login; `username` = the raw login name the backend resolved from `getTokenInfo`. Both are stashed at login by `persistLoginCredentials` into `login.token1`/`login.username` (`core/auth/login-credentials.ts`, scoped via [[scoped-storage]] so嵌入/独立会话隔离 + 随独立会话迁移). Only token login writes them → a non-token-login session has no `token1`, so `runTaskButton` silently skips the auth append (the jump still works, just without those two params). The frontend reads `username` straight from the new `UsernameLoginResponse.username` field (the stored email local-part is lossy — lowercased/normalized — so not used), **not** by decoding the JWT. **`idKind` (`task`/`action`) only matters for xdfx**: its deep-link `?taskId=` is an 行动id and the 任务id is resolved at login (`fetchTaskIdFromAction`) — `TaskContext` now also stashes the raw `actionId`, so a url button can append either (default `task`). Page `TaskButtonsPage.tsx` (route `/page/strategy/admin/task-buttons`, **admin-gated**, entry「按钮管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both full + collapsed modes) has per-business tabs, edits name/type/target/启停/排序/增删, saves the **whole set atomically** (replace-all), plus 「导入默认」 to merge code-seed defaults. **Seed/fallback** (like 敏感词管理): only when the whole table is empty (fresh deploy, never saved) do the workspace + page fall back to `defaultTaskButtons(cfg)` — rwfx's url buttons (编辑/上一步/下一步/3q详情) built from the **live** `/api/public/config/task-deeplink` plus cross-business switch buttons for **all three** businesses (each switches to the other two — rwfx→三情/行动, 3qfx→任务/行动, xdfx→任务/三情). Once saved, the store is the sole source of truth. **Bottom 快捷跳转 bar groups by prev/next**: `TaskJumpBar` renders only「上一步/下一步」-type buttons in the bottom bar — a `prev` group (id 含 `prev` 或文案「上一步」, 描边次要样式 + ←图标) rendered **左侧**, then a `next` group (id 含 `next` 或文案「下一步」, 实心主色) on the **右侧** (container `justify-end`); everything else goes to the right-edge floating menu. The default 上一步 button (`rwfx-prev`, `url`, target = `DASHBOARD_AGENT_ROUTE` `/page/strategy/dashboard-agent`, `appendTaskId` + `idParam=taskId` → `?taskId=任务id`, `openMode=self`) 默认指向**应用内**「大屏绘制」页,在 **both** 单智能体 (`TaskFxWorkspace`) 与 多智能体会商 (`RoundtablePlanningPage` 深链 rwfx-A) 都显示(共用 `TaskJumpBar`)。**`runTaskButton` 的 `url` 类型支持应用内路由**:target 以单个 `/` 开头时按内部路由处理——`self` 走 react-router 同窗导航(不刷新),`blank` 拼成当前源的 hash 路由新标签打开;外链行为不变。 The old hardcoded jump buttons were migrated out: `TaskFxWorkspace` no longer hardcodes 切换到三情分析, `TaskActionsPanel` keeps only the non-jump 清除记录, and `TaskPurposePanel` dropped its 3q详情 button (now configurable) **and defaults the 目的树 table to collapsed** (chevron toggle). Helpers `core/auth/task-buttons.ts` (`TaskButton`, `defaultTaskButtons`, `resolveTaskButtons`, `buttonsForBusiness`, `runTaskButton`), client+hooks `strategy-components/api/task-buttons.ts`. Backend: `/api/task-buttons` (global/shared; open read, admin replace-all), store `deerflow.persistence.task_buttons` (table `task_buttons`, auto-created by `create_all`), wired `app.state.task_button_store`. Tests: `tests/test_task_buttons.py`. - **State**: TanStack Query for server state, context providers (`SubtasksProvider`, `ArtifactsProvider`, `PromptInputProvider`) scoped to `WorkspaceRoutes` - **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE - **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/` diff --git a/frontend-web/src/core/auth/index.ts b/frontend-web/src/core/auth/index.ts index 23659489..deca84f7 100644 --- a/frontend-web/src/core/auth/index.ts +++ b/frontend-web/src/core/auth/index.ts @@ -7,6 +7,12 @@ export interface LoginByUsernameResponse { system_role: string; needs_setup: boolean; created: boolean; + /** + * 本次登录解析出的原始登录用户名(token 登录=getTokenInfo 解析值;用户名登录=所传用户名)。 + * 用于跳转外链时拼 ``?username=``——后端落库的邮箱前缀是归一化(小写/替换)后的,有损,故以此为准。 + * 旧后端可能不返回。 + */ + username?: string; } import { diff --git a/frontend-web/src/core/auth/login-credentials.ts b/frontend-web/src/core/auth/login-credentials.ts new file mode 100644 index 00000000..69495c95 --- /dev/null +++ b/frontend-web/src/core/auth/login-credentials.ts @@ -0,0 +1,61 @@ +/** + * 任务工作区跳转外链时要拼接的「登录态凭据」:token1 + 登录用户名 的存取。 + * + * - **token1** = token 登录用的上游 token(登录 URL 的 ``?authToken=``,即走 + * ``/api/v1/auth/login/token`` 这条 token 换登录路径用的 token)。 + * - **username** = 后端 token 登录时用该 token 调 ``getTokenInfo`` 解析出的**登录用户名** + * (见 ``auth.py`` ``/login/token`` → ``UsernameLoginResponse.username``)。**不是**邮箱 + * 前缀、也**不是**解 JWT 得来——后端把它落库时会 ``.lower()`` + 归一化,邮箱前缀是有损的, + * 所以以后端回传的原始值为准。 + * + * 二者均经 [[scoped-storage]] 的 ``authStorage*`` 存取(键用 ``login.`` 前缀,属其受保护 + * 鉴权键族):嵌入(iframe)/独立会话态自动落 sessionStorage 隔离、正常态落 localStorage, + * 并随「开启独立会话」一起被 ``migrateAuthToSession`` 迁移。 + * + * **门控**:仅 token 登录(``authToken`` 存在)才写入。非 token 登录不写 token1 → 跳转按钮 + * 的「追加登录态」开关此时读不到 token,自然不追加(符合「必须用 login/token 登录才能带 + * token 跳转」)。 + */ + +import { + authStorageGet, + authStorageRemove, + authStorageSet, +} from "@/core/auth/scoped-storage"; +import { getAccountDisplayName, type LoginByUsernameResponse } from "@/core/auth"; + +const LOGIN_TOKEN_KEY = "login.token1"; +const LOGIN_USERNAME_KEY = "login.username"; + +/** + * 持久化 token 登录的 token1 + 用户名(token 登录成功后调用)。 + * ``authToken`` 为空则整次忽略(非 token 登录不留痕)。 + */ +export function persistLoginCredentials( + authToken: string | null | undefined, + auth: LoginByUsernameResponse | null, +): void { + const token = authToken?.trim(); + if (!token) return; + authStorageSet(LOGIN_TOKEN_KEY, token); + // 优先用后端回传的原始登录名(getTokenInfo 解析值);旧后端无该字段时退回账号显示名 + // (邮箱 @ 前缀)兜底,保证总有个值可用。 + const username = auth?.username?.trim() || getAccountDisplayName(auth); + if (username) authStorageSet(LOGIN_USERNAME_KEY, username); +} + +/** 读 token1(无则 ``null`` → 跳转不追加登录态)。 */ +export function readLoginToken(): string | null { + return authStorageGet(LOGIN_TOKEN_KEY); +} + +/** 读登录用户名(无则 ``null``)。 */ +export function readLoginUsername(): string | null { + return authStorageGet(LOGIN_USERNAME_KEY); +} + +/** 清除登录态凭据(登出时调用)。 */ +export function clearLoginCredentials(): void { + authStorageRemove(LOGIN_TOKEN_KEY); + authStorageRemove(LOGIN_USERNAME_KEY); +} diff --git a/frontend-web/src/core/auth/task-buttons.ts b/frontend-web/src/core/auth/task-buttons.ts index 84be6bfe..4e03c71b 100644 --- a/frontend-web/src/core/auth/task-buttons.ts +++ b/frontend-web/src/core/auth/task-buttons.ts @@ -20,6 +20,10 @@ import { readCopTaskDetail, resolveTaskDeeplinkTarget, } from "@/core/auth/task-deeplink"; +import { + readLoginToken, + readLoginUsername, +} from "@/core/auth/login-credentials"; /** 「大屏绘制」独立页应用内路由(深链「上一步」默认指向它,跳转追加 ?taskId=)。 */ export const DASHBOARD_AGENT_ROUTE = "/page/strategy/dashboard-agent"; @@ -85,6 +89,16 @@ export interface TaskButton { idKind: TaskButtonIdKind; /** ``url`` 外链打开方式:``blank``=新窗口(默认)/ ``self``=当前窗口。仅 ``url`` 类型有意义。 */ openMode: TaskButtonOpenMode; + /** + * ``url`` 跳转是否追加登录态:token1(token 登录的上游 token)+ 登录用户名,供外链所在的 + * 外部系统识别当前登录用户。**仅 token 登录**(前端存有 token1,见 [[login-credentials]])时 + * 实际拼上;非 token 登录读不到 token1,自动跳过(按钮照常跳,只是不带这两个参数)。 + */ + appendAuth: boolean; + /** 追加 token 的查询参数名(空则用 ``token``)。 */ + authTokenParam: string; + /** 追加用户名的查询参数名(空则用 ``username``)。 */ + authNameParam: string; enabled: boolean; sortOrder: number; } @@ -120,6 +134,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] { idParam: "id", idKind: "task", openMode: "blank", + appendAuth: false, + authTokenParam: "token", + authNameParam: "username", enabled: true, sortOrder, }); @@ -134,6 +151,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] { idParam: "id", idKind: "task", openMode: "blank", + appendAuth: false, + authTokenParam: "token", + authNameParam: "username", enabled: true, sortOrder, }); @@ -149,6 +169,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] { idParam: "id", idKind: "task", openMode: "blank", + appendAuth: false, + authTokenParam: "token", + authNameParam: "username", enabled: true, sortOrder: 0, }, @@ -167,6 +190,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] { idParam: "taskId", idKind: "task", openMode: "self", + appendAuth: false, + authTokenParam: "token", + authNameParam: "username", enabled: true, sortOrder: 1, }, @@ -180,6 +206,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] { idParam: "id", idKind: "task", openMode: "blank", + appendAuth: false, + authTokenParam: "token", + authNameParam: "username", enabled: true, sortOrder: 2, }, @@ -193,6 +222,9 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] { idParam: "id", idKind: "task", openMode: "blank", + appendAuth: false, + authTokenParam: "token", + authNameParam: "username", enabled: true, sortOrder: 3, }, @@ -259,7 +291,19 @@ export async function runTaskButton(btn: TaskButton, ctx: RunTaskButtonCtx): Pro // 行动分析(xdfx)可选追加行动id;否则用任务id。行动id 缺失时回退任务id。 const id = btn.idKind === "action" ? ctx.actionId || ctx.taskId : ctx.taskId; const param = (btn.idParam || "id").trim() || "id"; - const url = btn.appendTaskId && id ? appendQueryParam(base, param, id) : base; + let url = btn.appendTaskId && id ? appendQueryParam(base, param, id) : base; + // 追加登录态:仅 token 登录(存在 token1)才拼 token + 用户名,供外链所在的外部系统识别 + // 当前登录用户。非 token 登录读不到 token1 → 整段跳过(不影响跳转本身)。 + if (btn.appendAuth) { + const token1 = readLoginToken(); + if (token1) { + url = appendQueryParam(url, (btn.authTokenParam || "token").trim() || "token", token1); + const name = readLoginUsername(); + if (name) { + url = appendQueryParam(url, (btn.authNameParam || "username").trim() || "username", name); + } + } + } // 应用内部路由(以单个 / 开头,如「上一步」→ 大屏绘制页): // self → 走 react-router 同窗导航(不刷新、保留 SPA);blank → 拼成当前源的 hash 路由新标签打开。 if (base.startsWith("/") && !base.startsWith("//")) { diff --git a/frontend-web/src/pages/LoginPage.tsx b/frontend-web/src/pages/LoginPage.tsx index d52b5c7e..f8aaea8d 100644 --- a/frontend-web/src/pages/LoginPage.tsx +++ b/frontend-web/src/pages/LoginPage.tsx @@ -33,6 +33,7 @@ import { } from '@/core/page-layout/sidebar-enabled'; import { fetchYLogConfig } from '@/core/log/y-log-config'; import { persistYLogToken } from '@/core/log/y-log-token'; +import { persistLoginCredentials } from '@/core/auth/login-credentials'; import { OptType, writeYLog } from '@/core/log/y-log'; const DEFAULT_USERNAME = 'guest'; @@ -167,6 +168,9 @@ export default function LoginPage() { // 持久化 token1(走 authStorage,嵌入态自动隔离)+ 预热上报配置 + 上报登录。 if (authToken) { persistYLogToken(authToken); + // 存下 token1 + 登录用户名(getTokenInfo 解析出的原始名,后端回传),供任务工作区 + // 跳转按钮的「追加登录态」拼到外链上(仅 token 登录才有这两个值)。 + persistLoginCredentials(authToken, auth); void fetchYLogConfig(); writeYLog({ optType: OptType.login, diff --git a/frontend-web/src/pages/TaskButtonsPage.tsx b/frontend-web/src/pages/TaskButtonsPage.tsx index 59d8373b..71312564 100644 --- a/frontend-web/src/pages/TaskButtonsPage.tsx +++ b/frontend-web/src/pages/TaskButtonsPage.tsx @@ -105,6 +105,9 @@ export default function TaskButtonsPage() { idParam: "id", idKind: "task", openMode: "blank", + appendAuth: false, + authTokenParam: "token", + authNameParam: "username", enabled: true, sortOrder: maxSort + 1, }, @@ -319,6 +322,37 @@ export default function TaskButtonsPage() { 当前窗口 + + {r.appendAuth && ( +
+ & + update(r.id, { authTokenParam: e.target.value })} + placeholder="token" + className="h-8 w-24" + title="token 的查询参数名(默认 token)" + /> + =token1 & + update(r.id, { authNameParam: e.target.value })} + placeholder="username" + className="h-8 w-28" + title="用户名的查询参数名(默认 username)" + /> + =用户名 +
+ )} ) : (