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>
This commit is contained in:
huangjianwu
2026-05-14 19:01:37 +08:00
parent 41f17592c2
commit 37f7ee6e15
22 changed files with 1273 additions and 651 deletions

View File

@@ -0,0 +1,89 @@
import { useEffect, useState } from 'react'
import toast from 'react-hot-toast'
import { Switch } from '@/components/ui/switch'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { getProxyConfig, updateProxyConfig } from '@/services/proxy'
// 全局代理配置:作用于 LLM API + 转写 APIGroq 等)+ yt-dlp 视频下载。
// 国内访问 OpenAI / Groq / YouTube 基本都要靠它。
const ProxyConfig = () => {
const [enabled, setEnabled] = useState(false)
const [url, setUrl] = useState('')
const [effective, setEffective] = useState('')
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
useEffect(() => {
;(async () => {
try {
const cfg = await getProxyConfig()
setEnabled(cfg.enabled)
setUrl(cfg.url)
setEffective(cfg.effective)
} catch {
/* 拦截器已 toast */
} finally {
setLoading(false)
}
})()
}, [])
const handleSave = async () => {
if (enabled && !url.trim()) {
toast.error('请填写代理地址,或关闭代理开关')
return
}
setSaving(true)
try {
const cfg = await updateProxyConfig({ enabled, url: url.trim() })
setEnabled(cfg.enabled)
setUrl(cfg.url)
setEffective(cfg.effective)
toast.success('代理配置已保存')
} catch {
/* 拦截器已 toast */
} finally {
setSaving(false)
}
}
if (loading) {
return <div className="text-xs text-gray-400"></div>
}
// env 兜底:配置没开但 effective 有值,说明来自 HTTP_PROXY 环境变量
const fromEnv = !enabled && !!effective
return (
<div className="flex flex-col gap-2 rounded border border-neutral-200 p-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium"></span>
<Switch checked={enabled} onCheckedChange={setEnabled} />
</div>
<p className="text-xs text-gray-400">
AI Groq YouTube
</p>
<Input
placeholder="http://127.0.0.1:7890"
value={url}
disabled={!enabled}
onChange={e => setUrl(e.target.value)}
className="text-sm"
/>
{fromEnv && (
<p className="text-xs text-amber-600">
{effective}
</p>
)}
{enabled && effective && (
<p className="text-xs text-green-600">{effective}</p>
)}
<Button size="sm" onClick={handleSave} disabled={saving}>
{saving ? '保存中…' : '保存代理配置'}
</Button>
</div>
)
}
export default ProxyConfig