refactor(backend): 重构后端异常处理和模型管理

- 新增自定义异常类 BizException、NoteError 和 ProviderError
- 优化了模型管理相关的逻辑,包括加载、删除和测试连接等功能
- 改进了 Douyin 下载器的错误处理
- 调整了任务重试逻辑和笔记生成的异常处理- 更新了相关组件和页面以适应新的异常处理机制
This commit is contained in:
JefferyHcool
2025-06-06 21:30:23 +08:00
parent df5c0f771a
commit 8b1bc54f2d
34 changed files with 661 additions and 660 deletions

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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

View File

@@ -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)

View File

@@ -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: [] }),
}))
)
)

View File

@@ -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)
}

View File

@@ -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 => ({

View File

@@ -7,3 +7,8 @@ export interface IProvider {
baseUrl: string
enabled: number
}
export interface IResponse<T> {
code: number
data:T
msg: string
}

View File

@@ -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