diff --git a/web-nodejs/public/js/rdclient/client.js b/web-nodejs/public/js/rdclient/client.js
index 605e3d6d..5dd392f1 100644
--- a/web-nodejs/public/js/rdclient/client.js
+++ b/web-nodejs/public/js/rdclient/client.js
@@ -13,7 +13,7 @@
* client.disconnect();
*/
-/* global RDConnection, RDProtocol, RDCrypto, RDVideo, RDAudio, RDRenderer, RDInput, RDFileConnection */
+/* global RDConnection, RDProtocol, RDCrypto, RDVideo, RDAudio, RDRenderer, RDInput, RDFileConnection, RDClipboard */
// eslint-disable-next-line no-unused-vars
class RDClient {
@@ -776,12 +776,18 @@ class RDClient {
return;
}
- // Clipboard
+ // Clipboard (legacy single entry)
if (msg.clipboard) {
this._handleClipboard(msg.clipboard);
return;
}
+ // Multi-format clipboard (RustDesk >= 1.3.0)
+ if (msg.multiClipboards) {
+ this._handleMultiClipboards(msg.multiClipboards);
+ return;
+ }
+
// Test delay (ping/pong)
if (msg.testDelay) {
this._handleTestDelay(msg.testDelay);
@@ -1046,19 +1052,30 @@ class RDClient {
}
}
- _handleClipboard(clipboard) {
- if (clipboard.content) {
- const decoder = new TextDecoder();
- const text = decoder.decode(clipboard.content);
- this._emit('clipboard', text);
+ async _applyRemoteClipboard(clipboards) {
+ const list = clipboards || [];
+ if (!list.length) return;
- // Copy to local clipboard only for the active viewer tab
- if (this._clipboardToLocalEnabled && navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(text).catch(() => {
- // Clipboard write permission denied - ignore
- });
- }
+ const decoded = await RDClipboard.decodeEntries(list);
+ const text = RDClipboard.pickBestText(decoded);
+ if (text) {
+ this._emit('clipboard', text);
}
+
+ await RDClipboard.applyToLocal(decoded, {
+ enabled: this._clipboardToLocalEnabled
+ });
+ }
+
+ _handleClipboard(clipboard) {
+ void this._applyRemoteClipboard([clipboard]);
+ }
+
+ _handleMultiClipboards(multiClipboards) {
+ const list = multiClipboards && multiClipboards.clipboards
+ ? multiClipboards.clipboards
+ : [];
+ void this._applyRemoteClipboard(list);
}
_handleTestDelay(testDelay) {
@@ -1411,7 +1428,11 @@ class RDClient {
*/
sendClipboard(text) {
if (this._state !== 'streaming') return;
- const msg = this.proto.buildClipboard(text);
+ void this._sendClipboard(text);
+ }
+
+ async _sendClipboard(text) {
+ const msg = await this.proto.buildClipboard(text);
this._sendPeerMessage(msg);
}
diff --git a/web-nodejs/public/js/rdclient/clipboard.js b/web-nodejs/public/js/rdclient/clipboard.js
new file mode 100644
index 00000000..6b2dc244
--- /dev/null
+++ b/web-nodejs/public/js/rdclient/clipboard.js
@@ -0,0 +1,283 @@
+/**
+ * BetterDesk Web Remote Client - inbound clipboard decode/apply
+ * Handles RustDesk Clipboard / MultiClipboards (zstd, text/html/image).
+ */
+
+/* global RDCompress */
+
+// eslint-disable-next-line no-unused-vars
+class RDClipboard {
+ static OWNER_FORMAT = 'dyn.com.rustdesk.owner';
+
+ static FORMAT = {
+ Text: 0,
+ Rtf: 1,
+ Html: 2,
+ ImageRgba: 21,
+ ImagePng: 22,
+ ImageSvg: 23,
+ Special: 31
+ };
+
+ /**
+ * @param {number|string} format
+ * @returns {number}
+ */
+ static formatId(format) {
+ if (format == null) return RDClipboard.FORMAT.Text;
+ if (typeof format === 'number') return format;
+ const map = {
+ Text: RDClipboard.FORMAT.Text,
+ Rtf: RDClipboard.FORMAT.Rtf,
+ Html: RDClipboard.FORMAT.Html,
+ ImageRgba: RDClipboard.FORMAT.ImageRgba,
+ ImagePng: RDClipboard.FORMAT.ImagePng,
+ ImageSvg: RDClipboard.FORMAT.ImageSvg,
+ Special: RDClipboard.FORMAT.Special
+ };
+ return map[format] != null ? map[format] : RDClipboard.FORMAT.Text;
+ }
+
+ /**
+ * @param {Object} clipboard
+ * @returns {boolean}
+ */
+ static shouldSkipEntry(clipboard) {
+ if (!clipboard) return true;
+ const fmt = RDClipboard.formatId(clipboard.format);
+ if (fmt !== RDClipboard.FORMAT.Special) return false;
+ const name = clipboard.specialName || clipboard.special_name || '';
+ return name === RDClipboard.OWNER_FORMAT;
+ }
+
+ /**
+ * @param {*} content
+ * @returns {Uint8Array|null}
+ */
+ static normalizeContent(content) {
+ if (!content) return null;
+ if (content instanceof Uint8Array) return content;
+ if (content instanceof ArrayBuffer) return new Uint8Array(content);
+ if (content.length != null) return new Uint8Array(content);
+ return null;
+ }
+
+ /**
+ * @param {string} html
+ * @returns {string}
+ */
+ static stripHtml(html) {
+ if (!html) return '';
+ if (typeof DOMParser !== 'undefined') {
+ try {
+ const doc = new DOMParser().parseFromString(html, 'text/html');
+ return (doc.body && doc.body.textContent) ? doc.body.textContent : '';
+ } catch (_) {
+ // fall through to regex
+ }
+ }
+ return html
+ .replace(/
+
+