Initial qiwei secondary development handoff
This commit is contained in:
188
wecom_history_sync_windows.go
Normal file
188
wecom_history_sync_windows.go
Normal file
@@ -0,0 +1,188 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = windowsLazyDLL("user32.dll")
|
||||
procEnumWindows = user32.NewProc("EnumWindows")
|
||||
procGetWindowTextW = user32.NewProc("GetWindowTextW")
|
||||
procGetWindowTextLengthW = user32.NewProc("GetWindowTextLengthW")
|
||||
procIsWindowVisible = user32.NewProc("IsWindowVisible")
|
||||
procSetForegroundWindow = user32.NewProc("SetForegroundWindow")
|
||||
procShowWindow = user32.NewProc("ShowWindow")
|
||||
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
|
||||
procGetWindowThreadProcess = user32.NewProc("GetWindowThreadProcessId")
|
||||
procAttachThreadInput = user32.NewProc("AttachThreadInput")
|
||||
procGetCurrentThreadID = user32.NewProc("GetCurrentThreadId")
|
||||
procSendInput = user32.NewProc("SendInput")
|
||||
)
|
||||
|
||||
const (
|
||||
swRestore = 9
|
||||
inputKeyboard = 1
|
||||
keyeventfKeyUp = 0x0002
|
||||
vkControl = 0x11
|
||||
vkA = 0x41
|
||||
vkC = 0x43
|
||||
)
|
||||
|
||||
type keyboardInput struct {
|
||||
Type uint32
|
||||
Ki keyboardInputData
|
||||
}
|
||||
|
||||
type keyboardInputData struct {
|
||||
Vk uint16
|
||||
Scan uint16
|
||||
Flags uint32
|
||||
Time uint32
|
||||
ExtraInfo uintptr
|
||||
Padding [8]byte
|
||||
}
|
||||
|
||||
func (a *App) prepareWeComHistoryCopy() (bool, string) {
|
||||
hwnd, title := findWeComWindow()
|
||||
if hwnd == 0 {
|
||||
return false, "没有找到企业微信窗口。请先打开企业微信并进入目标群聊。"
|
||||
}
|
||||
if err := activateWindow(hwnd); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
sendCtrlKey(vkA)
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
sendCtrlKey(vkC)
|
||||
if strings.TrimSpace(title) == "" {
|
||||
title = "企业微信"
|
||||
}
|
||||
return true, fmt.Sprintf("已向企业微信窗口 %q 发送复制命令", title)
|
||||
}
|
||||
|
||||
func windowsLazyDLL(name string) *syscall.LazyDLL {
|
||||
return syscall.NewLazyDLL(name)
|
||||
}
|
||||
|
||||
func findWeComWindow() (uintptr, string) {
|
||||
type candidate struct {
|
||||
hwnd uintptr
|
||||
title string
|
||||
score int
|
||||
}
|
||||
var best candidate
|
||||
cb := syscall.NewCallback(func(hwnd uintptr, lparam uintptr) uintptr {
|
||||
if isWindowVisible(hwnd) == 0 {
|
||||
return 1
|
||||
}
|
||||
title := windowTitle(hwnd)
|
||||
lower := strings.ToLower(title)
|
||||
score := 0
|
||||
switch {
|
||||
case strings.Contains(title, "企业微信"):
|
||||
score = 100
|
||||
case strings.Contains(lower, "wxwork"):
|
||||
score = 80
|
||||
case strings.Contains(lower, "wecom"):
|
||||
score = 70
|
||||
}
|
||||
if score == 0 && windowProcessName(hwnd) == "wxwork.exe" {
|
||||
score = 90
|
||||
if title == "" {
|
||||
title = "企业微信"
|
||||
}
|
||||
}
|
||||
if score > best.score {
|
||||
best = candidate{hwnd: hwnd, title: title, score: score}
|
||||
}
|
||||
return 1
|
||||
})
|
||||
procEnumWindows.Call(cb, 0)
|
||||
return best.hwnd, best.title
|
||||
}
|
||||
|
||||
func windowTitle(hwnd uintptr) string {
|
||||
length, _, _ := procGetWindowTextLengthW.Call(hwnd)
|
||||
if length == 0 {
|
||||
return ""
|
||||
}
|
||||
buf := make([]uint16, int(length)+1)
|
||||
procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
|
||||
return syscall.UTF16ToString(buf)
|
||||
}
|
||||
|
||||
func windowProcessName(hwnd uintptr) string {
|
||||
var pid uint32
|
||||
procGetWindowThreadProcess.Call(hwnd, uintptr(unsafe.Pointer(&pid)))
|
||||
if pid == 0 {
|
||||
return ""
|
||||
}
|
||||
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer windows.CloseHandle(handle)
|
||||
buf := make([]uint16, windows.MAX_PATH)
|
||||
size := uint32(len(buf))
|
||||
if err := windows.QueryFullProcessImageName(handle, 0, &buf[0], &size); err != nil {
|
||||
return ""
|
||||
}
|
||||
path := strings.ToLower(syscall.UTF16ToString(buf[:size]))
|
||||
idx := strings.LastIndexAny(path, `\/`)
|
||||
if idx >= 0 {
|
||||
return path[idx+1:]
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func isWindowVisible(hwnd uintptr) uintptr {
|
||||
ret, _, _ := procIsWindowVisible.Call(hwnd)
|
||||
return ret
|
||||
}
|
||||
|
||||
func activateWindow(hwnd uintptr) error {
|
||||
procShowWindow.Call(hwnd, swRestore)
|
||||
foreground, _, _ := procGetForegroundWindow.Call()
|
||||
currentThread, _, _ := procGetCurrentThreadID.Call()
|
||||
foregroundThread, _, _ := procGetWindowThreadProcess.Call(foreground, 0)
|
||||
targetThread, _, _ := procGetWindowThreadProcess.Call(hwnd, 0)
|
||||
if foregroundThread != 0 {
|
||||
procAttachThreadInput.Call(currentThread, foregroundThread, 1)
|
||||
defer procAttachThreadInput.Call(currentThread, foregroundThread, 0)
|
||||
}
|
||||
if targetThread != 0 && targetThread != foregroundThread {
|
||||
procAttachThreadInput.Call(currentThread, targetThread, 1)
|
||||
defer procAttachThreadInput.Call(currentThread, targetThread, 0)
|
||||
}
|
||||
ret, _, _ := procSetForegroundWindow.Call(hwnd)
|
||||
if ret == 0 {
|
||||
return fmt.Errorf("无法激活企业微信窗口,请手动切到目标群聊后重试。")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendCtrlKey(vk uint16) {
|
||||
inputs := []keyboardInput{
|
||||
keyDown(vkControl),
|
||||
keyDown(vk),
|
||||
keyUp(vk),
|
||||
keyUp(vkControl),
|
||||
}
|
||||
procSendInput.Call(uintptr(len(inputs)), uintptr(unsafe.Pointer(&inputs[0])), unsafe.Sizeof(inputs[0]))
|
||||
}
|
||||
|
||||
func keyDown(vk uint16) keyboardInput {
|
||||
return keyboardInput{Type: inputKeyboard, Ki: keyboardInputData{Vk: vk}}
|
||||
}
|
||||
|
||||
func keyUp(vk uint16) keyboardInput {
|
||||
return keyboardInput{Type: inputKeyboard, Ki: keyboardInputData{Vk: vk, Flags: keyeventfKeyUp}}
|
||||
}
|
||||
Reference in New Issue
Block a user