package main import ( "context" "embed" "fmt" "log" "os" "os/exec" "path/filepath" "runtime" "strings" "syscall" "time" "unsafe" "qiweimanager/config" "qiweimanager/logger" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/options/windows" ) //go:embed all:frontend/dist var assets embed.FS // 全局变量 var ( // 用于存储辅助程序的进程信息 helperProcess *os.Process // 全局日志器 globalLogger *logger.Logger // 日志总开关 logEnabled = true // 日志级别 logLevel = logger.LevelDebug ) // 检查并关闭helper.exe进程的函数 func checkAndCloseHelperProcess() { globalLogger.Info("检查系统中是否存在helper.exe进程...") // 打开系统快照 hSnapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0) if err != nil { globalLogger.Error("创建进程快照失败: %v", err) return } defer syscall.CloseHandle(hSnapshot) // 初始化进程信息结构 var pe32 syscall.ProcessEntry32 pe32.Size = uint32(unsafe.Sizeof(pe32)) // 获取第一个进程 if err := syscall.Process32First(hSnapshot, &pe32); err != nil { globalLogger.Error("获取第一个进程失败: %v", err) return } // 遍历所有进程查找helper.exe for { // 将进程名从UTF-16转换为Go字符串 processName := syscall.UTF16ToString(pe32.ExeFile[:]) // 匹配进程名 if strings.EqualFold(processName, "helper.exe") || strings.EqualFold(processName, "helper_auto_reply.exe") { pid := pe32.ProcessID // 不要关闭当前进程 if int(pid) == os.Getpid() { continue } globalLogger.Info("找到helper.exe进程,进程ID: %d,尝试关闭...", pid) // 尝试打开进程 processHandle, err := syscall.OpenProcess(syscall.PROCESS_TERMINATE, false, pid) if err != nil { globalLogger.Error("无法打开进程ID %d: %v", pid, err) } else { defer syscall.CloseHandle(processHandle) // 尝试终止进程 if err := syscall.TerminateProcess(processHandle, 0); err != nil { globalLogger.Error("终止进程ID %d失败: %v", pid, err) } else { globalLogger.Info("成功终止进程ID %d", pid) } } } // 获取下一个进程 if err := syscall.Process32Next(hSnapshot, &pe32); err != nil { // 如果没有更多进程,跳出循环 if err == syscall.ERROR_NO_MORE_FILES { break } globalLogger.Error("获取下一个进程失败: %v", err) break } } globalLogger.Info("helper.exe进程检查和关闭操作完成") } // 启动辅助程序的函数 func startHelperProgram() { // 检查是否已经有辅助进程在运行 if helperProcess != nil { // 尝试获取进程信息来验证它是否还在运行 process, err := os.FindProcess(helperProcess.Pid) if err == nil && process != nil { globalLogger.Info("辅助程序进程已在运行,PID: %d,无需再次启动", helperProcess.Pid) return } // 进程句柄存在但无法找到进程,可能已终止,继续启动新进程 globalLogger.Info("检测到已终止的辅助程序进程句柄,准备启动新进程") } // 获取当前可执行文件路径 exePath, err := os.Executable() if err != nil { globalLogger.Error("无法获取可执行文件路径: %v", err) return } // 获取当前目录 currentDir := filepath.Dir(exePath) globalLogger.Debug("当前可执行文件目录: %s", currentDir) // 构建辅助程序路径,优先使用自动客服修复版helper,避免旧helper.exe被系统占用时无法替换 helperPath := filepath.Join(currentDir, "helper_auto_reply.exe") if _, err := os.Stat(helperPath); os.IsNotExist(err) { helperPath = filepath.Join(currentDir, "helper.exe") } globalLogger.Debug("尝试启动辅助程序路径: %s", helperPath) // 检查辅助程序是否存在 if _, err := os.Stat(helperPath); os.IsNotExist(err) { globalLogger.Warn("32位辅助程序不存在: %s", helperPath) return } // 在Windows平台上,使用更底层的Windows API来创建进程,确保完全隐藏窗口 if runtime.GOOS == "windows" { // 使用Windows API直接创建进程,避免使用exec.Command可能导致的窗口闪烁 var si syscall.StartupInfo var pi syscall.ProcessInformation // 设置STARTUPINFO结构体以隐藏窗口 si.Cb = uint32(unsafe.Sizeof(si)) si.Flags = syscall.STARTF_USESHOWWINDOW si.ShowWindow = syscall.SW_HIDE // 定义Windows API常量 const ( CREATE_NO_WINDOW = 0x08000000 CREATE_NEW_PROCESS_GROUP = 0x00000200 DETACHED_PROCESS = 0x00000010 CREATE_BREAKAWAY_FROM_JOB = 0x01000000 // 新增标志,有助于完全独立于父进程 CREATE_DEFAULT_ERROR_MODE = 0x04000000 // 新增标志,避免显示系统错误对话框 ) // 进程创建标志组合 - 使用更多标志确保窗口完全隐藏 flags := uint32(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS | CREATE_BREAKAWAY_FROM_JOB | CREATE_DEFAULT_ERROR_MODE) // 调用Windows API创建进程 err := syscall.CreateProcess( nil, // lpApplicationName syscall.StringToUTF16Ptr(helperPath), // lpCommandLine nil, // lpProcessAttributes nil, // lpThreadAttributes false, // bInheritHandles flags, // dwCreationFlags nil, // lpEnvironment nil, // lpCurrentDirectory &si, // lpStartupInfo &pi, // lpProcessInformation ) if err != nil { globalLogger.Error("[辅助程序] 创建进程失败: %v", err) } else { // 保存进程句柄以便后续操作 helperProcess = &os.Process{Pid: int(pi.ProcessId)} // 关闭不需要的句柄 syscall.CloseHandle(syscall.Handle(pi.ThreadId)) syscall.CloseHandle(syscall.Handle(pi.ProcessId)) globalLogger.Info("[辅助程序] 已成功启动,进程ID: %d", pi.ProcessId) } } else { // 非Windows平台使用标准方法 cmd := exec.Command(helperPath) if err := cmd.Start(); err != nil { globalLogger.Error("[辅助程序] 启动失败: %v", err) } else { helperProcess = cmd.Process globalLogger.Info("[辅助程序] 已成功启动,进程ID: %d", helperProcess.Pid) } } // 添加延迟,确保辅助程序有足够时间启动 time.Sleep(1 * time.Second) // 验证辅助进程是否仍在运行 if helperProcess != nil { process, err := os.FindProcess(helperProcess.Pid) if err != nil { globalLogger.Error("无法查找辅助程序进程: %v", err) } else { globalLogger.Info("辅助程序进程验证成功: PID=%d", process.Pid) } } else { globalLogger.Warn("辅助程序进程句柄为空") } } // 优雅地关闭辅助程序 func shutdownHelperProgram() { if helperProcess != nil { globalLogger.Info("准备关闭辅助程序...") // 在Windows平台上使用更健壮的方式终止进程 if runtime.GOOS == "windows" { // 尝试通过Windows API打开进程 processHandle, err := syscall.OpenProcess(syscall.PROCESS_TERMINATE, false, uint32(helperProcess.Pid)) if err != nil { globalLogger.Error("无法打开辅助程序进程: %v", err) } else { defer syscall.CloseHandle(processHandle) // 先尝试使用TerminateProcess终止进程 if err := syscall.TerminateProcess(processHandle, 0); err != nil { globalLogger.Error("使用Windows API终止辅助程序失败: %v", err) } else { globalLogger.Info("使用Windows API成功终止辅助程序") // 等待一小段时间确保进程完全终止 time.Sleep(300 * time.Millisecond) } } // Windows 平台到此为止:helperProcess 是用 &os.Process{Pid:...} 手工构造的, // 没有有效的内部进程句柄。Go 1.25+ 对这种对象调用 Signal/Kill 会触发 // panic: handleTransientAcquire called in invalid mode,进而导致主程序在退出 // (含正常关闭窗口)时整体崩溃。Windows API 的 TerminateProcess 已是最终手段, // 直接返回,绝不能跌落到下面的通用 Signal/Kill 分支。 return } // 通用方法:先尝试优雅地终止进程(发送终止信号) err := helperProcess.Signal(syscall.SIGTERM) if err != nil { globalLogger.Error("无法发送终止信号到辅助程序: %v", err) // 如果优雅关闭失败,强制终止进程 err = helperProcess.Kill() if err != nil { globalLogger.Error("无法强制终止辅助程序: %v", err) } else { globalLogger.Info("已强制终止辅助程序") } } else { globalLogger.Info("已发送终止信号到辅助程序") // 等待一段时间让辅助程序有机会进行清理 time.Sleep(500 * time.Millisecond) } } else { globalLogger.Info("没有检测到运行中的辅助程序") } } func main() { // 初始化全局日志器 var err error // 不要使用os.Args[0],因为在Wails构建过程中它会返回临时文件名 // 直接使用固定的应用程序名称 exeName := "qiweimanager" // 添加调试信息,显示使用的exeName fmt.Fprintf(os.Stderr, "使用的应用程序名称(exeName): %s\n", exeName) globalLogger, err = logger.NewLogger(exeName, logEnabled, logLevel) if err != nil { // 如果初始化失败,使用标准错误输出 os.Stderr.WriteString(fmt.Sprintf("初始化日志系统失败: %v\n", err)) // 创建一个简单的控制台日志器作为备用 globalLogger = &logger.Logger{ Logger: log.New(os.Stderr, "", log.LstdFlags), LogLevel: logLevel, LogEnabled: true, } } else { defer globalLogger.Close() // 记录日志文件路径信息 fmt.Fprintf(os.Stderr, "日志系统初始化成功\n") } // 打印系统信息和架构 globalLogger.Debug("系统架构: %s", runtime.GOARCH) globalLogger.Debug("操作系统: %s", runtime.GOOS) // 初始化全局配置 if err := config.InitGlobalConfig(exeName, globalLogger); err != nil { globalLogger.Error("初始化配置失败: %v", err) } else { globalLogger.Info("配置系统初始化成功") } // 启动日志清理调度器,每天清理超过30天的旧日志 logDir := globalLogger.GetLogDir() globalLogger.Info("启动日志清理调度器,日志目录: %s", logDir) logger.StartLogCleanupScheduler(logDir, 30, 24*time.Hour) // 检查并关闭系统中已存在的helper.exe进程 checkAndCloseHelperProcess() // 尝试启动32位辅助程序 go startHelperProgram() // 允许应用程序在任何架构下运行 globalLogger.Debug("允许应用程序在当前架构下运行") // 打印调试信息 if globalLogger != nil { globalLogger.Info("Starting qiweimanager application...") } else { fmt.Fprintf(os.Stderr, "Warning: globalLogger is nil\n") } // Create an instance of the app structure globalLogger.Info("创建应用程序实例...") app := NewApp() globalLogger.Info("应用程序实例创建成功") // Create application with options globalLogger.Info("准备启动Wails应用...") err = wails.Run(&options.App{ Title: "灵泽万川企微售后客服", Width: 800, Height: 600, AssetServer: &assetserver.Options{ Assets: assets, }, // 使用更安全的背景色设置 BackgroundColour: &options.RGBA{R: 240, G: 240, B: 240, A: 255}, OnStartup: app.startup, OnBeforeClose: app.confirmArchivePendingAfterSalesBeforeClose, OnShutdown: func(ctx context.Context) { globalLogger.Info("Application is shutting down...") // 关闭辅助程序 shutdownHelperProgram() }, OnDomReady: func(ctx context.Context) { globalLogger.Info("DOM is ready!") }, Windows: &windows.Options{ // 设置有效的WebView2用户数据路径,避免初始化失败 WebviewUserDataPath: filepath.Join(os.TempDir(), "qiweimanager-webview"), // 禁用透明背景,提高稳定性 WebviewIsTransparent: false, }, // 可选:添加一些优化设置 SingleInstanceLock: &options.SingleInstanceLock{ UniqueId: "starbot-pro-application", }, Bind: []interface{}{ app, }, }) // 确保辅助程序在主程序退出时也退出 shutdownHelperProgram() if err != nil { globalLogger.Error("应用程序启动失败: %v", err) } else { globalLogger.Info("应用程序正常退出") } }