@yeaft/webchat-agent 1.0.428 → 1.0.430

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -112,6 +112,9 @@ function peerRecord(record, message) {
112
112
  function closePeer(record, peer, notify = false) {
113
113
  if (!record || !peer || record.peers.get(peer.peerId) !== peer) return false;
114
114
  record.peers.delete(peer.peerId);
115
+ for (const channel of Object.values(peer.channels || {})) {
116
+ try { channel.close(); } catch {}
117
+ }
115
118
  peer.connection.close();
116
119
  if (notify) bridgeSend(record, {
117
120
  type: 'peer_closed',
@@ -186,6 +189,7 @@ async function createPeer(record, message) {
186
189
  pendingCandidates: [],
187
190
  offerSent: false,
188
191
  localCandidates: [],
192
+ channels: {},
189
193
  };
190
194
  record.peers.set(peerId, peer);
191
195
  const isCurrent = () => peerRecord(record, peer) === peer;
@@ -209,6 +213,30 @@ async function createPeer(record, message) {
209
213
  state: connection.connectionState,
210
214
  });
211
215
  };
216
+ if (message.role === 'interactive') {
217
+ const control = connection.createDataChannel('browser.control.v1', { ordered: true });
218
+ const pointer = connection.createDataChannel('browser.pointer.v1', {
219
+ ordered: false,
220
+ maxRetransmits: 0,
221
+ });
222
+ peer.channels.control = control;
223
+ peer.channels.pointer = pointer;
224
+ for (const [channel, dataChannel] of Object.entries(peer.channels)) {
225
+ dataChannel.onmessage = event => {
226
+ if (!isCurrent() || typeof event.data !== 'string' || event.data.length > 32 * 1024) return;
227
+ let envelope;
228
+ try { envelope = JSON.parse(event.data); } catch { return; }
229
+ if (envelope?.version !== 1 || envelope.connectionGeneration !== connectionGeneration) return;
230
+ bridgeSend(record, {
231
+ type: 'peer_input',
232
+ peerId,
233
+ connectionGeneration,
234
+ channel,
235
+ envelope,
236
+ });
237
+ };
238
+ }
239
+ }
212
240
  const sender = connection.addTrack(record.track, record.stream);
213
241
  preferVp8(connection, sender);
214
242
  const parameters = sender.getParameters();
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  const moduleDir = dirname(fileURLToPath(import.meta.url));
7
7
  export const BROWSER_EXTENSION_DIR = join(moduleDir, 'extension');
8
8
  export const BROWSER_EXTENSION_NAME = 'Yeaft Browser Runtime';
9
- export const BROWSER_EXTENSION_SHA256 = '13279f6bb05975f9de9f92fdc13da28341ff99c161563cda25d27a14d5cdcdd5';
9
+ export const BROWSER_EXTENSION_SHA256 = 'c17d5b60ade5526168bf543c177370ee2ff6170c67cb197ac33c4bc015d151c4';
10
10
 
11
11
  async function walkFiles(root, current = root) {
12
12
  const entries = await readdir(current, { withFileTypes: true });
@@ -10,6 +10,7 @@ import { probeBrowserRuntime } from './probe.js';
10
10
  import { BrowserRuntimeError } from './errors.js';
11
11
  import { BrowserExtensionBridge } from './local-bridge.js';
12
12
  import { launchBrowserSession } from './chromium.js';
13
+ import { ProducerSequenceState } from './protocol.js';
13
14
 
14
15
  const REQUEST_TTL_MS = 10 * 60_000;
15
16
  const MAX_INSTALL_PROGRESS_LISTENERS = 8;
@@ -389,8 +390,10 @@ export class BrowserRuntimeService {
389
390
  captureMode: session.captureMode,
390
391
  viewport: session.viewport,
391
392
  viewerCount: session.peers.size,
392
- interactivePeerCount: 0,
393
- authorizedProducerCount: 0,
393
+ interactivePeerCount: [...session.peers.values()].filter(peer => peer.role === 'interactive').length,
394
+ authorizedProducerCount: [...session.peers.values()].filter(peer => (
395
+ peer.role === 'interactive' && peer.state === 'connected'
396
+ )).length,
394
397
  expiresAt: session.expiresAt,
395
398
  terminalReason: session.terminalReason || null,
396
399
  safeError: session.safeError || null,
@@ -405,6 +408,17 @@ export class BrowserRuntimeService {
405
408
 
406
409
  #dropPeer(session, peer, reason = 'peer_closed') {
407
410
  if (!peer || session.peers.get(peer.peerId) !== peer) return false;
411
+ if (peer.role === 'interactive') {
412
+ void peer.actionChain.finally(async () => {
413
+ const page = session.runtime?.page;
414
+ for (const key of ['Alt', 'Control', 'Meta', 'Shift']) {
415
+ try { await page?.keyboard?.up?.(key); } catch {}
416
+ }
417
+ for (const button of ['left', 'middle', 'right']) {
418
+ try { await page?.mouse?.up?.({ button }); } catch {}
419
+ }
420
+ });
421
+ }
408
422
  clearTimeout(peer.expiryTimer);
409
423
  peer.expiryTimer = null;
410
424
  session.peers.delete(peer.peerId);
@@ -591,13 +605,23 @@ export class BrowserRuntimeService {
591
605
  return true;
592
606
  }
593
607
  if (session.peers.size >= this.config.maxPeersPerSession) throw new BrowserRuntimeError('browser_peer_limit');
608
+ const role = message?.role === 'interactive' ? 'interactive' : 'viewer';
609
+ if (role === 'interactive' && [...session.peers.values()].some(item => item.role === 'interactive')) {
610
+ throw new BrowserRuntimeError('browser_interactive_peer_limit');
611
+ }
594
612
  const peer = {
595
613
  peerId,
596
614
  connectionGeneration,
597
615
  identity,
616
+ role,
598
617
  state: 'preparing',
599
618
  webCandidateCount: 0,
600
619
  agentCandidateCount: 0,
620
+ sequences: new ProducerSequenceState({ producerId: peerId, producerGeneration: connectionGeneration }),
621
+ actionChain: Promise.resolve(),
622
+ pendingControlCount: 0,
623
+ pendingPointer: null,
624
+ pointerDrainScheduled: false,
601
625
  expiresAt: Number(message?.routeExpiresAt) || Date.now() + 10 * 60_000,
602
626
  expiryTimer: null,
603
627
  };
@@ -613,6 +637,7 @@ export class BrowserRuntimeService {
613
637
  type: 'peer_prepare',
614
638
  peerId,
615
639
  connectionGeneration,
640
+ role: peer.role,
616
641
  iceServers: publicIceServers(message?.agentIceServers),
617
642
  iceTransportPolicy: message?.iceTransportPolicy === 'relay' ? 'relay' : 'all',
618
643
  maxBitrate: this.config.maxBitrate,
@@ -743,8 +768,102 @@ export class BrowserRuntimeService {
743
768
  });
744
769
  }
745
770
 
771
+ async #executeInteractiveAction(session, peer, envelope, pointer = false) {
772
+ if (session.state !== 'ready' || peer.role !== 'interactive' || peer.state !== 'connected') return;
773
+ const sequenceEnvelope = {
774
+ producerId: peer.peerId,
775
+ producerGeneration: peer.connectionGeneration,
776
+ ...(pointer ? { pointerSeq: Number(envelope?.pointerSeq) } : { controlSeq: Number(envelope?.controlSeq) }),
777
+ };
778
+ const accepted = pointer
779
+ ? peer.sequences.acceptPointer(sequenceEnvelope)
780
+ : peer.sequences.acceptControl(sequenceEnvelope);
781
+ if (!accepted.accepted) return;
782
+ const action = envelope?.action;
783
+ if (!action || typeof action !== 'object') return;
784
+ const page = session.runtime?.page;
785
+ if (!page) return;
786
+ if (pointer) {
787
+ const x = Math.min(session.viewport?.width || 1920, Math.max(0, Number(action.x) || 0));
788
+ const y = Math.min(session.viewport?.height || 1080, Math.max(0, Number(action.y) || 0));
789
+ if (action.type === 'pointerMove') await page.mouse.move(x, y);
790
+ return;
791
+ }
792
+ if (action.type === 'navigate') {
793
+ const url = safeInitialUrl(action.url);
794
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: this.config.maxActionRuntimeMs });
795
+ } else if (action.type === 'mouse') {
796
+ const hasPosition = Number.isFinite(Number(action.x)) && Number.isFinite(Number(action.y));
797
+ const x = Math.min(session.viewport?.width || 1920, Math.max(0, Number(action.x) || 0));
798
+ const y = Math.min(session.viewport?.height || 1080, Math.max(0, Number(action.y) || 0));
799
+ const button = ['left', 'middle', 'right'].includes(action.button) ? action.button : 'left';
800
+ if (hasPosition) await page.mouse.move(x, y);
801
+ if (action.event === 'down') await page.mouse.down({ button });
802
+ else if (action.event === 'up') await page.mouse.up({ button });
803
+ else if (action.event === 'click') await page.mouse.click(x, y, { button, clickCount: Math.min(3, Math.max(1, Number(action.clickCount) || 1)) });
804
+ } else if (action.type === 'wheel') {
805
+ await page.mouse.wheel({
806
+ deltaX: Math.max(-4000, Math.min(4000, Number(action.deltaX) || 0)),
807
+ deltaY: Math.max(-4000, Math.min(4000, Number(action.deltaY) || 0)),
808
+ });
809
+ } else if (action.type === 'key') {
810
+ const key = clean(action.key, 128);
811
+ if (!key) return;
812
+ if (action.event === 'down') await page.keyboard.down(key);
813
+ else if (action.event === 'up') await page.keyboard.up(key);
814
+ else await page.keyboard.press(key);
815
+ } else if (action.type === 'text') {
816
+ const text = typeof action.text === 'string' ? action.text.slice(0, 16 * 1024) : '';
817
+ if (text) await page.keyboard.type(text);
818
+ } else if (action.type === 'resetInput') {
819
+ for (const key of ['Alt', 'Control', 'Meta', 'Shift']) {
820
+ try { await page.keyboard.up(key); } catch {}
821
+ }
822
+ for (const button of ['left', 'middle', 'right']) {
823
+ try { await page.mouse.up({ button }); } catch {}
824
+ }
825
+ }
826
+ }
827
+
746
828
  #handleBridgeMessage(session, message) {
747
829
  if (this.sessions.get(session.browserSessionId) !== session) return;
830
+ if (message.type === 'peer_input') {
831
+ const peer = session.peers.get(clean(message.peerId));
832
+ if (!peer || peer.connectionGeneration !== Number(message.connectionGeneration)) return;
833
+ if (message.channel === 'pointer') {
834
+ peer.pendingPointer = message.envelope;
835
+ if (peer.pointerDrainScheduled) return;
836
+ peer.pointerDrainScheduled = true;
837
+ peer.actionChain = peer.actionChain.then(async () => {
838
+ while (peer.pendingPointer && session.peers.get(peer.peerId) === peer) {
839
+ const envelope = peer.pendingPointer;
840
+ peer.pendingPointer = null;
841
+ await this.#executeInteractiveAction(session, peer, envelope, true);
842
+ }
843
+ }).catch(() => {}).finally(() => { peer.pointerDrainScheduled = false; });
844
+ return;
845
+ }
846
+ const maxPending = Math.max(1, Number(this.config.maxQueuedActionsPerProducer) || 32);
847
+ if (peer.pendingControlCount >= maxPending) {
848
+ this.#dropPeer(session, peer, 'control_queue_saturated');
849
+ void this.#emit({
850
+ type: 'browser_peer_error',
851
+ browserSessionId: session.browserSessionId,
852
+ peerId: peer.peerId,
853
+ connectionGeneration: peer.connectionGeneration,
854
+ code: 'browser_control_queue_saturated',
855
+ safeError: 'Browser input queue saturated; reconnect required',
856
+ });
857
+ void this.#emitSnapshot(session);
858
+ return;
859
+ }
860
+ peer.pendingControlCount += 1;
861
+ peer.actionChain = peer.actionChain
862
+ .then(() => this.#executeInteractiveAction(session, peer, message.envelope, false))
863
+ .catch(() => {})
864
+ .finally(() => { peer.pendingControlCount = Math.max(0, peer.pendingControlCount - 1); });
865
+ return;
866
+ }
748
867
  if (message.type === 'peer_prepared' || message.type === 'peer_offer'
749
868
  || message.type === 'peer_ice_candidate' || message.type === 'peer_state'
750
869
  || message.type === 'peer_error') {
@@ -205,7 +205,7 @@ export async function handleClientBrowser(client, msg, checkAgentAccess) {
205
205
  client,
206
206
  requestId,
207
207
  connectionGeneration,
208
- role: 'viewer',
208
+ role: msg.role === 'interactive' ? 'interactive' : 'viewer',
209
209
  });
210
210
  if (reserved.error) return fail(client, msg, reserved.error);
211
211
  const peer = reserved.peer;
@@ -243,6 +243,7 @@ export async function handleClientBrowser(client, msg, checkAgentAccess) {
243
243
  connectionGeneration: peer.connectionGeneration,
244
244
  serverIdentity: identity,
245
245
  routeExpiresAt: peer.expiresAt,
246
+ role: peer.role,
246
247
  iceTransportPolicy: CONFIG.browserRuntime.iceTransportPolicy,
247
248
  agentIceServers: mintBrowserIceServers({ ...commonScope, endpointRole: 'agent' }),
248
249
  });
@@ -1 +1 @@
1
- {"version":"1.0.428"}
1
+ {"version":"1.0.430"}