feat: 无界 ticket 登录、AI 写作配置确认与定时任务弹窗修复

无界 onlineTicket 对接 MaxKey 与 consumer 接口并用 sub 登录;新增 setup_writing 写作配置确认流;定时任务创建弹窗限制多行高度并支持滚动。
This commit is contained in:
fengxiangdebaozi committed 2026-06-03 16:20:45 +08:00
1 parent ab876fc7d8
commit ad4b27b7ca
25 files changed
+980 -54

No files matched your search

@@ -13,6 +13,10 @@ export const EXTERNAL_LOGIN_STORAGE_KEYS = {
ticket: "login.ticket",
userId: "login.userId",
env: "login.env",
/** MaxKey getTokenByOnlineTicket 返回的 token */
maxkeyOnlineToken: "login.maxkeyOnlineToken",
/** consumer /cop/userDetailToken 返回的 data 整包(JSON) */
userDetailToken: "login.userDetailToken",
} as const;
export type ExternalLoginParams = {
@@ -0,0 +1,142 @@
import { requestCrudService } from "@/service/request-axios-service";
import {
EXTERNAL_LOGIN_STORAGE_KEYS,
persistExternalLoginParams,
} from "./external-login";
const AUTH_BASE_URL = (import.meta.env.VITE_AUTH_BASE_URL || "").replace(
/\/$/,
"",
);
const CONSUMER_URL = (import.meta.env.VITE_AIPI_CONSUMER_URL || "").replace(
/\/$/,
"",
);
/** MaxKey onlineTicket 换发的 token(后续请求可能用到) */
export const MAXKEY_ONLINE_TOKEN_STORAGE_KEY =
EXTERNAL_LOGIN_STORAGE_KEYS.maxkeyOnlineToken;
/** `/cop/userDetailToken` 返回的 data 整包(JWT 解析后的用户信息) */
export const USER_DETAIL_TOKEN_STORAGE_KEY =
EXTERNAL_LOGIN_STORAGE_KEYS.userDetailToken;
export type MaxkeyTokenByTicketResponse = {
code: number;
data: string;
message?: string;
};
export type UserDetailTokenPayload = {
sub?: string;
user_id?: string;
exp?: number;
expired?: boolean;
iat?: number;
institution?: string;
iss?: string;
locale?: string;
kid?: string;
jti?: string;
[key: string]: unknown;
};
type UserDetailTokenResponse = {
code?: number;
data?: UserDetailTokenPayload;
message?: string;
};
function persistMaxkeyOnlineToken(token: string): void {
try {
localStorage.setItem(MAXKEY_ONLINE_TOKEN_STORAGE_KEY, token);
} catch {
// 静默失败,不阻断登录
}
}
function persistUserDetailToken(data: UserDetailTokenPayload): void {
try {
localStorage.setItem(USER_DETAIL_TOKEN_STORAGE_KEY, JSON.stringify(data));
if (data.user_id) {
localStorage.setItem(EXTERNAL_LOGIN_STORAGE_KEYS.userId, String(data.user_id));
}
} catch {
// 静默失败
}
}
/**
* 无界 / 外部跳转:用 onlineTicket 换 MaxKey token,并拉取用户详情。
* - token 存入 localStorage(`login.maxkeyOnlineToken`)
* - userDetailToken 的 data 存入 localStorage(`login.userDetailToken`)
* @returns 用于 DeerFlow `loginByUsername` 的用户名(取自 data.sub)
*/
export async function resolveUsernameByOnlineTicket(
ticket: string,
): Promise<string> {
const onlineTicket = ticket.trim();
if (!onlineTicket) {
throw new Error("缺少 onlineTicket,无法完成无界登录。");
}
if (!AUTH_BASE_URL) {
throw new Error("未配置 VITE_AUTH_BASE_URL,无法换取 MaxKey token。");
}
if (!CONSUMER_URL) {
throw new Error("未配置 VITE_AIPI_CONSUMER_URL,无法获取用户详情。");
}
persistExternalLoginParams({ ticket: onlineTicket });
const tokenRes = (await requestCrudService.get(
`${AUTH_BASE_URL}/maxkey-mgt-api/sys/getTokenByOnlineTicket`,
{ onlineTicket },
)) as unknown as MaxkeyTokenByTicketResponse;
console.log("[wujie] getTokenByOnlineTicket:", tokenRes);
if (tokenRes.code !== 200 || !tokenRes.data) {
throw new Error(
tokenRes.message || "getTokenByOnlineTicket 未返回有效 token。",
);
}
persistMaxkeyOnlineToken(tokenRes.data);
const detailRes = (await requestCrudService.get(
`${CONSUMER_URL}/cop/userDetailToken`,
{ ticket: onlineTicket },
)) as unknown as UserDetailTokenResponse;
console.log("[wujie] userDetailToken:", detailRes);
const userData = detailRes.data;
if (!userData) {
throw new Error("userDetailToken 未返回用户 data。");
}
persistUserDetailToken(userData);
const username = (userData.sub ?? "").trim();
if (!username) {
throw new Error("userDetailToken 中缺少 sub(用户名)。");
}
return username;
}
export function readMaxkeyOnlineToken(): string {
try {
return localStorage.getItem(MAXKEY_ONLINE_TOKEN_STORAGE_KEY) ?? "";
} catch {
return "";
}
}
export function readUserDetailToken(): UserDetailTokenPayload | null {
try {
const raw = localStorage.getItem(USER_DETAIL_TOKEN_STORAGE_KEY);
if (!raw) return null;
return JSON.parse(raw) as UserDetailTokenPayload;
} catch {
return null;
}
}
+2 -2
View File
@@ -6,7 +6,7 @@
* 注入参数,子应用在 `window.$wujie.props` 上拿到**同一个对象**(无界在子应用
* bootstrap 之前就已注入,因此首帧即可读)。
*
* 本期范围:只「读出来 + 存起来 + 打印」,**不接入对方鉴权**。读出的
* ticket 在 LoginPage 会走 MaxKey + consumer 双接口换用户名并登录;读出的
* ticket/userId/env 复用 external-login 的 localStorage 存储(与 URL 跳转登录
* 同一套 key),theme 交给调用方(LoginPage)映射到 ThemeContext。
*/
@@ -16,7 +16,7 @@ import { persistExternalLoginParams } from "@/core/auth/external-login";
/** 无界父级注入的 props 形状。除已知字段外允许透传其余未知字段便于排查。 */
export interface WujieProps {
/** 第三方鉴权 ticket(本期仅存储待用,不消费)。 */
/** 第三方 onlineTicket(LoginPage 用于 MaxKey + userDetailToken 登录)。 */
ticket?: string;
/** 外部用户 id。 */
userId?: string;
+21 -1
View File
@@ -20,6 +20,8 @@ interface AssistantClarificationGroup extends GenericMessageGroup<"assistant:cla
interface AssistantSetupAgentApprovalGroup extends GenericMessageGroup<"assistant:setup-agent-approval"> {}
interface AssistantSetupWritingApprovalGroup extends GenericMessageGroup<"assistant:setup-writing-approval"> {}
interface AssistantSubagentGroup extends GenericMessageGroup<"assistant:subagent"> {}
type MessageGroup =
@@ -29,6 +31,7 @@ type MessageGroup =
| AssistantPresentFilesGroup
| AssistantClarificationGroup
| AssistantSetupAgentApprovalGroup
| AssistantSetupWritingApprovalGroup
| AssistantSubagentGroup;
export function groupMessages<T>(
@@ -50,7 +53,8 @@ export function groupMessages<T>(
last.type !== "human" &&
last.type !== "assistant" &&
last.type !== "assistant:clarification" &&
last.type !== "assistant:setup-agent-approval"
last.type !== "assistant:setup-agent-approval" &&
last.type !== "assistant:setup-writing-approval"
) {
return last;
}
@@ -88,6 +92,13 @@ export function groupMessages<T>(
type: "assistant:setup-agent-approval",
messages: [message],
});
} else if (isSetupWritingApprovalToolMessage(message)) {
lastOpenGroup()?.messages.push(message);
groups.push({
id: message.id,
type: "assistant:setup-writing-approval",
messages: [message],
});
} else {
const open = lastOpenGroup();
if (open) {
@@ -354,6 +365,15 @@ export function isSetupAgentApprovalToolMessage(message: Message) {
);
}
export function isSetupWritingApprovalToolMessage(message: Message) {
return (
message.type === "tool" &&
message.name === "setup_writing" &&
typeof message.additional_kwargs?.setup_writing_approval === "object" &&
message.additional_kwargs.setup_writing_approval !== null
);
}
export function extractPresentFilesFromMessage(message: Message) {
if (message.type !== "ai" || !hasPresentFiles(message)) {
return [];