From f6f92a8a6ee4a9ca41d67eb4e696c4625d16ee69 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 10 Sep 2026 17:56:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(plugins):=20=E4=BF=AE=E5=A4=8D=E4=B8=A4?= =?UTF-8?q?=E5=A4=84=E9=A2=84=E5=AD=98=E7=BC=BA=E9=99=B7=EF=BC=88agentcli?= =?UTF-8?q?=20=E4=B8=B4=E7=95=8C=E5=8C=BA=20/=20localuse=20=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E8=BD=AC=E4=B9=89=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agentcli cleanupLoop: 持锁期间为每个过期终端 go func 调 term.Close()(内部会 Kill 进程并等 <-t.done),临界区被拉长且与 TerminalSession 退出路径交错。改为持锁 只做筛选与摘除,锁外再逐个关闭。 localuse 脚本转义(两处真实 bug): 1. AppleScript 注入:handleScreensue 只把 `"` 转义为 `\"`,未转义 `\`。 内容结尾的反斜杠会吃掉闭合引号,使后续内容逃逸出字符串字面量。 改为 escapeAppleScriptString:**先转义反斜杠再转义双引号**(顺序 不可颠倒,否则刚插入的反斜杠会被二次转义)。 2. PowerShell 路径被错误加倍反斜杠:handleScreensee 把 Windows 临时 路径的 `\` 写成 `\\`。但 PowerShell 单引号串里反斜杠是普通字符 (转义符是反引号),加倍会让截图保存到错误路径。 新增 psSingleQuote 统一处理,并把 4 处零散的 `ReplaceAll(x,"'","''")` 收敛到该 helper。 --- internal/plugins/agentcli/plugin.go | 26 ++++++++++++---------- internal/plugins/localuse/plugin.go | 34 ++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/internal/plugins/agentcli/plugin.go b/internal/plugins/agentcli/plugin.go index 3b38520..8db7bce 100644 --- a/internal/plugins/agentcli/plugin.go +++ b/internal/plugins/agentcli/plugin.go @@ -993,24 +993,28 @@ func (p *Plugin) cleanupLoop(s *sdk.PluginSDK) { case <-p.stopCh: return case <-ticker.C: + // 先持锁筛选出待关闭的终终并移出 map,再在锁外逐个关闭。 + // 旧实现在持锁期间 go func 调 term.Close()(内部会 Kill 进程并等 + // <-t.done),临界区被拉长且与 TerminalSession 的退出路径交错。 + var expired []*TerminalSession p.mu.Lock() for id, t := range p.sessions { - if t.IsExpired() { + switch { + case t.IsExpired(): log.Printf("[agentcli] cleanup: terminal %s expired", id) - delete(p.sessions, id) - go func(term *TerminalSession) { - term.Close() - }(t) - } - if !terminalRunning(t) { + case !terminalRunning(t): log.Printf("[agentcli] cleanup: terminal %s process exited", id) - delete(p.sessions, id) - go func(term *TerminalSession) { - term.Close() - }(t) + default: + continue } + delete(p.sessions, id) + expired = append(expired, t) } p.mu.Unlock() + + for _, t := range expired { + go func(term *TerminalSession) { term.Close() }(t) + } } } } diff --git a/internal/plugins/localuse/plugin.go b/internal/plugins/localuse/plugin.go index 1716926..ad59703 100644 --- a/internal/plugins/localuse/plugin.go +++ b/internal/plugins/localuse/plugin.go @@ -114,7 +114,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { // ── clipboardsee ── s.RegisterTool("local_clipboardsee", sdk.ToolDef{ - Name: "local_clipboardsee", + Name: "local_clipboardsee", Description: "读取本机剪切板当前内容(用户最近复制/剪切的文字)。", Parameters: map[string]interface{}{ "type": "object", @@ -124,7 +124,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { // ── clipboardsue ── s.RegisterTool("local_clipboardsue", sdk.ToolDef{ - Name: "local_clipboardsue", + Name: "local_clipboardsue", Description: "将文字写入本机剪切板(用户随后可 Ctrl+V 粘贴)。", Parameters: map[string]interface{}{ "type": "object", @@ -173,6 +173,24 @@ func (p *Plugin) Stop() error { return nil } // ── screensee ────────────────────────────────────────────────────────────── +// psSingleQuote 生成 PowerShell **单引号字符串**的字面量内容。 +// +// 单引号串里只有单引号本身需要写成两个连续单引号;反斜杠是普通字符, +// **不能加倍**(PowerShell 的转义字符是反引号而不是反斜杠)。早期代码把 +// Windows 临时路径的反斜杠写成双写,会让截图保存到错误路径。 +func psSingleQuote(s string) string { + return strings.ReplaceAll(s, "'", "''") +} + +// escapeAppleScriptString 转义 AppleScript 双引号字符串字面量。 +// +// 顺序关键:**先转义反斜杠再转义双引号**。若反过来,刚插入的 `\"` 中的 +// 反斜杠会被再转义一遍变成 `\\"`(反斜杠 + 未转义引号),内容可越出字符串。 +func escapeAppleScriptString(s string) string { + s = strings.ReplaceAll(s, `\`, `\\`) + return strings.ReplaceAll(s, `"`, `\"`) +} + func (p *Plugin) handleScreensee(args map[string]interface{}) (interface{}, error) { platform := runtime.GOOS tmpDir := os.TempDir() @@ -203,7 +221,7 @@ func (p *Plugin) handleScreensee(args map[string]interface{}) (interface{}, erro $bitmap.Save('%s', [Drawing.Imaging.ImageFormat]::Png) $graphics.Dispose() $bitmap.Dispose() - `, strings.ReplaceAll(outFile, "\\", "\\\\")) + `, psSingleQuote(outFile)) cmd = exec.Command("powershell", "-NoProfile", "-Command", psScript) default: return map[string]interface{}{"isError": true, "content": "不支持的平台: " + platform}, nil @@ -361,7 +379,7 @@ func (p *Plugin) handleSpeakeruse(args map[string]interface{}) (interface{}, err Add-Type -AssemblyName System.Speech $synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer $synthesizer.Speak('%s') - `, strings.ReplaceAll(text, "'", "''")) + `, psSingleQuote(text)) cmd = exec.Command("powershell", "-NoProfile", "-Command", psScript) default: return map[string]interface{}{"isError": true, "content": "speakeruse 不支持平台: " + platform}, nil @@ -394,12 +412,12 @@ func (p *Plugin) handleScreensue(args map[string]interface{}) (interface{}, erro return p.screensueLinux(content, duration) case "darwin": // macOS 用 osascript 弹窗 - psScript := fmt.Sprintf(`display dialog "%s" with title "HomeAgent" buttons {"OK"} default button "OK giving up after %d"`, strings.ReplaceAll(content, `"`, `\"`), duration) + psScript := fmt.Sprintf(`display dialog "%s" with title "HomeAgent" buttons {"OK"} default button "OK giving up after %d"`, escapeAppleScriptString(content), duration) cmd := exec.Command("osascript", "-e", psScript) _ = cmd.Run() return map[string]interface{}{"content": "屏幕显示已触发"}, nil case "windows": - escContent := strings.ReplaceAll(content, "\x27", "\x27\x27") + escContent := psSingleQuote(content) psScript := fmt.Sprintf(` Add-Type -AssemblyName System.Windows.Forms $form = New-Object System.Windows.Forms.Form @@ -504,7 +522,7 @@ func (p *Plugin) handleClipboardsue(args map[string]interface{}) (interface{}, e return map[string]interface{}{"content": "剪切板已写入"}, nil case "windows": ps := fmt.Sprintf(`Add-Type -AssemblyName System.Windows.Forms; [Windows.Forms.Clipboard]::SetText('%s')`, - strings.ReplaceAll(text, "'", "''")) + psSingleQuote(text)) _ = exec.Command("powershell", "-NoProfile", "-Command", ps).Run() return map[string]interface{}{"content": "剪切板已写入"}, nil default: @@ -582,7 +600,7 @@ func (p *Plugin) computeruseWindows(action string, args map[string]interface{}) _ = exec.Command("powershell", "-NoProfile", "-Command", ps).Run() case "type": text, _ := args["text"].(string) - ps := fmt.Sprintf(`$w = New-Object -ComObject wscript.shell; $w.SendKeys('%s')`, strings.ReplaceAll(text, "'", "''")) + ps := fmt.Sprintf(`$w = New-Object -ComObject wscript.shell; $w.SendKeys('%s')`, psSingleQuote(text)) _ = exec.Command("powershell", "-NoProfile", "-Command", ps).Run() default: return map[string]interface{}{"isError": true, "content": "Windows computeruse 暂不支持 " + action}, nil