@yeaft/webchat-agent 1.0.417 → 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/messages.js +52 -2
- package/browser-runtime/service.js +221 -5
- package/index.js +12 -1
- package/local-runtime/server/browser-runtime-routes.js +22 -3
- package/local-runtime/server/client-protocol.js +4 -0
- package/local-runtime/server/handlers/agent-browser.js +88 -3
- package/local-runtime/server/handlers/agent-sync.js +14 -0
- package/local-runtime/server/handlers/client-browser.js +50 -3
- package/local-runtime/server/ws-agent.js +1 -0
- package/local-runtime/server/ws-client.js +9 -1
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +155 -90
- 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
|
@@ -31,25 +31,64 @@ export const BROWSER_RUNTIME_CHROME_ARCHIVES = Object.freeze({
|
|
|
31
31
|
linux: Object.freeze({
|
|
32
32
|
fileName: 'chrome-linux64.zip',
|
|
33
33
|
sha256: '6bd04aab53fba1544ce6027d9daddb24137295033124a61ecdf9840d785792e9',
|
|
34
|
+
downloadBytes: 193_285_407,
|
|
34
35
|
}),
|
|
35
36
|
mac: Object.freeze({
|
|
36
37
|
fileName: 'chrome-mac-x64.zip',
|
|
37
38
|
sha256: 'bedcd79ae533fed218c26232b74e73cffec2a7277fce42cafcf5ec7280e4f81c',
|
|
39
|
+
downloadBytes: 197_094_599,
|
|
38
40
|
}),
|
|
39
41
|
mac_arm: Object.freeze({
|
|
40
42
|
fileName: 'chrome-mac-arm64.zip',
|
|
41
43
|
sha256: '1c516b5d6c00a074034d5ce03dc1cc9bd2cde2a09293d9613244e0bc153cb80f',
|
|
44
|
+
downloadBytes: 187_099_652,
|
|
42
45
|
}),
|
|
43
46
|
win32: Object.freeze({
|
|
44
47
|
fileName: 'chrome-win32.zip',
|
|
45
48
|
sha256: '338f15dcf19d457f93f692c279843477a92324f0f91f78bf5380d3fe00a9796f',
|
|
49
|
+
downloadBytes: 175_083_534,
|
|
46
50
|
}),
|
|
47
51
|
win64: Object.freeze({
|
|
48
52
|
fileName: 'chrome-win64.zip',
|
|
49
53
|
sha256: '7ea2e94833ef710026c8cb08d0d2dafcb13f5d304d9c475ac07a3fa8c11d846c',
|
|
54
|
+
downloadBytes: 201_082_763,
|
|
50
55
|
}),
|
|
51
56
|
});
|
|
52
57
|
|
|
58
|
+
function browserPlatformForRuntime(platform, arch) {
|
|
59
|
+
if (platform === 'linux' && arch === 'x64') return 'linux';
|
|
60
|
+
if (platform === 'darwin' && arch === 'x64') return 'mac';
|
|
61
|
+
if (platform === 'darwin' && arch === 'arm64') return 'mac_arm';
|
|
62
|
+
if (platform === 'win32' && arch === 'ia32') return 'win32';
|
|
63
|
+
if (platform === 'win32' && arch === 'x64') return 'win64';
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Static, side-effect-free metadata shown before an explicit managed-browser install. */
|
|
68
|
+
export function managedBrowserDownloadInfo({
|
|
69
|
+
platform = process.platform,
|
|
70
|
+
arch = process.arch,
|
|
71
|
+
archives = BROWSER_RUNTIME_CHROME_ARCHIVES,
|
|
72
|
+
} = {}) {
|
|
73
|
+
const browserPlatform = browserPlatformForRuntime(platform, arch);
|
|
74
|
+
const archive = browserPlatform ? archives[browserPlatform] : null;
|
|
75
|
+
if (!archive) {
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
supported: false,
|
|
78
|
+
buildId: BROWSER_RUNTIME_CHROME_BUILD,
|
|
79
|
+
platform: browserPlatform,
|
|
80
|
+
downloadBytes: 0,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
supported: true,
|
|
85
|
+
buildId: BROWSER_RUNTIME_CHROME_BUILD,
|
|
86
|
+
platform: browserPlatform,
|
|
87
|
+
fileName: archive.fileName,
|
|
88
|
+
downloadBytes: Number(archive.downloadBytes) || 0,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
53
92
|
export function defaultBrowserCacheDir(yeaftDir) {
|
|
54
93
|
if (!yeaftDir) throw new Error('yeaftDir required');
|
|
55
94
|
return join(yeaftDir, 'managed-browser');
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import ctx from '../context.js';
|
|
2
|
+
import { BrowserRuntimeError } from './errors.js';
|
|
2
3
|
|
|
3
4
|
const BROWSER_MESSAGE_TYPES = new Set([
|
|
5
|
+
'browser_runtime_status',
|
|
6
|
+
'browser_runtime_install',
|
|
7
|
+
'browser_runtime_enable',
|
|
4
8
|
'browser_session_create',
|
|
5
9
|
'browser_session_get',
|
|
6
10
|
'browser_session_list',
|
|
@@ -12,9 +16,11 @@ const BROWSER_MESSAGE_TYPES = new Set([
|
|
|
12
16
|
]);
|
|
13
17
|
|
|
14
18
|
function errorMessage(msg, error) {
|
|
15
|
-
const
|
|
19
|
+
const type = String(msg?.type || '');
|
|
20
|
+
const peerScoped = type.startsWith('browser_peer_');
|
|
21
|
+
const setupScoped = type.startsWith('browser_runtime_');
|
|
16
22
|
return {
|
|
17
|
-
type: peerScoped ? 'browser_peer_error' : 'browser_session_error',
|
|
23
|
+
type: peerScoped ? 'browser_peer_error' : setupScoped ? 'browser_runtime_error' : 'browser_session_error',
|
|
18
24
|
requestId: msg?.requestId || null,
|
|
19
25
|
browserSessionId: msg?.browserSessionId || null,
|
|
20
26
|
...(peerScoped ? {
|
|
@@ -26,6 +32,15 @@ function errorMessage(msg, error) {
|
|
|
26
32
|
};
|
|
27
33
|
}
|
|
28
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
|
+
|
|
29
44
|
/** Route one authenticated Server command into the Agent-local Browser Runtime. */
|
|
30
45
|
export async function handleBrowserRuntimeMessage(msg, dependencies = {}) {
|
|
31
46
|
if (!BROWSER_MESSAGE_TYPES.has(msg?.type)) return false;
|
|
@@ -37,6 +52,41 @@ export async function handleBrowserRuntimeMessage(msg, dependencies = {}) {
|
|
|
37
52
|
}
|
|
38
53
|
try {
|
|
39
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;
|
|
40
90
|
case 'browser_session_create':
|
|
41
91
|
await runtime.createSession(msg);
|
|
42
92
|
break;
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { normaliseBrowserRuntimeSection } from './config.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
defaultBrowserCacheDir,
|
|
5
|
+
installManagedBrowser,
|
|
6
|
+
managedBrowserDownloadInfo,
|
|
7
|
+
resolveBrowserExecutable,
|
|
8
|
+
} from './browser-install.js';
|
|
4
9
|
import { probeBrowserRuntime } from './probe.js';
|
|
5
10
|
import { BrowserRuntimeError } from './errors.js';
|
|
6
11
|
import { BrowserExtensionBridge } from './local-bridge.js';
|
|
7
12
|
import { launchBrowserSession } from './chromium.js';
|
|
8
13
|
|
|
9
14
|
const REQUEST_TTL_MS = 10 * 60_000;
|
|
15
|
+
const MAX_INSTALL_PROGRESS_LISTENERS = 8;
|
|
10
16
|
|
|
11
17
|
function clean(value, max = 512) {
|
|
12
18
|
return typeof value === 'string' ? value.trim().slice(0, max) : '';
|
|
@@ -89,47 +95,156 @@ export class BrowserRuntimeService {
|
|
|
89
95
|
yeaftDir,
|
|
90
96
|
config,
|
|
91
97
|
probe = probeBrowserRuntime,
|
|
98
|
+
resolveBrowser = resolveBrowserExecutable,
|
|
99
|
+
installBrowser = installManagedBrowser,
|
|
100
|
+
downloadInfo = managedBrowserDownloadInfo,
|
|
101
|
+
saveSettings = null,
|
|
102
|
+
onCapabilitiesChanged = null,
|
|
92
103
|
bridge = new BrowserExtensionBridge(),
|
|
93
104
|
launchSession = launchBrowserSession,
|
|
94
105
|
send = null,
|
|
95
106
|
platform = process.platform,
|
|
107
|
+
arch = process.arch,
|
|
96
108
|
} = {}) {
|
|
97
109
|
if (!yeaftDir) throw new Error('yeaftDir required');
|
|
98
110
|
this.yeaftDir = yeaftDir;
|
|
99
111
|
this.config = normaliseBrowserRuntimeSection(config);
|
|
100
112
|
this.config.cacheDir ||= defaultBrowserCacheDir(yeaftDir);
|
|
101
113
|
this.probe = probe;
|
|
114
|
+
this.resolveBrowser = resolveBrowser;
|
|
115
|
+
this.installBrowser = installBrowser;
|
|
116
|
+
this.downloadInfo = downloadInfo;
|
|
117
|
+
this.saveSettings = typeof saveSettings === 'function' ? saveSettings : null;
|
|
118
|
+
this.onCapabilitiesChanged = typeof onCapabilitiesChanged === 'function' ? onCapabilitiesChanged : null;
|
|
102
119
|
this.bridge = bridge;
|
|
103
120
|
this.launchSession = launchSession;
|
|
104
121
|
this.send = typeof send === 'function' ? send : () => 'dropped';
|
|
105
122
|
this.platform = platform;
|
|
123
|
+
this.arch = arch;
|
|
124
|
+
const download = this.downloadInfo({ platform, arch });
|
|
125
|
+
this.setupInfo = Object.freeze({
|
|
126
|
+
...download,
|
|
127
|
+
// Phase 1 advertises only the real Linux tab-capture data plane. The CLI
|
|
128
|
+
// may manage pinned binaries on more platforms, but Web must not offer an
|
|
129
|
+
// install that can never produce a ready capability.
|
|
130
|
+
supported: download.supported === true && platform === 'linux' && arch === 'x64',
|
|
131
|
+
});
|
|
106
132
|
this.sessions = new Map();
|
|
107
133
|
this.requests = new Map();
|
|
108
134
|
this.probeResult = null;
|
|
135
|
+
this.installProgress = null;
|
|
136
|
+
this.lastSetupError = null;
|
|
109
137
|
this.state = this.config.enabled ? 'unprobed' : 'disabled';
|
|
110
138
|
this.#probePromise = null;
|
|
111
139
|
this.#probeAbort = null;
|
|
140
|
+
this.#installPromise = null;
|
|
141
|
+
this.#installAbort = null;
|
|
142
|
+
this.#installListeners = new Set();
|
|
143
|
+
this.#lastInstallProgressAt = 0;
|
|
112
144
|
this.#shutdownPromise = null;
|
|
113
145
|
}
|
|
114
146
|
|
|
115
147
|
#probePromise;
|
|
116
148
|
#probeAbort;
|
|
149
|
+
#installPromise;
|
|
150
|
+
#installAbort;
|
|
151
|
+
#installListeners;
|
|
152
|
+
#lastInstallProgressAt;
|
|
117
153
|
#shutdownPromise;
|
|
118
154
|
|
|
119
155
|
get enabled() { return this.config.enabled === true; }
|
|
120
156
|
get ready() { return this.state === 'ready' && this.probeResult?.ok === true; }
|
|
121
157
|
|
|
158
|
+
setupCapabilities() {
|
|
159
|
+
return this.setupInfo.supported ? ['browser_runtime_setup'] : [];
|
|
160
|
+
}
|
|
161
|
+
|
|
122
162
|
capabilities() {
|
|
123
163
|
if (!this.ready || this.platform !== 'linux' || this.probeResult?.captureMode !== 'tab') return [];
|
|
124
164
|
return ['browser_runtime', 'browser_webrtc', 'browser_capture_tab'];
|
|
125
165
|
}
|
|
126
166
|
|
|
127
|
-
async
|
|
167
|
+
async #notifyCapabilitiesChanged() {
|
|
168
|
+
try {
|
|
169
|
+
await this.onCapabilitiesChanged?.();
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.warn(`[BrowserRuntime] capability refresh failed: ${error?.message || error}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async #persistEnabled({ managed = false } = {}) {
|
|
176
|
+
const update = {
|
|
177
|
+
enabled: true,
|
|
178
|
+
...(managed ? { executablePath: null } : {}),
|
|
179
|
+
};
|
|
180
|
+
if (this.saveSettings) {
|
|
181
|
+
const saved = await this.saveSettings(update);
|
|
182
|
+
if (saved?.error) {
|
|
183
|
+
throw new BrowserRuntimeError('browser_config_update_failed', saved.error);
|
|
184
|
+
}
|
|
185
|
+
this.config = {
|
|
186
|
+
...this.config,
|
|
187
|
+
...normaliseBrowserRuntimeSection(saved),
|
|
188
|
+
cacheDir: this.config.cacheDir,
|
|
189
|
+
};
|
|
190
|
+
} else {
|
|
191
|
+
this.config = { ...this.config, ...update };
|
|
192
|
+
}
|
|
193
|
+
this.config.enabled = true;
|
|
194
|
+
if (managed) this.config.executablePath = null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async setupStatus() {
|
|
198
|
+
let executablePath = null;
|
|
199
|
+
if (this.setupInfo.supported) {
|
|
200
|
+
try {
|
|
201
|
+
executablePath = await this.resolveBrowser({
|
|
202
|
+
executablePath: this.config.executablePath,
|
|
203
|
+
cacheDir: this.config.cacheDir,
|
|
204
|
+
});
|
|
205
|
+
if (this.lastSetupError?.source === 'status') this.lastSetupError = null;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (!this.lastSetupError || this.lastSetupError.source === 'status') {
|
|
208
|
+
this.lastSetupError = {
|
|
209
|
+
source: 'status',
|
|
210
|
+
code: error?.code || 'browser_status_failed',
|
|
211
|
+
safeError: String(error?.message || error).slice(0, 500),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const installed = !!executablePath;
|
|
217
|
+
if (this.ready) this.lastSetupError = null;
|
|
218
|
+
const state = !this.setupInfo.supported ? 'unsupported'
|
|
219
|
+
: this.ready ? 'ready'
|
|
220
|
+
: this.#installPromise ? 'installing'
|
|
221
|
+
: this.state === 'probing' ? 'probing'
|
|
222
|
+
: this.probeResult && this.config.enabled ? 'unavailable'
|
|
223
|
+
: installed ? 'disabled'
|
|
224
|
+
: 'not_installed';
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
supported: this.setupInfo.supported,
|
|
227
|
+
state,
|
|
228
|
+
installed,
|
|
229
|
+
enabled: this.enabled,
|
|
230
|
+
ready: this.ready,
|
|
231
|
+
buildId: this.setupInfo.buildId,
|
|
232
|
+
platform: this.setupInfo.platform,
|
|
233
|
+
downloadBytes: this.setupInfo.downloadBytes,
|
|
234
|
+
downloadedBytes: Number(this.installProgress?.downloadedBytes) || 0,
|
|
235
|
+
totalBytes: Number(this.installProgress?.totalBytes) || this.setupInfo.downloadBytes,
|
|
236
|
+
probeCode: this.probeResult?.code || null,
|
|
237
|
+
safeError: this.lastSetupError?.safeError || this.probeResult?.safeError || null,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async startupProbe({ force = false } = {}) {
|
|
128
242
|
if (!this.enabled) return { ok: false, code: 'browser_runtime_disabled' };
|
|
129
243
|
if (this.#probePromise) return this.#probePromise;
|
|
244
|
+
if (!force && this.ready) return this.probeResult;
|
|
130
245
|
this.state = 'probing';
|
|
131
246
|
this.#probeAbort = new AbortController();
|
|
132
|
-
|
|
247
|
+
const probePromise = this.probe({
|
|
133
248
|
executablePath: this.config.executablePath,
|
|
134
249
|
cacheDir: this.config.cacheDir,
|
|
135
250
|
headless: this.config.headless,
|
|
@@ -148,8 +263,105 @@ export class BrowserRuntimeService {
|
|
|
148
263
|
});
|
|
149
264
|
this.state = 'unavailable';
|
|
150
265
|
return this.probeResult;
|
|
266
|
+
}).finally(() => {
|
|
267
|
+
if (this.#probePromise === probePromise) this.#probePromise = null;
|
|
268
|
+
this.#probeAbort = null;
|
|
151
269
|
});
|
|
152
|
-
|
|
270
|
+
this.#probePromise = probePromise;
|
|
271
|
+
return probePromise;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async enableAndProbe() {
|
|
275
|
+
if (!this.setupInfo.supported) throw new BrowserRuntimeError('browser_platform_unsupported');
|
|
276
|
+
this.lastSetupError = null;
|
|
277
|
+
try {
|
|
278
|
+
const executablePath = await this.resolveBrowser({
|
|
279
|
+
executablePath: this.config.executablePath,
|
|
280
|
+
cacheDir: this.config.cacheDir,
|
|
281
|
+
});
|
|
282
|
+
if (!executablePath) throw new BrowserRuntimeError('browser_executable_missing');
|
|
283
|
+
await this.#persistEnabled();
|
|
284
|
+
this.probeResult = null;
|
|
285
|
+
const probe = await this.startupProbe({ force: true });
|
|
286
|
+
await this.#notifyCapabilitiesChanged();
|
|
287
|
+
return { ...(await this.setupStatus()), probeCode: probe.code || null };
|
|
288
|
+
} catch (error) {
|
|
289
|
+
this.lastSetupError = {
|
|
290
|
+
source: 'enable',
|
|
291
|
+
code: error?.code || 'browser_enable_failed',
|
|
292
|
+
safeError: String(error?.message || error).slice(0, 500),
|
|
293
|
+
};
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async installAndEnable({ confirmedBuildId, confirmedDownloadBytes, onProgress = null } = {}) {
|
|
299
|
+
if (!this.setupInfo.supported) throw new BrowserRuntimeError('browser_platform_unsupported');
|
|
300
|
+
if (confirmedBuildId !== this.setupInfo.buildId
|
|
301
|
+
|| Number(confirmedDownloadBytes) !== this.setupInfo.downloadBytes) {
|
|
302
|
+
throw new BrowserRuntimeError('browser_install_confirmation_stale');
|
|
303
|
+
}
|
|
304
|
+
if (typeof onProgress === 'function') {
|
|
305
|
+
if (this.#installListeners.size >= MAX_INSTALL_PROGRESS_LISTENERS) {
|
|
306
|
+
throw new BrowserRuntimeError('browser_install_observer_limit');
|
|
307
|
+
}
|
|
308
|
+
this.#installListeners.add(onProgress);
|
|
309
|
+
}
|
|
310
|
+
if (!this.#installPromise) {
|
|
311
|
+
this.state = 'installing';
|
|
312
|
+
this.lastSetupError = null;
|
|
313
|
+
this.installProgress = Object.freeze({
|
|
314
|
+
downloadedBytes: 0,
|
|
315
|
+
totalBytes: this.setupInfo.downloadBytes,
|
|
316
|
+
});
|
|
317
|
+
this.#installAbort = new AbortController();
|
|
318
|
+
const installPromise = (async () => {
|
|
319
|
+
await this.installBrowser({
|
|
320
|
+
cacheDir: this.config.cacheDir,
|
|
321
|
+
signal: this.#installAbort.signal,
|
|
322
|
+
onProgress: (downloadedBytes, totalBytes) => {
|
|
323
|
+
const total = Number(totalBytes) || this.setupInfo.downloadBytes;
|
|
324
|
+
this.installProgress = Object.freeze({
|
|
325
|
+
downloadedBytes: Number(downloadedBytes) || 0,
|
|
326
|
+
totalBytes: total,
|
|
327
|
+
});
|
|
328
|
+
const now = Date.now();
|
|
329
|
+
if (now - this.#lastInstallProgressAt < 250 && downloadedBytes < total) return;
|
|
330
|
+
this.#lastInstallProgressAt = now;
|
|
331
|
+
for (const listener of this.#installListeners) {
|
|
332
|
+
try {
|
|
333
|
+
Promise.resolve(listener(this.installProgress)).catch(() => {});
|
|
334
|
+
} catch {}
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
await this.#persistEnabled({ managed: true });
|
|
339
|
+
this.probeResult = null;
|
|
340
|
+
await this.startupProbe({ force: true });
|
|
341
|
+
await this.#notifyCapabilitiesChanged();
|
|
342
|
+
return this.setupStatus();
|
|
343
|
+
})().catch(error => {
|
|
344
|
+
this.lastSetupError = {
|
|
345
|
+
source: 'install',
|
|
346
|
+
code: error?.code || 'browser_install_failed',
|
|
347
|
+
safeError: String(error?.message || error).slice(0, 500),
|
|
348
|
+
};
|
|
349
|
+
this.state = 'unavailable';
|
|
350
|
+
throw error;
|
|
351
|
+
}).finally(() => {
|
|
352
|
+
if (this.#installPromise === installPromise) {
|
|
353
|
+
this.#installPromise = null;
|
|
354
|
+
this.#installAbort = null;
|
|
355
|
+
}
|
|
356
|
+
this.#installListeners.clear();
|
|
357
|
+
});
|
|
358
|
+
this.#installPromise = installPromise;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
return await this.#installPromise;
|
|
362
|
+
} finally {
|
|
363
|
+
if (typeof onProgress === 'function') this.#installListeners.delete(onProgress);
|
|
364
|
+
}
|
|
153
365
|
}
|
|
154
366
|
|
|
155
367
|
#emit(message) {
|
|
@@ -625,7 +837,11 @@ export class BrowserRuntimeService {
|
|
|
625
837
|
async shutdown() {
|
|
626
838
|
if (this.#shutdownPromise) return this.#shutdownPromise;
|
|
627
839
|
this.#probeAbort?.abort(new BrowserRuntimeError('browser_runtime_shutdown'));
|
|
628
|
-
this.#
|
|
840
|
+
this.#installAbort?.abort(new BrowserRuntimeError('browser_runtime_shutdown'));
|
|
841
|
+
this.#shutdownPromise = Promise.allSettled([
|
|
842
|
+
Promise.resolve(this.#probePromise),
|
|
843
|
+
Promise.resolve(this.#installPromise),
|
|
844
|
+
]).then(async () => {
|
|
629
845
|
await Promise.allSettled([...this.sessions.values()]
|
|
630
846
|
.map(session => this.closeSessionRecord(session, 'browser_runtime_shutdown', { emit: false })));
|
|
631
847
|
await this.bridge.close();
|
package/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { connect } from './connection.js';
|
|
|
23
23
|
import { loadMcpServers } from './mcp.js';
|
|
24
24
|
import { SAFE_REMOTE_UPGRADE_CAPABILITY } from './upgrade-command.js';
|
|
25
25
|
import { loadConfig as loadYeaftConfig } from './yeaft/config.js';
|
|
26
|
+
import { updateBrowserRuntimeSettings } from './yeaft/config-api.js';
|
|
26
27
|
import { bootBrowserRuntime, shutdownBrowserRuntime } from './browser-runtime/index.js';
|
|
27
28
|
import {
|
|
28
29
|
ensureManagedCliTools,
|
|
@@ -164,7 +165,9 @@ async function detectCapabilities() {
|
|
|
164
165
|
if (process.platform === 'linux') capabilities.push('work_item_attachments');
|
|
165
166
|
const pty = await loadNodePty();
|
|
166
167
|
if (pty) capabilities.push('terminal');
|
|
167
|
-
if (ctx.browserRuntime)
|
|
168
|
+
if (ctx.browserRuntime) {
|
|
169
|
+
capabilities.push(...ctx.browserRuntime.setupCapabilities(), ...ctx.browserRuntime.capabilities());
|
|
170
|
+
}
|
|
168
171
|
|
|
169
172
|
console.log(`[Capabilities] Detected: ${capabilities.join(', ')}`);
|
|
170
173
|
return capabilities;
|
|
@@ -404,6 +407,14 @@ process.on('SIGTERM', async () => {
|
|
|
404
407
|
ctx.browserRuntime = await bootBrowserRuntime({
|
|
405
408
|
yeaftDir: YEAFT_DIR,
|
|
406
409
|
config: runtimeConfig,
|
|
410
|
+
saveSettings: update => updateBrowserRuntimeSettings(update, YEAFT_DIR),
|
|
411
|
+
onCapabilitiesChanged: async () => {
|
|
412
|
+
ctx.agentCapabilities = await detectCapabilities();
|
|
413
|
+
ctx.sendToServer?.({
|
|
414
|
+
type: 'agent_capabilities_updated',
|
|
415
|
+
capabilities: ctx.agentCapabilities,
|
|
416
|
+
});
|
|
417
|
+
},
|
|
407
418
|
send: message => ctx.sendToServer?.(message) || 'dropped',
|
|
408
419
|
});
|
|
409
420
|
const probe = ctx.browserRuntime.snapshot().probe;
|
|
@@ -6,6 +6,7 @@ const MAX_ROUTES = 2048;
|
|
|
6
6
|
const MAX_PEERS = 4096;
|
|
7
7
|
const MAX_CREATE_REQUESTS = 4096;
|
|
8
8
|
const CREATE_REQUEST_TTL_MS = 10 * 60_000;
|
|
9
|
+
const INSTALL_REQUEST_TTL_MS = 60 * 60_000;
|
|
9
10
|
|
|
10
11
|
export const browserRoutes = new Map();
|
|
11
12
|
export const browserPeers = new Map();
|
|
@@ -66,7 +67,13 @@ export function pruneBrowserRuntimeRoutes(now = Date.now()) {
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
export function registerBrowserCreateRequest({
|
|
70
|
+
export function registerBrowserCreateRequest({
|
|
71
|
+
agentId,
|
|
72
|
+
client,
|
|
73
|
+
requestId,
|
|
74
|
+
digest,
|
|
75
|
+
kind = 'browser_session_create',
|
|
76
|
+
}) {
|
|
70
77
|
pruneBrowserRuntimeRoutes();
|
|
71
78
|
const requestKey = createKey(agentId, client.connectionId, requestId);
|
|
72
79
|
const existing = browserRequests.get(requestKey);
|
|
@@ -84,17 +91,29 @@ export function registerBrowserCreateRequest({ agentId, client, requestId, diges
|
|
|
84
91
|
webConnectionId: client.connectionId,
|
|
85
92
|
webConnectionGeneration: client.connectionGeneration,
|
|
86
93
|
digest,
|
|
94
|
+
kind,
|
|
87
95
|
state: 'pending',
|
|
88
96
|
response: null,
|
|
89
|
-
expiresAt: Date.now() +
|
|
97
|
+
expiresAt: Date.now() + (kind === 'browser_runtime_install'
|
|
98
|
+
? INSTALL_REQUEST_TTL_MS
|
|
99
|
+
: CREATE_REQUEST_TTL_MS),
|
|
90
100
|
};
|
|
91
101
|
browserRequests.set(requestKey, request);
|
|
92
102
|
return { request };
|
|
93
103
|
}
|
|
94
104
|
|
|
95
|
-
export function
|
|
105
|
+
export function findBrowserRequest(agentId, serverRequestId) {
|
|
106
|
+
for (const request of browserRequests.values()) {
|
|
107
|
+
if (request.agentId === agentId && request.serverRequestId === serverRequestId) return request;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function completeBrowserRequest(agentId, msg, { consume = false, kinds = null } = {}) {
|
|
113
|
+
const allowedKinds = kinds == null ? null : new Set(Array.isArray(kinds) ? kinds : [kinds]);
|
|
96
114
|
for (const [requestKey, request] of browserRequests) {
|
|
97
115
|
if (request.agentId !== agentId || request.serverRequestId !== msg.requestId || request.state !== 'pending') continue;
|
|
116
|
+
if (allowedKinds && !allowedKinds.has(request.kind)) continue;
|
|
98
117
|
request.state = msg.type.endsWith('_error') ? 'failed' : 'completed';
|
|
99
118
|
request.response = { ...msg, requestId: request.requestId };
|
|
100
119
|
request.expiresAt = Date.now() + CREATE_REQUEST_TTL_MS;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const WORKBENCH_ROUTE_PROTOCOL = 1;
|
|
2
2
|
export const BROWSER_RUNTIME_PROTOCOL = 1;
|
|
3
|
+
export const BROWSER_RUNTIME_SETUP_PROTOCOL = 1;
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Apply the explicit browser protocol hello to one Server-owned client record.
|
|
@@ -14,5 +15,8 @@ export function applyClientHello(client, message) {
|
|
|
14
15
|
if (message.browserRuntimeProtocol === BROWSER_RUNTIME_PROTOCOL) {
|
|
15
16
|
client.browserRuntimeProtocol = BROWSER_RUNTIME_PROTOCOL;
|
|
16
17
|
}
|
|
18
|
+
if (message.browserRuntimeSetupProtocol === BROWSER_RUNTIME_SETUP_PROTOCOL) {
|
|
19
|
+
client.browserRuntimeSetupProtocol = BROWSER_RUNTIME_SETUP_PROTOCOL;
|
|
20
|
+
}
|
|
17
21
|
return true;
|
|
18
22
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
browserRoutes,
|
|
6
6
|
completeBrowserRequest,
|
|
7
7
|
deleteBrowserPeer,
|
|
8
|
+
findBrowserRequest,
|
|
8
9
|
deleteBrowserRoute,
|
|
9
10
|
getBrowserPeer,
|
|
10
11
|
getBrowserRoute,
|
|
@@ -12,6 +13,9 @@ import {
|
|
|
12
13
|
} from '../browser-runtime-routes.js';
|
|
13
14
|
|
|
14
15
|
const AGENT_BROWSER_TYPES = new Set([
|
|
16
|
+
'browser_runtime_status_result',
|
|
17
|
+
'browser_runtime_install_progress',
|
|
18
|
+
'browser_runtime_error',
|
|
15
19
|
'browser_session_created',
|
|
16
20
|
'browser_session_error',
|
|
17
21
|
'browser_session_snapshot',
|
|
@@ -56,6 +60,50 @@ function sessionSnapshot(msg) {
|
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
function safeByteCount(value) {
|
|
64
|
+
const number = Number(value);
|
|
65
|
+
if (!Number.isSafeInteger(number) || number < 0) return 0;
|
|
66
|
+
return Math.min(number, 4 * 1024 * 1024 * 1024);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function publicRuntimeMessage(agentId, requestId, msg) {
|
|
70
|
+
if (msg?.type === 'browser_runtime_error') {
|
|
71
|
+
return {
|
|
72
|
+
type: msg.type,
|
|
73
|
+
agentId,
|
|
74
|
+
requestId,
|
|
75
|
+
code: clean(msg.code, 128) || 'browser_runtime_error',
|
|
76
|
+
safeError: clean(msg.safeError, 500) || null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (msg?.type === 'browser_runtime_install_progress') {
|
|
80
|
+
return {
|
|
81
|
+
type: msg.type,
|
|
82
|
+
agentId,
|
|
83
|
+
requestId,
|
|
84
|
+
downloadedBytes: safeByteCount(msg.downloadedBytes),
|
|
85
|
+
totalBytes: safeByteCount(msg.totalBytes),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
type: 'browser_runtime_status_result',
|
|
90
|
+
agentId,
|
|
91
|
+
requestId,
|
|
92
|
+
supported: msg?.supported === true,
|
|
93
|
+
state: clean(msg?.state, 32) || 'unknown',
|
|
94
|
+
installed: msg?.installed === true,
|
|
95
|
+
enabled: msg?.enabled === true,
|
|
96
|
+
ready: msg?.ready === true,
|
|
97
|
+
buildId: clean(msg?.buildId, 128) || null,
|
|
98
|
+
platform: clean(msg?.platform, 32) || null,
|
|
99
|
+
downloadBytes: safeByteCount(msg?.downloadBytes),
|
|
100
|
+
downloadedBytes: safeByteCount(msg?.downloadedBytes),
|
|
101
|
+
totalBytes: safeByteCount(msg?.totalBytes),
|
|
102
|
+
probeCode: clean(msg?.probeCode, 128) || null,
|
|
103
|
+
safeError: clean(msg?.safeError, 500) || null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
59
107
|
function publicSessionMessage(agentId, msg) {
|
|
60
108
|
if (msg?.type === 'browser_session_error') {
|
|
61
109
|
return {
|
|
@@ -84,7 +132,7 @@ function publicSessionMessage(agentId, msg) {
|
|
|
84
132
|
}
|
|
85
133
|
|
|
86
134
|
async function sendCreateResponse(agentId, msg) {
|
|
87
|
-
const request = completeBrowserRequest(agentId, msg);
|
|
135
|
+
const request = completeBrowserRequest(agentId, msg, { kinds: 'browser_session_create' });
|
|
88
136
|
if (!request) return true;
|
|
89
137
|
const client = browserClientForPeer({
|
|
90
138
|
clientId: request.clientId,
|
|
@@ -130,13 +178,47 @@ async function sendCreateResponse(agentId, msg) {
|
|
|
130
178
|
/** Agent-authenticated Browser events routed only through Server-owned ledgers. */
|
|
131
179
|
export async function handleAgentBrowser(agentId, agent, msg) {
|
|
132
180
|
if (!AGENT_BROWSER_TYPES.has(msg?.type)) return false;
|
|
181
|
+
if (msg.type === 'browser_runtime_install_progress') {
|
|
182
|
+
const request = findBrowserRequest(agentId, msg.requestId);
|
|
183
|
+
if (!request || request.state !== 'pending' || request.kind !== 'browser_runtime_install') return true;
|
|
184
|
+
const client = browserClientForPeer({
|
|
185
|
+
clientId: request.clientId,
|
|
186
|
+
webConnectionId: request.webConnectionId,
|
|
187
|
+
webConnectionGeneration: request.webConnectionGeneration,
|
|
188
|
+
});
|
|
189
|
+
if (client?.userId === request.ownerUserId) {
|
|
190
|
+
await sendToWebClient(client, publicRuntimeMessage(agentId, request.requestId, msg));
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
if (msg.type === 'browser_runtime_status_result' || msg.type === 'browser_runtime_error') {
|
|
195
|
+
const pendingRequest = findBrowserRequest(agentId, msg.requestId);
|
|
196
|
+
const request = completeBrowserRequest(agentId, msg, {
|
|
197
|
+
consume: pendingRequest?.kind === 'browser_runtime_status',
|
|
198
|
+
kinds: ['browser_runtime_status', 'browser_runtime_install', 'browser_runtime_enable'],
|
|
199
|
+
});
|
|
200
|
+
if (!request) return true;
|
|
201
|
+
const client = browserClientForPeer({
|
|
202
|
+
clientId: request.clientId,
|
|
203
|
+
webConnectionId: request.webConnectionId,
|
|
204
|
+
webConnectionGeneration: request.webConnectionGeneration,
|
|
205
|
+
});
|
|
206
|
+
if (client?.userId !== request.ownerUserId) return true;
|
|
207
|
+
const response = publicRuntimeMessage(agentId, request.requestId, msg);
|
|
208
|
+
request.response = response;
|
|
209
|
+
await sendToWebClient(client, response);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
133
212
|
if (msg.type === 'browser_session_created'
|
|
134
213
|
|| (msg.type === 'browser_session_error' && msg.requestId)) {
|
|
135
214
|
return sendCreateResponse(agentId, msg);
|
|
136
215
|
}
|
|
137
216
|
|
|
138
217
|
if (msg.type === 'browser_session_list_result') {
|
|
139
|
-
const request = completeBrowserRequest(agentId, msg, {
|
|
218
|
+
const request = completeBrowserRequest(agentId, msg, {
|
|
219
|
+
consume: true,
|
|
220
|
+
kinds: 'browser_session_list',
|
|
221
|
+
});
|
|
140
222
|
if (!request) return true;
|
|
141
223
|
for (const snapshot of Array.isArray(msg.sessions) ? msg.sessions : []) {
|
|
142
224
|
if (!snapshot?.browserSessionId) continue;
|
|
@@ -154,7 +236,10 @@ export async function handleAgentBrowser(agentId, agent, msg) {
|
|
|
154
236
|
}
|
|
155
237
|
|
|
156
238
|
if (msg.type === 'browser_session_snapshot' && msg.requestId) {
|
|
157
|
-
const request = completeBrowserRequest(agentId, msg, {
|
|
239
|
+
const request = completeBrowserRequest(agentId, msg, {
|
|
240
|
+
consume: true,
|
|
241
|
+
kinds: ['browser_session_get', 'browser_session_close'],
|
|
242
|
+
});
|
|
158
243
|
if (!request) return true;
|
|
159
244
|
const client = browserClientForPeer({
|
|
160
245
|
clientId: request.clientId,
|