yunti-browser-runtime 0.1.0

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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +256 -0
  3. package/bin/yunti-browser-runtime.js +86 -0
  4. package/docs/EXECUTION_PLAN.md +1278 -0
  5. package/docs/INSTALL.md +205 -0
  6. package/docs/PROJECT_INTENT.md +44 -0
  7. package/docs/PROJECT_STATUS.md +263 -0
  8. package/docs/PUBLISHING_BLOCKERS.md +110 -0
  9. package/docs/RELEASE.md +148 -0
  10. package/docs/ROADMAP.md +42 -0
  11. package/docs/SECURITY.md +56 -0
  12. package/docs/TOOL_GUIDE.md +69 -0
  13. package/extension/background.js +55 -0
  14. package/extension/cdp.js +582 -0
  15. package/extension/content.css +9 -0
  16. package/extension/content.js +946 -0
  17. package/extension/manifest.json +35 -0
  18. package/extension/network-monitor.js +140 -0
  19. package/extension/popup.css +66 -0
  20. package/extension/popup.html +30 -0
  21. package/extension/popup.js +55 -0
  22. package/extension/session-manager.js +332 -0
  23. package/extension/settings.js +94 -0
  24. package/extension/tool-handlers.js +1158 -0
  25. package/lib/runtime-paths.js +39 -0
  26. package/mcp/bridge-hub.js +604 -0
  27. package/mcp/http-server.js +326 -0
  28. package/mcp/json-rpc.js +35 -0
  29. package/mcp/memory.js +126 -0
  30. package/mcp/redaction.js +94 -0
  31. package/mcp/server.js +269 -0
  32. package/mcp/tools.js +1092 -0
  33. package/package.json +60 -0
  34. package/scripts/check-package-metadata.js +131 -0
  35. package/scripts/check-published-package.js +113 -0
  36. package/scripts/doctor.js +163 -0
  37. package/scripts/package-extension.js +137 -0
  38. package/scripts/print-config.js +116 -0
  39. package/scripts/release-check.js +472 -0
  40. package/skills/yunti-browser-runtime/SKILL.md +77 -0
@@ -0,0 +1,582 @@
1
+ export function createCdpController({
2
+ sessionsByTab,
3
+ pollers,
4
+ postBridge,
5
+ startPolling,
6
+ forwardConsoleEvent,
7
+ }) {
8
+ const cdpAttachedTabs = new Set()
9
+ const cdpEnabledDomains = new Map()
10
+ const traceBuffers = new Map()
11
+
12
+ function installCdpEventForwarder() {
13
+ if (!chrome.debugger?.onEvent || !chrome.debugger?.onDetach) return
14
+ chrome.debugger.onEvent.addListener((source, method, params) => {
15
+ const tabId = source?.tabId
16
+ if (!tabId) return
17
+ const session = sessionsByTab.get(tabId)
18
+ if (!session) return
19
+ recordTraceEvent(tabId, method, params)
20
+ void postBridge("/extension/cdp-event", {
21
+ browserSessionId: session.browserSessionId,
22
+ tabId,
23
+ method,
24
+ params: params || {},
25
+ receivedAt: new Date().toISOString(),
26
+ }).catch(() => {})
27
+
28
+ if (method === "Runtime.consoleAPICalled" || method === "Log.entryAdded") {
29
+ void forwardConsoleEvent(session, tabId, method, params).catch(() => {})
30
+ }
31
+ })
32
+ chrome.debugger.onDetach.addListener((source, reason) => {
33
+ const tabId = source?.tabId
34
+ if (!tabId) return
35
+ cdpAttachedTabs.delete(tabId)
36
+ cdpEnabledDomains.delete(tabId)
37
+ traceBuffers.delete(tabId)
38
+ const session = sessionsByTab.get(tabId)
39
+ if (!session) return
40
+ void postBridge("/extension/cdp-event", {
41
+ browserSessionId: session.browserSessionId,
42
+ tabId,
43
+ method: "Debugger.detached",
44
+ params: { reason: reason || "unknown" },
45
+ receivedAt: new Date().toISOString(),
46
+ }).catch(() => {})
47
+ })
48
+ }
49
+
50
+ async function sendCdpCommand(tabId, session, args = {}) {
51
+ const method = String(args.method || "").trim()
52
+ if (!method) {
53
+ throw new Error("yunti_cdp_send_command requires method, for example { method: \"Runtime.evaluate\", params: { expression: \"document.title\", returnByValue: true } }. Do not run CDP method names in a shell.")
54
+ }
55
+ if (args.params !== undefined && (typeof args.params !== "object" || args.params === null || Array.isArray(args.params))) {
56
+ throw new Error("yunti_cdp_send_command params must be an object when provided. Example: { method: \"Runtime.evaluate\", params: { expression: \"document.title\" } }.")
57
+ }
58
+ const params = args.params || {}
59
+ const protocolVersion = String(args.protocolVersion || "1.3")
60
+ const targetTabId = await resolveCdpTargetTabId(tabId, args, params)
61
+ const explicitTargetId = explicitCdpTargetId(args, params)
62
+ const targetParams =
63
+ method.startsWith("Target.") && !params.targetId && explicitTargetId
64
+ ? { ...params, targetId: explicitTargetId }
65
+ : params
66
+
67
+ if (method === "Target.getTargets") {
68
+ return interceptTargetGetTargets(session)
69
+ }
70
+ if (method === "Target.closeTarget") {
71
+ return interceptTargetCloseTarget(targetParams, session)
72
+ }
73
+ if (method === "Target.createTarget") {
74
+ return interceptTargetCreateTarget(targetParams, session)
75
+ }
76
+ if (method === "Target.attachToTarget") {
77
+ return interceptTargetAttachToTarget(targetParams, protocolVersion)
78
+ }
79
+ if (method === "Target.detachFromTarget") {
80
+ return interceptTargetDetachFromTarget(targetParams)
81
+ }
82
+ if (method === "Target.activateTarget") {
83
+ return interceptTargetActivateTarget(targetParams)
84
+ }
85
+ if (method === "Target.getTargetInfo") {
86
+ return interceptTargetGetTargetInfo(targetParams)
87
+ }
88
+
89
+ let attach = { attached: false, reused: cdpAttachedTabs.has(targetTabId) }
90
+ if (args.attach !== false) {
91
+ attach = await ensureCdpAttached(targetTabId, protocolVersion)
92
+ }
93
+ const result = await chromeDebuggerSendCommand({ tabId: targetTabId }, method, params)
94
+ return {
95
+ browserSessionId: session.browserSessionId,
96
+ tabId: targetTabId,
97
+ routeTabId: tabId,
98
+ method,
99
+ params,
100
+ result: result || {},
101
+ attach,
102
+ sentAt: new Date().toISOString(),
103
+ }
104
+ }
105
+
106
+ async function interceptTargetGetTargets(session) {
107
+ const targetInfos = []
108
+ if (chrome.debugger?.getTargets) {
109
+ try {
110
+ const targets = await chromeDebuggerGetTargets()
111
+ for (const target of targets) {
112
+ const tabId =
113
+ Number.isFinite(Number(target.tabId)) && Number(target.tabId) > 0
114
+ ? Number(target.tabId)
115
+ : null
116
+ const tab = tabId ? await chrome.tabs.get(tabId).catch(() => null) : null
117
+ targetInfos.push({
118
+ targetId: target.id || (tabId ? `tab-${tabId}` : ""),
119
+ type: target.type || "other",
120
+ title: target.title || tab?.title || "",
121
+ url: target.url || tab?.url || "",
122
+ tabId,
123
+ windowId: tab?.windowId ?? null,
124
+ active: Boolean(tab?.active),
125
+ index: Number.isFinite(Number(tab?.index)) ? Number(tab.index) : null,
126
+ attached: tabId ? cdpAttachedTabs.has(tabId) : Boolean(target.attached),
127
+ canAccessOpener: false,
128
+ })
129
+ }
130
+ } catch {
131
+ // Fall through to chrome.tabs fallback.
132
+ }
133
+ }
134
+ if (!targetInfos.length) {
135
+ const tabs = await chrome.tabs.query({})
136
+ for (const tab of tabs) {
137
+ if (!tab.id) continue
138
+ targetInfos.push({
139
+ targetId: `tab-${tab.id}`,
140
+ type: "page",
141
+ title: tab.title || "",
142
+ url: tab.url || "",
143
+ tabId: tab.id,
144
+ windowId: tab.windowId ?? null,
145
+ active: Boolean(tab.active),
146
+ index: Number.isFinite(Number(tab.index)) ? Number(tab.index) : null,
147
+ attached: cdpAttachedTabs.has(tab.id),
148
+ canAccessOpener: false,
149
+ })
150
+ }
151
+ }
152
+ return {
153
+ browserSessionId: session.browserSessionId,
154
+ method: "Target.getTargets",
155
+ result: { targetInfos },
156
+ intercepted: true,
157
+ sentAt: new Date().toISOString(),
158
+ }
159
+ }
160
+
161
+ async function listBrowserTargets(session) {
162
+ const cdpResult = await interceptTargetGetTargets(session)
163
+ const targetInfos = Array.isArray(cdpResult?.result?.targetInfos)
164
+ ? cdpResult.result.targetInfos
165
+ : []
166
+ const pages = targetInfos
167
+ .filter((target) => target.type === "page")
168
+ .map((target) => ({
169
+ browserSessionId: session.browserSessionId,
170
+ routeBrowserSessionId: session.browserSessionId,
171
+ targetId: target.targetId || "",
172
+ tabId: target.tabId ?? null,
173
+ type: target.type || "",
174
+ url: target.url || "",
175
+ title: target.title || "",
176
+ active: Boolean(target.attached),
177
+ source: "Target.getTargets",
178
+ }))
179
+ return {
180
+ browserSessionId: session.browserSessionId,
181
+ pages,
182
+ targets: targetInfos,
183
+ targetInfos,
184
+ pageCount: pages.length,
185
+ total: targetInfos.length,
186
+ method: "Target.getTargets",
187
+ source: cdpResult.intercepted ? "extension.chrome.debugger" : "extension",
188
+ listedAt: new Date().toISOString(),
189
+ }
190
+ }
191
+
192
+ async function getBrowserTarget(session, args = {}) {
193
+ const targetId = String(args.targetId || "").trim()
194
+ const tabId = Number(args.tabId)
195
+ if (!targetId && !Number.isFinite(tabId)) {
196
+ throw new Error("targetId or tabId is required")
197
+ }
198
+ const inventory = await listBrowserTargets(session)
199
+ const target = inventory.targets.find((item) => {
200
+ if (targetId && item.targetId === targetId) return true
201
+ if (Number.isFinite(tabId) && Number(item.tabId) === tabId) return true
202
+ return false
203
+ })
204
+ if (!target) {
205
+ throw new Error(
206
+ targetId
207
+ ? `browser target not found: ${targetId}`
208
+ : `browser target not found for tabId: ${tabId}`
209
+ )
210
+ }
211
+ return {
212
+ browserSessionId: session.browserSessionId,
213
+ target,
214
+ method: "Target.getTargets",
215
+ source: inventory.source,
216
+ listedAt: inventory.listedAt,
217
+ }
218
+ }
219
+
220
+ async function interceptTargetCloseTarget(params = {}, session = null) {
221
+ const targetId = String(params.targetId || "")
222
+ if (!targetId) {
223
+ throw new Error("Target.closeTarget requires targetId. Pass top-level tabId or targetId to yunti_cdp_send_command after calling yunti_list_browser_targets.")
224
+ }
225
+ const tabId = await resolveTabIdFromTarget(targetId)
226
+ if (!tabId || !Number.isFinite(tabId)) {
227
+ throw new Error(`Cannot resolve tabId from targetId: ${targetId}. Call yunti_list_browser_targets and pass a returned top-level tabId or targetId to yunti_cdp_send_command.`)
228
+ }
229
+ const connected = [...sessionsByTab.keys()]
230
+ if (connected.length <= 1 && connected.includes(tabId)) {
231
+ throw new Error("Cannot close the last connected tab")
232
+ }
233
+ await detachCdpTab(tabId, null, "target_closed").catch(() => {})
234
+ await chrome.tabs.remove(tabId)
235
+ sessionsByTab.delete(tabId)
236
+ pollers.get(tabId)?.abort()
237
+ pollers.delete(tabId)
238
+ return {
239
+ browserSessionId: session?.browserSessionId || "",
240
+ method: "Target.closeTarget",
241
+ result: { success: true },
242
+ intercepted: true,
243
+ tabId,
244
+ sentAt: new Date().toISOString(),
245
+ }
246
+ }
247
+
248
+ async function interceptTargetCreateTarget(params = {}, parentSession = null) {
249
+ const url = String(params.url || "about:blank")
250
+ const tab = await chrome.tabs.create({ url, active: true })
251
+ const browserSessionId = `yunti-${tab.id}-${crypto.randomUUID ? crypto.randomUUID() : Date.now()}`
252
+ const sessionData = {
253
+ browserSessionId,
254
+ userId: parentSession?.userId || "",
255
+ displayName: parentSession?.displayName || "",
256
+ tabId: tab.id,
257
+ windowId: tab.windowId,
258
+ url: tab.url || url,
259
+ title: tab.title || "",
260
+ registeredAt: new Date().toISOString(),
261
+ }
262
+ sessionsByTab.set(tab.id, sessionData)
263
+ await postBridge("/sessions/register", sessionData).catch(() => null)
264
+ startPolling(tab.id)
265
+ return {
266
+ browserSessionId,
267
+ method: "Target.createTarget",
268
+ result: { targetId: `tab-${tab.id}` },
269
+ intercepted: true,
270
+ tabId: tab.id,
271
+ sentAt: new Date().toISOString(),
272
+ }
273
+ }
274
+
275
+ async function interceptTargetAttachToTarget(params = {}, protocolVersion = "1.3") {
276
+ const targetId = String(params.targetId || "")
277
+ const tabId = await resolveTabIdFromTarget(targetId)
278
+ if (!tabId) {
279
+ throw new Error(`Cannot resolve targetId to tab: ${targetId}. Use chrome.debugger.attach directly for non-tab targets.`)
280
+ }
281
+ if (cdpAttachedTabs.has(tabId)) {
282
+ return {
283
+ method: "Target.attachToTarget",
284
+ result: { sessionId: `yunti-session-${tabId}` },
285
+ intercepted: true,
286
+ tabId,
287
+ reused: true,
288
+ sentAt: new Date().toISOString(),
289
+ }
290
+ }
291
+ await chromeDebuggerAttach({ tabId }, protocolVersion)
292
+ cdpAttachedTabs.add(tabId)
293
+ return {
294
+ method: "Target.attachToTarget",
295
+ result: { sessionId: `yunti-session-${tabId}` },
296
+ intercepted: true,
297
+ tabId,
298
+ attached: true,
299
+ sentAt: new Date().toISOString(),
300
+ }
301
+ }
302
+
303
+ async function interceptTargetDetachFromTarget(params = {}) {
304
+ const targetId = String(params.targetId || "")
305
+ let tabId = await resolveTabIdFromTarget(targetId)
306
+ if (!tabId) {
307
+ const sessionMatch = targetId.match(/^yunti-session-(\d+)$/)
308
+ if (sessionMatch) tabId = Number(sessionMatch[1])
309
+ }
310
+ if (!tabId || !cdpAttachedTabs.has(tabId)) {
311
+ return {
312
+ method: "Target.detachFromTarget",
313
+ result: {},
314
+ intercepted: true,
315
+ detached: false,
316
+ reason: tabId ? "not_attached" : "unknown_target",
317
+ sentAt: new Date().toISOString(),
318
+ }
319
+ }
320
+ await chromeDebuggerDetach({ tabId })
321
+ cdpAttachedTabs.delete(tabId)
322
+ return {
323
+ method: "Target.detachFromTarget",
324
+ result: {},
325
+ intercepted: true,
326
+ tabId,
327
+ detached: true,
328
+ sentAt: new Date().toISOString(),
329
+ }
330
+ }
331
+
332
+ async function interceptTargetActivateTarget(params = {}) {
333
+ const targetId = String(params.targetId || "")
334
+ const tabId = await resolveTabIdFromTarget(targetId)
335
+ if (!tabId) throw new Error(`Cannot resolve targetId to tab: ${targetId}`)
336
+ await chrome.tabs.update(tabId, { active: true })
337
+ return {
338
+ method: "Target.activateTarget",
339
+ result: {},
340
+ intercepted: true,
341
+ tabId,
342
+ sentAt: new Date().toISOString(),
343
+ }
344
+ }
345
+
346
+ async function interceptTargetGetTargetInfo(params = {}) {
347
+ const targetId = String(params.targetId || "")
348
+ const tabId = await resolveTabIdFromTarget(targetId)
349
+ let info = { targetId, type: "page", title: "", url: "", attached: false, canAccessOpener: false }
350
+ if (tabId) {
351
+ const tab = await chrome.tabs.get(tabId).catch(() => null)
352
+ info = {
353
+ targetId,
354
+ type: "page",
355
+ title: tab?.title || "",
356
+ url: tab?.url || "",
357
+ attached: cdpAttachedTabs.has(tabId),
358
+ canAccessOpener: false,
359
+ }
360
+ }
361
+ return {
362
+ method: "Target.getTargetInfo",
363
+ result: { targetInfo: info },
364
+ intercepted: true,
365
+ sentAt: new Date().toISOString(),
366
+ }
367
+ }
368
+
369
+ async function resolveCdpTargetTabId(defaultTabId, args = {}, params = {}) {
370
+ const directTabId = Number(args.tabId ?? params.tabId)
371
+ if (Number.isFinite(directTabId) && directTabId > 0) return directTabId
372
+ const targetId = String(args.targetId || params.targetId || "").trim()
373
+ if (!targetId) return defaultTabId
374
+ const tabId = await resolveTabIdFromTarget(targetId)
375
+ if (!tabId) throw new Error(`Cannot resolve targetId to tab: ${targetId}`)
376
+ return tabId
377
+ }
378
+
379
+ function explicitCdpTargetId(args = {}, params = {}) {
380
+ const targetId = String(args.targetId || params.targetId || "").trim()
381
+ if (targetId) return targetId
382
+ const tabId = Number(args.tabId ?? params.tabId)
383
+ if (Number.isFinite(tabId) && tabId > 0) return `tab-${tabId}`
384
+ return ""
385
+ }
386
+
387
+ function resolveTabIdFromTargetSync(targetId) {
388
+ if (!targetId) return null
389
+ if (targetId.startsWith("tab-")) {
390
+ const id = Number(targetId.slice(4))
391
+ return Number.isFinite(id) && id > 0 ? id : null
392
+ }
393
+ const numericId = Number(targetId)
394
+ if (/^\d+$/.test(targetId) && Number.isFinite(numericId) && numericId > 0) {
395
+ return numericId
396
+ }
397
+ for (const [tabId, session] of sessionsByTab) {
398
+ if (session.browserSessionId === targetId) return tabId
399
+ }
400
+ return null
401
+ }
402
+
403
+ async function resolveTabIdFromTarget(targetId) {
404
+ const syncTabId = resolveTabIdFromTargetSync(targetId)
405
+ if (syncTabId) return syncTabId
406
+ if (chrome.debugger?.getTargets) {
407
+ try {
408
+ const targets = await chromeDebuggerGetTargets()
409
+ const found = targets.find((target) => target.id === targetId)
410
+ const tabId = Number(found?.tabId)
411
+ if (Number.isFinite(tabId) && tabId > 0) return tabId
412
+ } catch {}
413
+ }
414
+ return null
415
+ }
416
+
417
+ function chromeDebuggerGetTargets() {
418
+ return new Promise((resolve, reject) => {
419
+ chrome.debugger.getTargets((targets) => {
420
+ const error = chrome.runtime.lastError
421
+ if (error) reject(new Error(error.message))
422
+ else resolve(targets || [])
423
+ })
424
+ })
425
+ }
426
+
427
+ async function ensureCdpAttached(tabId, protocolVersion = "1.3") {
428
+ if (!chrome.debugger?.attach) {
429
+ throw new Error("chrome.debugger API is unavailable")
430
+ }
431
+ if (cdpAttachedTabs.has(tabId)) {
432
+ await ensureCdpDomains(tabId, ["Page", "Runtime", "Log"])
433
+ return { attached: false, reused: true, protocolVersion }
434
+ }
435
+ await chromeDebuggerAttach({ tabId }, protocolVersion)
436
+ cdpAttachedTabs.add(tabId)
437
+ await ensureCdpDomains(tabId, ["Page", "Runtime", "Log"])
438
+ return { attached: true, reused: false, protocolVersion }
439
+ }
440
+
441
+ async function ensureCdpDomains(tabId, domains = []) {
442
+ if (!cdpEnabledDomains.has(tabId)) cdpEnabledDomains.set(tabId, new Set())
443
+ const enabled = cdpEnabledDomains.get(tabId)
444
+ for (const domain of domains) {
445
+ if (enabled.has(domain)) continue
446
+ await chromeDebuggerSendCommand({ tabId }, `${domain}.enable`, {})
447
+ enabled.add(domain)
448
+ }
449
+ }
450
+
451
+ async function detachCdpTab(tabId, session = null, reason = "manual") {
452
+ if (!tabId || !cdpAttachedTabs.has(tabId)) {
453
+ return {
454
+ browserSessionId: session?.browserSessionId || "",
455
+ tabId,
456
+ detached: false,
457
+ reason: "not_attached",
458
+ }
459
+ }
460
+ await chromeDebuggerDetach({ tabId })
461
+ cdpAttachedTabs.delete(tabId)
462
+ cdpEnabledDomains.delete(tabId)
463
+ traceBuffers.delete(tabId)
464
+ return {
465
+ browserSessionId: session?.browserSessionId || "",
466
+ tabId,
467
+ detached: true,
468
+ reason,
469
+ detachedAt: new Date().toISOString(),
470
+ }
471
+ }
472
+
473
+ function chromeDebuggerAttach(target, protocolVersion) {
474
+ return new Promise((resolve, reject) => {
475
+ chrome.debugger.attach(target, protocolVersion, () => {
476
+ const error = chrome.runtime.lastError
477
+ if (error) reject(new Error(error.message))
478
+ else resolve(null)
479
+ })
480
+ })
481
+ }
482
+
483
+ function chromeDebuggerDetach(target) {
484
+ return new Promise((resolve, reject) => {
485
+ chrome.debugger.detach(target, () => {
486
+ const error = chrome.runtime.lastError
487
+ if (error) reject(new Error(error.message))
488
+ else resolve(null)
489
+ })
490
+ })
491
+ }
492
+
493
+ function chromeDebuggerSendCommand(target, method, params) {
494
+ return new Promise((resolve, reject) => {
495
+ chrome.debugger.sendCommand(target, method, params, (result) => {
496
+ const error = chrome.runtime.lastError
497
+ if (error) reject(new Error(error.message))
498
+ else resolve(result)
499
+ })
500
+ })
501
+ }
502
+
503
+ async function startPerformanceTrace(tabId, session, args = {}) {
504
+ const categoriesStr = String(args.categories || "blink,loading,rendering,page").trim()
505
+ const includedCategories = categoriesStr
506
+ .split(",")
507
+ .map((category) => category.trim())
508
+ .filter(Boolean)
509
+ await ensureCdpAttached(tabId, "1.3")
510
+ traceBuffers.set(tabId, {
511
+ events: [],
512
+ startedAt: Date.now(),
513
+ completed: false,
514
+ })
515
+ await chromeDebuggerSendCommand({ tabId }, "Tracing.start", {
516
+ traceConfig: {
517
+ includedCategories,
518
+ memoryDumpConfig: {},
519
+ },
520
+ })
521
+ return { tracing: true, categories: categoriesStr, browserSessionId: session.browserSessionId }
522
+ }
523
+
524
+ async function stopPerformanceTrace(tabId, session) {
525
+ await ensureCdpAttached(tabId, "1.3")
526
+ const buffer = traceBuffers.get(tabId) || {
527
+ events: [],
528
+ startedAt: Date.now(),
529
+ completed: false,
530
+ }
531
+ traceBuffers.set(tabId, buffer)
532
+ await chromeDebuggerSendCommand({ tabId }, "Tracing.end", {})
533
+ await waitForTraceComplete(tabId, 5000)
534
+ const events = buffer.events || []
535
+ const durationMs = Date.now() - (buffer.startedAt || Date.now())
536
+ traceBuffers.delete(tabId)
537
+ return {
538
+ events: events.slice(0, 1000),
539
+ eventsTruncated: events.length > 1000,
540
+ durationMs,
541
+ browserSessionId: session.browserSessionId,
542
+ }
543
+ }
544
+
545
+ function recordTraceEvent(tabId, method, params = {}) {
546
+ const buffer = traceBuffers.get(tabId)
547
+ if (!buffer) return
548
+ if (method === "Tracing.dataCollected") {
549
+ const value = Array.isArray(params.value) ? params.value : []
550
+ buffer.events.push(...value)
551
+ } else if (method === "Tracing.tracingComplete") {
552
+ buffer.completed = true
553
+ buffer.stream = params.stream || ""
554
+ }
555
+ }
556
+
557
+ async function waitForTraceComplete(tabId, timeoutMs = 5000) {
558
+ const start = Date.now()
559
+ while (Date.now() - start < timeoutMs) {
560
+ const buffer = traceBuffers.get(tabId)
561
+ if (!buffer || buffer.completed) return
562
+ await delayCdp(100)
563
+ }
564
+ }
565
+
566
+ function delayCdp(ms) {
567
+ return new Promise((resolve) => setTimeout(resolve, ms))
568
+ }
569
+
570
+ return {
571
+ chromeDebuggerSendCommand,
572
+ delayCdp,
573
+ detachCdpTab,
574
+ ensureCdpAttached,
575
+ getBrowserTarget,
576
+ installCdpEventForwarder,
577
+ listBrowserTargets,
578
+ sendCdpCommand,
579
+ startPerformanceTrace,
580
+ stopPerformanceTrace,
581
+ }
582
+ }
@@ -0,0 +1,9 @@
1
+ .yunti-browser-runtime-highlight {
2
+ outline: 2px solid #0f766e !important;
3
+ outline-offset: 2px !important;
4
+ background-image: linear-gradient(
5
+ 135deg,
6
+ rgba(15, 118, 110, 0.12),
7
+ rgba(245, 158, 11, 0.12)
8
+ ) !important;
9
+ }