修复: 粘贴图失效与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
+89
View File
@@ -0,0 +1,89 @@
package common
import (
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"sync"
"time"
)
// GetLogsDir 日志目录:~/.u-desk/logs(audit 日志同目录)
func GetLogsDir() string {
return filepath.Join(GetUserDataDir(), "logs")
}
// dailyFile 按天滚动的追加写文件:每次写时发现日期变化即切换新文件。
// 只做按天滚动,不搞大小上限/压缩/清理,崩溃取证够用。
type dailyFile struct {
mu sync.Mutex
dir string
naming func(t time.Time) string
f *os.File
day string
}
func newDailyFile(dir string, naming func(time.Time) string) *dailyFile {
return &dailyFile{dir: dir, naming: naming}
}
func (w *dailyFile) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
now := time.Now()
day := now.Format("2006-01-02")
if w.f != nil && day != w.day {
w.f.Close()
w.f = nil
}
if w.f == nil {
if err := os.MkdirAll(w.dir, 0755); err != nil {
return 0, err
}
f, err := os.OpenFile(filepath.Join(w.dir, w.naming(now)), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return 0, err
}
w.f, w.day = f, day
}
return w.f.Write(p)
}
func (w *dailyFile) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.f == nil {
return nil
}
err := w.f.Close()
w.f = nil
return err
}
// FileLogOut 应用文件日志的底层写出器(stderr + 按天文件 MultiWriter)。
// SetupFileLogging 之后有效;供需要独立过滤级别的 logger 复用同一落盘目的地
// (如 wails 系统日志降到 Warn,业务 slog.Default() 保持 INFO 全量)。
var FileLogOut io.Writer = os.Stderr
// SetupFileLogging slog 双路落盘:stderr + ~/.u-desk/logs/app-YYYY-MM-DD.log。
// windowsgui 下 stderr 被丢弃,文件路是日志唯一可见载体;
// 文件侧打不开时静默降级为仅 stderr,不因日志失败阻断启动。
func SetupFileLogging() {
out := io.Writer(os.Stderr)
w := newDailyFile(GetLogsDir(), func(t time.Time) string {
return fmt.Sprintf("app-%s.log", t.Format("2006-01-02"))
})
// 预开当天文件探测文件侧可用性(Write 内部含懒开/换天逻辑)
if _, err := w.Write(nil); err == nil {
// 文件必须放 MultiWriter 首位:windowsgui 下 stderr 句柄无效,
// MultiWriter 按序短路,stderr 在前会吞掉后续文件路(真机零日志的根因)
out = io.MultiWriter(w, os.Stderr)
} else {
w.Close()
}
FileLogOut = out
slog.SetDefault(slog.New(slog.NewTextHandler(out, nil)))
}
+46
View File
@@ -0,0 +1,46 @@
package common
import (
"fmt"
"os"
"path/filepath"
"runtime/debug"
"sync"
"time"
)
var panicLogMu sync.Mutex
// AppendPanicLog 把一次 panic 的来源名、值与完整栈追加写入
// ~/.u-desk/logs/panic-YYYYMMDD.log。写失败静默放弃:取证失败不能引发二次故障。
func AppendPanicLog(name string, r any) {
dir := GetLogsDir()
panicLogMu.Lock()
defer panicLogMu.Unlock()
if err := os.MkdirAll(dir, 0755); err != nil {
return
}
path := filepath.Join(dir, fmt.Sprintf("panic-%s.log", time.Now().Format("20060102")))
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return
}
defer f.Close()
// debug.Stack 在 defer 内 panic 未展开时调用,包含 panic 原始调用栈
fmt.Fprintf(f, "[%s] goroutine=%s panic=%v\n%s\n",
time.Now().Format("2006-01-02 15:04:05.000"), name, r, debug.Stack())
}
// Go 带 panic 兜底的 goroutine:panic 时写完整取证后仅结束该 goroutine,
// 不让单点 panic 杀掉整个进程(windowsgui 下默认静默退出零痕迹)。
func Go(name string, fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
AppendPanicLog(name, r)
}
}()
fn()
}()
}
+3 -2
View File
@@ -199,10 +199,11 @@ func RunConcurrent[T any](tasks []T, limit int, fn func(t T)) {
for _, task := range tasks {
sem <- struct{}{}
wg.Add(1)
go func(t T) {
t := task
Go("bounded-parallel", func() {
defer func() { <-sem; wg.Done() }()
fn(t)
}(task)
})
}
wg.Wait()
}