From a928e0e38f9ec097e143705f2f4217a14ffb1de2 Mon Sep 17 00:00:00 2001 From: huangjianwu Date: Sat, 9 May 2026 14:30:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20=E6=A1=8C=E9=9D=A2=E7=AB=AF?= =?UTF-8?q?=E9=A6=96=E5=90=AF=204=20=E6=AD=A5=E5=BC=95=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 桌面端用户安装后空白进 web 主页面,提示填模型但不知道从哪填、转写引擎从哪选、 为什么 fast-whisper 在下东西。新增首启 onboarding wizard,把四件事拉成一条线: 1. 后端连通性自检(启动后调 /api/get_all_providers,OK 才能进下一步) 2. LLM 供应商 + 模型:填 OpenAI 兼容 base_url + key + model_name,调 /add_provider 创建并 addModel 默认 model,附带 testConnection 3. 转写引擎:四选一,**默认推荐 Groq**(在线、免下载本地模型); 选 fast-whisper 时显式提示"将下载模型" 4. Cookie 同步说明:桌面端无 chrome.cookies API,引导手动配;并指向插件版 实现: - 新页 src/pages/Onboarding/index.tsx,单文件 stateful wizard - App.tsx 加 /onboarding 路由 + OnboardingGuard 路由守卫: · 仅 Tauri 桌面端(__TAURI_INTERNALS__)拦截,纯 web 端透传,不打扰 · localStorage('bilinote-onboarded') 不为 '1' 时强制跳 /onboarding - 完成第 4 步 markOnboarded() 写 localStorage 后 navigate('/') 回归风险:纯 web 用户无感知;旧桌面端用户的 localStorage 没这个 key, 首次升级到含此 PR 的版本时会跳一次 onboarding(建议在升级 release notes 里 说明,避免老用户疑惑)。 Co-Authored-By: Claude Opus 4.7 (1M context) --- BillNote_frontend/src/App.tsx | 13 +- .../src/pages/Onboarding/index.tsx | 265 ++++++++++++++++++ 2 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 BillNote_frontend/src/pages/Onboarding/index.tsx diff --git a/BillNote_frontend/src/App.tsx b/BillNote_frontend/src/App.tsx index 94616d4..682a75a 100644 --- a/BillNote_frontend/src/App.tsx +++ b/BillNote_frontend/src/App.tsx @@ -11,7 +11,17 @@ import Index from '@/pages/Index.tsx' import { HomePage } from './pages/HomePage/Home.tsx' // 非首屏页面使用 React.lazy 按需加载 +const Onboarding = lazy(() => import('@/pages/Onboarding')) const SettingPage = lazy(() => import('./pages/SettingPage/index.tsx')) + +// 桌面端首启引导守卫:未完成 onboarding 时强制跳到 /onboarding +function OnboardingGuard({ children }: { children: React.ReactNode }) { + const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window + // 仅在 Tauri 桌面端拦截;纯 web 端不打扰用户 + if (!isTauri) return <>{children} + if (localStorage.getItem('bilinote-onboarded') !== '1') return + return <>{children} +} const Model = lazy(() => import('@/pages/SettingPage/Model.tsx')) const ProviderForm = lazy(() => import('@/components/Form/modelForm/Form.tsx')) const AboutPage = lazy(() => import('@/pages/SettingPage/about.tsx')) @@ -50,7 +60,8 @@ function App() { 加载中…}> - }> + } /> + }> } /> }> } /> diff --git a/BillNote_frontend/src/pages/Onboarding/index.tsx b/BillNote_frontend/src/pages/Onboarding/index.tsx new file mode 100644 index 0000000..d60a6c1 --- /dev/null +++ b/BillNote_frontend/src/pages/Onboarding/index.tsx @@ -0,0 +1,265 @@ +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { addProvider, addModel, getProviderList, testConnection } from '@/services/model' +import { getTranscriberConfig, updateTranscriberConfig } from '@/services/transcriber' +import logo from '@/assets/icon.svg' + +// 桌面端首启 4 步引导。完成后写 localStorage('bilinote-onboarded') = '1',路由守卫不再拦。 +// +// 1. 后端连通性自检 +// 2. LLM 供应商 + 模型(最简:只引导填一个 OpenAI-兼容供应商 + 一个 model 名) +// 3. 转写引擎选择(推荐 Groq 在线,避开本地模型下载坑) +// 4. (可选)Cookie 同步说明(仅当用户关注 B 站等需要登录态的平台时) + +const ONBOARD_KEY = 'bilinote-onboarded' + +export function isOnboarded(): boolean { + return localStorage.getItem(ONBOARD_KEY) === '1' +} + +function markOnboarded() { + localStorage.setItem(ONBOARD_KEY, '1') +} + +const Onboarding = () => { + const navigate = useNavigate() + const [step, setStep] = useState(1) + const [error, setError] = useState('') + + // step 1 + const [pinging, setPinging] = useState(false) + const [backendOk, setBackendOk] = useState(null) + + // step 2 + const [providerName, setProviderName] = useState('OpenAI') + const [apiKey, setApiKey] = useState('') + const [baseUrl, setBaseUrl] = useState('https://api.openai.com/v1') + const [modelName, setModelName] = useState('gpt-4o-mini') + const [providerId, setProviderId] = useState(null) + const [savingProvider, setSavingProvider] = useState(false) + + // step 3 + const [transcriberType, setTranscriberType] = useState('groq') + const [savingTranscriber, setSavingTranscriber] = useState(false) + + function next() { + setError('') + setStep(s => s + 1) + } + function prev() { + setError('') + setStep(s => Math.max(1, s - 1)) + } + + // step 1: ping 后端 + useEffect(() => { + if (step !== 1) return + let cancelled = false + ;(async () => { + setPinging(true) + try { + await getProviderList() + if (!cancelled) setBackendOk(true) + } + catch { + if (!cancelled) setBackendOk(false) + } + finally { + if (!cancelled) setPinging(false) + } + })() + return () => { cancelled = true } + }, [step]) + + async function saveProvider() { + setError('') + if (!apiKey.trim()) { setError('请填 API Key'); return } + if (!baseUrl.trim()) { setError('请填 API 地址'); return } + if (!providerName.trim()) { setError('请填供应商名'); return } + if (!modelName.trim()) { setError('请填模型名'); return } + setSavingProvider(true) + try { + // 复用桌面 web 端的 add_provider;type 必须是 'custom'(backend 强制) + const res: any = await addProvider({ + name: providerName.trim(), + api_key: apiKey.trim(), + base_url: baseUrl.trim(), + type: 'custom', + logo: 'custom', + }) + const newId = (res?.data ?? res) as string | undefined + if (!newId) throw new Error('后端未返回 provider id') + setProviderId(newId) + + // 加一个默认 model + await addModel({ provider_id: newId, model_name: modelName.trim() }) + + // 测试连通 + try { await testConnection({ id: newId }) } + catch (e: any) { + // 测试失败不阻断流程,让用户自己决定继续 + console.warn('测试连接失败:', e?.message ?? e) + } + next() + } + catch (e: any) { + setError(`保存失败:${e?.message ?? e}`) + } + finally { + setSavingProvider(false) + } + } + + async function saveTranscriber() { + setError('') + setSavingTranscriber(true) + try { + // fast-whisper / mlx-whisper 需指定 model size;在线 (groq/bcut/kuaishou) 不用 + const needsSize = transcriberType === 'fast-whisper' || transcriberType === 'mlx-whisper' + await updateTranscriberConfig({ + transcriber_type: transcriberType, + ...(needsSize ? { whisper_model_size: 'tiny' } : {}), + } as any) + next() + } + catch (e: any) { + setError(`保存失败:${e?.message ?? e}`) + } + finally { + setSavingTranscriber(false) + } + } + + function finish() { + markOnboarded() + navigate('/', { replace: true }) + } + + return ( +
+
+
+ logo +
+

欢迎使用 BiliNote

+

几步配置后就可以开始把视频转笔记。

+
+
+ + {/* Stepper */} +
+ {[1, 2, 3, 4].map(s => ( +
+
= s ? 'border-blue-600 bg-blue-600 text-white' : 'border-gray-300 bg-white text-gray-400'}`} + >{s}
+ {s < 4 &&
s ? 'bg-blue-600' : 'bg-gray-300'}`} />} +
+ ))} +
+ + {step === 1 && ( +
+

第 1 步 · 后端连通性

+

桌面端会自动启动 Python 后端进程。检查连通中…

+ {pinging &&
检测中…
} + {backendOk === true &&
✓ 后端已就绪
} + {backendOk === false && ( +
+ ✗ 暂时连不上后端。可能正在初始化(首次启动会下载依赖),等 1-2 分钟再试。 + 右下角的「后端」状态点会持续监控。 +
+ )} +
+ +
+
+ )} + + {step === 2 && ( +
+

第 2 步 · 模型供应商

+

填一个 OpenAI 兼容供应商:DeepSeek / Qwen / Claude / 自托管 / OpenAI 都行。

+ + + + + {error &&
{error}
} +
+ + +
+
+ )} + + {step === 3 && ( +
+

第 3 步 · 音频转写引擎

+

把视频音频转成文字。推荐在线引擎,避免本地下载 ~600MB 的模型。

+
+ {[ + { value: 'groq', title: 'Groq(在线,推荐)', desc: '注册 https://groq.com/ 拿免费 key;速度快、英文语料佳。无需本地模型。' }, + { value: 'bcut', title: '必剪(在线,免登)', desc: '免登,中文表现好;偶尔限流。' }, + { value: 'kuaishou', title: '快手(在线,免登)', desc: '与必剪类似,备选。' }, + { value: 'fast-whisper', title: 'Faster Whisper(本地)', desc: '完全离线但首次需下载 ~75MB(tiny)至 ~3GB(large-v3)的模型。CPU 慢。' }, + ].map(opt => ( + + ))} +
+ {error &&
{error}
} +
+ + +
+
+ )} + + {step === 4 && ( +
+

第 4 步 · Cookie 同步(可选)

+

+ 想总结 B 站 / 抖音 / 快手 等需要登录态的平台时,需要把浏览器 cookie 复制到「下载配置」页。 +
+ YouTube 一般不需要 cookie。先跳过也没问题,到时再去配。 +

+
+ 提示:插件版(BillNote_extension)支持一键 cookie 同步;桌面版需手动复制。 +
+
+ + +
+
+ )} +
+
+ ) +} + +export default Onboarding