修复: 粘贴图失效与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
+30 -12
View File
@@ -10,11 +10,16 @@ import (
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"u-desk/internal/common"
)
// 连接建立全流程(拨号+认证+SFTP 子系统)的总超时预算
const connectTimeout = 10 * time.Second
// 拨号独立预算,小于总预算:冷连接卡死大多发生在 TCP 拨号,
// 独立预算保证握手认证阶段始终有剩余 deadline 可用
const dialTimeout = 6 * time.Second
// 保活参数:周期发送 keepalive 并等待回复,超过 keepaliveReplyWait 未回复判定连接已死
const (
keepaliveInterval = 30 * time.Second
@@ -45,7 +50,8 @@ func GetManager() *Manager {
}
// Connect 创建或复用 SFTP 连接
func (m *Manager) Connect(config *Config) (*Client, error) {
// onStage 在连接各阶段推进时回调(dial/auth/channel),可为 nil
func (m *Manager) Connect(config *Config, onStage func(stage string)) (*Client, error) {
key := fmt.Sprintf("%s:%d", config.Host, config.Port)
m.mu.Lock()
@@ -60,7 +66,7 @@ func (m *Manager) Connect(config *Config) (*Client, error) {
m.clients.Delete(key)
}
c, err := newClient(config)
c, err := newClient(config, onStage)
if err != nil {
return nil, err
}
@@ -102,8 +108,8 @@ func (m *Manager) Shutdown() {
// --- 内部 ---
// newClient 建立连接并启动保活循环
func newClient(config *Config) (*Client, error) {
c, err := buildClient(config)
func newClient(config *Config, onStage func(stage string)) (*Client, error) {
c, err := buildClient(config, onStage)
if err != nil {
return nil, err
}
@@ -112,7 +118,14 @@ func newClient(config *Config) (*Client, error) {
}
// buildClient 拨号+认证+SFTP 子系统建立(全程受 connectTimeout 约束),不启动保活
func buildClient(config *Config) (*Client, error) {
// onStage 标记各阶段开始:dial=拨号、auth=认证、channel=建通道;SSH 协议链路串行,
// 无法并行加速,分步回调让前端可见当前卡点
func buildClient(config *Config, onStage func(stage string)) (*Client, error) {
notify := func(stage string) {
if onStage != nil {
onStage(stage)
}
}
sshConfig := &ssh.ClientConfig{
Config: ssh.Config{
KeyExchanges: []string{
@@ -160,14 +173,17 @@ func buildClient(config *Config) (*Client, error) {
// JoinHostPort 保证 IPv6 主机地址被方括号包裹,fmt 拼接对 IPv6 产生非法地址
addr := net.JoinHostPort(config.Host, strconv.Itoa(config.Port))
// 拨号与认证、SFTP 子系统握手共享同一总超时预算,认证阶段无界挂起由 deadline 掐断
// 总 deadline 从拨号前起算:拨号占独立 6s 预算,认证与 SFTP 子系统握手受剩余总预算约束,
// 认证阶段无界挂起由 deadline 掐断
deadline := time.Now().Add(connectTimeout)
sshConn, err := net.DialTimeout("tcp", addr, connectTimeout)
notify("dial")
sshConn, err := net.DialTimeout("tcp", addr, dialTimeout)
if err != nil {
return nil, &ConnectionError{Op: "dial", Err: err}
}
sshConn.SetDeadline(deadline)
notify("auth")
sshConnConn, chans, reqs, err := ssh.NewClientConn(sshConn, addr, sshConfig)
if err != nil {
sshConn.Close()
@@ -176,6 +192,7 @@ func buildClient(config *Config) (*Client, error) {
sshClient := ssh.NewClient(sshConnConn, chans, reqs)
notify("channel")
sftpClient, err := sftp.NewClient(sshClient)
sshConn.SetDeadline(time.Time{})
if err != nil {
@@ -196,7 +213,7 @@ func (c *Client) startKeepalive() {
stop := make(chan struct{})
sshClient := c.sshClient
c.stopKeep = stop
go func() {
common.Go("sftp-keepalive", func() {
t := time.NewTicker(keepaliveInterval)
defer t.Stop()
for {
@@ -210,16 +227,16 @@ func (c *Client) startKeepalive() {
return
}
}
}()
})
}
// sshKeepalive 发送一次 keepalive 请求并限时等待回复,超时或出错视作连接已死
func sshKeepalive(sshClient *ssh.Client, wait time.Duration) bool {
done := make(chan error, 1)
go func() {
common.Go("sftp-keepalive-probe", func() {
_, _, err := sshClient.SendRequest("keepalive@openssh.com", true, nil)
done <- err
}()
})
select {
case err := <-done:
return err == nil
@@ -295,7 +312,8 @@ func (c *Client) WithRetry(fn func(*sftp.Client) error) error {
func (c *Client) reconnect() error {
// 用 buildClient 而非 newClient,避免保活循环绑定到即将丢弃的临时容器
nc, err := buildClient(c.config)
// 后台重连无阶段展示需求,回调传 nil
nc, err := buildClient(c.config, nil)
if err != nil {
return err
}
+3 -3
View File
@@ -844,9 +844,9 @@ func (s *Service) GetSystemInfo(connID string) (map[string]interface{}, error) {
out, e := c.RunCommand(cmd)
results <- cmdResult{key, out, e}
}
go runCmd("df", "df -B1 / 2>/dev/null | tail -1")
go runCmd("mem", "free -b 2>/dev/null | grep -E '^Mem:' || cat /proc/meminfo 2>/dev/null | head -2")
go runCmd("cpu", "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1")
common.Go("sftp-cmd-df", func() { runCmd("df", "df -B1 / 2>/dev/null | tail -1") })
common.Go("sftp-cmd-mem", func() { runCmd("mem", "free -b 2>/dev/null | grep -E '^Mem:' || cat /proc/meminfo 2>/dev/null | head -2") })
common.Go("sftp-cmd-cpu", func() { runCmd("cpu", "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1") })
var dfOut, memOut, cpuOut string
hasErr := false