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:
yuanyuanxianganddeepseek-v4-pro committed 2026-10-17 19:52:38 +02:00
1 parent e6b4fe3a85
commit 0c04f7516b
10 files changed
+290 -12

No files matched your search

+4
View File
@@ -1126,6 +1126,10 @@ VOID CScreenSpyDlg::OnReceiveComplete()
case TOKEN_CLIPBOARD_TEXT: {
Buffer str = m_ContextObject->InDeCompressedBuffer.GetMyBuffer(1);
UpdateServerClipboard(str.c_str(), str.length());
// 反向回传:远程剪贴板文本(UTF-8)→ Web 客户端
if (m_bIsWebSession && WebService().IsRunning()) {
WebService().NotifyClipboardText(m_ClientID, std::string(str.c_str(), str.length()));
}
break;
}
case TOKEN_BITMAPINFO: {
+111
View File
@@ -393,6 +393,10 @@ void CWebService::ServerThread(int port) {
HandleMouse(ws_ptr, msg);
} else if (cmd == "key") {
HandleKey(ws_ptr, msg);
} else if (cmd == "clipboard_set") {
HandleClipboardSet(ws_ptr, msg);
} else if (cmd == "clipboard_get") {
HandleClipboardGet(ws_ptr, token);
} else if (cmd == "rdp_reset") {
HandleRdpReset(ws_ptr, token);
} else if (cmd == "audio_toggle") {
@@ -929,6 +933,93 @@ void CWebService::HandleKey(void* ws_ptr, const std::string& msg) {
ctx->Send2Client(szData, length);
}
void CWebService::HandleClipboardSet(void* ws_ptr, const std::string& msg) {
// Parse JSON
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
return;
}
std::string token = root.get("token", "").asString();
std::string text = root.get("text", "").asString();
if (text.empty()) return;
// Validate token
std::string username, role;
if (!ValidateToken(token, username, role)) {
return;
}
// Get device being watched
uint64_t device_id = 0;
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
auto it = m_Clients.find(ws_ptr);
if (it != m_Clients.end()) {
device_id = it->second.watch_device_id;
it->second.last_activity = (uint64_t)time(nullptr);
}
}
if (device_id == 0) return;
// Get screen context
CONTEXT_OBJECT* ctx = GetScreenContext(device_id);
if (!ctx) {
return;
}
// UTF-8 → UTF-16 → GBK(936)。客户端 UpdateClientClipboard 以 CF_TEXT(ANSI/GBK) 落剪贴板,
// 故显式用 936 而非 CP_ACP,避免服务端 locale 非中文时出错。
int wlen = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), (int)text.size(), nullptr, 0);
if (wlen <= 0) return;
std::wstring wide(wlen, L'\0');
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), (int)text.size(), &wide[0], wlen);
int glen = WideCharToMultiByte(936, 0, wide.c_str(), wlen, nullptr, 0, nullptr, nullptr);
if (glen <= 0) return;
// 命令字节 + GBK 文本
std::vector<BYTE> szData(1 + glen);
szData[0] = COMMAND_SCREEN_SET_CLIPBOARD;
WideCharToMultiByte(936, 0, wide.c_str(), wlen, (char*)szData.data() + 1, glen, nullptr, nullptr);
ctx->Send2Client(szData.data(), (ULONG)szData.size());
}
void CWebService::HandleClipboardGet(void* ws_ptr, const std::string& token) {
// Validate token
std::string username, role;
if (!ValidateToken(token, username, role)) {
return;
}
// Get device being watched
uint64_t device_id = 0;
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
auto it = m_Clients.find(ws_ptr);
if (it != m_Clients.end()) {
device_id = it->second.watch_device_id;
it->second.last_activity = (uint64_t)time(nullptr);
}
}
if (device_id == 0) return;
// Get screen context
CONTEXT_OBJECT* ctx = GetScreenContext(device_id);
if (!ctx) {
return;
}
// 单字节命令,客户端以 SendClientClipboard → TOKEN_CLIPBOARD_TEXT 回包
BYTE bToken = COMMAND_SCREEN_GET_CLIPBOARD;
ctx->Send2Client(&bToken, sizeof(BYTE));
}
void CWebService::HandleRdpReset(void* ws_ptr, const std::string& token) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
@@ -1711,6 +1802,26 @@ void CWebService::BroadcastCursor(uint64_t device_id, uint8_t cursor_index) {
}
}
void CWebService::NotifyClipboardText(uint64_t device_id, const std::string& text) {
if (m_bStopping) return;
Json::Value res;
res["cmd"] = "clipboard_text";
res["id"] = device_id;
res["text"] = text;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string json = Json::writeString(builder, res);
std::lock_guard<std::mutex> lock(m_ClientsMutex);
for (auto& [ws_ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) {
SendText(ws_ptr, json);
}
}
}
bool CWebService::StartRemoteDesktop(uint64_t device_id) {
if (!m_pParentDlg) return false;
+5
View File
@@ -160,6 +160,9 @@ public:
// Cursor change notification (called from ScreenSpyDlg)
void BroadcastCursor(uint64_t device_id, uint8_t cursor_index);
// 远程剪贴板文本回传通知(called from ScreenSpyDlg,payload 为 UTF-8)
void NotifyClipboardText(uint64_t device_id, const std::string& text);
// Get count of web clients watching a device
int GetWebClientCount(uint64_t device_id);
@@ -187,6 +190,8 @@ private:
void HandlePing(void* ws_ptr, const std::string& token);
void HandleMouse(void* ws_ptr, const std::string& msg);
void HandleKey(void* ws_ptr, const std::string& msg);
void HandleClipboardSet(void* ws_ptr, const std::string& msg);
void HandleClipboardGet(void* ws_ptr, const std::string& token);
void HandleRdpReset(void* ws_ptr, const std::string& token);
void HandleAudioToggle(void* ws_ptr, const std::string& token);