mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-05-10 17:43:40 +08:00
refactor(backend): 重构后端异常处理和模型管理
- 新增自定义异常类 BizException、NoteError 和 ProviderError - 优化了模型管理相关的逻辑,包括加载、删除和测试连接等功能 - 改进了 Douyin 下载器的错误处理 - 调整了任务重试逻辑和笔记生成的异常处理- 更新了相关组件和页面以适应新的异常处理机制
This commit is contained in:
@@ -36,7 +36,7 @@ const DownloaderForm = () => {
|
||||
setLoading(true) // 🔁 切换平台时显示 loading
|
||||
try {
|
||||
const res = await getDownloaderCookie(id)
|
||||
const cookie = res?.data?.data?.cookie || ''
|
||||
const cookie = res?.cookie || ''
|
||||
form.reset({ cookie }) // ✅ 正确重置表单值
|
||||
} catch (e) {
|
||||
toast.error('加载 Cookie 失败: ' + e)
|
||||
|
||||
@@ -129,11 +129,10 @@ const ProviderForm = ({ isCreate = false }: { isCreate?: boolean }) => {
|
||||
|
||||
try {
|
||||
const res = await deleteModelById(modelId)
|
||||
if (res.data.code === 0) {
|
||||
toast.success('删除成功')
|
||||
} else {
|
||||
toast.error(res.data.msg || '删除失败')
|
||||
}
|
||||
console.log('🔧 删除结果:', res)
|
||||
|
||||
toast.success('删除成功')
|
||||
|
||||
} catch (e) {
|
||||
toast.error('删除异常')
|
||||
}
|
||||
@@ -151,16 +150,16 @@ const ProviderForm = ({ isCreate = false }: { isCreate?: boolean }) => {
|
||||
return
|
||||
}
|
||||
setTesting(true)
|
||||
const data = await testConnection({
|
||||
id
|
||||
})
|
||||
if (data.data.code === 0) {
|
||||
await testConnection({
|
||||
id
|
||||
})
|
||||
|
||||
toast.success('测试连通性成功 🎉')
|
||||
} else {
|
||||
toast.error(`连接失败: ${data.data.msg || '未知错误'}`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
toast.error('测试连通性异常')
|
||||
|
||||
toast.error(`连接失败: ${data.data.msg || '未知错误'}`)
|
||||
// toast.error('测试连通性异常')
|
||||
} finally {
|
||||
setTesting(false)
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ export const useTaskPolling = (interval = 3000) => {
|
||||
try {
|
||||
console.log('🔄 正在轮询任务:', task.id)
|
||||
const res = await get_task_status(task.id)
|
||||
const { status } = res.data
|
||||
const { status } = res
|
||||
|
||||
if (status && status !== task.status) {
|
||||
if (status === 'SUCCESS') {
|
||||
const { markdown, transcript, audio_meta } = res.data.result
|
||||
const { markdown, transcript, audio_meta } = res.result
|
||||
toast.success('笔记生成成功')
|
||||
updateTaskContent(task.id, {
|
||||
status,
|
||||
@@ -47,7 +47,7 @@ export const useTaskPolling = (interval = 3000) => {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('❌ 任务轮询失败:', e)
|
||||
toast.error(`生成失败 ${e.message || e}`)
|
||||
// toast.error(`生成失败 ${e.message || e}`)
|
||||
updateTaskContent(task.id, { status: 'FAILED' })
|
||||
// removeTask(task.id)
|
||||
}
|
||||
|
||||
@@ -173,6 +173,7 @@ const MarkdownViewer: FC<MarkdownViewerProps> = ({ status }) => {
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-red-500">笔记生成失败</p>
|
||||
<p className="mt-2 mb-2 text-xs text-red-400">请检查后台或稍后再试</p>
|
||||
|
||||
<Button onClick={() => retryTask(currentTask.id)} size="lg">
|
||||
重试
|
||||
</Button>
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
import { Input } from '@/components/ui/input.tsx'
|
||||
import { Textarea } from '@/components/ui/textarea.tsx'
|
||||
import { noteStyles, noteFormats, videoPlatforms } from '@/constant/note.ts'
|
||||
import { fetchModels } from '@/services/model.ts'
|
||||
|
||||
/* -------------------- 校验 Schema -------------------- */
|
||||
const formSchema = z
|
||||
@@ -206,7 +207,7 @@ const NoteForm = () => {
|
||||
}
|
||||
|
||||
message.success('已提交任务')
|
||||
const { data } = await generateNote(payload)
|
||||
const data = await generateNote(payload)
|
||||
addPendingTask(data.task_id, values.platform, payload)
|
||||
}
|
||||
const onInvalid = (errors: FieldErrors<NoteFormValues>) => {
|
||||
@@ -355,6 +356,9 @@ const NoteForm = () => {
|
||||
<FormItem>
|
||||
<SectionHeader title="模型选择" tip="不同模型效果不同,建议自行测试" />
|
||||
<Select
|
||||
onOpenChange={()=>{
|
||||
loadEnabledModels()
|
||||
}}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function AboutPage() {
|
||||
height={50}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
<h1 className="text-4xl font-bold">BiliNote v1.7.3</h1>
|
||||
<h1 className="text-4xl font-bold">BiliNote v1.7.4</h1>
|
||||
</div>
|
||||
<p className="text-muted-foreground mb-6 text-xl italic">
|
||||
AI 视频笔记生成工具 让 AI 为你的视频做笔记
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import request from '@/utils/request'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useTaskStore } from '@/store/taskStore'
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const generateNote = async (data: {
|
||||
video_url: string
|
||||
platform: string
|
||||
@@ -14,12 +13,13 @@ export const generateNote = async (data: {
|
||||
extras?: string
|
||||
video_understand?: boolean
|
||||
video_interval?: number
|
||||
grid_size:Array<number>
|
||||
grid_size: Array<number>
|
||||
}) => {
|
||||
try {
|
||||
console.log('generateNote', data)
|
||||
const response = await request.post('/generate_note', data)
|
||||
|
||||
if (response.data.code != 0) {
|
||||
if (!response) {
|
||||
if (response.data.msg) {
|
||||
toast.error(response.data.msg)
|
||||
}
|
||||
@@ -30,12 +30,12 @@ export const generateNote = async (data: {
|
||||
console.log('res', response)
|
||||
// 成功提示
|
||||
|
||||
return response.data
|
||||
return response
|
||||
} catch (e: any) {
|
||||
console.error('❌ 请求出错', e)
|
||||
|
||||
// 错误提示
|
||||
toast.error('笔记生成失败,请稍后重试')
|
||||
// toast.error('笔记生成失败,请稍后重试')
|
||||
|
||||
throw e // 抛出错误以便调用方处理
|
||||
}
|
||||
@@ -65,15 +65,9 @@ export const delete_task = async ({ video_id, platform }) => {
|
||||
|
||||
export const get_task_status = async (task_id: string) => {
|
||||
try {
|
||||
const response = await request.get('/task_status/' + task_id)
|
||||
|
||||
if (response.data.code == 0 && response.data.status == 'SUCCESS') {
|
||||
// toast.success("笔记生成成功")
|
||||
}
|
||||
console.log('res', response)
|
||||
// 成功提示
|
||||
|
||||
return response.data
|
||||
return await request.get('/task_status/' + task_id)
|
||||
} catch (e) {
|
||||
console.error('❌ 请求出错', e)
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import { fetchModels, addModel, fetchEnableModels, fetchEnableModelById, deleteModelById } from '@/services/model.ts'
|
||||
import {
|
||||
fetchModels,
|
||||
addModel,
|
||||
fetchEnableModels,
|
||||
fetchEnableModelById,
|
||||
deleteModelById
|
||||
} from '@/services/model'
|
||||
|
||||
interface IModel {
|
||||
id: string
|
||||
@@ -11,81 +17,93 @@ interface IModel {
|
||||
root: string
|
||||
}
|
||||
|
||||
interface IModelListItem {
|
||||
id: string
|
||||
provider_id: string
|
||||
model_name: string
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
interface ModelStore {
|
||||
models: IModel[]
|
||||
modelList: []
|
||||
modelList: IModelListItem[]
|
||||
loading: boolean
|
||||
selectedModel: string
|
||||
|
||||
loadModels: (providerId: string) => Promise<void>
|
||||
loadModelsById: (providerId: string) => Promise<IModelListItem[]>
|
||||
loadEnabledModels: () => Promise<void>
|
||||
loadModelsById : (providerId: string) => Promise<void>
|
||||
addNewModel: (providerId: string, modelId: string) => Promise<void>
|
||||
setSelectedModel: (modelId: string) => void
|
||||
deleteModel: (modelId: number) => Promise<void>
|
||||
setSelectedModel: (modelId: string) => void
|
||||
clearModels: () => void
|
||||
}
|
||||
|
||||
export const useModelStore = create<ModelStore>()(
|
||||
devtools(set => ({
|
||||
devtools((set) => ({
|
||||
models: [],
|
||||
modelList: [],
|
||||
loading: false,
|
||||
selectedModel: '',
|
||||
modelList: [],
|
||||
|
||||
// 获取所有可用模型 (全局可用模型列表)
|
||||
loadEnabledModels: async () => {
|
||||
try {
|
||||
set({ loading: true })
|
||||
const res = await fetchEnableModels()
|
||||
if (res.data.code === 0 && res.data.data.length > 0) {
|
||||
set({ modelList: res.data.data })
|
||||
} else {
|
||||
set({ modelList: [] })
|
||||
console.error('模型列表加载失败')
|
||||
}
|
||||
const list = await fetchEnableModels()
|
||||
set({ modelList: list })
|
||||
} catch (error) {
|
||||
set({ modelList: [] })
|
||||
console.error('加载模型出错', error)
|
||||
}
|
||||
},
|
||||
|
||||
deleteModel: async (modelId: number) => {
|
||||
await deleteModelById( modelId)
|
||||
},
|
||||
// 加载模型列表
|
||||
loadModels: async (providerId: string) => {
|
||||
try {
|
||||
set({ loading: true })
|
||||
const res = await fetchModels(providerId)
|
||||
if (res.data.code === 0 && res.data.data.models.data.length > 0) {
|
||||
set({ models: res.data.data.models.data })
|
||||
} else if (res.data.code === 0 && res.data.data.models.length > 0) {
|
||||
set({ models: res.data.data.models.data })
|
||||
} else {
|
||||
set({ models: [] })
|
||||
console.error('模型列表加载失败')
|
||||
}
|
||||
} catch (error) {
|
||||
set({ models: [] })
|
||||
console.error('加载模型出错', error)
|
||||
console.error('加载可用模型失败', error)
|
||||
} finally {
|
||||
set({ loading: false })
|
||||
}
|
||||
},
|
||||
loadModelsById: async (providerId: string)=>{
|
||||
const models = await fetchEnableModelById(providerId)
|
||||
if (models.data.code === 0) {
|
||||
console.log('模型列表加载成功:', models.data)
|
||||
return models.data.data
|
||||
|
||||
// 通过 provider 获取该供应商的模型列表
|
||||
loadModels: async (providerId: string) => {
|
||||
try {
|
||||
set({ loading: true })
|
||||
const res = await fetchModels(providerId)
|
||||
|
||||
let models: IModel[] = []
|
||||
|
||||
// 兼容 SyncPage 分页对象与普通数组两种格式
|
||||
if (Array.isArray(res.models)) {
|
||||
models = res.models
|
||||
} else if (res.models?.data && Array.isArray(res.models.data)) {
|
||||
models = res.models.data
|
||||
}
|
||||
|
||||
set({ models })
|
||||
} catch (error) {
|
||||
set({ models: [] })
|
||||
console.error('加载模型列表失败', error)
|
||||
} finally {
|
||||
set({ loading: false })
|
||||
}
|
||||
},
|
||||
// 新增模型
|
||||
},
|
||||
|
||||
// 单独获取某个供应商下已启用模型
|
||||
loadModelsById: async (providerId: string) => {
|
||||
try {
|
||||
const models = await fetchEnableModelById(providerId)
|
||||
console.log('获取供应商模型成功:', models)
|
||||
return models
|
||||
} catch (error) {
|
||||
console.error('加载供应商模型失败', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
// 新增模型逻辑
|
||||
addNewModel: async (providerId: string, modelId: string) => {
|
||||
try {
|
||||
const res = await addModel({ provider_id: providerId, model_name: modelId })
|
||||
|
||||
if (res.code === 0) {
|
||||
console.log('新增模型成功:', modelId)
|
||||
// ✅ 新增成功以后,前端直接追加一条到 models 列表
|
||||
set(state => ({
|
||||
set((state) => ({
|
||||
models: [
|
||||
...state.models,
|
||||
{
|
||||
@@ -99,17 +117,30 @@ export const useModelStore = create<ModelStore>()(
|
||||
],
|
||||
}))
|
||||
} else {
|
||||
console.error('新增模型失败')
|
||||
console.error('新增模型失败', res.msg)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加模型出错', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 设置选中的模型
|
||||
setSelectedModel: modelId => set({ selectedModel: modelId }),
|
||||
// 删除模型
|
||||
deleteModel: async (modelId: number) => {
|
||||
try {
|
||||
await deleteModelById(modelId)
|
||||
// 删除后更新本地状态(可选)
|
||||
set((state) => ({
|
||||
models: state.models.filter((model) => model.id !== modelId.toString())
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('删除模型失败', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 清空
|
||||
clearModels: () => set({ models: [], selectedModel: '' }),
|
||||
// 切换选中模型
|
||||
setSelectedModel: (modelId: string) => set({ selectedModel: modelId }),
|
||||
|
||||
// 清空
|
||||
clearModels: () => set({ models: [], selectedModel: '', modelList: [] }),
|
||||
}))
|
||||
)
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from 'zustand'
|
||||
import { IProvider } from '@/types'
|
||||
import { IProvider, IResponse } from '@/types'
|
||||
import {
|
||||
addProvider,
|
||||
getProviderById,
|
||||
@@ -38,10 +38,9 @@ export const useProviderStore = create<ProviderStore>((set, get) => ({
|
||||
// 设置整个 provider 列表
|
||||
setAllProviders: providers => set({ provider: providers }),
|
||||
loadProviderById: async (id: string) => {
|
||||
const res = await getProviderById(id)
|
||||
if (res.data.code === 0) {
|
||||
const item = res.data.data
|
||||
console.log('Provider ', item)
|
||||
const res:IResponse<IProvider> = await getProviderById(id)
|
||||
|
||||
const item = res
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
@@ -51,9 +50,7 @@ export const useProviderStore = create<ProviderStore>((set, get) => ({
|
||||
type: item.type,
|
||||
enabled: item.enabled,
|
||||
}
|
||||
} else {
|
||||
console.log('Provider not found')
|
||||
}
|
||||
|
||||
},
|
||||
addNewProvider: async (provider: IProvider) => {
|
||||
const payload = {
|
||||
@@ -96,16 +93,18 @@ export const useProviderStore = create<ProviderStore>((set, get) => ({
|
||||
getProviderList: () => get().provider,
|
||||
fetchProviderList: async () => {
|
||||
try {
|
||||
const res = await getProviderList()
|
||||
if (res.data.code === 0) {
|
||||
const res = await getProviderList()
|
||||
|
||||
set({
|
||||
provider: res.data.data.map(
|
||||
provider: res.map(
|
||||
(item: {
|
||||
id: string
|
||||
name: string
|
||||
logo: string
|
||||
api_key: string
|
||||
base_url: string
|
||||
type: string
|
||||
enabled: number
|
||||
}) => {
|
||||
return {
|
||||
id: item.id,
|
||||
@@ -119,7 +118,6 @@ export const useProviderStore = create<ProviderStore>((set, get) => ({
|
||||
}
|
||||
),
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching provider list:', error)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { delete_task, generateNote } from '@/services/note.ts'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
|
||||
export type TaskStatus = 'PENDING' | 'RUNNING' | 'SUCCESS' | 'FAILD'
|
||||
@@ -157,14 +158,19 @@ export const useTaskStore = create<TaskStore>()(
|
||||
return get().tasks.find(task => task.id === currentTaskId) || null
|
||||
},
|
||||
retryTask: async (id: string, payload?: any) => {
|
||||
|
||||
if (!id){
|
||||
toast.error('任务不存在')
|
||||
return
|
||||
}
|
||||
const task = get().tasks.find(task => task.id === id)
|
||||
console.log('retry',task)
|
||||
if (!task) return
|
||||
|
||||
const newFormData = payload || task.formData
|
||||
|
||||
await generateNote({
|
||||
task_id: id,
|
||||
...newFormData,
|
||||
task_id: id,
|
||||
})
|
||||
|
||||
set(state => ({
|
||||
|
||||
5
BillNote_frontend/src/types/index.d.ts
vendored
5
BillNote_frontend/src/types/index.d.ts
vendored
@@ -7,3 +7,8 @@ export interface IProvider {
|
||||
baseUrl: string
|
||||
enabled: number
|
||||
}
|
||||
export interface IResponse<T> {
|
||||
code: number
|
||||
data:T
|
||||
msg: string
|
||||
}
|
||||
@@ -1,27 +1,58 @@
|
||||
import axios from 'axios'
|
||||
const request = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000,
|
||||
})
|
||||
function handleErrorResponse(response: any) {
|
||||
if (!response) return '请求失败,请检查网络连接'
|
||||
if (typeof response.code !== 'number') return '系统异常'
|
||||
import axios, { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
// 错误码判断
|
||||
switch (response.code) {
|
||||
case 1001:
|
||||
return response.msg || '下载失败,请检查视频链接'
|
||||
case 1002:
|
||||
return response.msg || '转写失败,请稍后重试'
|
||||
case 1003:
|
||||
return response.msg || '总结失败,可能是模型服务异常'
|
||||
case 2001:
|
||||
case 2002:
|
||||
return Array.isArray(response.data)
|
||||
? response.data.map(e => `${e.field}: ${e.error}`).join('\n')
|
||||
: response.msg || '参数错误'
|
||||
default:
|
||||
return response.msg || '系统异常'
|
||||
}
|
||||
// 统一响应类型
|
||||
export interface IResponse<T = any> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
// 模拟一个消息提示函数 (实际项目中会使用UI库的组件,如 Ant Design 的 message 或 Element UI 的 ElMessage)
|
||||
// This function simulates a message display (in real projects, you'd use a UI library's component)
|
||||
|
||||
|
||||
// 创建实例
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: '/api', // 请确保你的开发服务器代理设置正确
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse<IResponse>) => {
|
||||
const res = response.data;
|
||||
if (res.code === 0) {
|
||||
// 业务成功,可以根据需要显示成功消息,或者不显示(如果操作本身就是可见的)
|
||||
// showMessage('success', res.msg || '操作成功'); // 如果需要显示成功消息
|
||||
return res.data; // 返回data部分,简化后续业务代码
|
||||
} else {
|
||||
// 业务错误,统一显示后端返回的错误消息
|
||||
// Business error, uniformly display the error message returned from the backend
|
||||
toast.error(res.msg || '操作失败,请稍后再试');
|
||||
return Promise.reject(res); // 拒绝Promise,让业务代码可以捕获并处理
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// 网络/服务器错误
|
||||
const res = error?.response?.data as IResponse | undefined;
|
||||
if (res) {
|
||||
// 如果后端有返回错误信息,则显示后端信息
|
||||
// If the backend returns an error message, display it
|
||||
|
||||
toast.error(res.msg || '服务器错误,请稍后再试');
|
||||
return Promise.reject(res);
|
||||
} else {
|
||||
// 没有响应数据(如网络中断),显示通用网络错误
|
||||
// No response data (e.g., network disconnected), display generic network error
|
||||
toast.error( '请求失败,请检查网络连接或稍后再试')
|
||||
return Promise.reject({
|
||||
code: -1,
|
||||
msg: '请求失败,请检查网络连接',
|
||||
data: null
|
||||
} as IResponse);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default request
|
||||
|
||||
Reference in New Issue
Block a user