523 lines
21 KiB
TypeScript
523 lines
21 KiB
TypeScript
'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 {
|
||
getRouteKeyLoginPassword,
|
||
resolveRouteKey,
|
||
resolveRouteKeyMapping,
|
||
ROUTE_KEY_PARAM,
|
||
} from '@/core/auth/route-key-login';
|
||
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 { isEmbeddedHost } from '@/core/embed/frame-context';
|
||
import { readEmbedParam } from '@/core/embed/embed-session';
|
||
import {
|
||
GENERAL_QA_ROUTE,
|
||
loginByYUserId,
|
||
persistAccessToken,
|
||
persistToolsToken,
|
||
} from '@/core/auth/yuser-login';
|
||
import {
|
||
buildStandardChatRedirectPath,
|
||
buildEmbeddedIframeRedirectPath,
|
||
buildChatsIframeRedirectPath,
|
||
hasIframePresentationOverride,
|
||
isChatsIframeLogin,
|
||
resolveChatsIframeEntryRedirect,
|
||
resolvePostLoginRedirectPath,
|
||
shouldUseChatsIframeProtocol,
|
||
} from '@/core/page-layout/chats-iframe-route';
|
||
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';
|
||
import { authStorageGet } from '@/core/auth/scoped-storage';
|
||
|
||
/** 令牌脱敏:日志里只露头尾,避免完整令牌泄漏。 */
|
||
function maskLoginToken(value: string): string {
|
||
if (!value) return '';
|
||
if (value.length <= 10) return `${value.slice(0, 2)}***(${value.length})`;
|
||
return `${value.slice(0, 6)}...${value.slice(-4)}(${value.length})`;
|
||
}
|
||
|
||
/** 读取已落地的 userInfo(脱敏 access token 后)供登录后排查。 */
|
||
function summarizeStoredUserInfo(): Record<string, unknown> | null {
|
||
try {
|
||
const raw = authStorageGet('userInfo');
|
||
if (!raw) return null;
|
||
const info = JSON.parse(raw) as Record<string, unknown>;
|
||
return {
|
||
keys: Object.keys(info),
|
||
yUserId: info.yUserId,
|
||
userId: info.userId,
|
||
userName: info.userName,
|
||
gateDepartName: info.gateDepartName,
|
||
typeName: info.typeName,
|
||
access_token: maskLoginToken(String(info.access_token ?? '')),
|
||
accessToken: maskLoginToken(String(info.accessToken ?? '')),
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const DEFAULT_USERNAME = 'guest';
|
||
const IFRAME_CHAT_MODE = 'iframe';
|
||
const DEFAULT_REDIRECT_PATH = '/page/workspace/chats/new';
|
||
const FULL_CHAT_MODE = 'full';
|
||
|
||
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 requestedChatMode =
|
||
searchParams.get('chatMode')?.trim().toLowerCase() ?? '';
|
||
const iframePresentationOverride = useMemo(
|
||
() => hasIframePresentationOverride(searchParams.toString()),
|
||
[searchParams],
|
||
);
|
||
const embeddedHost = useMemo(() => isEmbeddedHost(), []);
|
||
// `from=iframe` is the established entry contract for the dedicated iframe
|
||
// page. A standard presentation flag is the one intentional exception: it
|
||
// uses the normal full chat workspace so Header/PageSidebar visibility is
|
||
// controlled by hideHeader/hideChrome/embed instead.
|
||
const brainyIframeRequested = shouldUseChatsIframeProtocol(
|
||
searchParams.toString(),
|
||
embeddedHost,
|
||
);
|
||
// An explicit full-chat request and all standard presentation flags retain
|
||
// the regular workspace. Otherwise native iframe/Wujie hosts use the
|
||
// legacy dedicated AgentSelector entry, matching the established bare
|
||
// iframe login integration.
|
||
const fullChatRequested = useMemo(
|
||
() =>
|
||
!brainyIframeRequested &&
|
||
(isWujieEnv() ||
|
||
requestedChatMode === FULL_CHAT_MODE ||
|
||
// hideHeader/hideChrome/embed are explicit presentation contracts.
|
||
// They must retain their query parameters and enter the standard
|
||
// workspace even when a native iframe has no legacy `from` marker.
|
||
iframePresentationOverride),
|
||
[
|
||
brainyIframeRequested,
|
||
iframePresentationOverride,
|
||
requestedChatMode,
|
||
searchParams,
|
||
],
|
||
);
|
||
const isIframeLogin = useMemo(
|
||
() => brainyIframeRequested,
|
||
[brainyIframeRequested],
|
||
);
|
||
const resolvedUsername = useMemo(() => {
|
||
const raw = username?.trim();
|
||
return raw && raw.length > 0 ? raw : DEFAULT_USERNAME;
|
||
}, [username]);
|
||
// 外部跳转参数:普通登录的 theme 调 setTheme;iframe 登录由 forcedTheme 接管。
|
||
// ticket/userId/env 仍按既有登录存储策略持久化。
|
||
const yUserId = searchParams.get('yUserId')?.trim() ?? '';
|
||
const redirectPath = useMemo(
|
||
() => {
|
||
// Keep the mode on the destination URL so PageRoutes can retain the
|
||
// full chat workspace sidebar while hideChrome hides only the global
|
||
// application chrome.
|
||
if (brainyIframeRequested) {
|
||
return !isChatsIframeLogin(searchParams.toString()) &&
|
||
requestedChatMode !== IFRAME_CHAT_MODE
|
||
? buildEmbeddedIframeRedirectPath(searchParams.toString())
|
||
: buildChatsIframeRedirectPath(searchParams.toString());
|
||
}
|
||
if (fullChatRequested) {
|
||
return buildStandardChatRedirectPath(
|
||
searchParams.toString(),
|
||
DEFAULT_REDIRECT_PATH,
|
||
);
|
||
}
|
||
// External iframe callers frequently also provide yUserId for login.
|
||
// That identity parameter must not steal a deliberate iframe entry and
|
||
// send it to the generic full-chat home before the route split above.
|
||
if (yUserId) return GENERAL_QA_ROUTE;
|
||
return resolvePostLoginRedirectPath(
|
||
searchParams.toString(),
|
||
DEFAULT_REDIRECT_PATH,
|
||
);
|
||
},
|
||
[
|
||
brainyIframeRequested,
|
||
fullChatRequested,
|
||
requestedChatMode,
|
||
searchParams,
|
||
yUserId,
|
||
],
|
||
);
|
||
const toolToken = searchParams.get('token')?.trim() ?? '';
|
||
// 内网登录透传的访问令牌:存入 userInfo.accessToken,供 XK/PG 中任务 iframe 拼参
|
||
const access_token = searchParams.get('access_token')?.trim() ?? '';
|
||
// 用户名直登口令门:开启后端开关时,口令从 ?password= 透传
|
||
const gatePassword = searchParams.get('password') ?? '';
|
||
// token 换登录:?authToken= 传上游 token(与 tools_token 的 ?token= 区分开)
|
||
const authToken = searchParams.get('authToken')?.trim() ?? '';
|
||
const routeKey = searchParams.get(ROUTE_KEY_PARAM)?.trim() ?? '';
|
||
const routeKeyMapping = useMemo(
|
||
() => resolveRouteKeyMapping(routeKey),
|
||
[routeKey],
|
||
);
|
||
const routeKeyTarget = useMemo(() => resolveRouteKey(routeKey), [routeKey]);
|
||
const configuredRouteLoginPassword = useMemo(
|
||
() => (routeKeyTarget ? getRouteKeyLoginPassword() : ''),
|
||
[routeKeyTarget],
|
||
);
|
||
const effectiveGatePassword = configuredRouteLoginPassword || gatePassword;
|
||
const usesConfiguredRouteLoginPassword = Boolean(configuredRouteLoginPassword);
|
||
// 任务深链:外部系统带 ?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 =
|
||
routeKeyTarget ||
|
||
searchParams.get('redirect')?.trim() ||
|
||
searchParams.get('route')?.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 iframeEntryTarget = resolveChatsIframeEntryRedirect(
|
||
pathOnly,
|
||
searchParams.toString(),
|
||
embeddedHost,
|
||
);
|
||
if (iframeEntryTarget) {
|
||
return iframeEntryTarget;
|
||
}
|
||
// Keep the Wujie/full-chat decision on the target URL. The destination
|
||
// route uses it to retain the full agent/history workspace sidebar.
|
||
if (
|
||
fullChatRequested &&
|
||
pathOnly === DEFAULT_REDIRECT_PATH &&
|
||
!target.has('chatMode')
|
||
) {
|
||
target.set('chatMode', FULL_CHAT_MODE);
|
||
}
|
||
// 登录流程自身消费的参数,不透传到目标页(尤以鉴权类为甚)。
|
||
const LOGIN_OWNED = new Set([
|
||
'redirect', 'route', 'ticket', 'userId', 'env', 'theme', 'yUserId',
|
||
'token', 'access_token', 'password', 'authToken', 'goPath', 'from', ROUTE_KEY_PARAM,
|
||
]);
|
||
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;
|
||
}, [embeddedHost, fullChatRequested, routeKeyTarget, 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<LoginStatus>('loading');
|
||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||
|
||
// 无界(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。
|
||
// **嵌入模式和 chats_iframe 登录跳过**:setTheme 会写 localStorage `strategy-theme`,
|
||
// 跨标签污染正常系统页;iframe 主题改由 forcedTheme(按标签页、不持久化)接管。
|
||
useEffect(() => {
|
||
if (isEmbed || isIframeLogin) return;
|
||
// routeKey 中显式配置的主题优先于调用方 URL,确保短链无需额外拼
|
||
// `theme=dark-blue` 也能在登录完成前切换到指定主题。
|
||
const themeValue =
|
||
routeKeyMapping?.theme || wujieProps.theme || externalParams.theme;
|
||
if (!themeValue) return;
|
||
setTheme(resolveThemeFromQuery(themeValue));
|
||
}, [
|
||
isEmbed,
|
||
isIframeLogin,
|
||
routeKeyMapping?.theme,
|
||
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('');
|
||
|
||
// ── 调试:登录路由参数全打印 ──────────────────────────────────────────────
|
||
// token / yUserId / 用户名 / ticket 任一方式都打印(令牌脱敏),方便排查
|
||
// “userInfo 取不到值 / 传值不对” 一类问题。`embed=0`/正常态也照打。
|
||
const loginMethod = authToken
|
||
? 'token(authToken)'
|
||
: yUserId
|
||
? 'yUserId'
|
||
: onlineTicket
|
||
? 'onlineTicket'
|
||
: toolToken
|
||
? 'toolsToken'
|
||
: 'username';
|
||
console.info('[login] route params', {
|
||
method: loginMethod,
|
||
pathUsername: resolvedUsername,
|
||
yUserId,
|
||
access_token: maskLoginToken(access_token),
|
||
toolToken: maskLoginToken(toolToken),
|
||
authToken: maskLoginToken(authToken),
|
||
routeKey,
|
||
configuredRouteLoginPassword: usesConfiguredRouteLoginPassword,
|
||
gatePassword: effectiveGatePassword ? maskLoginToken(effectiveGatePassword) : '',
|
||
onlineTicket: onlineTicket ? maskLoginToken(onlineTicket) : '',
|
||
taskId,
|
||
goPath,
|
||
redirect: redirectTo,
|
||
isEmbed,
|
||
rawSearch: searchParams.toString(),
|
||
});
|
||
|
||
try {
|
||
let auth;
|
||
let loginUserName = '';
|
||
|
||
// 外部用户信息(yUserId → consumer 用户详情)与登录鉴权方式**解耦**:只要 URL
|
||
// 带 yUserId 就拉一次 userDetailByYuserId,把 yUserId/userName/gateDepartName/
|
||
// typeName/accessToken 落进 userInfo。之前 token(authToken) 登录会短路掉这一步,
|
||
// 导致 token 登录后 userInfo 残缺(只剩 userName)、iframe/轻应用拿不到这些值。
|
||
if (yUserId) {
|
||
if (authToken) {
|
||
// token 登录下 consumer 失败不应阻断登录(仅影响 userInfo 的富信息)。
|
||
try {
|
||
await loginByYUserId(yUserId, toolToken || null, access_token || null);
|
||
} catch (error) {
|
||
console.warn('[login] loginByYUserId failed under token login (non-fatal)', error);
|
||
}
|
||
} else {
|
||
await loginByYUserId(yUserId, toolToken || null, access_token || null);
|
||
}
|
||
}
|
||
|
||
if (authToken) {
|
||
// token 换登录:后端用 authToken 调上游接口换出用户名再登录
|
||
auth = await loginByToken(authToken);
|
||
// 让 token 登录也写入正确用户名(后端 getTokenInfo 解析出的原始登录名)。
|
||
// 若上面 yUserId 已从 consumer 落了 userName,则 setStoredUserName 不会覆盖它。
|
||
loginUserName = auth.username?.trim() ?? '';
|
||
} else {
|
||
let deerflowUsername = resolvedUsername;
|
||
// yUserId 优先(已在上面落好 userInfo + tools_token);其余方式仅在无 yUserId 时启用。
|
||
if (!yUserId && onlineTicket) {
|
||
deerflowUsername = await resolveUsernameByOnlineTicket(onlineTicket);
|
||
} else if (!yUserId && toolToken) {
|
||
persistToolsToken(toolToken);
|
||
}
|
||
|
||
auth = await loginByUsername(deerflowUsername, effectiveGatePassword);
|
||
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);
|
||
// 非 yUserId 登录仍可直接持久化 access_token;yUserId 登录已在 loginByYUserId 内按
|
||
// “接口 accessToken 优先,URL access_token 仅兜底”的规则处理。
|
||
if (!yUserId) {
|
||
persistAccessToken(access_token);
|
||
}
|
||
|
||
// ── 调试:登录落地结果(auth + 实际写入的 userInfo,access token 已脱敏) ──
|
||
console.info('[login] stored result', {
|
||
method: loginMethod,
|
||
user_id: auth.user_id,
|
||
email: auth.email,
|
||
authUsername: auth.username,
|
||
loginUserName,
|
||
userInfo: summarizeStoredUserInfo(),
|
||
});
|
||
|
||
// 嵌入模式不写 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(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,
|
||
access_token,
|
||
onlineTicket,
|
||
authToken,
|
||
effectiveGatePassword,
|
||
usesConfiguredRouteLoginPassword,
|
||
routeKey,
|
||
gatePassword,
|
||
taskId,
|
||
goPath,
|
||
redirectTo,
|
||
redirectPath,
|
||
isEmbed,
|
||
]);
|
||
|
||
return (
|
||
<div className="flex min-h-screen items-center justify-center bg-background px-6">
|
||
<div className="w-full max-w-lg rounded-xl border bg-card p-6 shadow-sm">
|
||
<h1 className="text-xl font-semibold">正在进入智能体模式</h1>
|
||
{status === 'error' && (
|
||
<p className="mt-4 text-sm text-red-600">
|
||
登录失败:{errorMessage || '未知错误'}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|