'use client'; import { useParams, useRouter, useSearchParams } from 'next/navigation'; import { useNavigate } from 'react-router-dom'; import { useEffect, useMemo, useState } from 'react'; import { useTheme } from '@/contexts/ThemeContext'; import { loginByToken, loginByUsername } from '@/core/auth/api'; import { setStoredAuth, setStoredUserName } from '@/core/auth'; import { persistExternalLoginParams, readExternalLoginParams, resolveThemeFromQuery, } from '@/core/auth/external-login'; import { fetchCopTaskDetail, persistCopTaskDetail, resolveDeeplinkTaskId, resolveTaskDeeplinkTarget, } from '@/core/auth/task-deeplink'; import { resolveUsernameByOnlineTicket } from '@/core/auth/wujie-ticket-login'; import { isWujieEnv, useWujieProps } from '@/core/embed/wujie'; import { readEmbedParam } from '@/core/embed/embed-session'; import { GENERAL_QA_ROUTE, loginByYUserId, persistAccessToken, persistToolsToken, } from '@/core/auth/yuser-login'; import { isSidebarEnabledFromSearch, persistSidebarEnabled, } 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'; const IFRAME_LOGIN_SOURCE = 'iframe'; const DEFAULT_REDIRECT_PATH = '/page/workspace/chats/new'; const IFRAME_REDIRECT_PATH = '/page/workspace/chats_iframe/new'; type LoginStatus = 'loading' | 'success' | 'error'; export default function LoginPage() { const { username } = useParams<{ username?: string }>(); const router = useRouter(); const navigate = useNavigate(); const searchParams = useSearchParams(); const { setTheme } = useTheme(); const sidebarEnabled = useMemo( () => isSidebarEnabledFromSearch(searchParams.toString()), [searchParams], ); // iframe 嵌入模式(外部系统 `&embed=1`):去 chrome + 按标签页强制主题。嵌入侧**不**走 // setTheme / persistSidebarEnabled(那俩落 localStorage、会跨标签污染正常系统页),改用 // sessionStorage 嵌入会话(见 embed-session.ts)。`embed=0` 显式关闭。 const isEmbed = useMemo( () => readEmbedParam(searchParams.toString()) === 'on', [searchParams], ); const redirectPath = useMemo( () => searchParams.get('from') === IFRAME_LOGIN_SOURCE ? IFRAME_REDIRECT_PATH : DEFAULT_REDIRECT_PATH, [searchParams], ); const resolvedUsername = useMemo(() => { const raw = username?.trim(); return raw && raw.length > 0 ? raw : DEFAULT_USERNAME; }, [username]); // 外部跳转参数:theme 直接调 setTheme;ticket/userId/env 持久化到 localStorage const yUserId = searchParams.get('yUserId')?.trim() ?? ''; const toolToken = searchParams.get('token')?.trim() ?? ''; // 内网登录透传的访问令牌:存入 userInfo.accessToken,供 XK/PG 中任务 iframe 拼参 const accessToken = searchParams.get('access_token')?.trim() ?? ''; // 用户名直登口令门:开启后端开关时,口令从 ?password= 透传 const gatePassword = searchParams.get('password') ?? ''; // token 换登录:?authToken= 传上游 token(与 tools_token 的 ?token= 区分开) const authToken = searchParams.get('authToken')?.trim() ?? ''; // 任务深链:外部系统带 ?taskId=&goPath= 时,登录后拉任务详情并直送对应问答/会商页 const taskId = searchParams.get('taskId')?.trim() ?? ''; const goPath = searchParams.get('goPath')?.trim() ?? ''; // 通用「登录后跳指定页」:?redirect=page/strategy/dashboard-agent(值可不带前导斜杠, // 也可自带 query,如 page/strategy/dashboard-agent?taskId=1580——记得整体 URL 编码)。 // 优先级最高:显式指定即按它跳,先于 taskId/goPath 深链与默认首页。出于安全只接受**站内 // 相对路径**:拒绝带协议的绝对 URL(http(s):、javascript: 等)、协议相对 URL(//host)与反斜杠, // 避免开放式跳转;缺前导斜杠时自动补全。 // // 参数透传:登录 URL 上「非登录自身消费」的其余 query(如 taskId / hideChrome)会被透传到 // 目标页 query——这样 `?redirect=/page/strategy/dashboard-agent&taskId=3334` 也能让目标页拿到 // taskId,无需把它编进 redirect 值里。目标已自带的同名参数优先(不覆盖);登录鉴权类参数 // (token/authToken/ticket 等)一律**不透传**,避免泄漏到应用页地址栏。 const redirectTo = useMemo(() => { const raw = searchParams.get('redirect')?.trim() ?? ''; if (!raw) return ''; if (/^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith('//') || raw.includes('\\')) return ''; const normalized = raw.startsWith('/') ? raw : `/${raw}`; const [pathOnly, ownQuery = ''] = normalized.split('?'); const target = new URLSearchParams(ownQuery); // 登录流程自身消费的参数,不透传到目标页(尤以鉴权类为甚)。 const LOGIN_OWNED = new Set([ 'redirect', 'ticket', 'userId', 'env', 'theme', 'yUserId', 'token', 'access_token', 'password', 'authToken', 'goPath', 'from', ]); new URLSearchParams(searchParams.toString()).forEach((value, key) => { if (LOGIN_OWNED.has(key)) return; if (target.has(key)) return; // 目标自带优先 target.set(key, value); }); const qs = target.toString(); return qs ? `${pathOnly}?${qs}` : pathOnly; }, [searchParams]); const externalParams = useMemo( () => ({ theme: searchParams.get('theme') ?? '', ticket: searchParams.get('ticket') ?? '', userId: searchParams.get('userId') ?? '', env: searchParams.get('env') ?? '', }), [searchParams], ); const [status, setStatus] = useState('loading'); const [errorMessage, setErrorMessage] = useState(''); // 无界(wujie)父级通过 $wujie.props 注入的参数(ticket / userId / theme / env)。 // hook 内部已把 ticket/userId/env 持久化到 localStorage(复用 external-login 存储)。 const wujieProps = useWujieProps(); /** onlineTicket:无界 props 优先,其次 URL query,最后读 localStorage */ const onlineTicket = useMemo(() => { const fromWujie = wujieProps.ticket?.trim() ?? ""; const fromQuery = externalParams.ticket.trim(); if (fromWujie || fromQuery) { return fromWujie || fromQuery; } return readExternalLoginParams().ticket.trim(); }, [wujieProps.ticket, externalParams.ticket]); // 调试:把无界注入的 props 打印出来(仅在无界子应用环境下,避免独立访问时刷日志)。 useEffect(() => { if (!isWujieEnv()) return; console.log('[wujie] sub-app props:', wujieProps); }, [wujieProps]); // 主题切换:无界 props.theme 优先,其次 URL query theme。两者取值约定一致 // (dark-blue → 深色;red → 浅色;blue 暂无该主题,先用浅色代替),统一走 resolveThemeFromQuery。 // **嵌入模式跳过**:setTheme 会写 localStorage `strategy-theme`,跨标签污染正常系统页; // 嵌入主题改由 forcedTheme(按标签页、不持久化)接管。 useEffect(() => { if (isEmbed) return; const themeValue = wujieProps.theme || externalParams.theme; if (!themeValue) return; setTheme(resolveThemeFromQuery(themeValue)); }, [isEmbed, wujieProps.theme, externalParams.theme, setTheme]); // ticket / userId / env 持久化(与 login 请求并行做,互不阻塞) useEffect(() => { persistExternalLoginParams({ ticket: onlineTicket || externalParams.ticket, userId: externalParams.userId, env: externalParams.env, }); }, [onlineTicket, externalParams.ticket, externalParams.userId, externalParams.env]); useEffect(() => { let cancelled = false; async function runLogin() { setStatus('loading'); setErrorMessage(''); try { let auth; let loginUserName = ''; if (authToken) { // token 换登录:后端用 authToken 调上游接口换出用户名再登录 auth = await loginByToken(authToken); } else { let deerflowUsername = resolvedUsername; if (onlineTicket) { deerflowUsername = await resolveUsernameByOnlineTicket(onlineTicket); } else if (yUserId) { await loginByYUserId(yUserId, toolToken || null); } else if (toolToken) { persistToolsToken(toolToken); } auth = await loginByUsername(deerflowUsername, gatePassword); loginUserName = deerflowUsername; } if (cancelled) { return; } setStoredAuth(auth); // 操作日志(Y-Log):只有 token 登录(存在 token1=authToken)才上报。 // 持久化 token1(走 authStorage,嵌入态自动隔离)+ 预热上报配置 + 上报登录。 if (authToken) { persistYLogToken(authToken); // 存下 token1 + 登录用户名(getTokenInfo 解析出的原始名,后端回传),供任务工作区 // 跳转按钮的「追加登录态」拼到外链上(仅 token 登录才有这两个值)。 persistLoginCredentials(authToken, auth); void fetchYLogConfig(); writeYLog({ optType: OptType.login, optDetail: 'CM助手-全量-登录', url: '/api/v1/auth/login/token', reqParam: { username: loginUserName || auth.user_id }, returnParam: { user_id: auth.user_id }, }); } // 标准登录把用户名写入 userInfo,供顶栏用户区显示(已有则不覆盖) setStoredUserName(loginUserName); // 内网透传的 access_token 合并进 userInfo(在 loginByYUserId 覆盖之后), // XK/PG 中任务 iframe 据此取 accessToken persistAccessToken(accessToken); // 嵌入模式不写 sidebarEnabled(localStorage 的「简洁/全量」偏好会跨标签污染正常页); // 去 chrome 由按标签页的嵌入会话(isEmbedActive)驱动,见 PageLayout。 if (!isEmbed) persistSidebarEnabled(sidebarEnabled); setStatus('success'); // 通用跳转:显式 ?redirect= 指定站内页面时,登录后直接跳过去(优先级最高, // 先于 taskId/goPath 深链与默认首页)。value 自带的 query 会被原样保留。 if (redirectTo) { navigate(redirectTo, { replace: true }); return; } // 任务深链:带 taskId + goPath 时拉任务详情并直送对应问答/会商页; // 任一环节失败(无地址/拉取失败/未配置智能体/goPath 无法识别)都回退默认首页。 if (taskId && goPath) { try { // xdfx 深链 ?taskId= 传入的是「行动id」,先换成真正的「任务id」;其它业务原样。 const realTaskId = await resolveDeeplinkTaskId(goPath, taskId); if (cancelled) return; const detail = await fetchCopTaskDetail(realTaskId); if (cancelled) return; if (detail) { persistCopTaskDetail(detail); const target = await resolveTaskDeeplinkTarget(goPath, detail, realTaskId, taskId); if (cancelled) return; if (target) { navigate(target.path, { replace: true, state: target.state }); return; } } } catch { /* 深链失败不阻断登录,落默认首页 */ } } router.replace(yUserId ? GENERAL_QA_ROUTE : redirectPath); } catch (error) { if (cancelled) { return; } setStatus('error'); setErrorMessage( error instanceof Error ? error.message : 'Login request failed.', ); } } void runLogin(); return () => { cancelled = true; }; }, [ resolvedUsername, router, navigate, sidebarEnabled, yUserId, toolToken, accessToken, onlineTicket, authToken, gatePassword, taskId, goPath, redirectTo, isEmbed, ]); return (

正在进入智能体模式

{status === 'error' && (

登录失败:{errorMessage || '未知错误'}

)}
); }