@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.
- package/browser-runtime/extension/offscreen.js +28 -0
- package/browser-runtime/extension.js +1 -1
- package/browser-runtime/service.js +121 -2
- package/local-runtime/server/handlers/client-browser.js +2 -1
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +144 -100
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
|
@@ -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 = '
|
|
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:
|
|
393
|
-
authorizedProducerCount:
|
|
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.
|
|
1
|
+
{"version":"1.0.430"}
|