代码修改
This commit is contained in:
1 parent
45049975c2
commit
63bddfdea7
3 files changed
+196
No files matched your search
@@ -84,6 +84,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
|||||||
- **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE
|
- **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/`
|
- **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
|
||||||
- **Shims**: `src/shims/` provides `next/link` and `next/navigation` polyfills (original code was Next.js)
|
- **Shims**: `src/shims/` provides `next/link` and `next/navigation` polyfills (original code was Next.js)
|
||||||
|
- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalysSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A (resolved via `/api/business-mapping`, opening message in `sessionStorage.pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. Any failure (no consumer URL / fetch error / unmapped goPath / no configured agent) silently falls back to the default home. Helpers: `core/auth/task-deeplink.ts`.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* 外部系统跳转登录时的「任务深链」:登录 URL 带 ``taskId`` + ``goPath`` 时,
|
||||||
|
* 拉取任务详情并据 goPath 把用户直接送到对应的智能体问答 / 多智能体会商页。
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 调 consumer 接口 ``GET {VITE_AIPI_CONSUMER_URL}/taskAnalysSearch/cop-task-detail?taskId=``,
|
||||||
|
* 请求头 ``Authorization: Bearer admin``(由 service 拦截器统一注入)。
|
||||||
|
* 2. 把返回的 data(id/overview/taskDirection/taskName/taskContent)整包存到 sessionStorage。
|
||||||
|
* 3. 按 goPath 解析出业务码 + 模式:
|
||||||
|
* - ``3qfx`` / ``rwfx`` / ``xdfx`` → 单智能体问答(用「配置管理」里该业务配置的智能体)
|
||||||
|
* - ``3qfx-A`` / ``rwfx-A`` / ``xdfx-A`` → 多智能体会商(用该业务配置的业务链条)
|
||||||
|
* 携带 taskName / taskContent 作为开场信息。
|
||||||
|
*
|
||||||
|
* 业务码映射与「配置管理」(`/api/business-mapping`) 一致:
|
||||||
|
* 3Q=三情分析(3QFX)、6BF=任务分析(任务FX)、7BF=行动分析(XDFX)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { requestCrudService } from "@/service/request-axios-service";
|
||||||
|
import { listBusinessMappings } from "@/strategy-components/api/business-mapping";
|
||||||
|
|
||||||
|
/** consumer 接口返回的任务详情(data 字段)。 */
|
||||||
|
export interface CopTaskDetail {
|
||||||
|
id: string | number | null;
|
||||||
|
overview: string;
|
||||||
|
taskDirection: string;
|
||||||
|
taskName: string;
|
||||||
|
taskContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 任务详情整包暂存的 sessionStorage key(目标页/后续可读取)。 */
|
||||||
|
export const COP_TASK_DETAIL_STORAGE_KEY = "login.copTaskDetail";
|
||||||
|
|
||||||
|
/** goPath 基础码 → 业务码(配置管理 businessCode)。 */
|
||||||
|
const GOPATH_BUSINESS_CODE: Record<string, string> = {
|
||||||
|
"3qfx": "3Q", // 三情分析
|
||||||
|
rwfx: "6BF", // 任务分析(任务FX)
|
||||||
|
xdfx: "7BF", // 行动分析(XDFX)
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ParsedGoPath {
|
||||||
|
businessCode: string;
|
||||||
|
/** true = 多智能体会商(``-A`` 后缀);false = 单智能体问答。 */
|
||||||
|
roundtable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 goPath(大小写不敏感);``-A`` 后缀表示多智能体会商。无法识别返回 null。 */
|
||||||
|
export function parseGoPath(goPath: string): ParsedGoPath | null {
|
||||||
|
const raw = goPath.trim().toLowerCase();
|
||||||
|
if (!raw) return null;
|
||||||
|
const roundtable = raw.endsWith("-a");
|
||||||
|
const base = roundtable ? raw.slice(0, -2) : raw;
|
||||||
|
const businessCode = GOPATH_BUSINESS_CODE[base];
|
||||||
|
if (!businessCode) return null;
|
||||||
|
return { businessCode, roundtable };
|
||||||
|
}
|
||||||
|
|
||||||
|
function consumerBaseUrl(): string {
|
||||||
|
const value = import.meta.env.VITE_AIPI_CONSUMER_URL;
|
||||||
|
return typeof value === "string" ? value.replace(/\/+$/, "") : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拉取任务详情;缺少 consumer 地址 / 返回不合法时返回 null。 */
|
||||||
|
export async function fetchCopTaskDetail(taskId: string): Promise<CopTaskDetail | null> {
|
||||||
|
const base = consumerBaseUrl();
|
||||||
|
if (!base || !taskId) return null;
|
||||||
|
// service 拦截器已统一注入 `Authorization: Bearer admin` 并返回 response.data(整包 body)。
|
||||||
|
const body = await requestCrudService.get(`${base}/taskAnalysSearch/cop-task-detail`, { taskId });
|
||||||
|
const data = body && typeof body === "object" ? (body as { data?: unknown }).data : null;
|
||||||
|
if (!data || typeof data !== "object") return null;
|
||||||
|
const d = data as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
id: (d.id as string | number | null) ?? null,
|
||||||
|
overview: typeof d.overview === "string" ? d.overview : "",
|
||||||
|
taskDirection: typeof d.taskDirection === "string" ? d.taskDirection : "",
|
||||||
|
taskName: typeof d.taskName === "string" ? d.taskName : "",
|
||||||
|
taskContent: typeof d.taskContent === "string" ? d.taskContent : "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把任务详情整包暂存到 sessionStorage(隐私模式/quota 异常时静默失败)。 */
|
||||||
|
export function persistCopTaskDetail(detail: CopTaskDetail): void {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(COP_TASK_DETAIL_STORAGE_KEY, JSON.stringify(detail));
|
||||||
|
} catch {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读取暂存的任务详情。 */
|
||||||
|
export function readCopTaskDetail(): CopTaskDetail | null {
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem(COP_TASK_DETAIL_STORAGE_KEY);
|
||||||
|
return raw ? (JSON.parse(raw) as CopTaskDetail) : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskDeeplinkTarget {
|
||||||
|
path: string;
|
||||||
|
/** react-router navigate 的 state(多智能体会商用 roundtablePrefill)。 */
|
||||||
|
state?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用 taskName / taskContent 拼出开场信息文本。 */
|
||||||
|
function buildOpeningText(detail: CopTaskDetail): string {
|
||||||
|
return [
|
||||||
|
detail.taskName ? `任务名称:${detail.taskName}` : "",
|
||||||
|
detail.taskContent ? `任务内容:${detail.taskContent}` : "",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 据 goPath + 任务详情解析跳转目标。
|
||||||
|
* - 多智能体会商:返回带 ``roundtablePrefill``(businessCode 让会商页自动选用配置的业务链条)的 state。
|
||||||
|
* - 单智能体问答:解析「配置管理」里该业务配置的智能体;把开场信息写入
|
||||||
|
* ``sessionStorage.pendingAgentMessage``(AgentChatPage 自动发出)。
|
||||||
|
* 无法识别 goPath / 未配置智能体时返回 null(调用方回退到默认首页)。
|
||||||
|
*/
|
||||||
|
export async function resolveTaskDeeplinkTarget(
|
||||||
|
goPath: string,
|
||||||
|
detail: CopTaskDetail,
|
||||||
|
): Promise<TaskDeeplinkTarget | null> {
|
||||||
|
const parsed = parseGoPath(goPath);
|
||||||
|
if (!parsed) return null;
|
||||||
|
|
||||||
|
if (parsed.roundtable) {
|
||||||
|
return {
|
||||||
|
path: "/page/strategy/qa/multi-agent-roundtable",
|
||||||
|
state: {
|
||||||
|
roundtablePrefill: {
|
||||||
|
taskTitle: detail.taskName,
|
||||||
|
taskGoal: detail.taskContent,
|
||||||
|
businessCode: parsed.businessCode,
|
||||||
|
autoStart: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let agentId: string | null = null;
|
||||||
|
try {
|
||||||
|
const mappings = await listBusinessMappings();
|
||||||
|
agentId = mappings.find((m) => m.businessCode === parsed.businessCode)?.agentId ?? null;
|
||||||
|
} catch {
|
||||||
|
agentId = null;
|
||||||
|
}
|
||||||
|
if (!agentId) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(
|
||||||
|
"pendingAgentMessage",
|
||||||
|
JSON.stringify({ agentId, text: buildOpeningText(detail) }),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
return { path: `/page/strategy/agents/${agentId}/chats/new` };
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
import { useTheme } from '@/contexts/ThemeContext';
|
||||||
@@ -11,6 +12,11 @@ import {
|
|||||||
readExternalLoginParams,
|
readExternalLoginParams,
|
||||||
resolveThemeFromQuery,
|
resolveThemeFromQuery,
|
||||||
} from '@/core/auth/external-login';
|
} from '@/core/auth/external-login';
|
||||||
|
import {
|
||||||
|
fetchCopTaskDetail,
|
||||||
|
persistCopTaskDetail,
|
||||||
|
resolveTaskDeeplinkTarget,
|
||||||
|
} from '@/core/auth/task-deeplink';
|
||||||
import { resolveUsernameByOnlineTicket } from '@/core/auth/wujie-ticket-login';
|
import { resolveUsernameByOnlineTicket } from '@/core/auth/wujie-ticket-login';
|
||||||
import { isWujieEnv, useWujieProps } from '@/core/embed/wujie';
|
import { isWujieEnv, useWujieProps } from '@/core/embed/wujie';
|
||||||
import {
|
import {
|
||||||
@@ -34,6 +40,7 @@ type LoginStatus = 'loading' | 'success' | 'error';
|
|||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const { username } = useParams<{ username?: string }>();
|
const { username } = useParams<{ username?: string }>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const navigate = useNavigate();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
const sidebarEnabled = useMemo(
|
const sidebarEnabled = useMemo(
|
||||||
@@ -60,6 +67,9 @@ export default function LoginPage() {
|
|||||||
const gatePassword = searchParams.get('password') ?? '';
|
const gatePassword = searchParams.get('password') ?? '';
|
||||||
// token 换登录:?authToken= 传上游 token(与 tools_token 的 ?token= 区分开)
|
// token 换登录:?authToken= 传上游 token(与 tools_token 的 ?token= 区分开)
|
||||||
const authToken = searchParams.get('authToken')?.trim() ?? '';
|
const authToken = searchParams.get('authToken')?.trim() ?? '';
|
||||||
|
// 任务深链:外部系统带 ?taskId=&goPath= 时,登录后拉任务详情并直送对应问答/会商页
|
||||||
|
const taskId = searchParams.get('taskId')?.trim() ?? '';
|
||||||
|
const goPath = searchParams.get('goPath')?.trim() ?? '';
|
||||||
const externalParams = useMemo(
|
const externalParams = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
theme: searchParams.get('theme') ?? '',
|
theme: searchParams.get('theme') ?? '',
|
||||||
@@ -145,6 +155,27 @@ export default function LoginPage() {
|
|||||||
persistAccessToken(accessToken);
|
persistAccessToken(accessToken);
|
||||||
persistSidebarEnabled(sidebarEnabled);
|
persistSidebarEnabled(sidebarEnabled);
|
||||||
setStatus('success');
|
setStatus('success');
|
||||||
|
|
||||||
|
// 任务深链:带 taskId + goPath 时拉任务详情并直送对应问答/会商页;
|
||||||
|
// 任一环节失败(无地址/拉取失败/未配置智能体/goPath 无法识别)都回退默认首页。
|
||||||
|
if (taskId && goPath) {
|
||||||
|
try {
|
||||||
|
const detail = await fetchCopTaskDetail(taskId);
|
||||||
|
if (cancelled) return;
|
||||||
|
if (detail) {
|
||||||
|
persistCopTaskDetail(detail);
|
||||||
|
const target = await resolveTaskDeeplinkTarget(goPath, detail);
|
||||||
|
if (cancelled) return;
|
||||||
|
if (target) {
|
||||||
|
navigate(target.path, { replace: true, state: target.state });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 深链失败不阻断登录,落默认首页 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
router.replace(yUserId ? GENERAL_QA_ROUTE : redirectPath);
|
router.replace(yUserId ? GENERAL_QA_ROUTE : redirectPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
@@ -164,6 +195,7 @@ export default function LoginPage() {
|
|||||||
}, [
|
}, [
|
||||||
resolvedUsername,
|
resolvedUsername,
|
||||||
router,
|
router,
|
||||||
|
navigate,
|
||||||
sidebarEnabled,
|
sidebarEnabled,
|
||||||
yUserId,
|
yUserId,
|
||||||
toolToken,
|
toolToken,
|
||||||
@@ -171,6 +203,8 @@ export default function LoginPage() {
|
|||||||
onlineTicket,
|
onlineTicket,
|
||||||
authToken,
|
authToken,
|
||||||
gatePassword,
|
gatePassword,
|
||||||
|
taskId,
|
||||||
|
goPath,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in new issue
Block a user