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

View File

@@ -0,0 +1,337 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"unsafe"
)
const fallbackDLLVersion = "4.1.33.6009"
const legacyLoaderSHA256 = "ce4557bf449fd53078f2eaa9becbf43b6f2bf40f16199e1a4bd6088ab233a65a"
type DLLBundle struct {
HelperPath string
LoaderPath string
HelperVersion string
LoaderVersion string
WxWorkPath string
WxWorkVersion string
Compatible bool
Message string
}
type vsFixedFileInfo struct {
Signature uint32
StrucVersion uint32
FileVersionMS uint32
FileVersionLS uint32
ProductVersionMS uint32
ProductVersionLS uint32
FileFlagsMask uint32
FileFlags uint32
FileOS uint32
FileType uint32
FileSubtype uint32
FileDateMS uint32
FileDateLS uint32
}
func resolveDLLBundle() DLLBundle {
exeDir := "."
if exePath, err := os.Executable(); err == nil {
exeDir = filepath.Dir(exePath)
}
wxWorkPath := detectWxWorkPath()
if wxWorkPath == "" {
wxWorkPath = `C:\企业微信\WXWork\WXWork.exe`
}
wxWorkVersion := ""
if _, err := os.Stat(wxWorkPath); err == nil {
wxWorkVersion, _ = getWindowsFileVersion(wxWorkPath)
}
versions := make([]string, 0, 4)
if wxWorkVersion != "" {
versions = append(versions, wxWorkVersion)
}
versions = append(versions, fallbackDLLVersion)
versions = append(versions, scanDLLVersions(exeDir)...)
seen := make(map[string]bool)
for _, version := range versions {
version = strings.TrimSpace(version)
if version == "" || seen[version] {
continue
}
seen[version] = true
helperPath := filepath.Join(exeDir, "Helper_"+version+".dll")
loaderPath := filepath.Join(exeDir, "Loader_"+version+".dll")
if fileExists(helperPath) && fileExists(loaderPath) {
compatible := wxWorkVersion != "" && sameVersionFamily(wxWorkVersion, version)
message := ""
if !compatible && wxWorkVersion != "" {
message = fmt.Sprintf("WXWork %s is not compatible with Helper/Loader %s. Put Helper_%s.dll and Loader_%s.dll in build/bin to enable account/message callbacks.", wxWorkVersion, version, wxWorkVersion, wxWorkVersion)
}
if compatible && version != fallbackDLLVersion {
fallbackLoaderPath := filepath.Join(exeDir, "Loader_"+fallbackDLLVersion+".dll")
if sameFileContent(loaderPath, fallbackLoaderPath) || fileMatchesSHA256(loaderPath, legacyLoaderSHA256) {
compatible = false
message = fmt.Sprintf("Loader_%s.dll has the same content as Loader_%s.dll. Replace it with the real Loader_%s.dll before starting WXWork.", version, fallbackDLLVersion, version)
}
}
return DLLBundle{
HelperPath: helperPath,
LoaderPath: loaderPath,
HelperVersion: version,
LoaderVersion: version,
WxWorkPath: wxWorkPath,
WxWorkVersion: wxWorkVersion,
Compatible: compatible,
Message: message,
}
}
}
message := "No Helper/Loader DLL pair found."
if wxWorkVersion != "" {
message = fmt.Sprintf("No Helper_%s.dll and Loader_%s.dll found in %s.", wxWorkVersion, wxWorkVersion, exeDir)
}
return DLLBundle{
WxWorkPath: wxWorkPath,
WxWorkVersion: wxWorkVersion,
Message: message,
}
}
func detectWxWorkPath() string {
if path := getRunningProcessImagePath("WXWork.exe"); path != "" {
return path
}
if path, err := getWxWorkInstallPath(); err == nil && fileExists(path) {
return path
}
candidates := []string{
`C:\Program Files (x86)\WXWork\WXWork.exe`,
`C:\Program Files\WXWork\WXWork.exe`,
`C:\企业微信\WXWork\WXWork.exe`,
}
for _, path := range candidates {
if fileExists(path) {
return path
}
}
return ""
}
func getRunningProcessImagePath(processName string) string {
hSnapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
if err != nil {
return ""
}
defer syscall.CloseHandle(hSnapshot)
var pe32 syscall.ProcessEntry32
pe32.Size = uint32(unsafe.Sizeof(pe32))
if err := syscall.Process32First(hSnapshot, &pe32); err != nil {
return ""
}
for {
name := syscall.UTF16ToString(pe32.ExeFile[:])
if strings.EqualFold(name, processName) {
if path := queryProcessImagePath(pe32.ProcessID); path != "" {
return path
}
}
if err := syscall.Process32Next(hSnapshot, &pe32); err != nil {
break
}
}
return ""
}
func queryProcessImagePath(pid uint32) string {
const processQueryLimitedInformation = 0x1000
handle, err := syscall.OpenProcess(processQueryLimitedInformation, false, pid)
if err != nil {
return ""
}
defer syscall.CloseHandle(handle)
kernel32 := syscall.NewLazyDLL("kernel32.dll")
queryFullProcessImageName := kernel32.NewProc("QueryFullProcessImageNameW")
buf := make([]uint16, 32768)
size := uint32(len(buf))
ret, _, _ := queryFullProcessImageName.Call(
uintptr(handle),
0,
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
)
if ret == 0 || size == 0 {
return ""
}
return syscall.UTF16ToString(buf[:size])
}
func sameFileContent(pathA, pathB string) bool {
infoA, errA := os.Stat(pathA)
infoB, errB := os.Stat(pathB)
if errA != nil || errB != nil || infoA.Size() != infoB.Size() {
return false
}
hashA, errA := fileSHA256(pathA)
hashB, errB := fileSHA256(pathB)
return errA == nil && errB == nil && hashA == hashB
}
func fileMatchesSHA256(path string, expected string) bool {
hash, err := fileSHA256(path)
return err == nil && strings.EqualFold(hex.EncodeToString(hash[:]), expected)
}
func fileSHA256(path string) ([32]byte, error) {
var zero [32]byte
f, err := os.Open(path)
if err != nil {
return zero, err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return zero, err
}
var sum [32]byte
copy(sum[:], h.Sum(nil))
return sum, nil
}
func scanDLLVersions(dir string) []string {
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
hasHelper := make(map[string]bool)
hasLoader := make(map[string]bool)
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
lower := strings.ToLower(name)
if strings.HasPrefix(lower, "helper_") && strings.HasSuffix(lower, ".dll") {
hasHelper[versionFromDLLName(name)] = true
}
if strings.HasPrefix(lower, "loader_") && strings.HasSuffix(lower, ".dll") {
hasLoader[versionFromDLLName(name)] = true
}
}
versions := make([]string, 0)
for version := range hasHelper {
if version != "" && hasLoader[version] {
versions = append(versions, version)
}
}
return versions
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func getWxWorkVersionDiagnostics() map[string]interface{} {
bundle := resolveDLLBundle()
return map[string]interface{}{
"wxWorkPath": bundle.WxWorkPath,
"wxWorkVersion": bundle.WxWorkVersion,
"helperPath": bundle.HelperPath,
"loaderPath": bundle.LoaderPath,
"helperVersion": bundle.HelperVersion,
"loaderVersion": bundle.LoaderVersion,
"compatible": bundle.Compatible,
"message": bundle.Message,
}
}
func versionFromDLLName(path string) string {
name := filepath.Base(path)
name = strings.TrimSuffix(name, filepath.Ext(name))
idx := strings.LastIndex(name, "_")
if idx < 0 || idx+1 >= len(name) {
return ""
}
return strings.TrimSpace(name[idx+1:])
}
func sameVersionFamily(actual string, expected string) bool {
actualParts := strings.Split(actual, ".")
expectedParts := strings.Split(expected, ".")
if len(actualParts) < 3 || len(expectedParts) < 3 {
return actual == expected
}
return actualParts[0] == expectedParts[0] &&
actualParts[1] == expectedParts[1] &&
actualParts[2] == expectedParts[2]
}
func getWindowsFileVersion(path string) (string, error) {
versionDLL := syscall.NewLazyDLL("version.dll")
getFileVersionInfoSize := versionDLL.NewProc("GetFileVersionInfoSizeW")
getFileVersionInfo := versionDLL.NewProc("GetFileVersionInfoW")
verQueryValue := versionDLL.NewProc("VerQueryValueW")
pathPtr, err := syscall.UTF16PtrFromString(path)
if err != nil {
return "", err
}
var handle uint32
size, _, _ := getFileVersionInfoSize.Call(
uintptr(unsafe.Pointer(pathPtr)),
uintptr(unsafe.Pointer(&handle)),
)
if size == 0 {
return "", fmt.Errorf("GetFileVersionInfoSizeW returned 0")
}
buf := make([]byte, size)
ret, _, err := getFileVersionInfo.Call(
uintptr(unsafe.Pointer(pathPtr)),
0,
size,
uintptr(unsafe.Pointer(&buf[0])),
)
if ret == 0 {
return "", fmt.Errorf("GetFileVersionInfoW failed: %v", err)
}
root, err := syscall.UTF16PtrFromString(`\`)
if err != nil {
return "", err
}
var block uintptr
var blockLen uint32
ret, _, err = verQueryValue.Call(
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(root)),
uintptr(unsafe.Pointer(&block)),
uintptr(unsafe.Pointer(&blockLen)),
)
if ret == 0 || block == 0 {
return "", fmt.Errorf("VerQueryValueW failed: %v", err)
}
info := (*vsFixedFileInfo)(unsafe.Pointer(block))
major := info.FileVersionMS >> 16
minor := info.FileVersionMS & 0xffff
build := info.FileVersionLS >> 16
patch := info.FileVersionLS & 0xffff
return fmt.Sprintf("%d.%d.%d.%d", major, minor, build, patch), nil
}