最近公司需求 用go写了一个自动模拟按键 添加客户微信的工具
浏览器可以直接使用以下方式来触发自动操作
暂时只适配了 dpi为1.0和1.5
<a href="ShangHao://13333333333">点击添加微信</a>
程序包含多个文件
main.go
package main
import (
"encoding/json"
"fmt"
"github.com/lxn/win"
"io"
"math"
"net/http"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"unsafe"
)
const VERSION = "0.0.5"
type INPUT struct {
Type uint32
Mi MOUSEINPUT
}
type MOUSEINPUT struct {
Dx int32
Dy int32
MouseData uint32
DwFlags uint32
Time uint32
DwExtraInfo uintptr
}
type POSITION struct {
AddressX int32
AddressY int32
NewContactX int32
NewContactY int32
SearchX int32
SearchY int32
MessageX int32
MessageY int32
AddX int32
AddY int32
ConfirmX int32
ConfirmY int32
//InputX int32
//InputY int32
}
type program struct{}
var (
phone string
position POSITION
)
const (
InputMouse = 0
MouseEventFLeftDown = 0x0002
MouseEventFLeftUp = 0x0004
)
func main() {
args := os.Args
//如果args[1]存在
if len(args) > 1 {
phone = args[1]
//正则去除非数字
reg, _ := regexp.Compile("[^0-9]+")
phone = reg.ReplaceAllString(phone, "")
versionUpgrade(phone)
} else {
versionUpgrade("")
fmt.Println("请输入手机号码")
os.Exit(1)
}
currentUser, _ := user.Current()
deskTopPath := filepath.Join(currentUser.HomeDir, "Desktop\\企业微信.lnk")
fmt.Printf("打开企业微信:%s\n", deskTopPath)
//检查是否存在
hwnd := win.FindWindow(syscall.StringToUTF16Ptr("WeWorkWindow"), syscall.StringToUTF16Ptr("企业微信"))
if hwnd == 0 {
// 未找到窗口
cmd := exec.Command("taskkill", "/F", "/IM", "WXWork.exe")
cmd.Run()
code, err := open(deskTopPath)
if code != 1 {
fmt.Print(err)
}
//打开企业微信成功
time.Sleep(5 * time.Second)
}
hwnd = win.FindWindow(syscall.StringToUTF16Ptr("WeWorkWindow"), syscall.StringToUTF16Ptr("企业微信"))
if hwnd == 0 {
fmt.Println("未找到企业微信窗口")
time.Sleep(5 * time.Second)
return
}
top := win.GetForegroundWindow()
if top != 0 {
win.ShowWindow(top, win.SW_MINIMIZE)
}
dpi := getDpi(hwnd)
fmt.Printf("屏幕的dpi:%.2f\n", dpi)
var err error
position, err = getPosition(dpi)
if err != nil {
fmt.Println(err)
time.Sleep(5 * time.Second)
os.Exit(1)
}
win.SendMessage(hwnd, win.WM_CLOSE, 0, 0)
//
win.ShowWindow(hwnd, win.SW_RESTORE) // 恢复窗口大小
min()
open(deskTopPath)
//获取企业微信路径
//开始点击
start()
if top != 0 {
win.ShowWindow(top, win.SW_RESTORE) // 恢复窗口大小
}
//检查是否有浏览器窗口
}
func min() {
screenWidth := int32(win.GetSystemMetrics(win.SM_CXSCREEN))
screenHeight := int32(win.GetSystemMetrics(win.SM_CYSCREEN))
fmt.Println(screenWidth, screenHeight)
mouseMove(int(screenWidth), int(screenHeight))
}
// 获取设备dpi
func getDpi(hwnd win.HWND) float64 {
err := setDpiAwareness()
if err != nil {
fmt.Println("无法设置 DPI 感知模式:", err)
return 0
}
factor, err := getScalingFactor(hwnd)
if err != nil {
return 0
}
return factor
//fmt.Printf("缩放比例: %.2f\n", factor)
}
// 打开企业微信
func open(path string) (int, string) {
cmd := exec.Command("cmd", "/C", "start", path)
//判断文件是否存在
_, err := os.Stat(path)
fmt.Print(err)
if err != nil {
return 0, err.Error()
}
err = cmd.Run()
if err != nil {
fmt.Println("Error opening shortcut:", err)
return 0, err.Error()
}
return 1, "success"
}
func mouseMove(x int, y int) {
//循环,保持鼠标位置
win.SetCursorPos(int32(x), int32(y))
for {
var mousePos win.POINT
win.GetCursorPos(&mousePos)
if !win.GetCursorPos(&mousePos) {
// 获取鼠标位置失败
break
}
//fmt.Printf("鼠标位置:%d,%d,目标位置:%d,%d\n", mousePos.X, mousePos.Y, x, y)
// 获取鼠标位置
if math.Abs(float64(x)-float64(mousePos.X)) <= 2 && math.Abs(float64(y)-float64(mousePos.Y)) <= 2 {
// 鼠标位置正确,执行点击操作
inputDown := INPUT{
Type: InputMouse,
Mi: MOUSEINPUT{
DwFlags: MouseEventFLeftDown,
},
}
win.SendInput(1, unsafe.Pointer(&inputDown), int32(unsafe.Sizeof(inputDown)))
break
}
win.SetCursorPos(int32(x), int32(y))
}
// 等待一段时间,模拟按下持续时间
time.Sleep(100 * time.Millisecond)
for {
var mousePos win.POINT
win.GetCursorPos(&mousePos)
if !win.GetCursorPos(&mousePos) {
// 获取鼠标位置失败
break
}
// 获取鼠标位置
if math.Abs(float64(x)-float64(mousePos.X)) <= 2 && math.Abs(float64(y)-float64(mousePos.Y)) <= 2 {
// 鼠标位置正确,执行点击操作
// 模拟鼠标左键释放
inputUp := INPUT{
Type: InputMouse,
Mi: MOUSEINPUT{
DwFlags: MouseEventFLeftUp,
},
}
win.SendInput(1, unsafe.Pointer(&inputUp), int32(unsafe.Sizeof(inputUp)))
break
}
win.SetCursorPos(int32(x), int32(y))
}
time.Sleep(500 * time.Millisecond)
}
func GetHwnd(hwnd win.HWND) win.HWND {
//var p win.POINT
//win.GetCursorPos(&p)
//hwnd := win.WindowFromPoint(p)
for win.GetParent(hwnd) != 0 {
hwnd = win.GetParent(hwnd)
}
return hwnd
}
func topWindow(hwnd win.HWND, windowWidth int32, windowHeight int32) (int32, int32, int32, int32) {
//hwnd = GetHwnd(hwnd)
// 获取屏幕的宽度和高度
screenWidth := int32(win.GetSystemMetrics(win.SM_CXSCREEN))
screenHeight := int32(win.GetSystemMetrics(win.SM_CYSCREEN))
// 计算窗口的位置和大小
//windowWidth := int32(1539) // 设置窗口宽度
//windowHeight := int32(1000) // 设置窗口高度
windowX := int32(screenWidth-windowWidth) / 2 // 计算窗口的左上角X坐标
windowY := int32(screenHeight-windowHeight) / 2 // 计算窗口的左上角Y坐标
win.SetForegroundWindow(hwnd) // 将窗口置于前台
////////
win.SetWindowPos(hwnd, win.HWND_TOPMOST, 0, 0, 0, 0, win.SWP_NOMOVE|win.SWP_NOSIZE)
time.Sleep(500 * time.Millisecond)
win.MoveWindow(hwnd, windowX, windowY, windowWidth, windowHeight, true) // 移动窗口位置和大小
// 设置窗口位置和大小
////////////////////////////////////////////////////////////////
// win.AnimateWindow(hwnd, 1, win.AW_HIDE|win.AW_CENTER)
// //win.AnimateWindow(hwnd, 1, win.AW_CENTER)
//
// time.Sleep(500 * time.Millisecond)
//
// win.SetWindowPos(hwnd, win.HWND_TOPMOST, windowX, windowY, windowWidth, windowHeight, win.SWP_NOMOVE|win.SWP_NOSIZE)
//
// win.MoveWindow(hwnd, windowX, windowY, windowWidth, windowHeight, true)
//
//win.SetForegroundWindow(hwnd) // 将窗口置于前台
//
// win.AnimateWindow(hwnd, 700, win.AW_BLEND) // 渐显
//
// time.Sleep(500 * time.Millisecond)
return windowX, windowY, windowWidth, windowHeight
}
func start() {
fmt.Printf("搜索企业微信窗口句柄...\n")
hwnd := win.FindWindow(syscall.StringToUTF16Ptr("WeWorkWindow"), syscall.StringToUTF16Ptr("企业微信"))
if hwnd == 0 {
// 未找到窗口
fmt.Printf("搜索失败,未找到窗口句柄。\n")
return
}
//窗口置顶 并居中固定大小
windowsX, windowsY, _, _ := topWindow(hwnd, 1539, 1000)
// 点击通讯录 40-654
mouseMove(int(windowsX+position.AddressX), int(windowsY+position.AddressY))
////点击添加新联系人
mouseMove(int(windowsX+position.NewContactX), int(windowsY+position.NewContactY))
//点击添加联系人 弹出搜索框
mouseMove(int(windowsX+position.SearchX), int(windowsY+position.SearchY))
time.Sleep(1 * time.Second)
searchHwnd := win.FindWindow(syscall.StringToUTF16Ptr("SearchExternalsWnd"), nil)
if searchHwnd == 0 {
// 未找到窗口
fmt.Printf("搜索外部联系人添加窗口句柄失败。\n")
restore(windowsX, windowsY)
return
}
fmt.Printf("搜索外部联系人添加窗口句柄成功...\n")
fmt.Printf("输入字符...\n")
//win.SetForegroundWindow(searchHwnd) // 将窗口置于前台
time.Sleep(500 * time.Millisecond)
searchX, searchY, _, _ := topWindow(searchHwnd, 600, 462)
win.SetFocus(searchHwnd)
//time.Sleep(500 * time.Millisecond)
//mouseMove(int(windowsX+position.InputX), int(windowsY+position.InputY))
fmt.Printf("点击输入框...\n")
// 循环发送每个字符的键盘消息
for _, c := range phone {
win.SendMessage(searchHwnd, win.WM_CHAR, uintptr(c), 0)
}
// 发送Enter键消息
win.SendMessage(searchHwnd, win.WM_KEYDOWN, uintptr(win.VK_RETURN), 0)
win.SendMessage(searchHwnd, win.WM_KEYUP, uintptr(win.VK_RETURN), 0)
time.Sleep(1000 * time.Millisecond)
win.SetForegroundWindow(searchHwnd) // 将窗口置于前台
time.Sleep(500 * time.Millisecond)
//点击添加
mouseMove(int(searchX+position.AddX), int(searchY+position.AddY))
time.Sleep(500 * time.Millisecond)
//添加确认框句柄搜索
inputReasonWndHwnd := win.FindWindow(syscall.StringToUTF16Ptr("InputReasonWnd"), nil)
if inputReasonWndHwnd == 0 {
// 未找到窗口
fmt.Printf("搜索添加确认框句柄搜索失败。\n")
restore(windowsX, windowsY)
return
}
//inputReasonX, inputReasonY, _, _ := topWindow(inputReasonWndHwnd, 589, 366)
time.Sleep(500 * time.Millisecond)
mouseMove(int(searchX+position.ConfirmX), int(searchY+position.ConfirmY))
restore(windowsX, windowsY)
}
// 恢复
func restore(windowsX, windowsY int32) {
notifyHwnd := win.FindWindow(syscall.StringToUTF16Ptr("WeWorkMessageBoxFrame"), nil)
if notifyHwnd != 0 {
win.PostMessage(notifyHwnd, win.WM_CLOSE, 0, 0)
}
searchHwnd := win.FindWindow(syscall.StringToUTF16Ptr("SearchExternalsWnd"), nil)
if searchHwnd != 0 {
// 发送关闭消息
win.SendMessage(searchHwnd, win.WM_CLOSE, 0, 0)
}
time.Sleep(500 * time.Millisecond)
////重新点回消息列表
//mouseMove(int(windowsX+position.MessageX), int(windowsY+position.MessageY))
//
//time.Sleep(500 * time.Millisecond)
hwnd := win.FindWindow(syscall.StringToUTF16Ptr("WeWorkWindow"), syscall.StringToUTF16Ptr("企业微信"))
if hwnd == 0 {
// 未找到窗口
fmt.Printf("搜索失败,未找到窗口句柄。\n")
return
}
win.SendMessage(hwnd, win.WM_CLOSE, 0, 0)
//
win.ShowWindow(hwnd, win.SW_RESTORE) // 恢复窗口大小
//win.ShowWindow(hwnd, win.SW_MINIMIZE)
//检查是否有提示框,如果有关闭它
}
type version struct {
Version string `json:"version"`
DownloadUrl string `json:"downloadUrl"`
}
func compareVersion(version1 string, version2 string) int {
var res int
// 通过字符串切割会生成一个数组
ver1Strs := strings.Split(version1, ".") //[1 2 3]
ver2Strs := strings.Split(version2, ".") //[2 3 4 5]
ver1Len := len(ver1Strs)
ver2Len := len(ver2Strs)
//fmt.Println(ver1Strs, ver2Strs)
verLen := ver1Len
if len(ver1Strs) < len(ver2Strs) {
verLen = ver2Len
}
for i := 0; i < verLen; i++ {
var ver1Int, ver2Int int
if i < ver1Len {
// 字符串转换成整数strconv.Atoi
ver1Int, _ = strconv.Atoi(ver1Strs[i])
}
if i < ver2Len {
ver2Int, _ = strconv.Atoi(ver2Strs[i])
}
if ver1Int < ver2Int {
res = -1
break
}
if ver1Int > ver2Int {
res = 1
break
}
}
return res
}
func versionUpgrade(phone string) {
url := "https://xxxx/version.json"
fmt.Printf("当前版本:%s\n", VERSION)
resp, err := http.Get(url)
if err != nil {
fmt.Printf("获取版本失败:%s\n", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("读取版本失败:%s\n", err)
}
var data version
err = json.Unmarshal(body, &data)
if err != nil {
fmt.Printf("解析版本失败:%s\n", err)
return
}
fmt.Printf("当前版本:%s,远程版本:%s\n", VERSION, data.Version)
//检查本地版本是否小于远程版本
if compareVersion(VERSION, data.Version) >= 0 {
return
}
fmt.Printf("需要更新\n")
//更新
file, err := http.Get(data.DownloadUrl)
if err != nil {
fmt.Printf("访问下载连接失败:%s\n", err)
return
}
defer file.Body.Close()
fileBytes, err := io.ReadAll(file.Body)
if err != nil {
fmt.Printf("更新失败:%s\n", err)
return
}
path, _ := os.Executable()
path = filepath.Dir(path)
dir := filepath.Join(path, "version")
fmt.Println(dir)
_ = os.Mkdir(dir, 0644)
err = os.WriteFile(filepath.Join(dir, "tmp.exe"), fileBytes, 0644)
if err != nil {
fmt.Printf("保存失败:%s\n", err)
return
}
fmt.Printf("保存成功\n")
var cmd *exec.Cmd
upgradePath := filepath.Join(path, "Upgrade.exe")
if phone != "" {
cmd = exec.Command("cmd.exe", "/C", "start", "/B", upgradePath, phone)
} else {
cmd = exec.Command("cmd.exe", "/C", "start", "/B", upgradePath)
}
if err = cmd.Start(); err != nil {
fmt.Println(err)
}
fmt.Println("拉起更新程序")
os.Exit(0)
}
position.go
package main
import (
"errors"
)
func getPosition(dpi float64) (POSITION, error) {
if dpi == 1.0 {
return POSITION{33, 445, 150, 80, 1473, 40, 33, 92, 449, 212, 300, 270}, nil
} else if dpi == 1.5 {
return POSITION{40, 654, 240, 120, 1470, 60, 50, 130, 520, 210, 300, 311}, nil
}
//抛出错误
return POSITION{}, errors.New("dpi must be 1.0 or 1.5")
}
windows.go
package main
import (
"github.com/lxn/win"
"syscall"
"unsafe"
)
var (
user32 = syscall.NewLazyDLL("user32.dll")
shcore = syscall.NewLazyDLL("shcore.dll")
getDpiForWindow = user32.NewProc("GetDpiForWindow")
getProcessDpiAwareness = shcore.NewProc("GetProcessDpiAwareness")
setProcessDpiAwareness = shcore.NewProc("SetProcessDpiAwareness")
processPerMonitorDpiAwareness = 2
procBlockInput = user32.NewProc("BlockInput")
)
func getDpiAwareness() (int, error) {
var awareness uintptr
r, _, err := getProcessDpiAwareness.Call(uintptr(0), uintptr(unsafe.Pointer(&awareness)))
if r != 0 {
return 0, err
}
return int(awareness), nil
}
func setDpiAwareness() error {
r, _, err := setProcessDpiAwareness.Call(uintptr(processPerMonitorDpiAwareness))
if r != 0 {
return err
}
return nil
}
func getScalingFactor(hwnd win.HWND) (float64, error) {
awareness, err := getDpiAwareness()
if err != nil {
return 0, err
}
if awareness == processPerMonitorDpiAwareness {
hdc := win.GetDC(hwnd)
defer win.ReleaseDC(hwnd, hdc)
dpi := win.GetDeviceCaps(hdc, win.LOGPIXELSX)
return float64(dpi) / 96.0, nil
}
hwndDpi, _, _ := getDpiForWindow.Call(uintptr(hwnd))
return float64(hwndDpi) / 96.0, nil
}
上面程序打包得到 ShangHao.exe
与之配套的,还有一个Upgrade程序
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
)
func main() {
fmt.Println("upgrade....")
fmt.Println("rename:")
path, err := os.Executable()
if err != nil {
return
}
path = filepath.Dir(path)
dir := filepath.Join(path, "version")
err = os.Rename(filepath.Join(dir, "tmp.exe"), filepath.Join(path, "ShangHao.exe"))
if err != nil {
return
}
fmt.Println(dir, path)
if err != nil {
return
}
fmt.Println("done")
time.Sleep(1 * time.Second)
args := os.Args
if len(args) > 1 {
phone := args[1]
var cmd *exec.Cmd
shanghao := filepath.Join(path, "ShangHao.exe")
if phone != "" {
cmd = exec.Command("cmd.exe", "/C", "start", shanghao, phone)
} else {
cmd = exec.Command("cmd.exe", "/C", "start", shanghao)
}
err := cmd.Start()
if err != nil {
return
}
} else {
os.Exit(0)
}
//重新吊起源程序
}
打包得到Upgrade.exe
将两个文件放到一起,放到任意目录下,并创建一个批处理文件,管理员权限执行批处理文件
@echo off
setlocal
set "script_dir=%~dp0"
reg delete HKEY_CLASSES_ROOT\ShangHao /f
echo %errorlevel%
reg add HKEY_CLASSES_ROOT\ShangHao /ve /d "URL:ShangHao Protocol Handler" /f
echo %errorlevel%
reg add HKEY_CLASSES_ROOT\ShangHao /v "URL Protocol" /t REG_SZ /d "" /f
echo %errorlevel%
reg add HKEY_CLASSES_ROOT\ShangHao\DefaultIcon /ve /d "%script_dir%ShangHao.exe" /f
echo %errorlevel%
reg add HKEY_CLASSES_ROOT\ShangHao\shell /ve /f
echo %errorlevel%
reg add HKEY_CLASSES_ROOT\ShangHao\shell\open /ve /f
echo %errorlevel%
reg add HKEY_CLASSES_ROOT\ShangHao\shell\open\command /ve /d "\"%script_dir%ShangHao.exe\" \"%%1\"" /f
echo %errorlevel%
endlocal
脚本执行完成后 就可以在浏览器直接调用exe并传参了
比如 企业微信添加1333333333这个手机号对应的微信
<a href="ShangHao://13333333333">点击添加微信</a>