修复设置保存卡顿并优化诊断页 AI 校对状态显示(0.1.2)
- 修复保存润色模式/切换麦克风等设置后卡顿 2-4 秒:get_client_runtime_info 改用原生接口(GetUserDefaultLocaleName / GlobalMemoryStatusEx)替代 PowerShell,并把机器信息采集移出设置刷新热路径、启动时取一次复用 - 诊断页「AI 校对」未开启时不再显示绿色对勾,改为中性「未开启」状态并温和提示 - 通用设置无标题卡片补充「偏好设置」标题 - 更新 CHANGELOG 与关于页发布亮点,标注 0.1.2 发布日期
This commit is contained in:
1 parent
fc1eb4b0e7
commit
d9ae553d3c
7 files changed
+68
-28
No files matched your search
+3
-1
@@ -9,14 +9,16 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [0.1.2] - 未发布
|
## [0.1.2] - 2026-07-24
|
||||||
|
|
||||||
### 优化
|
### 优化
|
||||||
- 优化「备份与迁移」:导出不再弹出保存位置对话框,「配置」与「全部数据」都直接保存到默认下载目录,导出完成后就地显示文件路径与「打开所在文件夹」按钮,和其他下载体验保持一致。同时优化了页面文案,更清楚地说明两种备份分别包含什么、适合什么场景(「全部数据」在「配置」基础上额外包含历史记录与录音)。
|
- 优化「备份与迁移」:导出不再弹出保存位置对话框,「配置」与「全部数据」都直接保存到默认下载目录,导出完成后就地显示文件路径与「打开所在文件夹」按钮,和其他下载体验保持一致。同时优化了页面文案,更清楚地说明两种备份分别包含什么、适合什么场景(「全部数据」在「配置」基础上额外包含历史记录与录音)。
|
||||||
|
- 优化诊断页面「AI 校对」的状态显示:未开启时不再显示为绿色对勾(易被误解为已启用),改为中性的「未开启」状态,并温和提示依赖 AI 的功能当前不会生效、以及在哪里开启。
|
||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
- 修复长时间后台运行后偶发「一直显示处理中、最后超时且无结果」的问题:SayIt 主窗口常驻后台隐藏时,系统会节流其心跳定时器,导致与服务器的连接被判为空闲而每隔约 5 分钟被回收;若录音恰好在连接被回收的瞬间开始,本次识别就会丢失并超时。现已让后台定时器不再被节流,保持心跳正常、连接稳定。
|
- 修复长时间后台运行后偶发「一直显示处理中、最后超时且无结果」的问题:SayIt 主窗口常驻后台隐藏时,系统会节流其心跳定时器,导致与服务器的连接被判为空闲而每隔约 5 分钟被回收;若录音恰好在连接被回收的瞬间开始,本次识别就会丢失并超时。现已让后台定时器不再被节流,保持心跳正常、连接稳定。
|
||||||
- 识别超时时不再丢失录音:即使这次识别超时没有结果,录音也会保存到历史记录中,可随时点「重新识别」重试。
|
- 识别超时时不再丢失录音:即使这次识别超时没有结果,录音也会保存到历史记录中,可随时点「重新识别」重试。
|
||||||
|
- 修复保存润色模式(以及切换麦克风、静音、剪贴板保护等设置)后软件卡顿 2-4 秒的问题:此前每次保存设置都会重新采集一次机器信息(系统区域、物理内存等),采集时会临时启动 PowerShell,其冷启动耗时叠加导致卡顿。现改为启动时采集一次并复用,且换用系统原生接口获取,保存设置不再卡顿。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -44,12 +44,14 @@ sherpa-onnx = { version = "1.13.3", default-features = false, features = ["stati
|
|||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "0.58", features = [
|
windows = { version = "0.58", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
|
"Win32_Globalization",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
"Win32_UI_Input_KeyboardAndMouse",
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
"Win32_UI_Accessibility",
|
"Win32_UI_Accessibility",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
"Win32_System_DataExchange",
|
"Win32_System_DataExchange",
|
||||||
"Win32_System_Memory",
|
"Win32_System_Memory",
|
||||||
|
"Win32_System_SystemInformation",
|
||||||
"Win32_System_Com",
|
"Win32_System_Com",
|
||||||
"Win32_Graphics_Gdi",
|
"Win32_Graphics_Gdi",
|
||||||
"Win32_Media_Audio",
|
"Win32_Media_Audio",
|
||||||
|
|||||||
@@ -116,17 +116,20 @@ fn get_local_ip() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_system_locale() -> String {
|
fn get_system_locale() -> String {
|
||||||
|
// 用原生 Win32 API 取区域设置,避免拉起 PowerShell(冷启动 1-2 秒)。
|
||||||
|
// GetUserDefaultLocaleName 返回形如 "zh-CN" 的 BCP-47 名称,与原 (Get-Culture).Name 一致。
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
use std::process::Command;
|
use windows::Win32::Globalization::GetUserDefaultLocaleName;
|
||||||
Command::new("powershell")
|
// LOCALE_NAME_MAX_LENGTH = 85
|
||||||
.args(["-NoProfile", "-Command", "(Get-Culture).Name"])
|
let mut buf = [0u16; 85];
|
||||||
.creation_flags(0x08000000)
|
let len = unsafe { GetUserDefaultLocaleName(&mut buf) };
|
||||||
.output()
|
if len > 1 {
|
||||||
.ok()
|
// 返回值含结尾的 NUL,切掉它。
|
||||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
String::from_utf16_lossy(&buf[..(len as usize - 1)])
|
||||||
.map(|s: String| s.trim().to_string())
|
} else {
|
||||||
.unwrap_or_else(|| "unknown".to_string())
|
"unknown".to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
{
|
{
|
||||||
@@ -135,18 +138,20 @@ fn get_system_locale() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_total_memory_mb() -> u64 {
|
fn get_total_memory_mb() -> u64 {
|
||||||
|
// 用原生 GlobalMemoryStatusEx 取物理内存,避免拉起 PowerShell。
|
||||||
|
// ullTotalPhys 为字节,/ 1MB 与原 TotalPhysicalMemory / 1MB 语义一致。
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
use std::process::Command;
|
use windows::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX};
|
||||||
Command::new("powershell")
|
let mut status = MEMORYSTATUSEX {
|
||||||
.args(["-NoProfile", "-Command", "(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1MB"])
|
dwLength: std::mem::size_of::<MEMORYSTATUSEX>() as u32,
|
||||||
.creation_flags(0x08000000)
|
..Default::default()
|
||||||
.output()
|
};
|
||||||
.ok()
|
if unsafe { GlobalMemoryStatusEx(&mut status) }.is_ok() {
|
||||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
status.ullTotalPhys / (1024 * 1024)
|
||||||
.and_then(|s: String| s.trim().parse::<f64>().ok())
|
} else {
|
||||||
.map(|v| v as u64)
|
0
|
||||||
.unwrap_or(0)
|
}
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { Card, CardContent } from '@/components/ui/card'
|
|||||||
import { getSetting } from '@/services/store'
|
import { getSetting } from '@/services/store'
|
||||||
import { getRuntimeEvents, type RuntimeEvent } from '@/services/debugLog'
|
import { getRuntimeEvents, type RuntimeEvent } from '@/services/debugLog'
|
||||||
import { getWorkMode } from '@/services/transcription'
|
import { getWorkMode } from '@/services/transcription'
|
||||||
import { FolderOpen, RefreshCw, CheckCircle2, XCircle, MinusCircle, HelpCircle, ChevronDown } from 'lucide-react'
|
import { FolderOpen, RefreshCw, CheckCircle2, XCircle, MinusCircle, CircleSlash, Info, HelpCircle, ChevronDown } from 'lucide-react'
|
||||||
import { Tooltip } from '@/components/ui/tooltip'
|
import { Tooltip } from '@/components/ui/tooltip'
|
||||||
import DiagnosticsReportPanel from './DiagnosticsReportPanel'
|
import DiagnosticsReportPanel from './DiagnosticsReportPanel'
|
||||||
|
|
||||||
type HealthStatus = 'ok' | 'error' | 'unknown'
|
type HealthStatus = 'ok' | 'error' | 'unknown' | 'disabled'
|
||||||
|
|
||||||
interface HealthItem {
|
interface HealthItem {
|
||||||
label: string
|
label: string
|
||||||
@@ -54,6 +54,8 @@ const FAQ_ITEMS: { q: string; answer: React.ReactNode }[] = [
|
|||||||
function StatusIcon({ status }: { status: HealthStatus }) {
|
function StatusIcon({ status }: { status: HealthStatus }) {
|
||||||
if (status === 'ok') return <CheckCircle2 className="h-4 w-4 text-green-500" />
|
if (status === 'ok') return <CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||||
if (status === 'error') return <XCircle className="h-4 w-4 text-red-500" />
|
if (status === 'error') return <XCircle className="h-4 w-4 text-red-500" />
|
||||||
|
// 未开启:灰色「关闭」图标,明确区别于「正常(绿勾)」和「失败(红叉)」
|
||||||
|
if (status === 'disabled') return <CircleSlash className="h-4 w-4 text-muted-foreground/50" />
|
||||||
return <MinusCircle className="h-4 w-4 text-muted-foreground/50" />
|
return <MinusCircle className="h-4 w-4 text-muted-foreground/50" />
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +82,9 @@ export default function DiagnosticsPage() {
|
|||||||
const [showFileLog, setShowFileLog] = useState(true)
|
const [showFileLog, setShowFileLog] = useState(true)
|
||||||
const [openFaq, setOpenFaq] = useState<number | null>(0)
|
const [openFaq, setOpenFaq] = useState<number | null>(0)
|
||||||
|
|
||||||
|
// AI 校对处于「未开启」状态时,给出温和提示(依赖 AI 的功能不会生效)
|
||||||
|
const aiProofreadDisabled = health.some((item) => item.label === 'AI 校对' && item.status === 'disabled')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void doHealthCheck()
|
void doHealthCheck()
|
||||||
setEvents(getRuntimeEvents())
|
setEvents(getRuntimeEvents())
|
||||||
@@ -147,7 +152,7 @@ export default function DiagnosticsPage() {
|
|||||||
if (workMode !== 'server') {
|
if (workMode !== 'server') {
|
||||||
const aiEnabled = await getSetting('aiEnabled', false) as boolean
|
const aiEnabled = await getSetting('aiEnabled', false) as boolean
|
||||||
if (!aiEnabled) {
|
if (!aiEnabled) {
|
||||||
items.push({ label: 'AI 校对', status: 'ok', detail: '已关闭(极速模式)' })
|
items.push({ label: 'AI 校对', status: 'disabled', detail: '未开启(极速模式)' })
|
||||||
} else {
|
} else {
|
||||||
const aiProvider = await getSetting('cloudAi.provider', '') as string
|
const aiProvider = await getSetting('cloudAi.provider', '') as string
|
||||||
const aiApiKey = await getSetting('cloudAi.apiKey', '') as string
|
const aiApiKey = await getSetting('cloudAi.apiKey', '') as string
|
||||||
@@ -243,6 +248,12 @@ export default function DiagnosticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{aiProofreadDisabled && (
|
||||||
|
<div className="mt-3 flex items-start gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-xs leading-relaxed text-muted-foreground">
|
||||||
|
<Info className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
||||||
|
<span>AI 校对未开启,润色模式等依赖 AI 的功能不会生效。可在主页右上角,或主页侧边栏「AI 整理」页中开启。</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export default function GeneralSettingsPage() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="space-y-4 p-6">
|
<CardContent className="space-y-4 p-6">
|
||||||
|
<h2 className="text-lg font-semibold">偏好设置</h2>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">录音就绪提示音</p>
|
<p className="text-sm font-medium">录音就绪提示音</p>
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ export const RELEASE_HIGHLIGHTS: ReleaseHighlights = {
|
|||||||
items: [
|
items: [
|
||||||
'修复长时间后台运行后偶发「一直处理中、超时无结果」的问题:后台连接更稳定,不再被误判空闲而断开',
|
'修复长时间后台运行后偶发「一直处理中、超时无结果」的问题:后台连接更稳定,不再被误判空闲而断开',
|
||||||
'识别超时不再丢录音:即使这次超时没结果,录音也会保存到历史,可随时「重新识别」',
|
'识别超时不再丢录音:即使这次超时没结果,录音也会保存到历史,可随时「重新识别」',
|
||||||
|
'修复保存润色模式等设置后卡顿 2-4 秒的问题:机器信息改为启动时采集一次,日常改设置不再卡顿',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -121,6 +121,9 @@ export class RecorderOrchestrator {
|
|||||||
private cachedActivePresetId = 'intent'
|
private cachedActivePresetId = 'intent'
|
||||||
private cachedAiEnabled = true
|
private cachedAiEnabled = true
|
||||||
private cachedClientRuntimeInfo: ClientRuntimeInfo | null = null
|
private cachedClientRuntimeInfo: ClientRuntimeInfo | null = null
|
||||||
|
/** 客户端运行时信息是否已成功采集一次。它是进程内静态元数据(区域/内存/主机名等),
|
||||||
|
* 只需启动时取一次,不该挂在每次改设置都触发的刷新热路径上。失败时保持 false 以便重试。 */
|
||||||
|
private clientRuntimeInfoLoaded = false
|
||||||
private cachedAppPromptRules: AppPromptRule[] = []
|
private cachedAppPromptRules: AppPromptRule[] = []
|
||||||
private cachedUserStats: UserStats = createDefaultUserStats()
|
private cachedUserStats: UserStats = createDefaultUserStats()
|
||||||
private cachedHotwords: string[] = []
|
private cachedHotwords: string[] = []
|
||||||
@@ -261,6 +264,8 @@ export class RecorderOrchestrator {
|
|||||||
|
|
||||||
startInsertionTargetTracking()
|
startInsertionTargetTracking()
|
||||||
await this.refreshRuntimeSettings()
|
await this.refreshRuntimeSettings()
|
||||||
|
// 静态机器信息只在启动采集一次;不 await,避免拖慢建连(clientMeta 可空,采集期间为 null 无碍)。
|
||||||
|
void this.ensureClientRuntimeInfo()
|
||||||
this.ensureConnection()
|
this.ensureConnection()
|
||||||
|
|
||||||
// 快捷键切换润色模式(由 Rust global_shortcut 触发)
|
// 快捷键切换润色模式(由 Rust global_shortcut 触发)
|
||||||
@@ -434,9 +439,11 @@ export class RecorderOrchestrator {
|
|||||||
this.cachedStreamingDisplay = Boolean(streamingDisplay)
|
this.cachedStreamingDisplay = Boolean(streamingDisplay)
|
||||||
await this.overlayService.refreshSettings()
|
await this.overlayService.refreshSettings()
|
||||||
|
|
||||||
// 热词 / 服务器语言 / 客户端运行时信息彼此独立,并行加载而非依次 await,
|
// 热词 / 服务器语言彼此独立,并行加载而非依次 await,减少本函数的总耗时
|
||||||
// 减少本函数的总耗时(每次切换 AI 整理开关等都会触发这里)。
|
// (每次切换 AI 整理开关等都会触发这里)。
|
||||||
const [hotwordsResult, languageResult, runtimeInfoResult] = await Promise.allSettled([
|
// 客户端运行时信息(区域/内存/主机名等)是进程内静态元数据,已移出此热路径,
|
||||||
|
// 仅在启动时经 ensureClientRuntimeInfo() 采集一次,避免改任意设置都重新采集。
|
||||||
|
const [hotwordsResult, languageResult] = await Promise.allSettled([
|
||||||
Promise.all([
|
Promise.all([
|
||||||
getSetting(BUILTIN_SET_WORDS_KEY, {}),
|
getSetting(BUILTIN_SET_WORDS_KEY, {}),
|
||||||
getSetting(BUILTIN_SET_ACTIVE_KEY, {}),
|
getSetting(BUILTIN_SET_ACTIVE_KEY, {}),
|
||||||
@@ -453,12 +460,23 @@ export class RecorderOrchestrator {
|
|||||||
const l = lang as string
|
const l = lang as string
|
||||||
return l && l !== 'auto' ? l : ''
|
return l && l !== 'auto' ? l : ''
|
||||||
}),
|
}),
|
||||||
bridge.getClientRuntimeInfo(),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
this.cachedHotwords = hotwordsResult.status === 'fulfilled' ? hotwordsResult.value : []
|
this.cachedHotwords = hotwordsResult.status === 'fulfilled' ? hotwordsResult.value : []
|
||||||
this.cachedLanguage = languageResult.status === 'fulfilled' ? languageResult.value : ''
|
this.cachedLanguage = languageResult.status === 'fulfilled' ? languageResult.value : ''
|
||||||
this.cachedClientRuntimeInfo = runtimeInfoResult.status === 'fulfilled' ? runtimeInfoResult.value : null
|
}
|
||||||
|
|
||||||
|
/** 采集一次客户端运行时信息并缓存。静态元数据,进程内只成功采集一次;
|
||||||
|
* 失败不置位 loaded,留待下次重试(避免把 unknown/0 永久缓存)。
|
||||||
|
* clientMeta 本就可空,采集期间为 null 不影响业务。 */
|
||||||
|
async ensureClientRuntimeInfo() {
|
||||||
|
if (this.clientRuntimeInfoLoaded) return
|
||||||
|
try {
|
||||||
|
this.cachedClientRuntimeInfo = await bridge.getClientRuntimeInfo()
|
||||||
|
this.clientRuntimeInfoLoaded = true
|
||||||
|
} catch {
|
||||||
|
this.cachedClientRuntimeInfo = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 工作模式或服务地址变更后强制重连 provider。
|
/** 工作模式或服务地址变更后强制重连 provider。
|
||||||
|
|||||||
Reference in new issue
Block a user