权限验证

This commit is contained in:
北枳 2025-08-27 20:11:08 +08:00
parent e7718340c4
commit c56043d85c
8 changed files with 260 additions and 48 deletions

View File

@ -16,22 +16,52 @@ const HTTP_ERROR_MESSAGES: Record<number, string> = {
503: "Service temporarily unavailable, please try again later.", 503: "Service temporarily unavailable, please try again later.",
504: "Gateway timeout, please try again later.", 504: "Gateway timeout, please try again later.",
}; };
/** /**
* *
*/ */
const DEFAULT_ERROR_MESSAGE = const DEFAULT_ERROR_MESSAGE = "网络异常,请重试。如果问题持续存在,请联系我们。";
"Please try again if the network is abnormal. If it happens again, please contact us.";
/** /**
* *
*/
const ERROR_HANDLERS: Record<number, () => void> = {
401: () => {
// 清除本地存储的 token
localStorage.removeItem('token');
// 跳转到登录页面
window.location.href = '/login';
},
403: () => {
// 显示积分不足通知
import('../utils/notifications').then(({ showInsufficientPointsNotification }) => {
showInsufficientPointsNotification();
});
}
};
/**
*
* @param code - HTTP错误码 * @param code - HTTP错误码
* @param customMessage - * @param customMessage -
*/ */
export const errorHandle = debounce( export const errorHandle = debounce(
(code: number, customMessage?: string): void => { (code: number, customMessage?: string): void => {
const errorMessage = const errorMessage =
customMessage || HTTP_ERROR_MESSAGES[code] || DEFAULT_ERROR_MESSAGE; customMessage || HTTP_ERROR_MESSAGES[code] || DEFAULT_ERROR_MESSAGE;
message.error(errorMessage);
// 显示错误提示
message.error({
content: errorMessage,
duration: 3,
className: 'custom-error-message'
});
// 执行特殊错误码的处理函数
const handler = ERROR_HANDLERS[code];
if (handler) {
handler();
}
}, },
100 100
); );

View File

@ -2,6 +2,25 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosR
import { message } from "antd"; import { message } from "antd";
import { BASE_URL } from './constants' import { BASE_URL } from './constants'
import { errorHandle } from './errorHandle'; import { errorHandle } from './errorHandle';
/**
*
* @param error -
* @param defaultMessage -
*/
const handleRequestError = (error: any, defaultMessage: string = '请求失败') => {
if (error.response) {
const { status, data } = error.response;
const errorMessage = data?.message || defaultMessage;
errorHandle(status, errorMessage);
} else if (error.request) {
// 请求已发出但没有收到响应
errorHandle(0, '网络请求失败,请检查网络连接');
} else {
// 请求配置出错
errorHandle(0, error.message || defaultMessage);
}
};
// 创建 axios 实例 // 创建 axios 实例
const request: AxiosInstance = axios.create({ const request: AxiosInstance = axios.create({
baseURL: BASE_URL, // 设置基础URL baseURL: BASE_URL, // 设置基础URL
@ -29,23 +48,32 @@ request.interceptors.request.use(
// 响应拦截器 // 响应拦截器
request.interceptors.response.use( request.interceptors.response.use(
(response: AxiosResponse) => { (response: AxiosResponse) => {
// 直接返回响应数据 // 检查业务状态码
if (response.data?.code !=0) { if (response.data?.code !== 0) {
// TODO 暂时固定报错信息,后续根据后端返回的错误码进行处理 // 处理业务层面的错误
errorHandle(0); const businessCode = response.data?.code;
const errorMessage = response.data?.message;
// 特殊处理 401 和 403 业务状态码
if (businessCode === 401) {
errorHandle(401, errorMessage);
return Promise.reject(new Error(errorMessage));
}
if (businessCode === 403) {
errorHandle(403, errorMessage);
return Promise.reject(new Error(errorMessage));
}
// 其他业务错误
errorHandle(0, errorMessage);
return Promise.reject(new Error(errorMessage));
} }
return response.data; return response.data;
}, },
(error) => { (error) => {
if (error.response) { handleRequestError(error);
errorHandle(error.response.status);
} else if (error.request) {
// 请求已发出但没有收到响应
errorHandle(0);
} else {
// 检修
console.error(error);
}
return Promise.reject(error); return Promise.reject(error);
} }
); );
@ -86,8 +114,23 @@ export async function streamJsonPost<T = any>(
body: JSON.stringify(body), body: JSON.stringify(body),
}); });
// 处理 HTTP 错误状态
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); const error = {
response: {
status: response.status,
data: { message: await response.text().then(text => {
try {
const data = JSON.parse(text);
return data.message || `HTTP error! status: ${response.status}`;
} catch {
return `HTTP error! status: ${response.status}`;
}
})}
}
};
handleRequestError(error);
throw error;
} }
if (!response.body) { if (!response.body) {
@ -207,7 +250,8 @@ export const stream = async <T>({
const response = await request(config); const response = await request(config);
onComplete?.(); onComplete?.();
return response; return response;
} catch (error) { } catch (error: any) {
handleRequestError(error, '流式请求失败');
onError?.(error); onError?.(error);
throw error; throw error;
} }
@ -239,8 +283,9 @@ export const downloadStream = async (
window.URL.revokeObjectURL(downloadUrl); window.URL.revokeObjectURL(downloadUrl);
return response; return response;
} catch (error) { } catch (error: any) {
console.error('文件下载失败:', error); console.error('文件下载失败:', error);
handleRequestError(error, '文件下载失败');
throw error; throw error;
} }
}; };

View File

@ -239,3 +239,7 @@ body {
.animate-fade-in { .animate-fade-in {
animation: fade-in 0.2s ease-out forwards; animation: fade-in 0.2s ease-out forwards;
} }
.ant-notification-notice-wrapper {
background: transparent !important;
}

View File

@ -124,6 +124,8 @@ export function MessageRenderer({ msg }: MessageRendererProps) {
); );
case "progress": case "progress":
return <ProgressBar key={idx} value={b.value} total={b.total} label={b.label} />; return <ProgressBar key={idx} value={b.value} total={b.total} label={b.label} />;
case "link":
return <a key={idx} href={b.url} className="underline">{b.text}</a>;
default: default:
return null; return null;
} }

View File

@ -25,7 +25,7 @@ const EMPTY_MESSAGES: RealApiMessage[] = [
role: 'assistant', role: 'assistant',
content: JSON.stringify([{ content: JSON.stringify([{
type: 'text', type: 'text',
content: '🌟欢迎来到 MovieFlow 🎬✨\n快把您的创意告诉我吧💡\n我是您的专属AI小伙伴🤖可以帮您\n🎭 生成专属演员形象\n📽 搭建场景 & 分镜\n🎞 完成整部视频创作\n\n一起开启奇妙的创作之旅吧❤️' content: '🌟Welcome to MovieFlow 🎬✨\nTell me your idea💡\nI am your AI assistant🤖, I can help you:\n🎭 Generate actor images\n📽 Generate scene & shot sketches\n🎞 Complete video creation\n\nLet\'s start our creative journey together!❤️'
}]), }]),
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
function_name: undefined, function_name: undefined,
@ -35,6 +35,19 @@ const EMPTY_MESSAGES: RealApiMessage[] = [
} }
]; ];
// 用户积分不足消息
const NoEnoughCreditsMessageBlocks: MessageBlock[] = [
{
type: 'text',
text: 'Insufficient credits.'
},
{
type: 'link',
text: 'Upgrade to continue.',
url: '/pricing'
}
];
/** /**
* *
*/ */
@ -195,7 +208,7 @@ function transformSystemMessage(
*/ */
function transformMessage(apiMessage: RealApiMessage): ChatMessage { function transformMessage(apiMessage: RealApiMessage): ChatMessage {
try { try {
const { id, role, content, created_at, function_name, custom_data, status, intent_type } = apiMessage; const { id, role, content, created_at, function_name, custom_data, status, intent_type, error_message } = apiMessage;
let message: ChatMessage = { let message: ChatMessage = {
id: id ? id.toString() : Date.now().toString(), id: id ? id.toString() : Date.now().toString(),
role: role, role: role,
@ -205,30 +218,36 @@ function transformMessage(apiMessage: RealApiMessage): ChatMessage {
status: status || 'success', status: status || 'success',
}; };
if (role === 'assistant' || role === 'user') { if (error_message && error_message === 'no enough credits') {
try { message.blocks = NoEnoughCreditsMessageBlocks;
const contentObj = JSON.parse(content); } else {
const contentArray = Array.isArray(contentObj) ? contentObj : [contentObj]; if (role === 'assistant' || role === 'user') {
contentArray.forEach((c: ApiMessageContent) => { try {
if (c.type === "text") { const contentObj = JSON.parse(content);
message.blocks.push({ type: "text", text: c.content }); const contentArray = Array.isArray(contentObj) ? contentObj : [contentObj];
} else if (c.type === "image") { contentArray.forEach((c: ApiMessageContent) => {
message.blocks.push({ type: "image", url: c.content }); if (c.type === "text") {
} else if (c.type === "video") { message.blocks.push({ type: "text", text: c.content });
message.blocks.push({ type: "video", url: c.content }); } else if (c.type === "image") {
} else if (c.type === "audio") { message.blocks.push({ type: "image", url: c.content });
message.blocks.push({ type: "audio", url: c.content }); } else if (c.type === "video") {
} message.blocks.push({ type: "video", url: c.content });
}); } else if (c.type === "audio") {
} catch (error) { message.blocks.push({ type: "audio", url: c.content });
// 如果 JSON 解析失败,将整个 content 作为文本内容 } else if (c.type === "link") {
message.blocks.push({ type: "link", text: c.content, url: c.url || '' });
}
});
} catch (error) {
// 如果 JSON 解析失败,将整个 content 作为文本内容
message.blocks.push({ type: "text", text: content });
}
} else if (role === 'system' && function_name && custom_data) {
// 处理系统消息
message.blocks = transformSystemMessage(function_name, content, custom_data);
} else {
message.blocks.push({ type: "text", text: content }); message.blocks.push({ type: "text", text: content });
} }
} else if (role === 'system' && function_name && custom_data) {
// 处理系统消息
message.blocks = transformSystemMessage(function_name, content, custom_data);
} else {
message.blocks.push({ type: "text", text: content });
} }
// 如果没有有效的 blocks至少添加一个文本块 // 如果没有有效的 blocks至少添加一个文本块

View File

@ -7,7 +7,8 @@ export type MessageBlock =
| { type: "image"; url: string; alt?: string } | { type: "image"; url: string; alt?: string }
| { type: "video"; url: string; poster?: string } | { type: "video"; url: string; poster?: string }
| { type: "audio"; url: string } | { type: "audio"; url: string }
| { type: "progress"; value: number; total?: number; label?: string }; | { type: "progress"; value: number; total?: number; label?: string }
| { type: "link"; text: string; url: string };
export interface ChatMessage { export interface ChatMessage {
id: string; id: string;
@ -127,6 +128,7 @@ export interface ShotVideoGeneration {
export interface ApiMessageContent { export interface ApiMessageContent {
type: ContentType; type: ContentType;
content: string; content: string;
url?: string;
} }
export interface RealApiMessage { export interface RealApiMessage {
@ -138,4 +140,5 @@ export interface RealApiMessage {
custom_data?: ProjectInit | ScriptSummary | CharacterGeneration | SketchGeneration | ShotSketchGeneration | ShotVideoGeneration; custom_data?: ProjectInit | ScriptSummary | CharacterGeneration | SketchGeneration | ShotSketchGeneration | ShotVideoGeneration;
status: MessageStatus; status: MessageStatus;
intent_type: IntentType; intent_type: IntentType;
error_message?: string;
} }

View File

@ -25,6 +25,7 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const currentUser = localStorage.getItem('currentUser'); const currentUser = localStorage.getItem('currentUser');
const [openModal, setOpenModal] = React.useState(false);
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
@ -102,9 +103,9 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
</Button> </Button>
{/* Notifications */} {/* Notifications */}
<Button variant="ghost" size="sm"> {/* <Button variant="ghost" size="sm" onClick={() => setOpenModal(true)}>
<Bell className="h-4 w-4" /> <Bell className="h-4 w-4" />
</Button> </Button> */}
{/* Theme Toggle */} {/* Theme Toggle */}
{/* <Button {/* <Button

108
utils/notifications.tsx Normal file
View File

@ -0,0 +1,108 @@
import { notification } from 'antd';
import { useRouter } from 'next/router';
type NotificationType = 'success' | 'info' | 'warning' | 'error';
const darkGlassStyle = {
background: 'rgba(30, 32, 40, 0.95)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.08)',
borderRadius: '8px',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',
padding: '12px 16px',
};
const messageStyle = {
fontSize: '13px',
fontWeight: 500,
color: '#ffffff',
marginBottom: '6px',
display: 'flex',
alignItems: 'center',
gap: '6px',
};
const iconStyle = {
color: '#F6B266', // 警告图标颜色
background: 'rgba(246, 178, 102, 0.15)',
padding: '4px',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
};
const descriptionStyle = {
fontSize: '12px',
color: 'rgba(255, 255, 255, 0.65)',
marginBottom: '12px',
lineHeight: '1.5',
};
const btnStyle = {
color: 'rgb(250 173 20 / 90%)',
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: 0,
fontSize: '12px',
fontWeight: 500,
textDecoration: 'underline',
textUnderlineOffset: '2px',
textDecorationColor: 'rgb(250 173 20 / 60%)',
transition: 'all 0.2s ease',
};
/**
*
* @description
*/
export const showInsufficientPointsNotification = () => {
notification.warning({
message: null,
description: (
<div data-alt="insufficient-points-notification" style={{ minWidth: '280px' }}>
<h3 style={messageStyle}>
</h3>
<p style={descriptionStyle}>使</p>
<button
onClick={() => window.location.href = '/pricing'}
style={btnStyle}
data-alt="recharge-button"
>
/
</button>
</div>
),
duration: 5,
placement: 'topRight',
style: darkGlassStyle,
className: 'dark-glass-notification',
closeIcon: (
<button
className="hover:text-white"
style={{
background: 'transparent',
border: 'none',
padding: '2px',
cursor: 'pointer',
color: 'rgba(255, 255, 255, 0.45)',
transition: 'color 0.2s ease',
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
),
});
};
/**
*
*/
notification.config({
maxCount: 3, // 最多同时显示3个通知
});