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.",
|
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
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,至少添加一个文本块
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
@ -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
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