chore: migrate and structure cmbone project
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"golang.design/x/hotkey"
|
||||
)
|
||||
|
||||
const (
|
||||
CF_UNICODETEXT = 13
|
||||
CF_DIB = 8
|
||||
GMEM_MOVEABLE = 0x0002
|
||||
//WM_HOTKEY = 0x0312
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
|
||||
procSetForeground = user32.NewProc("SetForegroundWindow")
|
||||
procShowWindow = user32.NewProc("ShowWindow")
|
||||
procGetForeground = user32.NewProc("GetForegroundWindow")
|
||||
)
|
||||
|
||||
const (
|
||||
SW_RESTORE = 9
|
||||
)
|
||||
|
||||
func SetForegroundWindow(hwnd uintptr) {
|
||||
procSetForeground.Call(hwnd)
|
||||
}
|
||||
|
||||
func ShowWindow(hwnd uintptr, nCmdShow int) {
|
||||
procShowWindow.Call(hwnd, uintptr(nCmdShow))
|
||||
}
|
||||
|
||||
func GetForegroundWindow() uintptr {
|
||||
hwnd, _, _ := procGetForeground.Call()
|
||||
return hwnd
|
||||
}
|
||||
|
||||
func ActivateApp() {
|
||||
hwnd := GetForegroundWindow() // 或你自己保存的主窗口句柄
|
||||
if hwnd != 0 {
|
||||
ShowWindow(hwnd, SW_RESTORE)
|
||||
SetForegroundWindow(hwnd)
|
||||
}
|
||||
}
|
||||
|
||||
// 改用"golang.design/x/hotkey" 调用热键,直接调用windows API会无法监听热键消息
|
||||
type HotKeyCallback func()
|
||||
|
||||
type hotkeyEntry struct {
|
||||
hk *hotkey.Hotkey
|
||||
key uint32
|
||||
mod uint32
|
||||
}
|
||||
|
||||
var (
|
||||
callbacks = make(map[int]hotkeyEntry) //make(map[int]*hotkey.Hotkey)
|
||||
cbHandlers = make(map[int]HotKeyCallback)
|
||||
idCounter = 1
|
||||
mutex sync.RWMutex
|
||||
startOnce sync.Once
|
||||
)
|
||||
|
||||
// RegisterHotKeyWithCallback 注册热键并绑定回调(使用 golang.design/x/hotkey 实现)
|
||||
func RegisterHotKeyWithCallback(keyCode, modifiers uint32, cb HotKeyCallback) {
|
||||
mod := toHotkeyModifiers(modifiers)
|
||||
key := toHotkeyKey(keyCode)
|
||||
|
||||
hk := hotkey.New(mod, key)
|
||||
err := hk.Register()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 热键注册失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
localID := idCounter
|
||||
idCounter++
|
||||
|
||||
mutex.Lock()
|
||||
callbacks[localID] = hotkeyEntry{hk: hk, key: keyCode, mod: modifiers} // hk
|
||||
cbHandlers[localID] = cb
|
||||
mutex.Unlock()
|
||||
|
||||
// 启动监听线程
|
||||
go func(id int, h *hotkey.Hotkey, cb HotKeyCallback) {
|
||||
for {
|
||||
<-h.Keydown()
|
||||
fmt.Printf("✅ 热键触发 (id=%d)\n", id)
|
||||
cb()
|
||||
}
|
||||
}(localID, hk, cb)
|
||||
// 仅启动一次监听线程
|
||||
// 启动统一监听线程(只启动一次)
|
||||
//startOnce.Do(startListener)
|
||||
|
||||
fmt.Printf("✈️ 注册热键 (id=%d, key=%v, mod=%v)\n", localID, key, mod)
|
||||
|
||||
}
|
||||
|
||||
// UnregisterHotKey 注销所有已注册热键
|
||||
func UnregisterHotKey(_keyCode, _modifiers uint32) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
for id, hk := range callbacks {
|
||||
if _keyCode == hk.key && _modifiers == hk.mod {
|
||||
_ = hk.hk.Unregister() // 注销热键
|
||||
delete(callbacks, id) // 删除回调
|
||||
//delete(cbHandlers, id) // 删除回调处理器
|
||||
fmt.Printf("🧹 注销热键 (id=%d, key=%v ", id, _keyCode)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func toHotkeyModifiers(modifiers uint32) []hotkey.Modifier {
|
||||
var mods []hotkey.Modifier
|
||||
if modifiers&1 != 0 {
|
||||
mods = append(mods, hotkey.ModAlt)
|
||||
}
|
||||
if modifiers&2 != 0 {
|
||||
mods = append(mods, hotkey.ModCtrl)
|
||||
}
|
||||
if modifiers&4 != 0 {
|
||||
mods = append(mods, hotkey.ModShift)
|
||||
}
|
||||
if modifiers&8 != 0 {
|
||||
mods = append(mods, hotkey.ModWin)
|
||||
}
|
||||
return mods
|
||||
}
|
||||
|
||||
func toHotkeyKey(keyCode uint32) hotkey.Key {
|
||||
return hotkey.Key(keyCode)
|
||||
}
|
||||
|
||||
func SimulateCmdC() {
|
||||
|
||||
}
|
||||
|
||||
func HideDock() {
|
||||
// Windows 不需要隐藏 Dock 图标
|
||||
// 但可以在这里添加其他逻辑
|
||||
fmt.Println("Windows 平台不支持隐藏 Dock 图标")
|
||||
}
|
||||
|
||||
// OCR功能 Windows OCR实现较复杂,暂时使用PowerShell脚本调用Windows OCR API进行识别 (需要Windows 10或更高版本)
|
||||
const ocrScript = `
|
||||
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
|
||||
$OutputEncoding=[System.Text.Encoding]::UTF8
|
||||
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
||||
|
||||
[void][Windows.Graphics.Imaging.BitmapDecoder, Windows.Graphics, ContentType=WindowsRuntime]
|
||||
[void][Windows.Graphics.Imaging.SoftwareBitmap, Windows.Graphics, ContentType=WindowsRuntime]
|
||||
[void][Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType=WindowsRuntime]
|
||||
[void][Windows.Media.Ocr.OcrResult, Windows.Foundation, ContentType=WindowsRuntime]
|
||||
[void][Windows.Storage.Streams.InMemoryRandomAccessStream, Windows.Storage, ContentType=WindowsRuntime]
|
||||
[void][Windows.Storage.Streams.DataWriter, Windows.Storage, ContentType=WindowsRuntime]
|
||||
|
||||
function Await($WinRtTask, $ResultType) {
|
||||
$methods = [System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object {
|
||||
$_.Name -eq 'AsTask' -and $_.IsGenericMethodDefinition -and $_.GetParameters().Count -eq 1
|
||||
}
|
||||
$method = $methods[0].MakeGenericMethod($ResultType)
|
||||
$netTask = $method.Invoke($null, @($WinRtTask))
|
||||
$netTask.Wait(-1) | Out-Null
|
||||
$netTask.Result
|
||||
}
|
||||
|
||||
# 用 System.IO 直接读字节,不依赖 StorageFile
|
||||
$bytes = [System.IO.File]::ReadAllBytes('{{IMAGE_PATH}}')
|
||||
|
||||
# 写入 InMemoryRandomAccessStream
|
||||
$stream = [Windows.Storage.Streams.InMemoryRandomAccessStream]::new()
|
||||
$writer = [Windows.Storage.Streams.DataWriter]::new($stream)
|
||||
$writer.WriteBytes($bytes)
|
||||
$storeTask = $writer.StoreAsync()
|
||||
$storeAsTask = [System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object {
|
||||
$_.Name -eq 'AsTask' -and $_.IsGenericMethodDefinition -and $_.GetParameters().Count -eq 1
|
||||
} | Select-Object -First 1
|
||||
$storeNetTask = $storeAsTask.MakeGenericMethod([uint32]).Invoke($null, @($storeTask))
|
||||
$storeNetTask.Wait(-1) | Out-Null
|
||||
$stream.Seek(0)
|
||||
|
||||
$decoder = Await ([Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream)) ([Windows.Graphics.Imaging.BitmapDecoder])
|
||||
$bitmap = Await ($decoder.GetSoftwareBitmapAsync()) ([Windows.Graphics.Imaging.SoftwareBitmap])
|
||||
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
|
||||
$result = Await ($engine.RecognizeAsync($bitmap)) ([Windows.Media.Ocr.OcrResult])
|
||||
Write-Output $result.Text
|
||||
`
|
||||
|
||||
func RecognizeImageBase64(base64str string) (string, error) {
|
||||
if idx := strings.Index(base64str, ","); idx != -1 {
|
||||
base64str = base64str[idx+1:]
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(base64str)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("base64 decode failed: %w", err)
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp("", "ocr-*.png")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create temp file failed: %w", err)
|
||||
}
|
||||
imgPath := tmp.Name()
|
||||
tmp.Write(data)
|
||||
tmp.Close()
|
||||
defer os.Remove(imgPath)
|
||||
|
||||
imgPath = strings.ReplaceAll(imgPath, "/", `\`)
|
||||
script := strings.ReplaceAll(ocrScript, "{{IMAGE_PATH}}", imgPath)
|
||||
|
||||
cmd := exec.Command("powershell",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-Command", script,
|
||||
)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err = cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("powershell error: %w\nstderr: %s", err, stderr.String())
|
||||
}
|
||||
|
||||
text := strings.TrimSpace(stdout.String())
|
||||
if text == "" {
|
||||
return "", errors.New("OCR returned empty string")
|
||||
}
|
||||
|
||||
return text, nil
|
||||
}
|
||||
Reference in New Issue
Block a user