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:
吉鑫升andClaude Opus 4.8 committed 2026-06-25 14:52:07 +08:00
1 parent 6989dd45f1
commit f8b40353ef
18 files changed
+298 -6

No files matched your search

+1 -1
View File
@@ -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-<appId>`). 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=<JWT>&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/`
+6
View File
@@ -7,6 +7,12 @@ export interface LoginByUsernameResponse {
system_role: string;
needs_setup: boolean;
created: boolean;
/**
* 本次登录解析出的原始登录用户名(token 登录=getTokenInfo 解析值;用户名登录=所传用户名)。
* 用于跳转外链时拼 ``?username=``——后端落库的邮箱前缀是归一化(小写/替换)后的,有损,故以此为准。
* 旧后端可能不返回。
*/
username?: string;
}
import {
@@ -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);
}
+45 -1
View File
@@ -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("//")) {
+4
View File
@@ -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,
@@ -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() {
<SelectItem value="self">当前窗口</SelectItem>
</SelectContent>
</Select>
<label
className="text-muted-foreground ml-2 flex shrink-0 items-center gap-1.5 text-xs"
title="跳转时在链接后追加 token + 用户名(仅 token 登录时生效)"
>
<Switch
checked={r.appendAuth}
onCheckedChange={(v) => update(r.id, { appendAuth: v })}
/>
追加登录态
</label>
{r.appendAuth && (
<div className="flex shrink-0 items-center gap-1 text-xs">
<span className="text-muted-foreground">&amp;</span>
<Input
value={r.authTokenParam}
onChange={(e) => update(r.id, { authTokenParam: e.target.value })}
placeholder="token"
className="h-8 w-24"
title="token 的查询参数名(默认 token)"
/>
<span className="text-muted-foreground">=token1 &amp;</span>
<Input
value={r.authNameParam}
onChange={(e) => update(r.id, { authNameParam: e.target.value })}
placeholder="username"
className="h-8 w-28"
title="用户名的查询参数名(默认 username)"
/>
<span className="text-muted-foreground">=用户名</span>
</div>
)}
</>
) : (
<Select value={r.target} onValueChange={(v) => update(r.id, { target: v })}>
@@ -31,6 +31,9 @@ interface ServerButton {
id_param: string;
id_kind: string;
open_mode: string;
append_auth: boolean;
auth_token_param: string;
auth_name_param: string;
enabled: boolean;
sort_order: number;
}
@@ -46,6 +49,9 @@ function toButton(s: ServerButton): TaskButton {
idParam: s.id_param || "id",
idKind: (s.id_kind as TaskButtonIdKind) ?? "task",
openMode: (s.open_mode as TaskButtonOpenMode) ?? "blank",
appendAuth: s.append_auth ?? false,
authTokenParam: s.auth_token_param || "token",
authNameParam: s.auth_name_param || "username",
enabled: s.enabled ?? true,
sortOrder: s.sort_order ?? 0,
};
@@ -62,6 +68,9 @@ function toBody(b: TaskButton): ServerButton {
id_param: b.idParam,
id_kind: b.idKind,
open_mode: b.openMode,
append_auth: b.appendAuth,
auth_token_param: b.authTokenParam,
auth_name_param: b.authNameParam,
enabled: b.enabled,
sort_order: b.sortOrder,
};
+2 -2
View File
@@ -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
+6 -1
View File
@@ -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):
@@ -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)
@@ -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,
@@ -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))
@@ -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;
+6
View File
@@ -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 关掉则不显示该入口。