Initial qiwei secondary development handoff

This commit is contained in:
2026-06-23 21:11:20 +08:00
commit 858cb68f4f
207 changed files with 52782 additions and 0 deletions

373
main.go Normal file
View File

@@ -0,0 +1,373 @@
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)
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("应用程序正常退出")
}
}