修复: 粘贴图失效与SFTP串台回退等一批问题

粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
This commit is contained in:
lxy
2026-10-24 13:48:54 +08:00
parent 4675ae381d
commit 75a42c0b6a
48 changed files with 1072 additions and 220 deletions
+65 -27
View File
@@ -10,6 +10,7 @@ import (
"unsafe"
"golang.org/x/sys/windows"
"u-desk/internal/common"
)
// Chrome_WidgetWin 系列类名是 WebView2 的宿主窗口层级
@@ -28,6 +29,12 @@ var (
// 修复去重:同一时刻只允许一个校验在跑(回调写共享切片要求互斥)
var webviewFixRunning atomic.Bool
// WM_SIZE 去重:脏标记 + 单 worker(每条 WM_SIZE 只置位,不再批量 spawn goroutine)
var (
webviewSizeDirty atomic.Bool
webviewSizeWorkerActive atomic.Bool
)
// 当前校验的目标主窗口(枚举回调过滤直属子窗口用)
var webviewTargetHwnd uintptr
@@ -77,32 +84,60 @@ func checkAndFixFill(hwnd uintptr) {
}
}
// runGuarded 统一 panic 兜底:windowsgui 下 goroutine panic 会静默杀进程
// runGuarded 同步调用点的 panic 兜底:panic 取证落盘后不外抛,
// 让调用方(如周期循环)能继续跑下一轮
func runGuarded(fn func()) {
defer func() { recover() }()
defer func() {
if r := recover(); r != nil {
common.AppendPanicLog("webview-fix", r)
}
}()
fn()
}
// fixWebviewFill WM_SIZE 路径:等 resize 序列结束、wails 内部去抖落定后校验一次
// fixWebviewFill WM_SIZE 路径:等 resize 序列结束、wails 内部去抖落定后校验一次。
// WM_SIZE 只置脏标记;活跃期间的后续 WM_SIZE 合并进去(worker 循环消费),
// 拖拽缩放的风暴从"每条消息一个 goroutine"降为全程单 worker
func fixWebviewFill(hwnd uintptr) {
if !webviewFixRunning.CompareAndSwap(false, true) {
webviewTargetHwnd = hwnd
webviewSizeDirty.Store(true)
if !webviewSizeWorkerActive.CompareAndSwap(false, true) {
return
}
go func() {
defer webviewFixRunning.Store(false)
runGuarded(func() {
common.Go("webview-fix", func() {
defer webviewSizeWorkerActive.Store(false)
for webviewSizeDirty.CompareAndSwap(true, false) {
time.Sleep(150 * time.Millisecond)
checkAndFixFill(hwnd)
})
}()
if webviewFixRunning.CompareAndSwap(false, true) {
runGuarded(func() { checkAndFixFill(webviewTargetHwnd) })
webviewFixRunning.Store(false)
}
}
})
}
// stopWebviewWatch 周期兜底循环的停止信号;StartWebviewFillWatch 重入时先关旧的(重启语义)
var stopWebviewWatch chan struct{}
// StartWebviewFillWatch 周期兜底:覆盖"无新 WM_SIZE 的迟到重放"场景
// (主线程忙时排队的过期重绘在窗口静止时才执行,此时不会产生任何 WM_SIZE)
func StartWebviewFillWatch() {
go runGuarded(func() {
if stopWebviewWatch != nil {
select {
case <-stopWebviewWatch: // 旧循环已自行退出
default:
close(stopWebviewWatch)
}
}
stop := make(chan struct{})
stopWebviewWatch = stop
common.Go("webview-watch", func() {
for {
time.Sleep(3 * time.Second)
select {
case <-stop:
return
case <-time.After(3 * time.Second):
}
if webviewFixRunning.CompareAndSwap(false, true) {
runGuarded(func() {
checkAndFixFill(findMainWindowHwnd())
@@ -114,20 +149,23 @@ func StartWebviewFillWatch() {
}
// findMainWindowHwnd 找本进程的可见主窗口
// 回调必须进程级复用(同 enumChildWebviewHosts):runtime 的 Windows callback
// 硬上限 2000 个且按闭包指针去重,每 3 秒新建一个约 100 分钟即耗尽,
// 触发 runtime throw("too many callback functions") 直接静默退出(无法 recover)
var mainWindowHwndResult uintptr
var enumMainWindowHwnd = syscall.NewCallback(func(h uintptr, _ uintptr) uintptr {
var winPid uint32
windows.GetWindowThreadProcessId(windows.HWND(h), &winPid)
if winPid == windows.GetCurrentProcessId() && windows.IsWindowVisible(windows.HWND(h)) {
mainWindowHwndResult = h
return 0
}
return 1
})
func findMainWindowHwnd() uintptr {
var found uintptr
pid := windows.GetCurrentProcessId()
cb := syscall.NewCallback(func(h uintptr, _ uintptr) uintptr {
var winPid uint32
windows.GetWindowThreadProcessId(windows.HWND(h), &winPid)
if winPid == pid && windows.IsWindowVisible(windows.HWND(h)) {
found = h
return 0
}
return 1
})
runGuarded(func() {
windows.EnumWindows(cb, nil)
})
return found
mainWindowHwndResult = 0
windows.EnumWindows(enumMainWindowHwnd, nil)
return mainWindowHwndResult
}