From 1f725abec29ad465f3c73c28c360818139761212 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 03 Jul 2026 05:28:17 +0800
Subject: [PATCH] Record R221 2.2.2 release candidate
---
C1.source/privatevoice.src/services/diagnostics_service.go | 196 +++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 196 insertions(+), 0 deletions(-)
diff --git a/C1.source/privatevoice.src/services/diagnostics_service.go b/C1.source/privatevoice.src/services/diagnostics_service.go
new file mode 100644
index 0000000..15f79f0
--- /dev/null
+++ b/C1.source/privatevoice.src/services/diagnostics_service.go
@@ -0,0 +1,196 @@
+package services
+
+import (
+ "archive/zip"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "runtime"
+ "time"
+ "voicesnap/internal/config"
+ "voicesnap/internal/model"
+ "voicesnap/internal/modelselection"
+ "voicesnap/internal/paths"
+)
+
+type DiagnosticsService struct {
+ cfg *config.Config
+ version string
+}
+
+func NewDiagnosticsService(cfg *config.Config, version string) *DiagnosticsService {
+ if cfg == nil {
+ cfg = config.Default()
+ }
+ return &DiagnosticsService{cfg: cfg, version: version}
+}
+
+func (s *DiagnosticsService) GetRuntimeStats() map[string]interface{} {
+ var mem runtime.MemStats
+ runtime.ReadMemStats(&mem)
+ return map[string]interface{}{
+ "version": s.version,
+ "goos": runtime.GOOS,
+ "goarch": runtime.GOARCH,
+ "go_version": runtime.Version(),
+ "goroutines": runtime.NumGoroutine(),
+ "cpu_count": runtime.NumCPU(),
+ "heap_alloc_mb": bytesToMiB(mem.HeapAlloc),
+ "heap_sys_mb": bytesToMiB(mem.HeapSys),
+ "stack_sys_mb": bytesToMiB(mem.StackSys),
+ "next_gc_mb": bytesToMiB(mem.NextGC),
+ "num_gc": mem.NumGC,
+ "data_dir": paths.AppSupportDir(),
+ "models_dir": paths.ModelsRoot(),
+ }
+}
+
+func (s *DiagnosticsService) ExportDiagnostics() (string, error) {
+ dir := filepath.Join(paths.AppSupportDir(), "diagnostics")
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return "", err
+ }
+ outPath := filepath.Join(dir, "privatevoice-diagnostics-"+time.Now().Format("20060102-150405")+".zip")
+
+ out, err := os.Create(outPath)
+ if err != nil {
+ return "", err
+ }
+ defer out.Close()
+
+ zw := zip.NewWriter(out)
+ if err := s.addJSON(zw, "runtime.json", s.GetRuntimeStats()); err != nil {
+ zw.Close()
+ return "", err
+ }
+ if err := s.addJSON(zw, "config.redacted.json", redactedConfig(s.cfg)); err != nil {
+ zw.Close()
+ return "", err
+ }
+ if err := s.addJSON(zw, "models.json", modelDiagnostics(s.cfg)); err != nil {
+ zw.Close()
+ return "", err
+ }
+ if err := addLogFiles(zw); err != nil {
+ zw.Close()
+ return "", err
+ }
+ if err := zw.Close(); err != nil {
+ return "", err
+ }
+ return outPath, nil
+}
+
+func (s *DiagnosticsService) addJSON(zw *zip.Writer, name string, value interface{}) error {
+ data, err := json.MarshalIndent(value, "", " ")
+ if err != nil {
+ return err
+ }
+ w, err := zw.Create(name)
+ if err != nil {
+ return err
+ }
+ _, err = w.Write(append(data, '\n'))
+ return err
+}
+
+func addLogFiles(zw *zip.Writer) error {
+ for _, name := range []string{"app.log", "app.log.1", "app.log.2", "app.log.3"} {
+ path := filepath.Join(paths.AppSupportDir(), name)
+ if err := addFileIfExists(zw, path, filepath.Join("logs", name)); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func addFileIfExists(zw *zip.Writer, path, zipName string) error {
+ in, err := os.Open(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil
+ }
+ return err
+ }
+ defer in.Close()
+
+ info, err := in.Stat()
+ if err != nil {
+ return err
+ }
+ header, err := zip.FileInfoHeader(info)
+ if err != nil {
+ return err
+ }
+ header.Name = zipName
+ header.Method = zip.Deflate
+ w, err := zw.CreateHeader(header)
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(w, in)
+ return err
+}
+
+func redactedConfig(cfg *config.Config) map[string]interface{} {
+ if cfg == nil {
+ cfg = config.Default()
+ }
+ return map[string]interface{}{
+ "HotkeyVK": cfg.HotkeyVK,
+ "HotkeyMode": cfg.HotkeyMode,
+ "AutoHide": cfg.AutoHide,
+ "SoundFeedback": cfg.SoundFeedback,
+ "HideDockIcon": cfg.HideDockIcon,
+ "CopyToClipboard": cfg.CopyToClipboard,
+ "DeviceNameSet": cfg.DeviceName != "",
+ "SelectedModelID": cfg.SelectedModelID,
+ "ModelSelectionMode": cfg.ModelSelectionMode,
+ "LanguageMode": cfg.LanguageMode,
+ "LanguageID": cfg.LanguageID,
+ }
+}
+
+func modelDiagnostics(cfg *config.Config) map[string]interface{} {
+ current := modelselection.Resolve(cfg, nil)
+ items := make([]map[string]interface{}, 0)
+ for _, profile := range model.ListModelProfiles() {
+ resolved, err := model.ResolveModel(profile.ID)
+ item := map[string]interface{}{
+ "model_id": profile.ID,
+ "display_name": profile.DisplayName,
+ "backend": profile.BackendKind,
+ "tier": profile.Tier,
+ "num_threads": profile.NumThreads,
+ "current": profile.ID == current.ModelID,
+ "supported": model.IsModelSupportedInCurrentBuild(profile.ID),
+ "download_size": profile.ApproxSize,
+ }
+ if err != nil {
+ item["status"] = "resolve_error"
+ item["error"] = err.Error()
+ } else {
+ item["status"] = string(resolved.Status)
+ item["usable"] = resolved.IsUsable()
+ item["source_dir_kind"] = resolved.SourceDirKind
+ item["missing"] = resolved.Missing
+ item["problems"] = resolved.Problems
+ }
+ items = append(items, item)
+ }
+ return map[string]interface{}{
+ "current_model_id": current.ModelID,
+ "selection_mode": current.SelectionMode,
+ "effective_language_id": current.LanguageSettings.EffectiveLanguageID,
+ "fallback_reason": current.FallbackReason,
+ "has_any_usable_model": model.HasAnyUsableModel(),
+ "supported_build_note": fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
+ "models": items,
+ }
+}
+
+func bytesToMiB(value uint64) float64 {
+ return float64(value) / 1024.0 / 1024.0
+}
--
Gitblit v1.9.3