@yeaft/webchat-agent 1.0.416 → 1.0.418
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/browser-install.js +39 -0
- 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 +123 -0
- package/browser-runtime/service.js +769 -30
- package/connection/index.js +2 -0
- package/connection/message-router.js +2 -0
- package/index.js +13 -0
- package/local-runtime/server/.env.example +10 -0
- package/local-runtime/server/browser-runtime-routes.js +284 -0
- package/local-runtime/server/client-protocol.js +8 -0
- package/local-runtime/server/config.js +28 -0
- package/local-runtime/server/handlers/agent-browser.js +393 -0
- package/local-runtime/server/handlers/agent-sync.js +14 -0
- package/local-runtime/server/handlers/client-browser.js +337 -0
- package/local-runtime/server/ws-agent.js +9 -1
- package/local-runtime/server/ws-client.js +41 -2
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +220 -98
- 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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
3
|
+
|
|
4
|
+
const MAX_BRIDGE_MESSAGE_BYTES = 128 * 1024;
|
|
5
|
+
|
|
6
|
+
function bridgeToken() {
|
|
7
|
+
return randomBytes(32).toString('base64url');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseMessage(raw) {
|
|
11
|
+
const bytes = Buffer.isBuffer(raw) ? raw.length : Buffer.byteLength(String(raw || ''), 'utf8');
|
|
12
|
+
if (bytes <= 0 || bytes > MAX_BRIDGE_MESSAGE_BYTES) return null;
|
|
13
|
+
try {
|
|
14
|
+
const value = JSON.parse(raw.toString());
|
|
15
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Loopback-only authenticated bridge between the Agent and its bundled MV3
|
|
23
|
+
* extension. Tokens are random per Browser Session and never leave the Agent.
|
|
24
|
+
*/
|
|
25
|
+
export class BrowserExtensionBridge {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.server = null;
|
|
28
|
+
this.address = null;
|
|
29
|
+
this.sessionsByToken = new Map();
|
|
30
|
+
this.sessionsById = new Map();
|
|
31
|
+
this.startPromise = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async start() {
|
|
35
|
+
if (this.server) return this.address;
|
|
36
|
+
if (this.startPromise) return this.startPromise;
|
|
37
|
+
this.startPromise = new Promise((resolve, reject) => {
|
|
38
|
+
const server = new WebSocketServer({
|
|
39
|
+
host: '127.0.0.1',
|
|
40
|
+
port: 0,
|
|
41
|
+
maxPayload: MAX_BRIDGE_MESSAGE_BYTES,
|
|
42
|
+
perMessageDeflate: false,
|
|
43
|
+
});
|
|
44
|
+
const fail = error => {
|
|
45
|
+
server.close();
|
|
46
|
+
this.startPromise = null;
|
|
47
|
+
reject(error);
|
|
48
|
+
};
|
|
49
|
+
server.once('error', fail);
|
|
50
|
+
server.once('listening', () => {
|
|
51
|
+
server.off('error', fail);
|
|
52
|
+
server.on('error', error => console.warn('[BrowserRuntime] extension bridge error:', error.message));
|
|
53
|
+
server.on('connection', (socket, request) => this.#accept(socket, request));
|
|
54
|
+
const address = server.address();
|
|
55
|
+
if (!address || typeof address === 'string') return fail(new Error('Browser bridge address unavailable'));
|
|
56
|
+
this.server = server;
|
|
57
|
+
this.address = Object.freeze({ host: '127.0.0.1', port: address.port });
|
|
58
|
+
resolve(this.address);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
return this.startPromise;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async registerSession(browserSessionId, handlers = {}) {
|
|
65
|
+
if (!browserSessionId || this.sessionsById.has(browserSessionId)) {
|
|
66
|
+
throw new Error('Browser bridge session already registered');
|
|
67
|
+
}
|
|
68
|
+
const address = await this.start();
|
|
69
|
+
const token = bridgeToken();
|
|
70
|
+
const record = {
|
|
71
|
+
browserSessionId,
|
|
72
|
+
token,
|
|
73
|
+
socket: null,
|
|
74
|
+
ready: false,
|
|
75
|
+
closed: false,
|
|
76
|
+
readyMessage: null,
|
|
77
|
+
readyWaiters: new Set(),
|
|
78
|
+
onMessage: typeof handlers.onMessage === 'function' ? handlers.onMessage : () => {},
|
|
79
|
+
onDisconnect: typeof handlers.onDisconnect === 'function' ? handlers.onDisconnect : () => {},
|
|
80
|
+
};
|
|
81
|
+
this.sessionsByToken.set(token, record);
|
|
82
|
+
this.sessionsById.set(browserSessionId, record);
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
token,
|
|
85
|
+
bridgeUrl: `ws://${address.host}:${address.port}/browser-runtime/${token}`,
|
|
86
|
+
waitUntilReady: timeoutMs => this.waitUntilReady(browserSessionId, timeoutMs),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#accept(socket, request) {
|
|
91
|
+
const pathname = (() => {
|
|
92
|
+
try { return new URL(request.url || '/', 'ws://127.0.0.1').pathname; } catch { return ''; }
|
|
93
|
+
})();
|
|
94
|
+
const prefix = '/browser-runtime/';
|
|
95
|
+
const token = pathname.startsWith(prefix) ? pathname.slice(prefix.length) : '';
|
|
96
|
+
const record = this.sessionsByToken.get(token);
|
|
97
|
+
if (!record || record.closed) {
|
|
98
|
+
socket.close(1008, 'Unknown Browser Session');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (record.socket && record.socket.readyState === WebSocket.OPEN) {
|
|
102
|
+
record.socket.close(1008, 'Superseded extension connection');
|
|
103
|
+
}
|
|
104
|
+
record.socket = socket;
|
|
105
|
+
record.ready = false;
|
|
106
|
+
|
|
107
|
+
socket.on('message', raw => {
|
|
108
|
+
if (record.closed || record.socket !== socket) return;
|
|
109
|
+
const message = parseMessage(raw);
|
|
110
|
+
if (!message || message.browserSessionId !== record.browserSessionId) {
|
|
111
|
+
socket.close(1008, 'Invalid Browser Runtime bridge message');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!record.ready) {
|
|
115
|
+
if (message.type !== 'runtime_ready') {
|
|
116
|
+
socket.close(1008, 'Browser Runtime bridge not ready');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
record.ready = true;
|
|
120
|
+
record.readyMessage = message;
|
|
121
|
+
const waiters = [...record.readyWaiters];
|
|
122
|
+
record.readyWaiters.clear();
|
|
123
|
+
for (const waiter of waiters) waiter.resolve(message);
|
|
124
|
+
}
|
|
125
|
+
record.onMessage(message);
|
|
126
|
+
});
|
|
127
|
+
socket.on('close', () => {
|
|
128
|
+
if (record.socket !== socket) return;
|
|
129
|
+
record.socket = null;
|
|
130
|
+
record.ready = false;
|
|
131
|
+
record.readyMessage = null;
|
|
132
|
+
if (!record.closed) record.onDisconnect();
|
|
133
|
+
});
|
|
134
|
+
socket.on('error', () => {});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async waitUntilReady(browserSessionId, timeoutMs = 15_000) {
|
|
138
|
+
const record = this.sessionsById.get(browserSessionId);
|
|
139
|
+
if (!record || record.closed) throw new Error('Browser bridge session unavailable');
|
|
140
|
+
if (record.ready) return record.readyMessage || { type: 'runtime_ready', browserSessionId };
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const waiter = { resolve, reject, timer: null };
|
|
143
|
+
waiter.timer = setTimeout(() => {
|
|
144
|
+
record.readyWaiters.delete(waiter);
|
|
145
|
+
reject(new Error('Browser extension bridge timed out'));
|
|
146
|
+
}, Math.max(1, timeoutMs));
|
|
147
|
+
waiter.timer.unref?.();
|
|
148
|
+
const settle = fn => value => {
|
|
149
|
+
clearTimeout(waiter.timer);
|
|
150
|
+
record.readyWaiters.delete(waiter);
|
|
151
|
+
fn(value);
|
|
152
|
+
};
|
|
153
|
+
waiter.resolve = settle(resolve);
|
|
154
|
+
waiter.reject = settle(reject);
|
|
155
|
+
record.readyWaiters.add(waiter);
|
|
156
|
+
if (record.ready) waiter.resolve(record.readyMessage || { type: 'runtime_ready', browserSessionId });
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
send(browserSessionId, message) {
|
|
161
|
+
const record = this.sessionsById.get(browserSessionId);
|
|
162
|
+
if (!record?.ready || record.socket?.readyState !== WebSocket.OPEN) return false;
|
|
163
|
+
const payload = JSON.stringify({ ...message, browserSessionId });
|
|
164
|
+
if (Buffer.byteLength(payload, 'utf8') > MAX_BRIDGE_MESSAGE_BYTES) return false;
|
|
165
|
+
record.socket.send(payload);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
unregisterSession(browserSessionId, reason = 'Browser Session closed') {
|
|
170
|
+
const record = this.sessionsById.get(browserSessionId);
|
|
171
|
+
if (!record) return false;
|
|
172
|
+
record.closed = true;
|
|
173
|
+
this.sessionsById.delete(browserSessionId);
|
|
174
|
+
this.sessionsByToken.delete(record.token);
|
|
175
|
+
const waiters = [...record.readyWaiters];
|
|
176
|
+
record.readyWaiters.clear();
|
|
177
|
+
for (const waiter of waiters) waiter.reject(new Error(reason));
|
|
178
|
+
try { record.socket?.close(1000, reason.slice(0, 120)); } catch {}
|
|
179
|
+
record.socket = null;
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async close() {
|
|
184
|
+
for (const browserSessionId of [...this.sessionsById.keys()]) {
|
|
185
|
+
this.unregisterSession(browserSessionId, 'Browser Runtime shutdown');
|
|
186
|
+
}
|
|
187
|
+
const server = this.server;
|
|
188
|
+
this.server = null;
|
|
189
|
+
this.address = null;
|
|
190
|
+
this.startPromise = null;
|
|
191
|
+
if (!server) return;
|
|
192
|
+
await new Promise(resolve => server.close(() => resolve()));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import ctx from '../context.js';
|
|
2
|
+
import { BrowserRuntimeError } from './errors.js';
|
|
3
|
+
|
|
4
|
+
const BROWSER_MESSAGE_TYPES = new Set([
|
|
5
|
+
'browser_runtime_status',
|
|
6
|
+
'browser_runtime_install',
|
|
7
|
+
'browser_runtime_enable',
|
|
8
|
+
'browser_session_create',
|
|
9
|
+
'browser_session_get',
|
|
10
|
+
'browser_session_list',
|
|
11
|
+
'browser_session_close',
|
|
12
|
+
'browser_peer_prepare',
|
|
13
|
+
'browser_peer_answer',
|
|
14
|
+
'browser_peer_ice_candidate',
|
|
15
|
+
'browser_peer_detach',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function errorMessage(msg, error) {
|
|
19
|
+
const type = String(msg?.type || '');
|
|
20
|
+
const peerScoped = type.startsWith('browser_peer_');
|
|
21
|
+
const setupScoped = type.startsWith('browser_runtime_');
|
|
22
|
+
return {
|
|
23
|
+
type: peerScoped ? 'browser_peer_error' : setupScoped ? 'browser_runtime_error' : 'browser_session_error',
|
|
24
|
+
requestId: msg?.requestId || null,
|
|
25
|
+
browserSessionId: msg?.browserSessionId || null,
|
|
26
|
+
...(peerScoped ? {
|
|
27
|
+
peerId: msg?.peerId || null,
|
|
28
|
+
connectionGeneration: msg?.connectionGeneration || null,
|
|
29
|
+
} : {}),
|
|
30
|
+
code: error?.code || 'browser_runtime_error',
|
|
31
|
+
safeError: String(error?.message || error).slice(0, 500),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertSetupIdentity(message) {
|
|
36
|
+
const identity = message?.serverIdentity;
|
|
37
|
+
if (!identity || typeof identity !== 'object'
|
|
38
|
+
|| !identity.ownerUserId || !identity.clientId
|
|
39
|
+
|| !identity.webConnectionId || !identity.webConnectionGeneration) {
|
|
40
|
+
throw new BrowserRuntimeError('browser_identity_required');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Route one authenticated Server command into the Agent-local Browser Runtime. */
|
|
45
|
+
export async function handleBrowserRuntimeMessage(msg, dependencies = {}) {
|
|
46
|
+
if (!BROWSER_MESSAGE_TYPES.has(msg?.type)) return false;
|
|
47
|
+
const runtime = dependencies.runtime || ctx.browserRuntime;
|
|
48
|
+
const send = dependencies.send || ctx.sendToServer;
|
|
49
|
+
if (!runtime) {
|
|
50
|
+
await send?.(errorMessage(msg, new Error('browser_runtime_unavailable')));
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
switch (msg.type) {
|
|
55
|
+
case 'browser_runtime_status':
|
|
56
|
+
assertSetupIdentity(msg);
|
|
57
|
+
await send?.({
|
|
58
|
+
type: 'browser_runtime_status_result',
|
|
59
|
+
requestId: msg.requestId || null,
|
|
60
|
+
...(await runtime.setupStatus()),
|
|
61
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
case 'browser_runtime_install': {
|
|
64
|
+
assertSetupIdentity(msg);
|
|
65
|
+
const progress = async value => send?.({
|
|
66
|
+
type: 'browser_runtime_install_progress',
|
|
67
|
+
requestId: msg.requestId || null,
|
|
68
|
+
...value,
|
|
69
|
+
});
|
|
70
|
+
const result = await runtime.installAndEnable({
|
|
71
|
+
confirmedBuildId: msg.confirmedBuildId,
|
|
72
|
+
confirmedDownloadBytes: msg.confirmedDownloadBytes,
|
|
73
|
+
onProgress: progress,
|
|
74
|
+
});
|
|
75
|
+
await send?.({
|
|
76
|
+
type: 'browser_runtime_status_result',
|
|
77
|
+
requestId: msg.requestId || null,
|
|
78
|
+
...result,
|
|
79
|
+
});
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 'browser_runtime_enable':
|
|
83
|
+
assertSetupIdentity(msg);
|
|
84
|
+
await send?.({
|
|
85
|
+
type: 'browser_runtime_status_result',
|
|
86
|
+
requestId: msg.requestId || null,
|
|
87
|
+
...(await runtime.enableAndProbe()),
|
|
88
|
+
});
|
|
89
|
+
break;
|
|
90
|
+
case 'browser_session_create':
|
|
91
|
+
await runtime.createSession(msg);
|
|
92
|
+
break;
|
|
93
|
+
case 'browser_session_get':
|
|
94
|
+
await runtime.getSession(msg);
|
|
95
|
+
break;
|
|
96
|
+
case 'browser_session_list':
|
|
97
|
+
await runtime.listSessions(msg);
|
|
98
|
+
break;
|
|
99
|
+
case 'browser_session_close':
|
|
100
|
+
await runtime.closeSession(msg);
|
|
101
|
+
break;
|
|
102
|
+
case 'browser_peer_prepare':
|
|
103
|
+
await runtime.preparePeer(msg);
|
|
104
|
+
break;
|
|
105
|
+
case 'browser_peer_answer':
|
|
106
|
+
await runtime.answerPeer(msg);
|
|
107
|
+
break;
|
|
108
|
+
case 'browser_peer_ice_candidate':
|
|
109
|
+
await runtime.addPeerIceCandidate(msg);
|
|
110
|
+
break;
|
|
111
|
+
case 'browser_peer_detach':
|
|
112
|
+
await runtime.detachPeer(msg);
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
await send?.(errorMessage(msg, error));
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { BROWSER_MESSAGE_TYPES };
|