Files
qiweimanager-master/main.go
yuanzhipeng a926ee6b1b chore(build): 更新.gitignore配置和清理Wails临时文件
- 添加dist/目录到.gitignore,用于排除打包输出的绿色免安装版
- 添加Wails打包过程中的临时文件和自动生成文件到.gitignore
- 删除build/windows/installer/wails_tools.nsh自动生成文件
- 添加Windows安装器临时目录和Webview2安装文件到忽略列表

feat(docs): 添加万川平台对接文档和产品素材

- 创建万川平台登录到获取模型信息的流程说明文档
- 添加万川平台对接实施计划文档
- 新增产品图片、公司简介图、宣传海报、教程截图、案例展示等素材文件

refactor(runtime): 扩展通知功能类型定义

- 添加NotificationOptions接口定义
- 添加NotificationAction接口定义
- 添加NotificationCategory接口定义
- 扩展通知相关的运行时API类型声明,包括初始化、发送、注册分类等功能
2026-06-25 18:13:11 +08:00

379 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("应用程序正常退出")
}
}