@kingsoftcloud/agentkit-personal-assistant 0.0.0-stage → 0.1.7

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.
Files changed (70) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +38 -2
  4. package/cordis.patch.yml +6 -0
  5. package/dist/client.js +6 -0
  6. package/package.json +83 -4
  7. package/personal-assistant.plugin.json +31 -0
  8. package/personal-assistant.plugin.schema.json +43 -0
  9. package/runtime/assistant-runtime.mjs +317 -0
  10. package/runtime/check-manifest.mjs +21 -0
  11. package/runtime/cloud-browser-cdp.mjs +313 -0
  12. package/runtime/cloud-browser.mjs +972 -0
  13. package/runtime/cloud-execution.mjs +320 -0
  14. package/runtime/cloud-session-channel.mjs +45 -0
  15. package/runtime/computer-agentd.mjs +488 -0
  16. package/runtime/desktop-clipboard-limit.mjs +2 -0
  17. package/runtime/desktop-launcher.mjs +106 -0
  18. package/runtime/desktop-permit.mjs +42 -0
  19. package/runtime/desktop-session.mjs +840 -0
  20. package/runtime/desktop-ws.mjs +371 -0
  21. package/runtime/prepare-dsh-profile.mjs +17 -0
  22. package/runtime/welcome-server.mjs +144 -0
  23. package/scripts/assistant-runtime-launcher.sh +3 -0
  24. package/scripts/build-artifacts.mjs +81 -0
  25. package/scripts/build-client.mjs +82 -0
  26. package/scripts/computer-agentd-launcher.sh +3 -0
  27. package/scripts/deterministic_tar.py +21 -0
  28. package/scripts/install-local.mjs +262 -0
  29. package/scripts/scan-secrets.mjs +41 -0
  30. package/scripts/sync-public.mjs +52 -0
  31. package/scripts/verification/gateway-desktop-client.mjs +142 -0
  32. package/scripts/verification/verify-desktop-applications-linux.mjs +46 -0
  33. package/scripts/verification/verify-desktop-input-linux.mjs +127 -0
  34. package/scripts/verification/verify-desktop-pod-gateway.mjs +204 -0
  35. package/scripts/verify-cloud-turn.mjs +18 -0
  36. package/scripts/verify-computer-broker.mjs +10 -0
  37. package/src/automation-tools.mjs +18 -0
  38. package/src/bot-private-tools.mjs +23 -0
  39. package/src/bot-registry.mjs +157 -0
  40. package/src/bot-tool-policy.mjs +48 -0
  41. package/src/bot-tool-scope.mjs +40 -0
  42. package/src/client/bot-directory.mjs +23 -0
  43. package/src/client/components/desktop-clipboard.mjs +194 -0
  44. package/src/client/conversation.mjs +558 -0
  45. package/src/client/desktop-control.mjs +70 -0
  46. package/src/client/desktop-view.mjs +68 -0
  47. package/src/client/entry.mjs +8 -0
  48. package/src/client/index.mjs +733 -0
  49. package/src/client/markdown.mjs +130 -0
  50. package/src/client/native-workbench.mjs +62 -0
  51. package/src/cloud-computer-tools.mjs +245 -0
  52. package/src/cloud-files.mjs +57 -0
  53. package/src/cloud-model.mjs +10 -0
  54. package/src/cloud-session-transport.mjs +76 -0
  55. package/src/computer-broker-protocol.mjs +116 -0
  56. package/src/computer-broker.mjs +220 -0
  57. package/src/desktop-tools.mjs +64 -0
  58. package/src/dsh-session-bridge.mjs +207 -0
  59. package/src/dsh-web-bridge.mjs +127 -0
  60. package/src/host-gateway.mjs +281 -0
  61. package/src/host-session-channel.mjs +410 -0
  62. package/src/implementation-capabilities.mjs +10 -0
  63. package/src/index.mjs +148 -0
  64. package/src/persistent-state.mjs +201 -0
  65. package/src/platform-manifest.mjs +24 -0
  66. package/src/plugin-manifest.mjs +82 -0
  67. package/src/schedule-manager.mjs +88 -0
  68. package/src/system-fetch.mjs +72 -0
  69. package/src/workspace-tools.mjs +22 -0
  70. package/src/workspace.mjs +22 -0
@@ -0,0 +1,313 @@
1
+ /** Minimal Chromium DevTools Protocol transport: one browser, one WebSocket, no reconnect. */
2
+ import { spawn as spawnChild } from 'node:child_process';
3
+ import { mkdtempSync, readFileSync, rmSync, mkdirSync, chmodSync, lstatSync } from 'node:fs';
4
+ import { rm } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { join, isAbsolute } from 'node:path';
7
+
8
+ export const DEVTOOLS_PORT_FILE = 'DevToolsActivePort';
9
+ export const DEVTOOLS_LAUNCH_TIMEOUT_MS = 30_000;
10
+ export const CDP_COMMAND_TIMEOUT_MS = 30_000;
11
+ const CDP_CONNECT_TIMEOUT_MS = 10_000;
12
+ const MAX_STDERR_BYTES = 4096;
13
+ const persistentProfileOwners = new Map();
14
+
15
+ const fail = (message, code = 'ASSISTANT_BROWSER_CDP_UNAVAILABLE') => Object.assign(new Error(message), { code });
16
+ const delay = ms => new Promise(resolvePromise => setTimeout(resolvePromise, ms));
17
+
18
+ /**
19
+ * Chromium flags for the managed image. Chromium's own sandbox is unavailable in
20
+ * most managed containers, so the egress proxy stays authoritative for network
21
+ * policy; `--proxy-bypass-list=<-loopback>` keeps loopback inside the proxy too.
22
+ * 桌面模式的首屏是本机 127.0.0.1 的欢迎页,因此回环必须显式放行,否则页面会被
23
+ * 出口代理吃掉而白屏;公网流量仍然全部走代理。
24
+ * 桌面窗口固定 `--class=Chromium`:否则 `--app=<地址>` 会把 WM_CLASS 设成地址本身,
25
+ * dock 的任务按钮匹配不到 chromium.desktop,只能显示一个灰色兜底图标。
26
+ * 托管容器里 Chromium 自带沙箱不可用,`--no-sandbox` 会让窗口版 Chromium 顶部弹出
27
+ * “You are using an unsupported command-line flag” 信息栏;`--test-type` 抑制该提示栏,
28
+ * 不改变 CDP/自动化语义,也不影响页面能力。
29
+ */
30
+ export function chromiumArgs({ userDataDir, proxy, windowSize = '1280,800', desktop = false, startUrl, openUrl }) {
31
+ return [
32
+ ...(desktop ? ['--ozone-platform=x11', '--window-position=520,90', '--test-type', '--lang=zh-CN', '--accept-lang=zh-CN,zh', '--class=Chromium', '--hide-crash-restore-bubble'] : ['--headless=new']),
33
+ // 应用窗口承载首屏:没有地址栏、没有多余标签条。
34
+ ...(desktop && startUrl ? [`--app=${startUrl}`] : []),
35
+ // 普通窗口模式:把首页作为位置参数交给 Chromium,避免先出一个空白标签再补一个页面。
36
+ ...(desktop && !startUrl && openUrl ? [openUrl] : []),
37
+ '--disable-gpu', '--disable-dev-shm-usage', '--no-sandbox',
38
+ '--no-first-run', '--no-default-browser-check', '--disable-extensions', '--disable-sync',
39
+ '--disable-background-networking', '--disable-breakpad', '--disable-crash-reporter',
40
+ '--disable-translate', '--mute-audio', '--hide-scrollbars',
41
+ `--window-size=${desktop ? '740,620' : windowSize}`,
42
+ '--remote-debugging-port=0', '--remote-debugging-address=127.0.0.1',
43
+ `--user-data-dir=${userDataDir}`,
44
+ `--proxy-server=${proxy}`,
45
+ '--proxy-bypass-list=<-loopback>,127.0.0.1,localhost',
46
+ ];
47
+ }
48
+
49
+ /** Read `<user-data-dir>/DevToolsActivePort`; Chromium writes port and browser WebSocket path. */
50
+ export function readDevToolsEndpoint(userDataDir) {
51
+ let content;
52
+ try { content = readFileSync(join(userDataDir, DEVTOOLS_PORT_FILE), 'utf8'); } catch { return null; }
53
+ const [portLine, pathLine] = content.split(/\r?\n/);
54
+ const port = Number(portLine);
55
+ const path = (pathLine || '').trim();
56
+ if (!Number.isInteger(port) || port < 1 || port > 65535 || !path.startsWith('/')) return null;
57
+ return { port, path, endpoint: `ws://127.0.0.1:${port}${path}` };
58
+ }
59
+
60
+ export function createProfileDir() {
61
+ return mkdtempSync(join(tmpdir(), 'agentkit-cloud-browser-'));
62
+ }
63
+
64
+ /** Kill the detached Chromium process group; fall back to the single pid. */
65
+ export function killProcessTree(pid, signal = 'SIGKILL') {
66
+ if (!Number.isSafeInteger(pid) || pid <= 0) return;
67
+ try { process.kill(-pid, signal); } catch { try { process.kill(pid, signal); } catch {} }
68
+ }
69
+
70
+ function collectStderr(child, state) {
71
+ child?.stderr?.on?.('data', chunk => {
72
+ if (state.bytes >= MAX_STDERR_BYTES) return;
73
+ const slice = chunk.subarray(0, MAX_STDERR_BYTES - state.bytes);
74
+ state.text += slice.toString('utf8');
75
+ state.bytes += slice.length;
76
+ });
77
+ }
78
+
79
+ const childExited = child => child.exitCode !== null && child.exitCode !== undefined || Boolean(child.signalCode);
80
+ const waitForOwnChild = (child, timeoutMs) => {
81
+ if (!child || childExited(child)) return Promise.resolve();
82
+ return new Promise((resolvePromise, rejectPromise) => {
83
+ const finish = error => { clearTimeout(timer); child.removeListener?.('exit', exited); error ? rejectPromise(error) : resolvePromise(); };
84
+ const exited = () => finish();
85
+ const timer = setTimeout(() => finish(fail('自有 Chromium 进程仍未退出,拒绝复用私有配置目录', 'ASSISTANT_BROWSER_PROCESS_STILL_RUNNING')), timeoutMs);
86
+ child.once('exit', exited);
87
+ if (childExited(child)) finish();
88
+ });
89
+ };
90
+ async function stopOwnChromium(child, kill, { wait = false, graceMs = 0 } = {}) {
91
+ if (!child?.pid || childExited(child)) return;
92
+ // 正常关窗/Browser.close 可先完成 SQLite 等状态写入;只等待自己的 child。
93
+ if (wait && graceMs) { try { await waitForOwnChild(child, graceMs); return; } catch (error) { if (error.code !== 'ASSISTANT_BROWSER_PROCESS_STILL_RUNNING') throw error; } }
94
+ try { kill(child.pid, 'SIGKILL'); } catch {
95
+ if (wait) throw fail('无法停止自有 Chromium,拒绝复用私有配置目录', 'ASSISTANT_BROWSER_PROCESS_STILL_RUNNING');
96
+ }
97
+ if (wait) await waitForOwnChild(child, 2000);
98
+ }
99
+
100
+ function preparePersistentProfile(userDataDir) {
101
+ if (!userDataDir || !isAbsolute(userDataDir)) throw fail('持久浏览器配置目录必须为绝对路径', 'ASSISTANT_BROWSER_PROFILE_INVALID');
102
+ try {
103
+ mkdirSync(userDataDir, { recursive: true, mode: 0o700 });
104
+ if (!lstatSync(userDataDir).isDirectory() || lstatSync(userDataDir).isSymbolicLink()) throw fail('浏览器私有配置目录不能是符号链接', 'ASSISTANT_BROWSER_PROFILE_INVALID');
105
+ chmodSync(userDataDir, 0o700);
106
+ // 旧 child 已由调用方 dispose 等待退出。只清调试端口元数据,不碰锁或登录数据。
107
+ rmSync(join(userDataDir, DEVTOOLS_PORT_FILE), { force: true });
108
+ } catch (error) {
109
+ if (String(error.code).startsWith('ASSISTANT_BROWSER_')) throw error;
110
+ throw fail(`浏览器私有配置目录不可用: ${error.code || 'filesystem-error'}`, 'ASSISTANT_BROWSER_PROFILE_UNAVAILABLE');
111
+ }
112
+ }
113
+
114
+ export async function launchChromium({
115
+ chromium, proxy, userDataDir, spawn = spawnChild,
116
+ launchTimeoutMs = DEVTOOLS_LAUNCH_TIMEOUT_MS, now = Date.now, pollMs = 25, env = process.env,
117
+ preserveUserDataDir = false, startUrl, openUrl,
118
+ } = {}) {
119
+ if (preserveUserDataDir) {
120
+ const previous = persistentProfileOwners.get(userDataDir);
121
+ if (previous && !childExited(previous)) throw fail('自有 Chromium 仍占用私有配置目录,拒绝启动第二个实例', 'ASSISTANT_BROWSER_PROCESS_STILL_RUNNING');
122
+ preparePersistentProfile(userDataDir);
123
+ }
124
+ else if (userDataDir === undefined) userDataDir = createProfileDir();
125
+ const cleanupProfile = () => { if (!preserveUserDataDir) rmSync(userDataDir, { recursive: true, force: true }); };
126
+ const desktop = env.AGENTKIT_DESKTOP_MODE === '1';
127
+ // 桌面浏览器展示的是本地时间(欢迎页时钟、页面里的 Date),必须与桌面其它
128
+ // 进程同一时区;非桌面模式只沿用宿主 TZ,不改变既有口径。
129
+ const timeZone = typeof env.TZ === 'string' && env.TZ
130
+ ? env.TZ
131
+ : desktop ? String(env.AGENTKIT_DESKTOP_TIME_ZONE || 'Asia/Shanghai') : undefined;
132
+ const args = chromiumArgs({ userDataDir, proxy, desktop, startUrl, openUrl });
133
+ const stderr = { text: '', bytes: 0 };
134
+ let child;
135
+ try {
136
+ child = spawn(chromium, args, {
137
+ detached: true,
138
+ stdio: ['ignore', 'ignore', 'pipe'],
139
+ env: {
140
+ PATH: '/usr/local/bin:/usr/bin:/bin', HOME: userDataDir, TMPDIR: '/tmp',
141
+ // 桌面窗口给用户看:界面与页面语言都用简体中文;LC_ALL 会盖过 LANG,桌面模式不能设。
142
+ // 无头自动化不改变既有口径(历史行为是 C.UTF-8 + 默认语言)。
143
+ ...(desktop
144
+ ? { LANG: 'zh_CN.UTF-8', LANGUAGE: 'zh_CN:zh' }
145
+ : { LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8' }),
146
+ ...(timeZone ? { TZ: timeZone } : {}),
147
+ ...(desktop ? { DISPLAY: env.DISPLAY || ':99' } : {}),
148
+ },
149
+ });
150
+ } catch (error) {
151
+ cleanupProfile();
152
+ throw fail(`Chromium 启动失败: ${error?.code || 'spawn-error'}`, 'ASSISTANT_BROWSER_LAUNCH_FAILED');
153
+ }
154
+ if (preserveUserDataDir && child?.pid) {
155
+ persistentProfileOwners.set(userDataDir, child);
156
+ child.once('exit', () => { if (persistentProfileOwners.get(userDataDir) === child) persistentProfileOwners.delete(userDataDir); });
157
+ }
158
+ collectStderr(child, stderr);
159
+ let exit, spawnError;
160
+ child?.once?.('exit', (code, signal) => { exit = { code, signal }; });
161
+ child?.once?.('error', error => { spawnError = error; });
162
+ const deadline = now() + launchTimeoutMs;
163
+ while (now() < deadline) {
164
+ const endpoint = readDevToolsEndpoint(userDataDir);
165
+ if (endpoint) return { child, userDataDir, endpoint, stderr };
166
+ if (spawnError) { cleanupProfile(); throw fail(`Chromium 启动失败: ${spawnError.code || 'spawn-error'}`, 'ASSISTANT_BROWSER_LAUNCH_FAILED'); }
167
+ if (exit) {
168
+ await stopOwnChromium(child, killProcessTree, { wait: preserveUserDataDir });
169
+ cleanupProfile();
170
+ const detail = stderr.text.trim().slice(0, 500);
171
+ throw fail(`Chromium 在调试端口就绪前退出(${exit.code ?? exit.signal})${detail ? `: ${detail}` : ''}`, 'ASSISTANT_BROWSER_LAUNCH_FAILED');
172
+ }
173
+ await delay(pollMs);
174
+ }
175
+ await stopOwnChromium(child, killProcessTree, { wait: preserveUserDataDir });
176
+ cleanupProfile();
177
+ throw fail('Chromium 调试端口在限定时间内未就绪', 'ASSISTANT_BROWSER_LAUNCH_FAILED');
178
+ }
179
+
180
+ /** One JSON-RPC channel. Commands time out individually; a closed channel never reconnects. */
181
+ export class CdpConnection {
182
+ #ws; #pending = new Map(); #nextId = 0; #closed = false; #commandTimeoutMs;
183
+
184
+ constructor(ws, { commandTimeoutMs = CDP_COMMAND_TIMEOUT_MS } = {}) {
185
+ this.#ws = ws;
186
+ this.#commandTimeoutMs = commandTimeoutMs;
187
+ ws.addEventListener?.('message', event => this.#onMessage(event));
188
+ ws.addEventListener?.('close', () => this.#rejectAll(fail('云端浏览器调试连接已关闭', 'ASSISTANT_BROWSER_CDP_CLOSED')));
189
+ ws.addEventListener?.('error', () => this.#rejectAll(fail('云端浏览器调试连接出错', 'ASSISTANT_BROWSER_CDP_ERROR')));
190
+ }
191
+
192
+ get closed() { return this.#closed; }
193
+
194
+ send(method, params = {}, { sessionId, timeoutMs = this.#commandTimeoutMs } = {}) {
195
+ if (this.#closed) return Promise.reject(fail('云端浏览器调试连接已关闭', 'ASSISTANT_BROWSER_CDP_CLOSED'));
196
+ if (typeof method !== 'string' || !method) return Promise.reject(fail('CDP 方法名无效', 'ASSISTANT_BROWSER_CDP_ERROR'));
197
+ const id = ++this.#nextId;
198
+ return new Promise((resolvePromise, rejectPromise) => {
199
+ // Keep the timeout ref'd: an in-flight command must keep the process
200
+ // alive long enough for the timeout to fire instead of leaving a
201
+ // promise that the event loop silently abandons (Node 22 cancels it).
202
+ const timer = setTimeout(() => {
203
+ this.#pending.delete(id);
204
+ rejectPromise(fail(`云端浏览器命令 ${method} 超时`, 'ASSISTANT_BROWSER_CDP_TIMEOUT'));
205
+ }, timeoutMs);
206
+ this.#pending.set(id, { resolve: resolvePromise, reject: rejectPromise, timer, method });
207
+ try { this.#ws.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) })); }
208
+ catch {
209
+ this.#pending.delete(id);
210
+ clearTimeout(timer);
211
+ rejectPromise(fail(`云端浏览器命令 ${method} 发送失败`, 'ASSISTANT_BROWSER_CDP_ERROR'));
212
+ }
213
+ });
214
+ }
215
+
216
+ #onMessage(event) {
217
+ let message;
218
+ try { message = JSON.parse(typeof event === 'string' ? event : event?.data); } catch { return; }
219
+ if (!message || typeof message.id !== 'number') return;
220
+ const pending = this.#pending.get(message.id);
221
+ if (!pending) return;
222
+ this.#pending.delete(message.id);
223
+ clearTimeout(pending.timer);
224
+ if (message.error) pending.reject(fail(`CDP ${pending.method} 失败: ${String(message.error.message || '').slice(0, 300)}`, 'ASSISTANT_BROWSER_CDP_ERROR'));
225
+ else pending.resolve(message.result || {});
226
+ }
227
+
228
+ #rejectAll(error) {
229
+ if (this.#closed && this.#pending.size === 0) return;
230
+ this.#closed = true;
231
+ for (const pending of this.#pending.values()) {
232
+ clearTimeout(pending.timer);
233
+ pending.reject(error);
234
+ }
235
+ this.#pending.clear();
236
+ }
237
+
238
+ close() {
239
+ if (this.#closed) return;
240
+ this.#rejectAll(fail('云端浏览器调试连接已关闭', 'ASSISTANT_BROWSER_CDP_CLOSED'));
241
+ try { this.#ws.close(); } catch {}
242
+ }
243
+ }
244
+
245
+ /** Connect once to the browser-level WebSocket endpoint. */
246
+ export async function connectCdp({
247
+ endpoint, WebSocketImpl = globalThis.WebSocket,
248
+ commandTimeoutMs = CDP_COMMAND_TIMEOUT_MS, connectTimeoutMs = CDP_CONNECT_TIMEOUT_MS,
249
+ } = {}) {
250
+ if (typeof WebSocketImpl !== 'function') throw fail('运行环境缺少 WebSocket 实现', 'ASSISTANT_BROWSER_CDP_UNAVAILABLE');
251
+ if (typeof endpoint !== 'string' || !endpoint.startsWith('ws://')) throw fail('Chromium 调试端点无效', 'ASSISTANT_BROWSER_LAUNCH_FAILED');
252
+ const ws = new WebSocketImpl(endpoint);
253
+ await new Promise((resolvePromise, rejectPromise) => {
254
+ let settled = false;
255
+ const timer = setTimeout(() => {
256
+ if (settled) return;
257
+ settled = true;
258
+ try { ws.close(); } catch {}
259
+ rejectPromise(fail('连接 Chromium 调试端口超时', 'ASSISTANT_BROWSER_CDP_TIMEOUT'));
260
+ }, connectTimeoutMs);
261
+ timer.unref?.();
262
+ const finish = error => {
263
+ if (settled) return;
264
+ settled = true;
265
+ clearTimeout(timer);
266
+ if (error) rejectPromise(error); else resolvePromise();
267
+ };
268
+ ws.addEventListener?.('open', () => finish());
269
+ ws.addEventListener?.('error', () => finish(fail('无法连接 Chromium 调试端口', 'ASSISTANT_BROWSER_CDP_UNAVAILABLE')));
270
+ ws.addEventListener?.('close', () => finish(fail('Chromium 调试端口连接已关闭', 'ASSISTANT_BROWSER_CDP_UNAVAILABLE')));
271
+ });
272
+ return new CdpConnection(ws, { commandTimeoutMs });
273
+ }
274
+
275
+ /**
276
+ * Launch and connect one Chromium. `dispose()` closes the channel, kills the
277
+ * process group. 普通临时配置会删除;Computer 明确声明的持久私有配置保留。
278
+ */
279
+ export async function startCdpTransport({
280
+ chromium, proxy, spawn = spawnChild, WebSocketImpl = globalThis.WebSocket,
281
+ kill = killProcessTree, launchTimeoutMs = DEVTOOLS_LAUNCH_TIMEOUT_MS,
282
+ commandTimeoutMs = CDP_COMMAND_TIMEOUT_MS, now = Date.now, userDataDir, endpoint: fixedEndpoint,
283
+ env = process.env,
284
+ preserveUserDataDir = false, startUrl,
285
+ } = {}) {
286
+ if (preserveUserDataDir && (!userDataDir || !isAbsolute(userDataDir))) throw fail('持久浏览器配置目录必须为绝对路径', 'ASSISTANT_BROWSER_PROFILE_INVALID');
287
+ const launched = fixedEndpoint
288
+ ? { child: undefined, userDataDir, endpoint: { endpoint: fixedEndpoint }, stderr: { text: '' } }
289
+ : await launchChromium({ chromium, proxy, spawn, launchTimeoutMs, now, userDataDir, env, preserveUserDataDir, startUrl });
290
+ let connection;
291
+ try { connection = await connectCdp({ endpoint: launched.endpoint.endpoint, WebSocketImpl, commandTimeoutMs }); }
292
+ catch (error) {
293
+ await stopOwnChromium(launched.child, kill, { wait: preserveUserDataDir });
294
+ if (launched.userDataDir && !preserveUserDataDir) await rm(launched.userDataDir, { recursive: true, force: true }).catch(() => {});
295
+ throw error;
296
+ }
297
+ let disposing;
298
+ return {
299
+ connection,
300
+ child: launched.child,
301
+ userDataDir: launched.userDataDir,
302
+ stderr: launched.stderr,
303
+ dispose() {
304
+ if (disposing) return disposing;
305
+ disposing = (async () => {
306
+ try { connection.close(); } catch {}
307
+ await stopOwnChromium(launched.child, kill, { wait: preserveUserDataDir, graceMs: preserveUserDataDir ? 1000 : 0 });
308
+ if (launched.userDataDir && !preserveUserDataDir) await rm(launched.userDataDir, { recursive: true, force: true }).catch(() => {});
309
+ })();
310
+ return disposing;
311
+ },
312
+ };
313
+ }