Files
BiliNote/BillNote_frontend/src/store/taskStore/index.ts
huangjianwu 37f7ee6e15 feat(desktop): 后端健康监控韧性 + onboarding 修复 + 全局代理 UI
- 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>
2026-05-14 19:01:37 +08:00

245 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
},
})),
}
)
)