feat: 设备桥共享库 + CLI全能力补齐 + GUI omniparse/computeruse 重构

- 抽取设备桥 WS 协议层为共享库 (internal/devicebridge/client/)
- CLI 补齐 11 项 caps 能力(screensee/screensue/speakeruse/camerasue/...)
- GUI 新增 omniparse 能力(Windows UIA 窗口解析)
- GUI computeruse 改用 koffi 直接调用 user32.dll,不再依赖 PowerShell C# 编译
- GUI computeruse JSON 解析兼容非标准格式 {x:500,y:300}
- 新增 mock-server 用于本地测试设备桥协议
- 新增 GUI DLL 桥接模块 (devicebridge_dll.js)
This commit is contained in:
JianFeeeee
2026-08-23 20:17:38 +08:00
parent a014f449f6
commit a024dc3f5f
13 changed files with 3401 additions and 362 deletions

View File

@ -0,0 +1,360 @@
package client
import (
"bufio"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"fmt"
"io"
"net"
"net/url"
"strings"
"time"
)
const (
wsGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
wsVersion = "13"
crlf = "\r\n"
)
// wsConn 封装一条 WebSocket 连接(客户端视角,帧带 mask
type wsConn struct {
conn net.Conn
br *bufio.Reader
bw *bufio.Writer
closed bool
}
// dialWS 发起 WS 客户端握手升级。
// 支持 ws:// 和 wss://wss 暂未实现,若需要需加 TLS dial
func dialWS(rawURL, token string, timeout time.Duration) (*wsConn, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("devicebridge: invalid ws url %q: %w", rawURL, err)
}
host := u.Host
if u.Port() == "" {
if u.Scheme == "wss" {
host = host + ":443"
} else {
host = host + ":80"
}
}
path := u.Path
if u.RawQuery != "" {
path = path + "?" + u.RawQuery
}
if path == "" {
path = "/"
}
// 默认路径
if token != "" && strings.Index(path, "token=") < 0 {
if strings.ContainsRune(path, '?') {
path = path + "&token=" + urlEscape(token)
} else {
path = path + "?token=" + urlEscape(token)
}
}
dialer := net.Dialer{Timeout: timeout}
conn, err := dialer.Dial("tcp", host)
if err != nil {
return nil, fmt.Errorf("devicebridge: dial %s: %w", host, err)
}
key := wsKey()
var sb strings.Builder
sb.WriteString("GET " + path + " HTTP/1.1" + crlf)
sb.WriteString("Host: " + host + crlf)
sb.WriteString("Upgrade: websocket" + crlf)
sb.WriteString("Connection: Upgrade" + crlf)
sb.WriteString("Sec-WebSocket-Key: " + key + crlf)
sb.WriteString("Sec-WebSocket-Version: " + wsVersion + crlf + crlf)
if _, err := conn.Write([]byte(sb.String())); err != nil {
conn.Close()
return nil, fmt.Errorf("devicebridge: write upgrade: %w", err)
}
br := bufio.NewReader(conn)
var headerBuf strings.Builder
for {
line, err := br.ReadString('\n')
if err != nil {
conn.Close()
return nil, fmt.Errorf("devicebridge: read upgrade resp: %w", err)
}
headerBuf.WriteString(line)
if strings.Contains(headerBuf.String(), crlf+crlf) {
break
}
}
if !strings.Contains(headerBuf.String(), " 101 ") {
conn.Close()
return nil, fmt.Errorf("devicebridge: upgrade failed: %s", firstLine(headerBuf.String()))
}
return &wsConn{conn: conn, br: br, bw: bufio.NewWriter(conn)}, nil
}
// writeText 发送 WS 文本帧0x1带 mask
func (w *wsConn) writeText(payload []byte) error {
return w.writeFrame(0x1, payload)
}
// writeBinary 发送 WS 二进制帧0x2带 mask
func (w *wsConn) writeBinary(payload []byte) error {
return w.writeFrame(0x2, payload)
}
// writeFrame 发送一个 WS 帧(客户端 mask 模式)。
func (w *wsConn) writeFrame(opcode byte, payload []byte) error {
if w.closed {
return fmt.Errorf("devicebridge: connection closed")
}
length := len(payload)
// 帧头
hdrLen := 2
switch {
case length < 126:
// 1 byte length
case length <= 0xffff:
hdrLen += 2
default:
hdrLen += 8
}
hdrLen += 4 // mask key
hdr := make([]byte, hdrLen)
hdr[0] = 0x80 | opcode
switch {
case length < 126:
hdr[1] = 0x80 | byte(length)
case length <= 0xffff:
hdr[1] = 0x80 | 126
binary.BigEndian.PutUint16(hdr[2:4], uint16(length))
default:
hdr[1] = 0x80 | 127
binary.BigEndian.PutUint64(hdr[2:10], uint64(length))
}
// mask key
var maskKey [4]byte
rand.Read(maskKey[:])
copy(hdr[hdrLen-4:], maskKey[:])
// mask payload
masked := make([]byte, length)
for i := 0; i < length; i++ {
masked[i] = payload[i] ^ maskKey[i&3]
}
if _, err := w.bw.Write(hdr); err != nil {
return err
}
if _, err := w.bw.Write(masked); err != nil {
return err
}
return w.bw.Flush()
}
// readFrame 读取一个 WS 帧。返回 (payload, isClose, opcode, error)。
// 客户端收到的帧不带 mask。
func (w *wsConn) readFrame() ([]byte, bool, byte, error) {
if w.closed {
return nil, true, 0, fmt.Errorf("devicebridge: connection closed")
}
b0, err := w.br.ReadByte()
if err != nil {
return nil, true, 0, err
}
opcode := b0 & 0x0f
b1, err := w.br.ReadByte()
if err != nil {
return nil, true, 0, err
}
length := uint64(b1 & 0x7f)
if length == 126 {
var ext [2]byte
if _, err := io.ReadFull(w.br, ext[:]); err != nil {
return nil, true, opcode, err
}
length = uint64(binary.BigEndian.Uint16(ext[:]))
} else if length == 127 {
var ext [8]byte
if _, err := io.ReadFull(w.br, ext[:]); err != nil {
return nil, true, opcode, err
}
length = binary.BigEndian.Uint64(ext[:])
}
// 二进制帧允许更大8MB文本帧 1MB
maxFrame := uint64(1 << 20)
if opcode == 0x2 {
maxFrame = 8 << 20
}
if length > maxFrame {
return nil, true, opcode, fmt.Errorf("devicebridge: frame too large (%d bytes)", length)
}
payload := make([]byte, length)
if _, err := io.ReadFull(w.br, payload); err != nil {
return nil, true, opcode, err
}
switch opcode {
case 0x1, 0x2:
return payload, false, opcode, nil
case 0x8:
return nil, true, opcode, nil
case 0x9: // ping
return nil, false, opcode, errPing
case 0xa: // pong
return nil, false, opcode, nil
default:
return nil, false, opcode, fmt.Errorf("devicebridge: unsupported opcode %x", opcode)
}
}
// writePong 发送 pong 帧。
func (w *wsConn) writePong() error {
return w.writeFrame(0xa, nil)
}
// close 发送关闭帧并关闭连接。
func (w *wsConn) close() error {
w.closed = true
_ = w.writeFrame(0x8, nil)
return w.conn.Close()
}
// ===== 辅助函数 =====
var errPing = fmt.Errorf("ping")
func wsKey() string {
var b [16]byte
rand.Read(b[:])
return base64.StdEncoding.EncodeToString(b[:])
}
func urlEscape(s string) string {
var sb strings.Builder
const hex = "0123456789ABCDEF"
for i := 0; i < len(s); i++ {
c := s[i]
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~' {
sb.WriteByte(c)
} else {
sb.WriteByte('%')
sb.WriteByte(hex[c>>4])
sb.WriteByte(hex[c&0xf])
}
}
return sb.String()
}
func firstLine(s string) string {
if i := strings.IndexByte(s, '\n'); i >= 0 {
return strings.TrimSpace(s[:i])
}
return strings.TrimSpace(s)
}
func wsAccept(key string) string {
h := sha256.Sum256([]byte(key + wsGUID))
return base64.StdEncoding.EncodeToString(h[:])
}
// readWSFrame 读取一个 WS 帧(从已有的 bufio.Reader兼容非 wsConn 场景)。
func readWSFrame(br *bufio.Reader) ([]byte, bool, byte, error) {
b0, err := br.ReadByte()
if err != nil {
return nil, true, 0, err
}
opcode := b0 & 0x0f
b1, err := br.ReadByte()
if err != nil {
return nil, true, 0, err
}
length := uint64(b1 & 0x7f)
if length == 126 {
var ext [2]byte
if _, err := io.ReadFull(br, ext[:]); err != nil {
return nil, true, opcode, err
}
length = uint64(binary.BigEndian.Uint16(ext[:]))
} else if length == 127 {
var ext [8]byte
if _, err := io.ReadFull(br, ext[:]); err != nil {
return nil, true, opcode, err
}
length = binary.BigEndian.Uint64(ext[:])
}
maxFrame := uint64(1 << 20)
if opcode == 0x2 {
maxFrame = 8 << 20
}
if length > maxFrame {
return nil, true, opcode, fmt.Errorf("frame too large")
}
payload := make([]byte, length)
if _, err := io.ReadFull(br, payload); err != nil {
return nil, true, opcode, err
}
switch opcode {
case 0x1, 0x2:
return payload, false, opcode, nil
case 0x8:
return nil, true, opcode, nil
default:
return nil, false, opcode, nil
}
}
// writeWSFrame 发送一个 WS 帧(非 mask 模式,服务端用)。
func writeWSFrame(w io.Writer, opcode byte, payload []byte) error {
length := len(payload)
hdr := []byte{0x80 | opcode}
switch {
case length < 126:
hdr = append(hdr, byte(length))
case length <= 0xffff:
hdr = append(hdr, 126, 0, 0)
binary.BigEndian.PutUint16(hdr[len(hdr)-2:], uint16(length))
default:
hdr = append(hdr, 127, 0, 0, 0, 0, 0, 0, 0, 0)
binary.BigEndian.PutUint64(hdr[len(hdr)-8:], uint64(length))
}
if _, err := w.Write(hdr); err != nil {
return err
}
if _, err := w.Write(payload); err != nil {
return err
}
return nil
}
// ensureTimeout 设置连接读写超时。
func (w *wsConn) setDeadline(t time.Time) {
if w.conn != nil {
w.conn.SetDeadline(t)
}
}
// LocalAddr 返回本地地址。
func (w *wsConn) LocalAddr() net.Addr {
if w.conn != nil {
return w.conn.LocalAddr()
}
return nil
}
// RemoteAddr 返回远程地址。
func (w *wsConn) RemoteAddr() net.Addr {
if w.conn != nil {
return w.conn.RemoteAddr()
}
return nil
}