@yeaft/webchat-agent 1.0.415 → 1.0.417
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/chromium.js +192 -0
- package/browser-runtime/cli.js +1 -1
- package/browser-runtime/extension/manifest.json +3 -0
- package/browser-runtime/extension/offscreen.js +201 -25
- package/browser-runtime/extension/popup.js +4 -1
- package/browser-runtime/extension/service-worker.js +47 -7
- package/browser-runtime/extension.js +1 -1
- package/browser-runtime/index.js +3 -0
- package/browser-runtime/local-bridge.js +194 -0
- package/browser-runtime/messages.js +73 -0
- package/browser-runtime/service.js +549 -26
- package/connection/index.js +2 -0
- package/connection/message-router.js +2 -0
- package/index.js +2 -0
- package/local-runtime/agent/container-manager.js +189 -0
- package/local-runtime/server/.env.example +10 -0
- package/local-runtime/server/browser-runtime-routes.js +265 -0
- package/local-runtime/server/client-protocol.js +4 -0
- package/local-runtime/server/config.js +28 -0
- package/local-runtime/server/handlers/agent-browser.js +308 -0
- package/local-runtime/server/handlers/client-browser.js +290 -0
- package/local-runtime/server/ws-agent.js +8 -1
- package/local-runtime/server/ws-client.js +33 -2
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +152 -95
- 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
- package/scripts/prepare-local-runtime.js +39 -20
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { sendToAgent, sendToWebClient } from '../ws-utils.js';
|
|
2
|
+
import {
|
|
3
|
+
browserClientForPeer,
|
|
4
|
+
browserPeers,
|
|
5
|
+
browserRoutes,
|
|
6
|
+
completeBrowserRequest,
|
|
7
|
+
deleteBrowserPeer,
|
|
8
|
+
deleteBrowserRoute,
|
|
9
|
+
getBrowserPeer,
|
|
10
|
+
getBrowserRoute,
|
|
11
|
+
installBrowserRoute,
|
|
12
|
+
} from '../browser-runtime-routes.js';
|
|
13
|
+
|
|
14
|
+
const AGENT_BROWSER_TYPES = new Set([
|
|
15
|
+
'browser_session_created',
|
|
16
|
+
'browser_session_error',
|
|
17
|
+
'browser_session_snapshot',
|
|
18
|
+
'browser_session_list_result',
|
|
19
|
+
'browser_peer_prepared',
|
|
20
|
+
'browser_peer_offer',
|
|
21
|
+
'browser_peer_ice_candidate',
|
|
22
|
+
'browser_peer_state',
|
|
23
|
+
'browser_peer_error',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function clean(value, max = 512) {
|
|
27
|
+
return typeof value === 'string' ? value.trim().slice(0, max) : '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sessionSnapshot(msg) {
|
|
31
|
+
return {
|
|
32
|
+
browserSessionId: clean(msg?.browserSessionId),
|
|
33
|
+
revision: Number(msg?.revision) || 1,
|
|
34
|
+
state: clean(msg?.state, 32) || 'unknown',
|
|
35
|
+
activeUrl: clean(msg?.activeUrl, 4096) || 'about:blank',
|
|
36
|
+
title: clean(msg?.title, 512),
|
|
37
|
+
pageRevision: Number(msg?.pageRevision) || 1,
|
|
38
|
+
captureMode: clean(msg?.captureMode, 32) || null,
|
|
39
|
+
viewport: msg?.viewport && typeof msg.viewport === 'object' ? {
|
|
40
|
+
width: Number(msg.viewport.width) || 0,
|
|
41
|
+
height: Number(msg.viewport.height) || 0,
|
|
42
|
+
deviceScaleFactor: Number(msg.viewport.deviceScaleFactor) || 1,
|
|
43
|
+
} : null,
|
|
44
|
+
viewerCount: Math.max(0, Number(msg?.viewerCount) || 0),
|
|
45
|
+
interactivePeerCount: Math.max(0, Number(msg?.interactivePeerCount) || 0),
|
|
46
|
+
authorizedProducerCount: Math.max(0, Number(msg?.authorizedProducerCount) || 0),
|
|
47
|
+
expiresAt: Number(msg?.expiresAt) || null,
|
|
48
|
+
terminalReason: clean(msg?.terminalReason, 128) || null,
|
|
49
|
+
safeError: clean(msg?.safeError, 500) || null,
|
|
50
|
+
sourceRef: msg?.sourceRef && typeof msg.sourceRef === 'object' ? {
|
|
51
|
+
kind: clean(msg.sourceRef.kind, 32),
|
|
52
|
+
sessionId: clean(msg.sourceRef.sessionId) || undefined,
|
|
53
|
+
conversationId: clean(msg.sourceRef.conversationId) || undefined,
|
|
54
|
+
workItemId: clean(msg.sourceRef.workItemId) || undefined,
|
|
55
|
+
} : null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function publicSessionMessage(agentId, msg) {
|
|
60
|
+
if (msg?.type === 'browser_session_error') {
|
|
61
|
+
return {
|
|
62
|
+
type: 'browser_session_error',
|
|
63
|
+
agentId,
|
|
64
|
+
requestId: clean(msg.requestId) || null,
|
|
65
|
+
browserSessionId: clean(msg.browserSessionId) || null,
|
|
66
|
+
code: clean(msg.code, 128) || 'browser_runtime_error',
|
|
67
|
+
safeError: clean(msg.safeError, 500) || null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (msg?.type === 'browser_session_list_result') {
|
|
71
|
+
return {
|
|
72
|
+
type: msg.type,
|
|
73
|
+
agentId,
|
|
74
|
+
requestId: clean(msg.requestId) || null,
|
|
75
|
+
sessions: (Array.isArray(msg.sessions) ? msg.sessions : []).slice(0, 64).map(sessionSnapshot),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
type: clean(msg?.type, 64),
|
|
80
|
+
agentId,
|
|
81
|
+
requestId: clean(msg?.requestId) || null,
|
|
82
|
+
...sessionSnapshot(msg),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function sendCreateResponse(agentId, msg) {
|
|
87
|
+
const request = completeBrowserRequest(agentId, msg);
|
|
88
|
+
if (!request) return true;
|
|
89
|
+
const client = browserClientForPeer({
|
|
90
|
+
clientId: request.clientId,
|
|
91
|
+
webConnectionId: request.webConnectionId,
|
|
92
|
+
webConnectionGeneration: request.webConnectionGeneration,
|
|
93
|
+
});
|
|
94
|
+
if (!client || client.userId !== request.ownerUserId) return true;
|
|
95
|
+
if (msg.type === 'browser_session_created') {
|
|
96
|
+
const route = installBrowserRoute({ agentId, ownerUserId: request.ownerUserId, msg });
|
|
97
|
+
if (!route) {
|
|
98
|
+
await sendToWebClient(client, {
|
|
99
|
+
type: 'browser_session_error',
|
|
100
|
+
agentId,
|
|
101
|
+
requestId: request.requestId,
|
|
102
|
+
browserSessionId: clean(msg.browserSessionId) || null,
|
|
103
|
+
code: 'browser_route_capacity',
|
|
104
|
+
safeError: 'browser_route_capacity',
|
|
105
|
+
});
|
|
106
|
+
await sendToAgent(agent, {
|
|
107
|
+
type: 'browser_session_close',
|
|
108
|
+
agentId,
|
|
109
|
+
requestId: `server-cleanup-${request.serverRequestId}`,
|
|
110
|
+
browserSessionId: clean(msg.browserSessionId),
|
|
111
|
+
expectedRevision: Number(msg.revision) || 1,
|
|
112
|
+
serverIdentity: {
|
|
113
|
+
ownerUserId: request.ownerUserId,
|
|
114
|
+
clientId: request.clientId,
|
|
115
|
+
webConnectionId: request.webConnectionId,
|
|
116
|
+
webConnectionGeneration: request.webConnectionGeneration,
|
|
117
|
+
},
|
|
118
|
+
}).catch(() => {});
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const publicResponse = publicSessionMessage(agentId, request.response);
|
|
123
|
+
// Create request replay is served from the ledger. Persist only the explicit
|
|
124
|
+
// public projection, never the Agent-originated object.
|
|
125
|
+
request.response = publicResponse;
|
|
126
|
+
await sendToWebClient(client, publicResponse);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Agent-authenticated Browser events routed only through Server-owned ledgers. */
|
|
131
|
+
export async function handleAgentBrowser(agentId, agent, msg) {
|
|
132
|
+
if (!AGENT_BROWSER_TYPES.has(msg?.type)) return false;
|
|
133
|
+
if (msg.type === 'browser_session_created'
|
|
134
|
+
|| (msg.type === 'browser_session_error' && msg.requestId)) {
|
|
135
|
+
return sendCreateResponse(agentId, msg);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (msg.type === 'browser_session_list_result') {
|
|
139
|
+
const request = completeBrowserRequest(agentId, msg, { consume: true });
|
|
140
|
+
if (!request) return true;
|
|
141
|
+
for (const snapshot of Array.isArray(msg.sessions) ? msg.sessions : []) {
|
|
142
|
+
if (!snapshot?.browserSessionId) continue;
|
|
143
|
+
installBrowserRoute({ agentId, ownerUserId: request.ownerUserId, msg: snapshot });
|
|
144
|
+
}
|
|
145
|
+
const client = browserClientForPeer({
|
|
146
|
+
clientId: request.clientId,
|
|
147
|
+
webConnectionId: request.webConnectionId,
|
|
148
|
+
webConnectionGeneration: request.webConnectionGeneration,
|
|
149
|
+
});
|
|
150
|
+
if (client?.userId === request.ownerUserId) {
|
|
151
|
+
await sendToWebClient(client, publicSessionMessage(agentId, request.response));
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (msg.type === 'browser_session_snapshot' && msg.requestId) {
|
|
157
|
+
const request = completeBrowserRequest(agentId, msg, { consume: true });
|
|
158
|
+
if (!request) return true;
|
|
159
|
+
const client = browserClientForPeer({
|
|
160
|
+
clientId: request.clientId,
|
|
161
|
+
webConnectionId: request.webConnectionId,
|
|
162
|
+
webConnectionGeneration: request.webConnectionGeneration,
|
|
163
|
+
});
|
|
164
|
+
if (client?.userId === request.ownerUserId) {
|
|
165
|
+
const route = getBrowserRoute(agentId, msg.browserSessionId);
|
|
166
|
+
if (route) {
|
|
167
|
+
route.revision = Number(msg.revision) || route.revision;
|
|
168
|
+
route.state = clean(msg.state, 32) || route.state;
|
|
169
|
+
if (route.state === 'closed' || route.state === 'failed') {
|
|
170
|
+
deleteBrowserRoute(agentId, route.browserSessionId);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
await sendToWebClient(client, publicSessionMessage(agentId, request.response));
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (msg.type === 'browser_session_snapshot') {
|
|
179
|
+
const route = getBrowserRoute(agentId, msg.browserSessionId);
|
|
180
|
+
if (!route) return true;
|
|
181
|
+
route.revision = Number(msg.revision) || route.revision;
|
|
182
|
+
route.state = clean(msg.state, 32) || route.state;
|
|
183
|
+
route.updatedAt = Date.now();
|
|
184
|
+
const recipients = [];
|
|
185
|
+
for (const peer of browserPeers.values()) {
|
|
186
|
+
if (peer.agentId !== agentId || peer.browserSessionId !== route.browserSessionId) continue;
|
|
187
|
+
const client = browserClientForPeer(peer);
|
|
188
|
+
if (client && !recipients.includes(client)) recipients.push(client);
|
|
189
|
+
}
|
|
190
|
+
if (route.state === 'closed' || route.state === 'failed') {
|
|
191
|
+
deleteBrowserRoute(agentId, route.browserSessionId);
|
|
192
|
+
}
|
|
193
|
+
const snapshot = publicSessionMessage(agentId, msg);
|
|
194
|
+
for (const client of recipients) await sendToWebClient(client, snapshot);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const peer = getBrowserPeer(msg.peerId);
|
|
199
|
+
if (!peer || peer.agentId !== agentId
|
|
200
|
+
|| peer.browserSessionId !== msg.browserSessionId
|
|
201
|
+
|| peer.connectionGeneration !== Number(msg.connectionGeneration)) return true;
|
|
202
|
+
const route = getBrowserRoute(agentId, peer.browserSessionId);
|
|
203
|
+
if (!route || route.ownerUserId !== peer.ownerUserId) {
|
|
204
|
+
deleteBrowserPeer(peer.peerId);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
const client = browserClientForPeer(peer);
|
|
208
|
+
if (!client || client.userId !== route.ownerUserId) {
|
|
209
|
+
deleteBrowserPeer(peer.peerId);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (msg.type === 'browser_peer_prepared') {
|
|
214
|
+
peer.state = 'prepared';
|
|
215
|
+
await sendToWebClient(client, {
|
|
216
|
+
type: msg.type,
|
|
217
|
+
agentId,
|
|
218
|
+
browserSessionId: peer.browserSessionId,
|
|
219
|
+
peerId: peer.peerId,
|
|
220
|
+
requestId: peer.requestId,
|
|
221
|
+
connectionGeneration: peer.connectionGeneration,
|
|
222
|
+
iceTransportPolicy: peer.iceTransportPolicy,
|
|
223
|
+
iceServers: peer.webIceServers || [],
|
|
224
|
+
role: peer.role,
|
|
225
|
+
});
|
|
226
|
+
if (peer.pendingOffer) {
|
|
227
|
+
await sendToWebClient(client, peer.pendingOffer);
|
|
228
|
+
peer.pendingOffer = null;
|
|
229
|
+
peer.state = 'offered';
|
|
230
|
+
}
|
|
231
|
+
for (const candidate of peer.pendingCandidates.splice(0)) {
|
|
232
|
+
await sendToWebClient(client, candidate);
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
if (msg.type === 'browser_peer_offer') {
|
|
237
|
+
const description = msg.description;
|
|
238
|
+
if (description?.type !== 'offer' || typeof description.sdp !== 'string' || description.sdp.length > 96 * 1024) {
|
|
239
|
+
deleteBrowserPeer(peer.peerId);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
const offer = {
|
|
243
|
+
type: msg.type,
|
|
244
|
+
agentId,
|
|
245
|
+
browserSessionId: peer.browserSessionId,
|
|
246
|
+
peerId: peer.peerId,
|
|
247
|
+
requestId: peer.requestId,
|
|
248
|
+
connectionGeneration: peer.connectionGeneration,
|
|
249
|
+
description: { type: 'offer', sdp: description.sdp },
|
|
250
|
+
iceServers: peer.webIceServers || [],
|
|
251
|
+
role: peer.role,
|
|
252
|
+
};
|
|
253
|
+
if (peer.state !== 'prepared' && peer.state !== 'offered') {
|
|
254
|
+
peer.pendingOffer = offer;
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
peer.state = 'offered';
|
|
258
|
+
await sendToWebClient(client, offer);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if (msg.type === 'browser_peer_ice_candidate') {
|
|
262
|
+
if (peer.agentCandidateCount >= 128) return true;
|
|
263
|
+
const raw = msg.candidate;
|
|
264
|
+
if (raw != null && (typeof raw !== 'object'
|
|
265
|
+
|| typeof raw.candidate !== 'string'
|
|
266
|
+
|| raw.candidate.length > 4096)) return true;
|
|
267
|
+
peer.agentCandidateCount += 1;
|
|
268
|
+
const candidate = {
|
|
269
|
+
type: msg.type,
|
|
270
|
+
agentId,
|
|
271
|
+
browserSessionId: peer.browserSessionId,
|
|
272
|
+
peerId: peer.peerId,
|
|
273
|
+
connectionGeneration: peer.connectionGeneration,
|
|
274
|
+
candidate: raw == null ? null : {
|
|
275
|
+
candidate: raw.candidate,
|
|
276
|
+
sdpMid: clean(raw.sdpMid, 256) || null,
|
|
277
|
+
sdpMLineIndex: Number.isInteger(raw.sdpMLineIndex) ? raw.sdpMLineIndex : null,
|
|
278
|
+
usernameFragment: clean(raw.usernameFragment, 256) || null,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
if (peer.state !== 'prepared' && peer.state !== 'offered') {
|
|
282
|
+
if (peer.pendingCandidates.length < 128) peer.pendingCandidates.push(candidate);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
await sendToWebClient(client, candidate);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
if (msg.type === 'browser_peer_state') {
|
|
289
|
+
peer.state = clean(msg.state, 32) || peer.state;
|
|
290
|
+
if (peer.state === 'connected') peer.expiresAt = null;
|
|
291
|
+
if (['failed', 'disconnected', 'closed'].includes(peer.state)) deleteBrowserPeer(peer.peerId);
|
|
292
|
+
}
|
|
293
|
+
if (msg.type === 'browser_peer_error') deleteBrowserPeer(peer.peerId);
|
|
294
|
+
await sendToWebClient(client, {
|
|
295
|
+
type: msg.type,
|
|
296
|
+
agentId,
|
|
297
|
+
browserSessionId: peer.browserSessionId,
|
|
298
|
+
peerId: peer.peerId,
|
|
299
|
+
connectionGeneration: peer.connectionGeneration,
|
|
300
|
+
requestId: peer.requestId,
|
|
301
|
+
state: clean(msg.state, 32) || null,
|
|
302
|
+
code: clean(msg.code, 128) || null,
|
|
303
|
+
safeError: clean(msg.safeError, 500) || null,
|
|
304
|
+
});
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export { AGENT_BROWSER_TYPES };
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { CONFIG } from '../config.js';
|
|
3
|
+
import { agents } from '../context.js';
|
|
4
|
+
import { sendToAgent, sendToWebClient } from '../ws-utils.js';
|
|
5
|
+
import {
|
|
6
|
+
browserPeerMatchesClient,
|
|
7
|
+
browserServerIdentity,
|
|
8
|
+
deleteBrowserPeer,
|
|
9
|
+
getBrowserPeer,
|
|
10
|
+
getBrowserRoute,
|
|
11
|
+
mintBrowserIceServers,
|
|
12
|
+
registerBrowserCreateRequest,
|
|
13
|
+
registerBrowserRequest,
|
|
14
|
+
reserveBrowserPeer,
|
|
15
|
+
} from '../browser-runtime-routes.js';
|
|
16
|
+
|
|
17
|
+
const CLIENT_BROWSER_TYPES = new Set([
|
|
18
|
+
'browser_session_create',
|
|
19
|
+
'browser_session_get',
|
|
20
|
+
'browser_session_list',
|
|
21
|
+
'browser_session_close',
|
|
22
|
+
'browser_peer_attach',
|
|
23
|
+
'browser_peer_answer',
|
|
24
|
+
'browser_peer_ice_candidate',
|
|
25
|
+
'browser_peer_detach',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function clean(value, max = 512) {
|
|
29
|
+
return typeof value === 'string' ? value.trim().slice(0, max) : '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function digest(value) {
|
|
33
|
+
return createHash('sha256').update(JSON.stringify(value || null)).digest('hex');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function fail(client, msg, code, safeError = code) {
|
|
37
|
+
await sendToWebClient(client, {
|
|
38
|
+
type: String(msg.type || '').startsWith('browser_peer_') ? 'browser_peer_error' : 'browser_session_error',
|
|
39
|
+
requestId: clean(msg.requestId) || null,
|
|
40
|
+
browserSessionId: clean(msg.browserSessionId) || null,
|
|
41
|
+
peerId: clean(msg.peerId) || null,
|
|
42
|
+
connectionGeneration: Number(msg.connectionGeneration) || null,
|
|
43
|
+
code,
|
|
44
|
+
safeError,
|
|
45
|
+
});
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function agentSupportsBrowser(agent) {
|
|
50
|
+
const capabilities = new Set(agent?.capabilities || []);
|
|
51
|
+
return capabilities.has('browser_runtime')
|
|
52
|
+
&& capabilities.has('browser_webrtc')
|
|
53
|
+
&& (capabilities.has('browser_capture_tab') || capabilities.has('browser_capture_cdp'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function safeInitialUrl(value) {
|
|
57
|
+
const raw = clean(value, 4096) || 'about:blank';
|
|
58
|
+
if (raw === 'about:blank') return raw;
|
|
59
|
+
try {
|
|
60
|
+
const url = new URL(raw);
|
|
61
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return null;
|
|
62
|
+
return url.href;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function canonicalCreate(msg) {
|
|
69
|
+
const initialUrl = safeInitialUrl(msg.options?.initialUrl);
|
|
70
|
+
if (!initialUrl) return null;
|
|
71
|
+
return {
|
|
72
|
+
sourceRef: msg.sourceRef && typeof msg.sourceRef === 'object' ? {
|
|
73
|
+
kind: clean(msg.sourceRef.kind, 32),
|
|
74
|
+
sessionId: clean(msg.sourceRef.sessionId),
|
|
75
|
+
conversationId: clean(msg.sourceRef.conversationId),
|
|
76
|
+
workItemId: clean(msg.sourceRef.workItemId),
|
|
77
|
+
} : null,
|
|
78
|
+
options: {
|
|
79
|
+
initialUrl,
|
|
80
|
+
viewport: {
|
|
81
|
+
width: Math.min(1920, Math.max(320, Math.floor(Number(msg.options?.viewport?.width) || 1280))),
|
|
82
|
+
height: Math.min(1080, Math.max(240, Math.floor(Number(msg.options?.viewport?.height) || 720))),
|
|
83
|
+
deviceScaleFactor: Math.min(2, Math.max(1, Number(msg.options?.viewport?.deviceScaleFactor) || 1)),
|
|
84
|
+
},
|
|
85
|
+
locale: clean(msg.options?.locale, 32) || 'en-US',
|
|
86
|
+
capturePreference: ['auto', 'tab'].includes(msg.options?.capturePreference)
|
|
87
|
+
? msg.options.capturePreference : 'auto',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ownerRoute(client, msg) {
|
|
93
|
+
const route = getBrowserRoute(msg.agentId, msg.browserSessionId);
|
|
94
|
+
return route && route.ownerUserId === client.userId ? route : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Owner-checked Web → Server → Agent Browser lifecycle and signaling relay. */
|
|
98
|
+
export async function handleClientBrowser(client, msg, checkAgentAccess) {
|
|
99
|
+
if (!CLIENT_BROWSER_TYPES.has(msg?.type)) return false;
|
|
100
|
+
if (!CONFIG.browserRuntime.enabled) return fail(client, msg, 'browser_runtime_disabled');
|
|
101
|
+
if (client.browserRuntimeProtocol !== 1) return fail(client, msg, 'browser_protocol_required');
|
|
102
|
+
const agentId = clean(msg.agentId);
|
|
103
|
+
if (!await checkAgentAccess(agentId)) return true;
|
|
104
|
+
const agent = agents.get(agentId);
|
|
105
|
+
if (!agentSupportsBrowser(agent)) return fail(client, msg, 'browser_runtime_unavailable');
|
|
106
|
+
const requestId = clean(msg.requestId);
|
|
107
|
+
const requestRequired = msg.type !== 'browser_peer_answer'
|
|
108
|
+
&& msg.type !== 'browser_peer_ice_candidate';
|
|
109
|
+
if (requestRequired && !requestId) return fail(client, msg, 'browser_request_id_required');
|
|
110
|
+
const identity = browserServerIdentity(client);
|
|
111
|
+
|
|
112
|
+
if (msg.type === 'browser_session_create') {
|
|
113
|
+
const canonical = canonicalCreate(msg);
|
|
114
|
+
if (!canonical) return fail(client, msg, 'browser_url_invalid');
|
|
115
|
+
const registration = registerBrowserCreateRequest({
|
|
116
|
+
agentId,
|
|
117
|
+
client,
|
|
118
|
+
requestId,
|
|
119
|
+
digest: digest(canonical),
|
|
120
|
+
});
|
|
121
|
+
if (registration.conflict) return fail(client, msg, 'browser_request_conflict');
|
|
122
|
+
if (registration.capacity) return fail(client, msg, 'browser_request_capacity');
|
|
123
|
+
if (registration.duplicate) {
|
|
124
|
+
if (registration.request.response) {
|
|
125
|
+
await sendToWebClient(client, registration.request.response);
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
await sendToAgent(agent, {
|
|
130
|
+
type: msg.type,
|
|
131
|
+
agentId,
|
|
132
|
+
requestId: registration.request.serverRequestId,
|
|
133
|
+
...canonical,
|
|
134
|
+
serverIdentity: identity,
|
|
135
|
+
});
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (msg.type === 'browser_session_list') {
|
|
140
|
+
const request = registerBrowserRequest({ agentId, client, requestId, kind: msg.type });
|
|
141
|
+
if (!request) return fail(client, msg, 'browser_request_conflict');
|
|
142
|
+
await sendToAgent(agent, {
|
|
143
|
+
type: msg.type,
|
|
144
|
+
agentId,
|
|
145
|
+
requestId: request.serverRequestId,
|
|
146
|
+
serverIdentity: identity,
|
|
147
|
+
});
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (msg.type === 'browser_peer_attach') {
|
|
152
|
+
if (!ownerRoute(client, msg)) return fail(client, msg, 'browser_session_not_found');
|
|
153
|
+
const connectionGeneration = Number(msg.connectionGeneration);
|
|
154
|
+
const reserved = reserveBrowserPeer({
|
|
155
|
+
agentId,
|
|
156
|
+
browserSessionId: clean(msg.browserSessionId),
|
|
157
|
+
client,
|
|
158
|
+
requestId,
|
|
159
|
+
connectionGeneration,
|
|
160
|
+
role: 'viewer',
|
|
161
|
+
});
|
|
162
|
+
if (reserved.error) return fail(client, msg, reserved.error);
|
|
163
|
+
const peer = reserved.peer;
|
|
164
|
+
if (reserved.duplicate) {
|
|
165
|
+
if (peer.state === 'prepared' || peer.state === 'offered' || peer.state === 'connected') {
|
|
166
|
+
await sendToWebClient(client, {
|
|
167
|
+
type: 'browser_peer_prepared',
|
|
168
|
+
agentId,
|
|
169
|
+
browserSessionId: peer.browserSessionId,
|
|
170
|
+
peerId: peer.peerId,
|
|
171
|
+
requestId: peer.requestId,
|
|
172
|
+
connectionGeneration: peer.connectionGeneration,
|
|
173
|
+
iceTransportPolicy: peer.iceTransportPolicy,
|
|
174
|
+
iceServers: peer.webIceServers || [],
|
|
175
|
+
role: peer.role,
|
|
176
|
+
});
|
|
177
|
+
if (peer.pendingOffer) await sendToWebClient(client, peer.pendingOffer);
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
const commonScope = {
|
|
182
|
+
ownerUserId: client.userId,
|
|
183
|
+
agentId,
|
|
184
|
+
browserSessionId: peer.browserSessionId,
|
|
185
|
+
peerId: peer.peerId,
|
|
186
|
+
connectionGeneration: peer.connectionGeneration,
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
await sendToAgent(agent, {
|
|
190
|
+
type: 'browser_peer_prepare',
|
|
191
|
+
agentId,
|
|
192
|
+
browserSessionId: peer.browserSessionId,
|
|
193
|
+
peerId: peer.peerId,
|
|
194
|
+
requestId,
|
|
195
|
+
connectionGeneration: peer.connectionGeneration,
|
|
196
|
+
serverIdentity: identity,
|
|
197
|
+
routeExpiresAt: peer.expiresAt,
|
|
198
|
+
iceTransportPolicy: CONFIG.browserRuntime.iceTransportPolicy,
|
|
199
|
+
agentIceServers: mintBrowserIceServers({ ...commonScope, endpointRole: 'agent' }),
|
|
200
|
+
});
|
|
201
|
+
peer.webIceServers = mintBrowserIceServers({ ...commonScope, endpointRole: 'web' });
|
|
202
|
+
peer.state = 'preparing';
|
|
203
|
+
} catch (error) {
|
|
204
|
+
deleteBrowserPeer(peer.peerId);
|
|
205
|
+
return fail(client, msg, 'browser_peer_prepare_failed', String(error?.message || error).slice(0, 500));
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (msg.type === 'browser_session_get' || msg.type === 'browser_session_close') {
|
|
211
|
+
const route = ownerRoute(client, msg);
|
|
212
|
+
if (!route) return fail(client, msg, 'browser_session_not_found');
|
|
213
|
+
const request = registerBrowserRequest({
|
|
214
|
+
agentId,
|
|
215
|
+
client,
|
|
216
|
+
requestId,
|
|
217
|
+
kind: msg.type,
|
|
218
|
+
browserSessionId: route.browserSessionId,
|
|
219
|
+
});
|
|
220
|
+
if (!request) return fail(client, msg, 'browser_request_conflict');
|
|
221
|
+
await sendToAgent(agent, {
|
|
222
|
+
type: msg.type,
|
|
223
|
+
agentId,
|
|
224
|
+
requestId: request.serverRequestId,
|
|
225
|
+
browserSessionId: route.browserSessionId,
|
|
226
|
+
...(msg.type === 'browser_session_close' ? { expectedRevision: Number(msg.expectedRevision) || route.revision } : {}),
|
|
227
|
+
serverIdentity: identity,
|
|
228
|
+
});
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const peer = getBrowserPeer(msg.peerId);
|
|
233
|
+
if (!browserPeerMatchesClient(peer, client, msg)) return fail(client, msg, 'browser_peer_stale');
|
|
234
|
+
if (msg.type === 'browser_peer_answer') {
|
|
235
|
+
const description = msg.description;
|
|
236
|
+
if (description?.type !== 'answer' || typeof description.sdp !== 'string' || description.sdp.length > 96 * 1024) {
|
|
237
|
+
return fail(client, msg, 'browser_sdp_invalid');
|
|
238
|
+
}
|
|
239
|
+
peer.state = 'answering';
|
|
240
|
+
await sendToAgent(agent, {
|
|
241
|
+
type: msg.type,
|
|
242
|
+
agentId,
|
|
243
|
+
browserSessionId: peer.browserSessionId,
|
|
244
|
+
peerId: peer.peerId,
|
|
245
|
+
connectionGeneration: peer.connectionGeneration,
|
|
246
|
+
description: { type: 'answer', sdp: description.sdp },
|
|
247
|
+
serverIdentity: identity,
|
|
248
|
+
});
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
if (msg.type === 'browser_peer_ice_candidate') {
|
|
252
|
+
if (peer.webCandidateCount >= 128) return fail(client, msg, 'browser_candidate_limit');
|
|
253
|
+
const candidate = msg.candidate;
|
|
254
|
+
if (candidate != null && (typeof candidate !== 'object'
|
|
255
|
+
|| typeof candidate.candidate !== 'string'
|
|
256
|
+
|| candidate.candidate.length > 4096)) return fail(client, msg, 'browser_candidate_invalid');
|
|
257
|
+
peer.webCandidateCount += 1;
|
|
258
|
+
await sendToAgent(agent, {
|
|
259
|
+
type: msg.type,
|
|
260
|
+
agentId,
|
|
261
|
+
browserSessionId: peer.browserSessionId,
|
|
262
|
+
peerId: peer.peerId,
|
|
263
|
+
connectionGeneration: peer.connectionGeneration,
|
|
264
|
+
candidate: candidate == null ? null : {
|
|
265
|
+
candidate: candidate.candidate,
|
|
266
|
+
sdpMid: clean(candidate.sdpMid, 256) || null,
|
|
267
|
+
sdpMLineIndex: Number.isInteger(candidate.sdpMLineIndex) ? candidate.sdpMLineIndex : null,
|
|
268
|
+
usernameFragment: clean(candidate.usernameFragment, 256) || null,
|
|
269
|
+
},
|
|
270
|
+
serverIdentity: identity,
|
|
271
|
+
});
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
if (msg.type === 'browser_peer_detach') {
|
|
275
|
+
deleteBrowserPeer(peer.peerId);
|
|
276
|
+
await sendToAgent(agent, {
|
|
277
|
+
type: msg.type,
|
|
278
|
+
agentId,
|
|
279
|
+
requestId,
|
|
280
|
+
browserSessionId: peer.browserSessionId,
|
|
281
|
+
peerId: peer.peerId,
|
|
282
|
+
connectionGeneration: peer.connectionGeneration,
|
|
283
|
+
serverIdentity: identity,
|
|
284
|
+
});
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export { CLIENT_BROWSER_TYPES };
|
|
@@ -16,6 +16,8 @@ import { handleAgentFileTerminal } from './handlers/agent-file-terminal.js';
|
|
|
16
16
|
import { handleAgentSync } from './handlers/agent-sync.js';
|
|
17
17
|
import { recordPerfTraceEvent } from './perf-trace.js';
|
|
18
18
|
import { clearWorkbenchCorrelationsForAgent } from './workbench-correlation.js';
|
|
19
|
+
import { clearBrowserRuntimeForAgent } from './browser-runtime-routes.js';
|
|
20
|
+
import { handleAgentBrowser } from './handlers/agent-browser.js';
|
|
19
21
|
import { markAgentHeartbeatSeen } from './heartbeat-policy.js';
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -229,6 +231,7 @@ function handleAgentDisconnect(agentId, agentName, ws) {
|
|
|
229
231
|
// Phase 4: 清理目录缓存
|
|
230
232
|
clearAgentDirCache(agentId);
|
|
231
233
|
clearWorkbenchCorrelationsForAgent(agentId);
|
|
234
|
+
clearBrowserRuntimeForAgent(agentId);
|
|
232
235
|
// Phase 1: 清理同步超时
|
|
233
236
|
if (agent._syncTimeout) {
|
|
234
237
|
clearTimeout(agent._syncTimeout);
|
|
@@ -294,6 +297,7 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
|
|
|
294
297
|
agents.get(agentId)._syncTimeout = syncTimeout;
|
|
295
298
|
|
|
296
299
|
if (existingAgent?.ws && existingAgent.ws !== ws) {
|
|
300
|
+
clearBrowserRuntimeForAgent(agentId);
|
|
297
301
|
existingAgent.ws.close(1008, 'Superseded by a newer Agent connection');
|
|
298
302
|
}
|
|
299
303
|
|
|
@@ -347,7 +351,9 @@ async function handleAgentMessage(agentId, msg, ws) {
|
|
|
347
351
|
'yeaft_history_chunk', 'yeaft_history_outline', 'yeaft_history_search_result', 'yeaft_history_window', 'slash_commands_update', 'agent_metrics',
|
|
348
352
|
'file_content', 'file_saved', 'file_op_result', 'file_search_result',
|
|
349
353
|
'git_status_result', 'git_diff_result', 'git_op_result',
|
|
350
|
-
'terminal_created', 'terminal_output', 'terminal_closed', 'terminal_error'
|
|
354
|
+
'terminal_created', 'terminal_output', 'terminal_closed', 'terminal_error',
|
|
355
|
+
'browser_session_created', 'browser_session_error', 'browser_session_snapshot', 'browser_session_list_result',
|
|
356
|
+
'browser_peer_prepared', 'browser_peer_offer', 'browser_peer_ice_candidate', 'browser_peer_state', 'browser_peer_error'
|
|
351
357
|
]);
|
|
352
358
|
if (msg.conversationId && !CONV_EXEMPT_TYPES.has(msg.type)) {
|
|
353
359
|
if (!agent.conversations.has(msg.conversationId)) {
|
|
@@ -358,6 +364,7 @@ async function handleAgentMessage(agentId, msg, ws) {
|
|
|
358
364
|
|
|
359
365
|
// Dispatch to handler sub-modules
|
|
360
366
|
if (await handleAgentConversation(agentId, agent, msg)) return;
|
|
367
|
+
if (await handleAgentBrowser(agentId, agent, msg)) return;
|
|
361
368
|
if (await handleAgentWorkCenter(agentId, msg)) return;
|
|
362
369
|
if (await handleAgentOutput(agentId, agent, msg)) return;
|
|
363
370
|
if (await handleAgentFileTerminal(agentId, agent, msg)) return;
|