Feature: Web remote desktop bidirectional text clipboard sync
Add keyboard-shortcut (Ctrl+C / Ctrl+X / Ctrl+V) text clipboard transfer between the browser and the remote desktop, reusing the clipboard protocol that already exists in the native viewer (COMMAND_SCREEN_GET_CLIPBOARD=24, COMMAND_SCREEN_SET_CLIPBOARD=25, TOKEN_CLIPBOARD_TEXT=118) so the client needs no change. Local -> remote: pressing Ctrl+V reads the browser clipboard, pushes it to the remote clipboard (UTF-8 -> GBK, matching the client's CF_TEXT/ANSI handling) and then synthesizes a remote Ctrl+V so the pasted text is the newly delivered one. Remote -> local: Ctrl+C or Ctrl+X is forwarded to the remote as usual and, after a short delay, the remote clipboard is pulled back and written to the browser clipboard for a later local Ctrl+V. Both the MFC and Go servers get the same three clipboard signaling paths (clipboard_set, clipboard_get, clipboard_text) so the shared single-page frontend behaves identically on either backend. Co-Authored-By: deepseek-v4-pro
This commit is contained in:
1 parent
e6b4fe3a85
commit
0c04f7516b
10 files changed
+290
-12
No files matched your search
+20
-3
@@ -114,9 +114,9 @@ type wsHub struct {
|
||||
|
||||
func newWSHub(auth *wsauth.Authenticator, devices *hub.Hub, log *logger.Logger) *wsHub {
|
||||
h := &wsHub{
|
||||
auth: auth,
|
||||
devices: devices,
|
||||
log: log,
|
||||
auth: auth,
|
||||
devices: devices,
|
||||
log: log,
|
||||
clients: make(map[*wsClient]struct{}),
|
||||
screenCloseTimers: make(map[string]*time.Timer),
|
||||
}
|
||||
@@ -300,6 +300,23 @@ func (h *wsHub) OnResolutionChange(deviceID string, width, height int) {
|
||||
}
|
||||
}
|
||||
|
||||
// OnClipboardText relays a device's clipboard text back to every browser
|
||||
// watching it (in response to a "clipboard_get" request).
|
||||
func (h *wsHub) OnClipboardText(deviceID string, text string) {
|
||||
msg := mustJSON(map[string]any{
|
||||
"cmd": "clipboard_text",
|
||||
"id": deviceID,
|
||||
"text": text,
|
||||
})
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
for c := range h.clients {
|
||||
if c.watching == deviceID && c.token != "" {
|
||||
c.queue(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnScreenFrame ships a screen packet to every browser currently watching
|
||||
// this device. We hold the read lock for the whole iteration, but each
|
||||
// queueBinary is non-blocking (drops on backpressure) so a slow viewer
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/hub"
|
||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol"
|
||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/wsauth"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
)
|
||||
|
||||
// rotateChallenge replaces the client's nonce with a fresh one and pushes a
|
||||
@@ -64,6 +65,10 @@ func (h *wsHub) dispatch(c *wsClient, cmd string, raw []byte) {
|
||||
h.handleMouse(c, raw)
|
||||
case "key":
|
||||
h.handleKey(c, raw)
|
||||
case "clipboard_set":
|
||||
h.handleClipboardSet(c, raw)
|
||||
case "clipboard_get":
|
||||
h.handleClipboardGet(c, raw)
|
||||
case "term_open":
|
||||
h.handleTermOpen(c, raw)
|
||||
case "term_input":
|
||||
@@ -559,6 +564,59 @@ func (h *wsHub) handleKey(c *wsClient, raw []byte) {
|
||||
_ = h.devices.SendToScreen(deviceID, pkt)
|
||||
}
|
||||
|
||||
// handleClipboardSet relays the browser's local clipboard text into the remote
|
||||
// device's clipboard via COMMAND_SCREEN_SET_CLIPBOARD (25). The client's
|
||||
// UpdateClientClipboard stores the payload as CF_TEXT (ANSI/GBK), so we encode
|
||||
// the UTF-8 input to GBK here — matching the C++ WebService (explicit code
|
||||
// page 936) and the existing MCP remote_clipboard convention. Non-GBK glyphs
|
||||
// (emoji, some accented Latin) collapse to '?' — an accepted limitation.
|
||||
func (h *wsHub) handleClipboardSet(c *wsClient, raw []byte) {
|
||||
if !h.requireAuth(c, raw, "clipboard_set_result") {
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &in); err != nil {
|
||||
return
|
||||
}
|
||||
if in.Text == "" {
|
||||
return
|
||||
}
|
||||
deviceID := c.watching
|
||||
if deviceID == "" {
|
||||
return
|
||||
}
|
||||
gbk, err := simplifiedchinese.GBK.NewEncoder().Bytes([]byte(in.Text))
|
||||
if err != nil {
|
||||
h.log.Warn("clipboard_set: GBK encode failed: %v", err)
|
||||
return
|
||||
}
|
||||
payload := append([]byte{protocol.CommandScreenSetClipboard}, gbk...)
|
||||
if err := h.devices.SendToScreen(deviceID, payload); err != nil {
|
||||
h.log.Warn("clipboard_set: device=%s: %v", deviceID, err)
|
||||
return
|
||||
}
|
||||
h.log.Info("clipboard_set sent: device=%s len=%d", deviceID, len(gbk))
|
||||
}
|
||||
|
||||
// handleClipboardGet asks the remote device for its clipboard text via
|
||||
// COMMAND_SCREEN_GET_CLIPBOARD (24, a single byte). The device replies with
|
||||
// TOKEN_CLIPBOARD_TEXT, which the hub relays back to the browser.
|
||||
func (h *wsHub) handleClipboardGet(c *wsClient, raw []byte) {
|
||||
if !h.requireAuth(c, raw, "clipboard_get_result") {
|
||||
return
|
||||
}
|
||||
deviceID := c.watching
|
||||
if deviceID == "" {
|
||||
return
|
||||
}
|
||||
if err := h.devices.SendToScreen(deviceID, []byte{protocol.CommandScreenGetClipboard}); err != nil {
|
||||
h.log.Warn("clipboard_get: device=%s: %v", deviceID, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleCreateUser provisions a new web account. Admin-only. Mirrors the
|
||||
// C++ CWebService::HandleCreateUser semantics (WebService.cpp:1009): the
|
||||
// password is hashed with a fresh salt, the user table is persisted, and
|
||||
|
||||
Reference in new issue
Block a user