chore: migrate and structure cmbone project

This commit is contained in:
QiuSW
2026-07-08 15:13:26 +08:00
parent 519a532038
commit e5d10577c0
123 changed files with 9076 additions and 25 deletions
+87
View File
@@ -0,0 +1,87 @@
//go:build darwin
// +build darwin
package platform
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Cocoa -framework Carbon -framework Vision -framework Foundation
#include <stdlib.h>
#include <AppKit/AppKit.h>
#include "system.h"
*/
import "C"
import (
"encoding/base64"
"fmt"
"sync"
"unsafe"
)
type HotKeyCallback func()
var (
callbacks = make(map[string]HotKeyCallback)
mutex sync.RWMutex
)
//export hotKeyCallback
func hotKeyCallback(keyCode, modifiers C.uint) {
id := fmt.Sprintf("%d_%d", uint32(keyCode), uint32(modifiers))
mutex.RLock()
cb, exists := callbacks[id]
mutex.RUnlock()
if exists {
//fmt.Printf("✅ 触发热键: %s\n", id)
cb()
} else {
fmt.Printf("❌ 找不到回调: %s\n", id)
}
}
// RegisterHotKeyWithCallback 注册热键并绑定 Go 回调
func RegisterHotKeyWithCallback(keyCode, modifiers uint32, cb HotKeyCallback) {
id := fmt.Sprintf("%d_%d", keyCode, modifiers)
mutex.Lock()
callbacks[id] = cb
mutex.Unlock()
//fmt.Printf("✈️ Registering hotkey: %d %d\n", keyCode, modifiers)
C.RegisterHotKeyDynamic(C.uint(keyCode), C.uint(modifiers))
}
// 激活当前应用程序
func ActivateApp() {
C.NSAppActivateIgnoringOtherApps() // 激活当前应用程序
}
// UnregisterHotKey 注销热键(Go -> C)
func UnregisterHotKey(keyCode, modifiers uint32) {
id := fmt.Sprintf("%d_%d", keyCode, modifiers)
mutex.Lock()
delete(callbacks, id)
mutex.Unlock()
//fmt.Printf("🧹 Unregistering hotkey: %d %d\n", keyCode, modifiers)
C.UnregisterHotKey(C.uint(keyCode), C.uint(modifiers))
}
func HideDock() {
C.HideDockIcon()
}
// OCR功能
func RecognizeImageBase64(base64str string) (string, error) {
data, err := base64.StdEncoding.DecodeString(base64str)
if err != nil {
return "", fmt.Errorf("base64 decode failed: %w", err)
}
tmp := C.CBytes(data)
defer C.free(tmp)
result := C.VisionOCRFromMemory(tmp, C.int(len(data)))
defer C.free(unsafe.Pointer(result))
return C.GoString(result), nil
}
+247
View File
@@ -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
}
+15
View File
@@ -0,0 +1,15 @@
#ifndef CLIPBOARD_H
#define CLIPBOARD_H
#include <stdbool.h>
// 热键相关函数
void RegisterHotKeyDynamic(unsigned int keyCode, unsigned int modifiers);
void InstallHotKeyHandler(void);
void NSAppActivateIgnoringOtherApps(); // ✅ 只声明,不要实现!
void UnregisterHotKey(unsigned int keyCode, unsigned int modifiers);
void HideDockIcon(void);
// OCR功能
const char* VisionOCR(const char* path);//ocr
const char *VisionOCRFromMemory(const void *data, int size);
#endif
+142
View File
@@ -0,0 +1,142 @@
#import <Cocoa/Cocoa.h>
#import <Carbon/Carbon.h>
#import <stdlib.h>
#import <AppKit/AppKit.h>
#import <ApplicationServices/ApplicationServices.h>
#import <Vision/Vision.h>
#import <Foundation/Foundation.h>
#import "system.h"
// ============ 热键相关 ============
// 声明 Go 回调函数
extern void hotKeyCallback(unsigned int keyCode, unsigned int modifiers);
// 定义热键最大数量
#define MAX_HOTKEYS 10
// 保存注册的 HotKeyRef
static EventHotKeyRef hotKeyRefs[MAX_HOTKEYS];
static EventHotKeyID hotKeyIDs[MAX_HOTKEYS];
static int hotKeyCount = 0;
static int handlerInstalled = 0;
OSStatus MyHotKeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) {
EventHotKeyID hkID;
GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hkID), NULL, &hkID);
unsigned int keyCode = hkID.id >> 16;
unsigned int modifiers = hkID.id & 0xFFFF;
hotKeyCallback(keyCode, modifiers); // ✅ 正确传值给 Go
return noErr;
}
void InstallHotKeyHandler() {
if (handlerInstalled) return;
EventTypeSpec eventType;
eventType.eventClass = kEventClassKeyboard;
eventType.eventKind = kEventHotKeyPressed;
InstallApplicationEventHandler(&MyHotKeyHandler, 1, &eventType, NULL, NULL);
handlerInstalled = 1;
}
void RegisterHotKeyDynamic(unsigned int keyCode, unsigned int modifiers) {
InstallHotKeyHandler();
if (hotKeyCount >= MAX_HOTKEYS) return;
EventHotKeyID hkID;
hkID.signature = 'htk1';
hkID.id = (keyCode << 16) | (modifiers & 0xFFFF); // ✅ 编码 keyCode 和 modifiers
RegisterEventHotKey(keyCode, modifiers, hkID, GetApplicationEventTarget(), 0, &hotKeyRefs[hotKeyCount]);
// ✅ 保存 ID
hotKeyIDs[hotKeyCount] = hkID;
hotKeyCount++;
}
void NSAppActivateIgnoringOtherApps() {
[[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
}
void UnregisterHotKey(unsigned int keyCode, unsigned int modifiers) {
unsigned int targetID = (keyCode << 16) | (modifiers & 0xFFFF);
for (int i = 0; i < hotKeyCount; i++) {
unsigned int existingID = hotKeyIDs[i].id;
if (existingID == targetID) {
// 注销
UnregisterEventHotKey(hotKeyRefs[i]);
// 清理数组(用后面的项覆盖)
for (int j = i; j < hotKeyCount - 1; j++) {
hotKeyRefs[j] = hotKeyRefs[j + 1];
hotKeyIDs[j] = hotKeyIDs[j + 1];
}
hotKeyCount--;
return;
}
}
}
void HideDockIcon(void) {
NSApplication *app = [NSApplication sharedApplication];
[app setActivationPolicy:NSApplicationActivationPolicyProhibited];
}
// ============ OCR功能 ============
const char* VisionOCR(const char* path) {
@autoreleasepool {
NSString* imagePath = [NSString stringWithUTF8String:path];
NSData* imageData = [NSData dataWithContentsOfFile:imagePath];
NSImage* image = [[NSImage alloc] initWithData:imageData];
if (!image) return "Image load failed";
CGImageRef cgRef = [image CGImageForProposedRect:NULL context:nil hints:nil];
if (!cgRef) return "CGImage conversion failed";
VNRecognizeTextRequest *request = [[VNRecognizeTextRequest alloc] init];
request.recognitionLevel = VNRequestTextRecognitionLevelAccurate;
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCGImage:cgRef options:@{}];
NSError *error = nil;
[handler performRequests:@[request] error:&error];
if (error) return [[error.localizedDescription UTF8String] copy];
NSMutableString *result = [NSMutableString string];
for (VNRecognizedTextObservation *obs in request.results) {
VNRecognizedText *topCandidate = [[obs topCandidates:1] firstObject];
if (topCandidate) {
[result appendString:topCandidate.string];
[result appendString:@"\n"];
}
}
return strdup([result UTF8String]);
}
}
const char *VisionOCRFromMemory(const void *data, int size) {
@autoreleasepool {
NSData *imageData = [NSData dataWithBytes:data length:size];
NSImage *image = [[NSImage alloc] initWithData:imageData];
CGImageRef cgRef = [image CGImageForProposedRect:nil context:nil hints:nil];
VNRecognizeTextRequest *req = [[VNRecognizeTextRequest alloc] init];
req.recognitionLevel = VNRequestTextRecognitionLevelAccurate;
req.recognitionLanguages = @[@"zh-Hans", @"en-US"];
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCGImage:cgRef options:@{}];
NSError *err = nil;
[handler performRequests:@[req] error:&err];
if (err) return strdup([[err localizedDescription] UTF8String]);
NSMutableString *out = [NSMutableString string];
for (VNRecognizedTextObservation *obs in req.results) {
VNRecognizedText *cand = [[obs topCandidates:1] firstObject];
if (cand) {
[out appendString:cand.string];
[out appendString:@"\n"];
}
}
return strdup([out UTF8String]);
}
}