package main import ( "fmt" "io" "net/http" "os" "os/exec" "path/filepath" goruntime "runtime" "strings" "time" "github.com/wailsapp/wails/v3/pkg/application" ) // App 经 Wails v3 的 Service 绑定暴露给前端,承载只有桌面端能做的原生能力: // 文件读写、系统"另存为"框、用系统默认应用打开、原生通知、本地执行 runner(JARVIS 的手)。 // v3 中不再需要注入 ctx——对话框经 application.Get().Dialog 获取。 type App struct { runner LocalRunner // 本地执行器(localrunner.go):用户显式开启才连接 } // Ping 供前端探活 Go 桥是否就绪。 func (a *App) Ping() string { return "sundynix-desktop ok" } // SaveReportAs 弹原生"另存为"对话框,把 url 指向的报告(.docx)下载到用户选定路径。 // 返回保存路径;用户取消则返回空串。 func (a *App) SaveReportAs(url, filename string) (string, error) { if filename == "" { filename = "report.docx" } path, err := application.Get().Dialog.SaveFile(). SetFilename(filename). AddFilter("Word 文档 (*.docx)", "*.docx"). PromptForSingleSelection() if err != nil || path == "" { return "", err } if err := download(url, path); err != nil { return "", err } return path, nil } // PrintReportPage 把报告打印视图 HTML 落到临时文件,交系统默认浏览器打开(在那里 ⌘P → // 存储为 PDF)。Wails 的 WKWebView 会把 window.open 拦成 null,前端弹打印窗那条路在壳内 // 走不通;转交系统浏览器后「前端打印出 PDF、CJK 零字体依赖」的原有优势不变。返回落盘路径。 func (a *App) PrintReportPage(filename, html string) (string, error) { if filename == "" { filename = "report" } // 文件名进过滤:主题可能含 / 之类的路径字符。 safe := strings.Map(func(r rune) rune { if strings.ContainsRune(`/\:*?"<>|`, r) { return '_' } return r }, filename) dst := filepath.Join(os.TempDir(), "sundynix-print-"+safe+".html") if err := os.WriteFile(dst, []byte(html), 0o600); err != nil { return "", err } return dst, openInSystem(dst) } // Notify 弹一条系统通知(best-effort:macOS 用 osascript,其它平台暂静默)。 func (a *App) Notify(title, body string) { if goruntime.GOOS == "darwin" { script := fmt.Sprintf("display notification %q with title %q", body, title) _ = exec.Command("osascript", "-e", script).Start() } } // downloadClient 给下载加超时:默认 http.Get 用的 client 没有超时, // 上游卡住的话「另存为」会永远转下去,用户只能强杀 app。 var downloadClient = &http.Client{Timeout: 3 * time.Minute} // download 把 url 下载到 dst。失败一律不留残file: // 中途断开会在用户选定的路径上留个半截 .docx,带着用户起的名字,以后双击打不开。 func download(url, dst string) (err error) { resp, err := downloadClient.Get(url) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("下载失败 HTTP %d", resp.StatusCode) } f, err := os.Create(dst) if err != nil { return err } defer func() { // Close 的错误不能吞:写文件时 io.Copy 成功不代表数据落了盘, // flush 失败只会在 Close 上报出来——吞掉就是静默截断。 cerr := f.Close() if err == nil { err = cerr } if err != nil { _ = os.Remove(dst) // 失败不留残file } }() _, err = io.Copy(f, resp.Body) return err } func openInSystem(path string) error { switch goruntime.GOOS { case "darwin": return exec.Command("open", path).Start() case "windows": return exec.Command("rundll32", "url.dll,FileProtocolHandler", path).Start() default: return exec.Command("xdg-open", path).Start() } }