Files
NanoKVM-MIRROR/server/service/mcp/remote.go
SiYue-ZO 64ce12cc0d feat(mcp): add authenticated remote control backend
Add an MCP Streamable HTTP server with token authentication, screenshot capture, keyboard, and mouse tools.

Introduce shared control-mode and input-control coordination so MCP, PicoClaw, and local HID paths serialize ownership safely.

Integrate PicoClaw gateway/runtime control handoff with PID-managed startup and focused unit coverage.
2026-07-29 15:16:06 +08:00

235 lines
5.6 KiB
Go

package mcpservice
import (
"context"
"fmt"
"sync"
"time"
"NanoKVM-Server/service/hid"
)
type HIDWriter interface {
WriteKeyboardReport([]byte) error
WriteRelativeMouseReport([]byte) error
WriteAbsoluteMouseReport([]byte) error
}
type Remote struct {
hid HIDWriter
hidMu sync.Mutex
}
func NewRemote() *Remote {
return &Remote{hid: hid.GetHid()}
}
func newRemoteWithHID(writer HIDWriter) *Remote {
return &Remote{hid: writer}
}
func (r *Remote) TypeText(ctx context.Context, text string, delay time.Duration) ([]rune, error) {
reports, skipped := buildTypeReports(text)
r.hidMu.Lock()
defer r.hidMu.Unlock()
keyDown := false
for index, report := range reports {
if err := contextError(ctx); err != nil {
if keyDown {
_ = r.hid.WriteKeyboardReport(keyUpReport)
}
return skipped, err
}
if err := r.hid.WriteKeyboardReport(report); err != nil {
_ = r.hid.WriteKeyboardReport(keyUpReport)
return skipped, err
}
keyDown = index%2 == 0
if !keyDown && index+1 < len(reports) {
if err := sleepContext(ctx, delay); err != nil {
return skipped, err
}
}
}
return skipped, nil
}
func (r *Remote) PressKeys(ctx context.Context, keys []string, hold time.Duration) error {
report, err := buildKeyComboReport(keys)
if err != nil {
return err
}
r.hidMu.Lock()
defer r.hidMu.Unlock()
if err := contextError(ctx); err != nil {
return err
}
if err := r.hid.WriteKeyboardReport(report); err != nil {
_ = r.hid.WriteKeyboardReport(keyUpReport)
return err
}
if err := sleepContext(ctx, hold); err != nil {
_ = r.hid.WriteKeyboardReport(keyUpReport)
return err
}
return r.hid.WriteKeyboardReport(keyUpReport)
}
func (r *Remote) MoveAbsolute(ctx context.Context, x float64, y float64) error {
if x < 0 || x > 1 || y < 0 || y > 1 {
return fmt.Errorf("absolute coordinates must be between 0 and 1")
}
report := buildAbsolutePointerReport(absoluteCoordinate(x), absoluteCoordinate(y), 0, 0)
r.hidMu.Lock()
defer r.hidMu.Unlock()
if err := contextError(ctx); err != nil {
return err
}
return r.hid.WriteAbsoluteMouseReport(report)
}
func (r *Remote) TouchAbsoluteButton(ctx context.Context, x float64, y float64, button string, hold time.Duration) error {
if x < 0 || x > 1 || y < 0 || y > 1 {
return fmt.Errorf("absolute coordinates must be between 0 and 1")
}
bit, err := mouseButtonBit(button)
if err != nil {
return err
}
hidX := absoluteCoordinate(x)
hidY := absoluteCoordinate(y)
release := buildAbsolutePointerReport(hidX, hidY, 0, 0)
r.hidMu.Lock()
defer r.hidMu.Unlock()
if err := contextError(ctx); err != nil {
return err
}
if err := r.hid.WriteAbsoluteMouseReport(buildAbsolutePointerReport(hidX, hidY, bit, 0)); err != nil {
_ = r.hid.WriteAbsoluteMouseReport(release)
return err
}
if err := sleepContext(ctx, hold); err != nil {
_ = r.hid.WriteAbsoluteMouseReport(release)
return err
}
return r.hid.WriteAbsoluteMouseReport(release)
}
func (r *Remote) MoveRelative(ctx context.Context, deltaX int, deltaY int) error {
reports, err := buildRelativeMoveReports(deltaX, deltaY)
if err != nil {
return err
}
r.hidMu.Lock()
defer r.hidMu.Unlock()
for _, report := range reports {
if err := contextError(ctx); err != nil {
_ = r.hid.WriteRelativeMouseReport(buildRelativeMouseReport(0, 0, 0, 0))
return err
}
if err := r.hid.WriteRelativeMouseReport(report); err != nil {
_ = r.hid.WriteRelativeMouseReport(buildRelativeMouseReport(0, 0, 0, 0))
return err
}
}
return nil
}
func (r *Remote) Click(ctx context.Context, button string, clicks int, delay time.Duration) error {
bit, err := mouseButtonBit(button)
if err != nil {
return err
}
clicks = clampInt(clicks, 1, 5)
release := buildRelativeMouseReport(0, 0, 0, 0)
r.hidMu.Lock()
defer r.hidMu.Unlock()
for i := 0; i < clicks; i++ {
if err := contextError(ctx); err != nil {
_ = r.hid.WriteRelativeMouseReport(release)
return err
}
if err := r.hid.WriteRelativeMouseReport(buildRelativeMouseReport(0, 0, bit, 0)); err != nil {
_ = r.hid.WriteRelativeMouseReport(release)
return err
}
if err := sleepContext(ctx, delay); err != nil {
_ = r.hid.WriteRelativeMouseReport(release)
return err
}
if err := r.hid.WriteRelativeMouseReport(release); err != nil {
_ = r.hid.WriteRelativeMouseReport(release)
return err
}
if i+1 < clicks {
if err := sleepContext(ctx, delay); err != nil {
return err
}
}
}
return nil
}
func (r *Remote) Scroll(ctx context.Context, deltaY int) error {
if reportsForDelta(deltaY) > maxRelativeMouseReports {
return fmt.Errorf("scroll exceeds %d reports", maxRelativeMouseReports)
}
x := absoluteCoordinate(0.5)
y := absoluteCoordinate(0.5)
release := buildAbsolutePointerReport(x, y, 0, 0)
r.hidMu.Lock()
defer r.hidMu.Unlock()
for deltaY != 0 {
if err := contextError(ctx); err != nil {
_ = r.hid.WriteAbsoluteMouseReport(release)
return err
}
wheel := clampInt(deltaY, -127, 127)
if err := r.hid.WriteAbsoluteMouseReport(buildAbsolutePointerReport(x, y, 0, wheel)); err != nil {
_ = r.hid.WriteAbsoluteMouseReport(release)
return err
}
if err := r.hid.WriteAbsoluteMouseReport(release); err != nil {
_ = r.hid.WriteAbsoluteMouseReport(release)
return err
}
deltaY -= wheel
}
return nil
}
func sleepContext(ctx context.Context, delay time.Duration) error {
if delay <= 0 {
return contextError(ctx)
}
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return context.Cause(ctx)
case <-timer.C:
return nil
}
}
func contextError(ctx context.Context) error {
if ctx == nil {
return nil
}
select {
case <-ctx.Done():
return context.Cause(ctx)
default:
return nil
}
}