forked from 77media/video-flow
权限验证
This commit is contained in:
parent
e7718340c4
commit
c56043d85c
@ -16,22 +16,52 @@ const HTTP_ERROR_MESSAGES: Record<number, string> = {
|
||||
503: "Service temporarily unavailable, please try again later.",
|
||||
504: "Gateway timeout, please try again later.",
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认错误提示信息
|
||||
*/
|
||||
const DEFAULT_ERROR_MESSAGE =
|
||||
"Please try again if the network is abnormal. If it happens again, please contact us.";
|
||||
const DEFAULT_ERROR_MESSAGE = "网络异常,请重试。如果问题持续存在,请联系我们。";
|
||||
|
||||
/**
|
||||
* 根据错误码显示对应的提示信息
|
||||
* 特殊错误码的处理函数
|
||||
*/
|
||||
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 customMessage - 自定义错误信息(可选)
|
||||
*/
|
||||
export const errorHandle = debounce(
|
||||
(code: number, customMessage?: string): void => {
|
||||
const errorMessage =
|
||||
const errorMessage =
|
||||
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
|
||||
);
|
||||
|
||||
@ -2,6 +2,25 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosR
|
||||
import { message } from "antd";
|
||||
import { BASE_URL } from './constants'
|
||||
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 实例
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: BASE_URL, // 设置基础URL
|
||||
@ -29,23 +48,32 @@ request.interceptors.request.use(
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 直接返回响应数据
|
||||
if (response.data?.code !=0) {
|
||||
// TODO 暂时固定报错信息,后续根据后端返回的错误码进行处理
|
||||
errorHandle(0);
|
||||
// 检查业务状态码
|
||||
if (response.data?.code !== 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;
|
||||
},
|
||||
(error) => {
|
||||
if (error.response) {
|
||||
errorHandle(error.response.status);
|
||||
} else if (error.request) {
|
||||
// 请求已发出但没有收到响应
|
||||
errorHandle(0);
|
||||
} else {
|
||||
// 检修
|
||||
console.error(error);
|
||||
}
|
||||
handleRequestError(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
@ -86,8 +114,23 @@ export async function streamJsonPost<T = any>(
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
// 处理 HTTP 错误状态
|
||||
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) {
|
||||
@ -207,7 +250,8 @@ export const stream = async <T>({
|
||||
const response = await request(config);
|
||||
onComplete?.();
|
||||
return response;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
handleRequestError(error, '流式请求失败');
|
||||
onError?.(error);
|
||||
throw error;
|
||||
}
|
||||
@ -239,8 +283,9 @@ export const downloadStream = async (
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('文件下载失败:', error);
|
||||
handleRequestError(error, '文件下载失败');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@ -239,3 +239,7 @@ body {
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.2s ease-out forwards;
|
||||
}
|
||||
|
||||
.ant-notification-notice-wrapper {
|
||||
background: transparent !important;
|
||||
}
|
||||
@ -124,6 +124,8 @@ export function MessageRenderer({ msg }: MessageRendererProps) {
|
||||
);
|
||||
case "progress":
|
||||
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:
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ const EMPTY_MESSAGES: RealApiMessage[] = [
|
||||
role: 'assistant',
|
||||
content: JSON.stringify([{
|
||||
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(),
|
||||
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 {
|
||||
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 = {
|
||||
id: id ? id.toString() : Date.now().toString(),
|
||||
role: role,
|
||||
@ -205,30 +218,36 @@ function transformMessage(apiMessage: RealApiMessage): ChatMessage {
|
||||
status: status || 'success',
|
||||
};
|
||||
|
||||
if (role === 'assistant' || role === 'user') {
|
||||
try {
|
||||
const contentObj = JSON.parse(content);
|
||||
const contentArray = Array.isArray(contentObj) ? contentObj : [contentObj];
|
||||
contentArray.forEach((c: ApiMessageContent) => {
|
||||
if (c.type === "text") {
|
||||
message.blocks.push({ type: "text", text: c.content });
|
||||
} else if (c.type === "image") {
|
||||
message.blocks.push({ type: "image", url: c.content });
|
||||
} else if (c.type === "video") {
|
||||
message.blocks.push({ type: "video", url: c.content });
|
||||
} else if (c.type === "audio") {
|
||||
message.blocks.push({ type: "audio", url: c.content });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// 如果 JSON 解析失败,将整个 content 作为文本内容
|
||||
if (error_message && error_message === 'no enough credits') {
|
||||
message.blocks = NoEnoughCreditsMessageBlocks;
|
||||
} else {
|
||||
if (role === 'assistant' || role === 'user') {
|
||||
try {
|
||||
const contentObj = JSON.parse(content);
|
||||
const contentArray = Array.isArray(contentObj) ? contentObj : [contentObj];
|
||||
contentArray.forEach((c: ApiMessageContent) => {
|
||||
if (c.type === "text") {
|
||||
message.blocks.push({ type: "text", text: c.content });
|
||||
} else if (c.type === "image") {
|
||||
message.blocks.push({ type: "image", url: c.content });
|
||||
} else if (c.type === "video") {
|
||||
message.blocks.push({ type: "video", url: c.content });
|
||||
} else if (c.type === "audio") {
|
||||
message.blocks.push({ type: "audio", url: c.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 });
|
||||
}
|
||||
} 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,至少添加一个文本块
|
||||
|
||||
@ -7,7 +7,8 @@ export type MessageBlock =
|
||||
| { type: "image"; url: string; alt?: string }
|
||||
| { type: "video"; url: string; poster?: 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 {
|
||||
id: string;
|
||||
@ -127,6 +128,7 @@ export interface ShotVideoGeneration {
|
||||
export interface ApiMessageContent {
|
||||
type: ContentType;
|
||||
content: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface RealApiMessage {
|
||||
@ -138,4 +140,5 @@ export interface RealApiMessage {
|
||||
custom_data?: ProjectInit | ScriptSummary | CharacterGeneration | SketchGeneration | ShotSketchGeneration | ShotVideoGeneration;
|
||||
status: MessageStatus;
|
||||
intent_type: IntentType;
|
||||
error_message?: string;
|
||||
}
|
||||
@ -25,6 +25,7 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const currentUser = localStorage.getItem('currentUser');
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@ -102,9 +103,9 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
|
||||
</Button>
|
||||
|
||||
{/* Notifications */}
|
||||
<Button variant="ghost" size="sm">
|
||||
{/* <Button variant="ghost" size="sm" onClick={() => setOpenModal(true)}>
|
||||
<Bell className="h-4 w-4" />
|
||||
</Button>
|
||||
</Button> */}
|
||||
|
||||
{/* Theme Toggle */}
|
||||
{/* <Button
|
||||
|
||||
108
utils/notifications.tsx
Normal file
108
utils/notifications.tsx
Normal 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个通知
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user