189 lines
5.0 KiB
Go
189 lines
5.0 KiB
Go
//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}}
|
|
}
|