mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-06-15 20:50:43 +08:00
- useCheckBackend 重写:60s 总超时取代 while(true) 死轮询,订阅 Tauri backend-ready/terminated/startup-timeout 事件,裸 fetch 探测避免 启动期 toast 叠堆 - Tauri lib.rs:spawn 后 HTTP 探针轮询 /api/sys_check 拿 200 才算就绪 (之前 TCP connect 会被孤儿进程误判);RunEvent::Exit 钩子退出前 kill sidecar,修孤儿进程占端口;restart 前发 backend-restarting 让前端忽略主动 kill 引发的 terminated - BackendInitDialog:失败态展示原因 + 最近 stderr + 重启/复制日志按钮 - StartupBanner:收到 restarted/ready 自动清「已退出」横幅 - BackendHealthIndicator:修 /api/api/sys_health 双前缀 404 - Onboarding:step1 后端连通改自动重试 + 事件触发 + 手动按钮;step2 撞预置供应商名时改为更新已存在供应商;errText 统一错误文案 - 全局代理 UI:下载配置页新增代理卡片(services/proxy.ts + ProxyConfig) - request.ts 加 suppressToast 配置位,预期失败不弹全局红 toast - NoteForm/taskStore:捕获就绪门禁错误,引导去音频转写配置页下载 - providerCard:整行可点切换(之前只有 icon 区域响应) - Monitor 页 Whisper 卡显示模型本地下载状态 - tauri/api 升级对齐 2.11,修 vite build 版本不匹配 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
245 lines
6.8 KiB
TypeScript
245 lines
6.8 KiB
TypeScript
import { create } from 'zustand'
|
||
import { persist, createJSONStorage } from 'zustand/middleware'
|
||
import { delete_task, generateNote } from '@/services/note.ts'
|
||
import { v4 as uuidv4 } from 'uuid'
|
||
import toast from 'react-hot-toast'
|
||
import { get, set, del } from 'idb-keyval'
|
||
|
||
|
||
export type TaskStatus = 'PENDING' | 'RUNNING' | 'SUCCESS' | 'FAILD'
|
||
|
||
export interface AudioMeta {
|
||
cover_url: string
|
||
duration: number
|
||
file_path: string
|
||
platform: string
|
||
raw_info: any
|
||
title: string
|
||
video_id: string
|
||
}
|
||
|
||
export interface Segment {
|
||
start: number
|
||
end: number
|
||
text: string
|
||
}
|
||
|
||
export interface Transcript {
|
||
full_text: string
|
||
language: string
|
||
raw: any
|
||
segments: Segment[]
|
||
}
|
||
export interface Markdown {
|
||
ver_id: string
|
||
content: string
|
||
style: string
|
||
model_name: string
|
||
created_at: string
|
||
}
|
||
|
||
export interface Task {
|
||
id: string
|
||
markdown: string|Markdown [] //为了兼容之前的笔记
|
||
transcript: Transcript
|
||
status: TaskStatus
|
||
audioMeta: AudioMeta
|
||
createdAt: string
|
||
formData: {
|
||
video_url: string
|
||
link: undefined | boolean
|
||
screenshot: undefined | boolean
|
||
platform: string
|
||
quality: string
|
||
model_name: string
|
||
provider_id: string
|
||
}
|
||
}
|
||
|
||
interface TaskStore {
|
||
tasks: Task[]
|
||
currentTaskId: string | null
|
||
addPendingTask: (taskId: string, platform: string) => void
|
||
updateTaskContent: (id: string, data: Partial<Omit<Task, 'id' | 'createdAt'>>) => void
|
||
removeTask: (id: string) => void
|
||
clearTasks: () => void
|
||
setCurrentTask: (taskId: string | null) => void
|
||
getCurrentTask: () => Task | null
|
||
retryTask: (id: string) => void
|
||
}
|
||
|
||
export const useTaskStore = create<TaskStore>()(
|
||
persist(
|
||
(set, get) => ({
|
||
tasks: [],
|
||
currentTaskId: null,
|
||
|
||
addPendingTask: (taskId: string, platform: string, formData: any) =>
|
||
|
||
set(state => ({
|
||
tasks: [
|
||
{
|
||
formData: formData,
|
||
id: taskId,
|
||
status: 'PENDING',
|
||
markdown: '',
|
||
platform: platform,
|
||
transcript: {
|
||
full_text: '',
|
||
language: '',
|
||
raw: null,
|
||
segments: [],
|
||
},
|
||
createdAt: new Date().toISOString(),
|
||
audioMeta: {
|
||
cover_url: '',
|
||
duration: 0,
|
||
file_path: '',
|
||
platform: '',
|
||
raw_info: null,
|
||
title: '',
|
||
video_id: '',
|
||
},
|
||
},
|
||
...state.tasks,
|
||
],
|
||
currentTaskId: taskId, // 默认设置为当前任务
|
||
})),
|
||
|
||
updateTaskContent: (id, data) =>
|
||
set(state => ({
|
||
tasks: state.tasks.map(task => {
|
||
if (task.id !== id) return task
|
||
|
||
if (task.status === 'SUCCESS' && data.status === 'SUCCESS') return task
|
||
|
||
// 如果是 markdown 字符串,封装为版本
|
||
if (typeof data.markdown === 'string') {
|
||
const prev = task.markdown
|
||
const newVersion: Markdown = {
|
||
ver_id: `${task.id}-${uuidv4()}`,
|
||
content: data.markdown,
|
||
style: task.formData.style || '',
|
||
model_name: task.formData.model_name || '',
|
||
created_at: new Date().toISOString(),
|
||
}
|
||
|
||
let updatedMarkdown: Markdown[]
|
||
if (Array.isArray(prev)) {
|
||
updatedMarkdown = [newVersion, ...prev]
|
||
} else {
|
||
updatedMarkdown = [
|
||
newVersion,
|
||
...(typeof prev === 'string' && prev
|
||
? [{
|
||
ver_id: `${task.id}-${uuidv4()}`,
|
||
content: prev,
|
||
style: task.formData.style || '',
|
||
model_name: task.formData.model_name || '',
|
||
created_at: new Date().toISOString(),
|
||
}]
|
||
: []),
|
||
]
|
||
}
|
||
|
||
return {
|
||
...task,
|
||
...data,
|
||
markdown: updatedMarkdown,
|
||
}
|
||
}
|
||
|
||
return { ...task, ...data }
|
||
}),
|
||
})),
|
||
|
||
|
||
getCurrentTask: () => {
|
||
const currentTaskId = get().currentTaskId
|
||
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
|
||
try {
|
||
await generateNote({
|
||
...newFormData,
|
||
task_id: id,
|
||
})
|
||
} catch (e: any) {
|
||
// 就绪门禁:转写模型未下载好。不要把任务标成 PENDING(会一直转),
|
||
// 给提示让用户先去下载。
|
||
if (e?.data?.reason === 'transcriber_model_not_ready') {
|
||
toast.error(
|
||
e?.data?.downloading
|
||
? '转写模型正在下载中,请稍候再重试'
|
||
: '转写模型尚未下载,请先去「设置 → 音频转写配置」页下载',
|
||
)
|
||
return
|
||
}
|
||
console.error('重试任务失败:', e)
|
||
return
|
||
}
|
||
|
||
set(state => ({
|
||
tasks: state.tasks.map(t =>
|
||
t.id === id
|
||
? {
|
||
...t,
|
||
formData: newFormData, // ✅ 显式更新 formData
|
||
status: 'PENDING',
|
||
}
|
||
: t
|
||
),
|
||
}))
|
||
},
|
||
|
||
|
||
removeTask: async id => {
|
||
const task = get().tasks.find(t => t.id === id)
|
||
|
||
// 更新 Zustand 状态
|
||
set(state => ({
|
||
tasks: state.tasks.filter(task => task.id !== id),
|
||
currentTaskId: state.currentTaskId === id ? null : state.currentTaskId,
|
||
}))
|
||
|
||
// 调用后端删除接口(如果找到了任务)
|
||
if (task) {
|
||
await delete_task({
|
||
video_id: task.audioMeta.video_id,
|
||
platform: task.platform,
|
||
})
|
||
}
|
||
},
|
||
|
||
clearTasks: () => set({ tasks: [], currentTaskId: null }),
|
||
|
||
setCurrentTask: taskId => set({ currentTaskId: taskId }),
|
||
}),
|
||
{
|
||
name: 'task-storage',
|
||
storage: createJSONStorage(() => ({
|
||
getItem: async (name: string): Promise<string | null> => {
|
||
const value = await get(name)
|
||
return value ?? null
|
||
},
|
||
setItem: async (name: string, value: string): Promise<void> => {
|
||
await set(name, value)
|
||
},
|
||
removeItem: async (name: string): Promise<void> => {
|
||
await del(name)
|
||
},
|
||
})),
|
||
}
|
||
)
|
||
)
|