From 6378cd33bd344c19feb7eb3584bdc663f59737ce Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Mon, 29 Jun 2026 23:00:31 +0800 Subject: [PATCH] =?UTF-8?q?=EF=BB=BF=E7=BB=9F=E4=B8=80=E5=A4=96=E9=83=A8?= =?UTF-8?q?=20access=5Ftoken=20=E5=AD=98=E5=82=A8=E4=B8=8E=E8=BD=BB?= =?UTF-8?q?=E5=BA=94=E7=94=A8=20URL=20=E6=8B=BC=E6=8E=A5=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=B5=8C=E5=85=A5=E4=BC=9A=E8=AF=9D=E9=89=B4=E6=9D=83?= =?UTF-8?q?=E9=9A=94=E7=A6=BB=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 access_token 纳入 scoped storage 迁移,yUserId 登录优先并独立持久化 token;轻应用/按钮管理默认改用 access_token 参数名,支持 hash 路由占位符替换与自动追加。 --- .../src/core/auth/external-access-token.ts | 54 +++++++++ frontend-web/src/core/auth/scoped-storage.ts | 2 +- frontend-web/src/core/auth/yuser-login.ts | 106 +++++++++++++++--- .../src/core/strategy/external-links.ts | 4 +- .../src/pages/LightAppManagementPage.tsx | 14 +-- frontend-web/src/pages/LoginPage.tsx | 6 +- frontend-web/src/pages/TaskButtonsPage.tsx | 2 +- .../strategy-components/api/task-buttons.ts | 2 +- .../components/chat-qa/GeneralQA.tsx | 3 +- .../src/strategy-components/lib/light-app.ts | 56 +++++++-- 10 files changed, 210 insertions(+), 39 deletions(-) diff --git a/frontend-web/src/core/auth/external-access-token.ts b/frontend-web/src/core/auth/external-access-token.ts index 27356aa8..3a3b4d8d 100644 --- a/frontend-web/src/core/auth/external-access-token.ts +++ b/frontend-web/src/core/auth/external-access-token.ts @@ -1,3 +1,7 @@ +import { authStorageGet, authStorageSet } from "@/core/auth/scoped-storage"; + +export const EXTERNAL_ACCESS_TOKEN_STORAGE_KEY = "access_token"; + export function normalizeExternalAccessToken(value: unknown): string { if (value == null) return ""; const token = String(value).trim(); @@ -5,3 +9,53 @@ export function normalizeExternalAccessToken(value: unknown): string { const lowered = token.toLowerCase(); return lowered === "null" || lowered === "undefined" ? "" : token; } + +function readTokenFromStoredValue(raw: string | null): string { + const token = normalizeExternalAccessToken(raw); + if (!token) return ""; + if (!token.startsWith("{")) return token; + try { + const parsed = JSON.parse(token); + if (!parsed || typeof parsed !== "object") return token; + const obj = parsed as Record; + return ( + normalizeExternalAccessToken(obj.access_token) || + normalizeExternalAccessToken(obj.accessToken) || + normalizeExternalAccessToken(obj.token) + ); + } catch { + return token; + } +} + +export function readStoredExternalAccessTokenValue(): string { + return readTokenFromStoredValue(authStorageGet(EXTERNAL_ACCESS_TOKEN_STORAGE_KEY)); +} + +export function readExternalAccessTokenValue(userInfo: Record): string { + return normalizeExternalAccessToken(userInfo.access_token) || normalizeExternalAccessToken(userInfo.accessToken); +} + +export function readCurrentExternalAccessTokenValue(userInfo: Record): string { + return readStoredExternalAccessTokenValue() || readExternalAccessTokenValue(userInfo); +} + +export function writeExternalAccessTokenFields( + userInfo: Record, + token: string, +): void { + userInfo.access_token = token; + userInfo.accessToken = token; +} + +export function writeStoredExternalAccessToken(token: string): void { + const normalized = normalizeExternalAccessToken(token); + if (normalized) { + authStorageSet(EXTERNAL_ACCESS_TOKEN_STORAGE_KEY, normalized); + } +} + +export function deleteExternalAccessTokenFields(userInfo: Record): void { + delete userInfo.access_token; + delete userInfo.accessToken; +} diff --git a/frontend-web/src/core/auth/scoped-storage.ts b/frontend-web/src/core/auth/scoped-storage.ts index eb66ea70..09cfeba6 100644 --- a/frontend-web/src/core/auth/scoped-storage.ts +++ b/frontend-web/src/core/auth/scoped-storage.ts @@ -62,7 +62,7 @@ export function authStorageRemove(key: string): void { * ticket/userId/env/maxkeyOnlineToken/userDetailToken)。用于「开启独立会话」时把当前登录态从 * localStorage 快照搬进 sessionStorage,避免切换存储后被登出。 */ -const PROTECTED_AUTH_EXACT_KEYS = ["deerflow.auth", "userInfo", "userId", "tools_token"]; +const PROTECTED_AUTH_EXACT_KEYS = ["deerflow.auth", "userInfo", "userId", "tools_token", "access_token"]; const PROTECTED_AUTH_PREFIX = "login."; function isProtectedAuthKey(key: string): boolean { diff --git a/frontend-web/src/core/auth/yuser-login.ts b/frontend-web/src/core/auth/yuser-login.ts index 5787963d..a063149c 100644 --- a/frontend-web/src/core/auth/yuser-login.ts +++ b/frontend-web/src/core/auth/yuser-login.ts @@ -1,6 +1,13 @@ import { requestCrudService } from "@/service/request-axios-service"; import { authStorageGet, authStorageSet } from "@/core/auth/scoped-storage"; -import { normalizeExternalAccessToken } from "@/core/auth/external-access-token"; +import { + deleteExternalAccessTokenFields, + normalizeExternalAccessToken, + readStoredExternalAccessTokenValue, + readExternalAccessTokenValue, + writeExternalAccessTokenFields, + writeStoredExternalAccessToken, +} from "@/core/auth/external-access-token"; const CONSUMER_URL = import.meta.env.VITE_AIPI_CONSUMER_URL || ""; @@ -36,6 +43,38 @@ function readStoredYUserId(info: Record): string { return value == null ? "" : String(value).trim(); } +function normalizeExternalUserId(value: unknown): string { + if (value == null) return ""; + const text = String(value).trim(); + if (!text) return ""; + const lowered = text.toLowerCase(); + return lowered === "null" || lowered === "undefined" ? "" : text; +} + +function maskToken(token: string): string { + if (!token) return ""; + if (token.length <= 10) return `${token.slice(0, 2)}***(${token.length})`; + return `${token.slice(0, 6)}...${token.slice(-4)}(${token.length})`; +} + +function summarizeUserInfo(info: Record): Record { + return { + keys: Object.keys(info), + yUserId: info.yUserId, + userId: info.userId, + userName: info.userName, + gateDepartName: info.gateDepartName, + typeName: info.typeName, + access_token: maskToken(normalizeExternalAccessToken(info.access_token)), + accessToken: maskToken(normalizeExternalAccessToken(info.accessToken)), + }; +} + +function logYUserLogin(step: string, payload: Record): void { + if (typeof console === "undefined") return; + console.info(`[yUserId-login] ${step}`, payload); +} + function isPlainObject(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } @@ -95,28 +134,34 @@ function buildExternalUserInfo( } } next.yUserId = yUserId; + const externalUserId = + normalizeExternalUserId(result.id) || + normalizeExternalUserId(result.userId) || + normalizeExternalUserId(result.user_id) || + yUserId; + next.userId = externalUserId; - const incomingAccessToken = normalizeExternalAccessToken(result.accessToken); + const incomingAccessToken = readExternalAccessTokenValue(result); if (incomingAccessToken) { - next.accessToken = incomingAccessToken; + writeExternalAccessTokenFields(next, incomingAccessToken); return next; } const queryAccessToken = normalizeExternalAccessToken(access_token); if (queryAccessToken) { - next.accessToken = queryAccessToken; + writeExternalAccessTokenFields(next, queryAccessToken); return next; } const previousYUserId = readStoredYUserId(previous); const previousAccessToken = - previousYUserId === yUserId ? normalizeExternalAccessToken(previous.accessToken) : ""; - const cachedAccessToken = normalizeExternalAccessToken(cached.accessToken); + previousYUserId === yUserId ? readExternalAccessTokenValue(previous) : ""; + const cachedAccessToken = readExternalAccessTokenValue(cached); const fallbackAccessToken = previousAccessToken || cachedAccessToken; if (fallbackAccessToken) { - next.accessToken = fallbackAccessToken; + writeExternalAccessTokenFields(next, fallbackAccessToken); } else { - delete next.accessToken; + deleteExternalAccessTokenFields(next); } return next; } @@ -134,8 +179,16 @@ export async function loginByYUserId( authStorageSet(TOOLS_TOKEN_STORAGE_KEY, token); } + const requestUrl = `${CONSUMER_URL}/cop/userDetailByYuserId`; + logYUserLogin("request", { + requestUrl, + yUserId, + hasToolsToken: Boolean(token), + queryAccessToken: maskToken(normalizeExternalAccessToken(access_token)), + }); + const res = (await requestCrudService.get( - `${CONSUMER_URL}/cop/userDetailByYuserId`, + requestUrl, { yuserId: yUserId }, )) as UserDetailResponse; @@ -143,12 +196,38 @@ export async function loginByYUserId( // 一并存入路由参数 yUserId,使「应用管理」里 source=yUserId 的路由参数能取到(与 // source=userName/gateDepartName/typeName 直接取接口值统一走 userInfo[source])。 // 注意:接口返回的 accessToken 优先;登录 URL 的 access_token 只在接口值为空时兜底。 + // The consumer API may return accessToken. URL access_token is only used as + // a fallback when that API token is empty. const result = res.data ?? {}; + logYUserLogin("response", { + keys: Object.keys(result), + id: result.id, + userId: result.userId, + user_id: result.user_id, + userName: result.userName, + gateDepartName: result.gateDepartName, + typeName: result.typeName, + access_token: maskToken(normalizeExternalAccessToken(result.access_token)), + accessToken: maskToken(normalizeExternalAccessToken(result.accessToken)), + }); const previous = readExternalUserInfo(); const cached = readCachedYUserInfo(yUserId); const next = buildExternalUserInfo(yUserId, result, access_token, previous, cached); + const externalUserId = normalizeExternalUserId(next.userId); + if (externalUserId) { + authStorageSet(EXTERNAL_USER_ID_STORAGE_KEY, externalUserId); + } + const externalAccessToken = readExternalAccessTokenValue(next); + if (externalAccessToken) { + writeStoredExternalAccessToken(externalAccessToken); + } authStorageSet(EXTERNAL_USER_INFO_STORAGE_KEY, JSON.stringify(next)); writeCachedYUserInfo(yUserId, next); + logYUserLogin("stored", { + userId: externalUserId, + topLevelAccessToken: maskToken(readStoredExternalAccessTokenValue()), + userInfo: summarizeUserInfo(next), + }); } export function persistToolsToken(token: string): void { @@ -156,17 +235,18 @@ export function persistToolsToken(token: string): void { } /** - * 把内网登录路由参数 access_token 合并写入 userInfo.accessToken。 + * 把内网登录路由参数 access_token 合并写入 userInfo.access_token / userInfo.accessToken。 * 据此拼参的有:XK/PG 中任务 iframe(XK 用 accessToken、PG 用 access_token,二者均读 - * external-links.getAccessToken() = userInfo.accessToken);以及「应用管理」里 - * source=accessToken 的轻应用路由参数(resolveRouteParamValue 取 userInfo.accessToken)。 + * external-links.getAccessToken());以及「应用管理」里 source=access_token / accessToken + * 的轻应用路由参数(resolveRouteParamValue 统一读两种字段)。 * 用合并而非覆盖整对象,避免抹掉 loginByYUserId 从 consumer 拿到的用户信息。 */ export function persistAccessToken(token: string): void { const trimmed = normalizeExternalAccessToken(token); if (!trimmed) return; const info = readExternalUserInfo(); - info.accessToken = trimmed; + writeExternalAccessTokenFields(info, trimmed); + writeStoredExternalAccessToken(trimmed); authStorageSet(EXTERNAL_USER_INFO_STORAGE_KEY, JSON.stringify(info)); const yUserId = readStoredYUserId(info); if (yUserId) { diff --git a/frontend-web/src/core/strategy/external-links.ts b/frontend-web/src/core/strategy/external-links.ts index bed25bc1..f1ad3698 100644 --- a/frontend-web/src/core/strategy/external-links.ts +++ b/frontend-web/src/core/strategy/external-links.ts @@ -1,7 +1,7 @@ /** Strategy 模块外部 iframe / 嵌入页链接(来自 Vite 环境变量) */ import { authStorageGet } from "@/core/auth/scoped-storage"; -import { normalizeExternalAccessToken } from "@/core/auth/external-access-token"; +import { readCurrentExternalAccessTokenValue } from "@/core/auth/external-access-token"; function trimTrailingSlash(url: string): string { return url.replace(/\/+$/, ""); @@ -44,7 +44,7 @@ function readUserInfo(): Record { /** 当前登录态 accessToken(无则空串)。 */ export function getAccessToken(): string { - return normalizeExternalAccessToken(readUserInfo().accessToken); + return readCurrentExternalAccessTokenValue(readUserInfo()); } /** diff --git a/frontend-web/src/pages/LightAppManagementPage.tsx b/frontend-web/src/pages/LightAppManagementPage.tsx index 679c6e95..6fe09c3d 100644 --- a/frontend-web/src/pages/LightAppManagementPage.tsx +++ b/frontend-web/src/pages/LightAppManagementPage.tsx @@ -89,9 +89,9 @@ const emptyForm = (): FormState => ({ }); const newParam = (): LightAppRouteParam => ({ - key: "", + key: "access_token", paramType: "login", - source: "accessToken", + source: "access_token", customValue: "", themeValue: "red=red;blue=blue;dark-blue=dark-blue", }); @@ -107,7 +107,7 @@ function formFromApp(app: LightApp): FormState { routeParams: app.routeParams.map((p) => ({ key: p.key, paramType: p.paramType, - source: p.source ?? "accessToken", + source: p.source ?? "access_token", customValue: p.customValue ?? "", themeValue: p.themeValue ?? "", })), @@ -118,7 +118,7 @@ function getParamSourceLabel(param: LightAppRouteParam): string { if (param.paramType === "theme") { return `主题参数: ${param.themeValue || ""}`; } - const source = param.source || "accessToken"; + const source = param.source || "access_token"; const option = LOGIN_SOURCE_OPTIONS.find((o) => o.value === source); if (source === "other") { return `登录参数: ${param.customValue || ""}`; @@ -977,7 +977,7 @@ export default function LightAppManagementPage() { onChange={(v) => patchParam(idx, { paramType: v as "login" | "theme", - source: v === "login" ? (p.source || "accessToken") : undefined, + source: v === "login" ? (p.source || "access_token") : undefined, }) } /> @@ -988,7 +988,7 @@ export default function LightAppManagementPage() {