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.
- package/LICENSE +21 -0
- package/README.md +256 -0
- package/bin/yunti-browser-runtime.js +86 -0
- package/docs/EXECUTION_PLAN.md +1278 -0
- package/docs/INSTALL.md +205 -0
- package/docs/PROJECT_INTENT.md +44 -0
- package/docs/PROJECT_STATUS.md +263 -0
- package/docs/PUBLISHING_BLOCKERS.md +110 -0
- package/docs/RELEASE.md +148 -0
- package/docs/ROADMAP.md +42 -0
- package/docs/SECURITY.md +56 -0
- package/docs/TOOL_GUIDE.md +69 -0
- package/extension/background.js +55 -0
- package/extension/cdp.js +582 -0
- package/extension/content.css +9 -0
- package/extension/content.js +946 -0
- package/extension/manifest.json +35 -0
- package/extension/network-monitor.js +140 -0
- package/extension/popup.css +66 -0
- package/extension/popup.html +30 -0
- package/extension/popup.js +55 -0
- package/extension/session-manager.js +332 -0
- package/extension/settings.js +94 -0
- package/extension/tool-handlers.js +1158 -0
- package/lib/runtime-paths.js +39 -0
- package/mcp/bridge-hub.js +604 -0
- package/mcp/http-server.js +326 -0
- package/mcp/json-rpc.js +35 -0
- package/mcp/memory.js +126 -0
- package/mcp/redaction.js +94 -0
- package/mcp/server.js +269 -0
- package/mcp/tools.js +1092 -0
- package/package.json +60 -0
- package/scripts/check-package-metadata.js +131 -0
- package/scripts/check-published-package.js +113 -0
- package/scripts/doctor.js +163 -0
- package/scripts/package-extension.js +137 -0
- package/scripts/print-config.js +116 -0
- package/scripts/release-check.js +472 -0
- package/skills/yunti-browser-runtime/SKILL.md +77 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "Yunti Browser Runtime",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Local browser runtime for AI agents through MCP.",
|
|
6
|
+
"permissions": [
|
|
7
|
+
"activeTab",
|
|
8
|
+
"debugger",
|
|
9
|
+
"storage",
|
|
10
|
+
"tabs",
|
|
11
|
+
"webRequest"
|
|
12
|
+
],
|
|
13
|
+
"host_permissions": [
|
|
14
|
+
"https://*/*",
|
|
15
|
+
"http://*/*",
|
|
16
|
+
"http://127.0.0.1/*",
|
|
17
|
+
"http://localhost/*"
|
|
18
|
+
],
|
|
19
|
+
"background": {
|
|
20
|
+
"service_worker": "background.js",
|
|
21
|
+
"type": "module"
|
|
22
|
+
},
|
|
23
|
+
"action": {
|
|
24
|
+
"default_title": "Yunti Browser Runtime",
|
|
25
|
+
"default_popup": "popup.html"
|
|
26
|
+
},
|
|
27
|
+
"content_scripts": [
|
|
28
|
+
{
|
|
29
|
+
"matches": ["http://*/*", "https://*/*"],
|
|
30
|
+
"js": ["content.js"],
|
|
31
|
+
"css": ["content.css"],
|
|
32
|
+
"run_at": "document_idle"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { DEFAULT_PLATFORM_MATCHES, isPlatformUrl } from "./settings.js"
|
|
2
|
+
|
|
3
|
+
const HTTPS_NETWORK_FILTER = { urls: ["https://*/*"] }
|
|
4
|
+
const SENSITIVE_FIELD_RE =
|
|
5
|
+
/cookie|authorization|token|secret|password|passwd|pwd|sid|session|ticket|tgc|captcha|验证码|密码/i
|
|
6
|
+
|
|
7
|
+
export function installNetworkMonitor({ sessionsByTab, postBridge, getPlatformMatches }) {
|
|
8
|
+
const networkRequests = new Map()
|
|
9
|
+
|
|
10
|
+
chrome.webRequest.onBeforeRequest.addListener(
|
|
11
|
+
(details) => {
|
|
12
|
+
beginNetworkEvent({ details, sessionsByTab, networkRequests, getPlatformMatches })
|
|
13
|
+
},
|
|
14
|
+
HTTPS_NETWORK_FILTER,
|
|
15
|
+
["requestBody"]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
chrome.webRequest.onCompleted.addListener((details) => {
|
|
19
|
+
finishNetworkEvent({
|
|
20
|
+
details,
|
|
21
|
+
outcome: { completed: true },
|
|
22
|
+
sessionsByTab,
|
|
23
|
+
networkRequests,
|
|
24
|
+
getPlatformMatches,
|
|
25
|
+
postBridge,
|
|
26
|
+
})
|
|
27
|
+
}, HTTPS_NETWORK_FILTER)
|
|
28
|
+
|
|
29
|
+
chrome.webRequest.onErrorOccurred.addListener((details) => {
|
|
30
|
+
finishNetworkEvent({
|
|
31
|
+
details,
|
|
32
|
+
outcome: { error: details.error || "request failed" },
|
|
33
|
+
sessionsByTab,
|
|
34
|
+
networkRequests,
|
|
35
|
+
getPlatformMatches,
|
|
36
|
+
postBridge,
|
|
37
|
+
})
|
|
38
|
+
}, HTTPS_NETWORK_FILTER)
|
|
39
|
+
|
|
40
|
+
return { networkRequests }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function beginNetworkEvent({ details, sessionsByTab, networkRequests, getPlatformMatches }) {
|
|
44
|
+
if (details.tabId < 0) return
|
|
45
|
+
const session = sessionsByTab.get(details.tabId)
|
|
46
|
+
if (!session) return
|
|
47
|
+
if (!isSupportedNetworkUrl(details.url, getPlatformMatches)) return
|
|
48
|
+
networkRequests.set(details.requestId, {
|
|
49
|
+
browserSessionId: session.browserSessionId,
|
|
50
|
+
tabId: details.tabId,
|
|
51
|
+
requestId: details.requestId,
|
|
52
|
+
url: details.url,
|
|
53
|
+
method: details.method,
|
|
54
|
+
type: details.type,
|
|
55
|
+
initiator: details.initiator || "",
|
|
56
|
+
documentUrl: details.documentUrl || "",
|
|
57
|
+
startedAt: new Date(details.timeStamp || Date.now()).toISOString(),
|
|
58
|
+
startTime: details.timeStamp || Date.now(),
|
|
59
|
+
requestBody: summarizeRequestBody(details.requestBody),
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function finishNetworkEvent({
|
|
64
|
+
details,
|
|
65
|
+
outcome,
|
|
66
|
+
sessionsByTab,
|
|
67
|
+
networkRequests,
|
|
68
|
+
getPlatformMatches,
|
|
69
|
+
postBridge,
|
|
70
|
+
}) {
|
|
71
|
+
if (!isSupportedNetworkUrl(details.url, getPlatformMatches)) return
|
|
72
|
+
const event = networkRequests.get(details.requestId) || {
|
|
73
|
+
tabId: details.tabId,
|
|
74
|
+
requestId: details.requestId,
|
|
75
|
+
url: details.url,
|
|
76
|
+
method: details.method,
|
|
77
|
+
type: details.type,
|
|
78
|
+
initiator: details.initiator || "",
|
|
79
|
+
documentUrl: details.documentUrl || "",
|
|
80
|
+
startedAt: new Date(details.timeStamp || Date.now()).toISOString(),
|
|
81
|
+
startTime: details.timeStamp || Date.now(),
|
|
82
|
+
requestBody: null,
|
|
83
|
+
}
|
|
84
|
+
networkRequests.delete(details.requestId)
|
|
85
|
+
if (!event.browserSessionId && details.tabId >= 0) {
|
|
86
|
+
event.browserSessionId =
|
|
87
|
+
sessionsByTab.get(details.tabId)?.browserSessionId || ""
|
|
88
|
+
}
|
|
89
|
+
if (!event.browserSessionId) return
|
|
90
|
+
const completedAt = new Date(details.timeStamp || Date.now()).toISOString()
|
|
91
|
+
void postBridge("/extension/network-event", {
|
|
92
|
+
...event,
|
|
93
|
+
completedAt,
|
|
94
|
+
durationMs: Math.max(
|
|
95
|
+
0,
|
|
96
|
+
Math.round((details.timeStamp || Date.now()) - event.startTime)
|
|
97
|
+
),
|
|
98
|
+
statusCode: details.statusCode ?? null,
|
|
99
|
+
ok: outcome.completed
|
|
100
|
+
? details.statusCode >= 200 && details.statusCode < 400
|
|
101
|
+
: false,
|
|
102
|
+
fromCache: Boolean(details.fromCache),
|
|
103
|
+
error: outcome.error || "",
|
|
104
|
+
}).catch(() => {})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isSupportedNetworkUrl(url, getPlatformMatches) {
|
|
108
|
+
return isPlatformUrl(url, {
|
|
109
|
+
platformMatches: getPlatformMatches?.() || DEFAULT_PLATFORM_MATCHES,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function summarizeRequestBody(requestBody) {
|
|
114
|
+
if (!requestBody) return null
|
|
115
|
+
const fieldNames = []
|
|
116
|
+
const formData = {}
|
|
117
|
+
if (requestBody.formData && typeof requestBody.formData === "object") {
|
|
118
|
+
for (const [key, values] of Object.entries(requestBody.formData)) {
|
|
119
|
+
fieldNames.push(key)
|
|
120
|
+
formData[key] = safeFormPreview(key, values)
|
|
121
|
+
}
|
|
122
|
+
return { kind: "formData", fieldNames, formData }
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(requestBody.raw)) {
|
|
125
|
+
const rawBytes = requestBody.raw.reduce(
|
|
126
|
+
(total, item) => total + (item.bytes?.byteLength || 0),
|
|
127
|
+
0
|
|
128
|
+
)
|
|
129
|
+
return { kind: "raw", rawBytes }
|
|
130
|
+
}
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function safeFormPreview(key, values) {
|
|
135
|
+
if (SENSITIVE_FIELD_RE.test(String(key || ""))) return "[REDACTED]"
|
|
136
|
+
const joined = Array.isArray(values)
|
|
137
|
+
? values.map((value) => String(value)).join(",")
|
|
138
|
+
: String(values)
|
|
139
|
+
return joined.length > 160 ? `${joined.slice(0, 160)}...` : joined
|
|
140
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
body {
|
|
2
|
+
width: 260px;
|
|
3
|
+
margin: 0;
|
|
4
|
+
font-family:
|
|
5
|
+
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
6
|
+
color: #0f172a;
|
|
7
|
+
background: #fff;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
main {
|
|
11
|
+
display: grid;
|
|
12
|
+
gap: 9px;
|
|
13
|
+
padding: 12px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
h1 {
|
|
17
|
+
margin: 0;
|
|
18
|
+
font-size: 16px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
p {
|
|
22
|
+
min-height: 34px;
|
|
23
|
+
margin: 0;
|
|
24
|
+
color: #475569;
|
|
25
|
+
font-size: 12px;
|
|
26
|
+
line-height: 1.4;
|
|
27
|
+
white-space: pre-wrap;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
label {
|
|
31
|
+
display: grid;
|
|
32
|
+
gap: 4px;
|
|
33
|
+
color: #334155;
|
|
34
|
+
font-size: 12px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
input,
|
|
38
|
+
textarea {
|
|
39
|
+
width: 100%;
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
border: 1px solid #cbd5e1;
|
|
42
|
+
border-radius: 6px;
|
|
43
|
+
padding: 6px 7px;
|
|
44
|
+
color: #0f172a;
|
|
45
|
+
font: 12px/1.3 system-ui;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
textarea {
|
|
49
|
+
resize: vertical;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
button {
|
|
53
|
+
min-height: 32px;
|
|
54
|
+
border: 1px solid #cbd5e1;
|
|
55
|
+
border-radius: 7px;
|
|
56
|
+
background: #fff;
|
|
57
|
+
color: #0f172a;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
font: 500 13px/1 system-ui;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
button:first-of-type {
|
|
63
|
+
border-color: #0f766e;
|
|
64
|
+
background: #0f766e;
|
|
65
|
+
color: white;
|
|
66
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Yunti Browser Runtime</title>
|
|
7
|
+
<link rel="stylesheet" href="popup.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>
|
|
11
|
+
<h1>Yunti Browser Runtime</h1>
|
|
12
|
+
<p id="status">正在读取状态...</p>
|
|
13
|
+
<label>
|
|
14
|
+
Bridge URL
|
|
15
|
+
<input id="bridgeUrl" autocomplete="off" />
|
|
16
|
+
</label>
|
|
17
|
+
<label>
|
|
18
|
+
Bridge Token
|
|
19
|
+
<input id="bridgeToken" autocomplete="off" type="password" />
|
|
20
|
+
</label>
|
|
21
|
+
<label>
|
|
22
|
+
页面匹配
|
|
23
|
+
<textarea id="platformMatches" rows="3"></textarea>
|
|
24
|
+
</label>
|
|
25
|
+
<button id="save">保存设置</button>
|
|
26
|
+
<button id="refresh">刷新状态</button>
|
|
27
|
+
</main>
|
|
28
|
+
<script type="module" src="popup.js"></script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const statusEl = document.querySelector("#status")
|
|
2
|
+
const bridgeUrlEl = document.querySelector("#bridgeUrl")
|
|
3
|
+
const bridgeTokenEl = document.querySelector("#bridgeToken")
|
|
4
|
+
const platformMatchesEl = document.querySelector("#platformMatches")
|
|
5
|
+
document.querySelector("#save").addEventListener("click", save)
|
|
6
|
+
document.querySelector("#refresh").addEventListener("click", refresh)
|
|
7
|
+
|
|
8
|
+
void refresh()
|
|
9
|
+
|
|
10
|
+
async function refresh() {
|
|
11
|
+
const state = await send({ type: "yunti_panel_get_state" })
|
|
12
|
+
if (!state.ok) {
|
|
13
|
+
setStatus(state.error || "状态读取失败")
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
renderSettings(state.settings || {})
|
|
17
|
+
setStatus(
|
|
18
|
+
state.activeSession
|
|
19
|
+
? `页面已连接\n${state.activeSession.title || state.activeSession.url}`
|
|
20
|
+
: "请打开任意 http/https 页面,并确认本地 bridge 已启动。"
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function save() {
|
|
25
|
+
const state = await send({
|
|
26
|
+
type: "yunti_panel_save_settings",
|
|
27
|
+
settings: {
|
|
28
|
+
bridgeUrl: bridgeUrlEl.value,
|
|
29
|
+
bridgeToken: bridgeTokenEl.value,
|
|
30
|
+
platformMatches: platformMatchesEl.value,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
if (!state.ok) {
|
|
34
|
+
setStatus(state.error || "保存失败")
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
renderSettings(state.settings || {})
|
|
38
|
+
setStatus("设置已保存,请刷新目标页面完成重新注册。")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderSettings(settings) {
|
|
42
|
+
bridgeUrlEl.value = settings.bridgeUrl || "http://127.0.0.1:48887"
|
|
43
|
+
bridgeTokenEl.value = settings.bridgeToken || ""
|
|
44
|
+
platformMatchesEl.value = Array.isArray(settings.platformMatches)
|
|
45
|
+
? settings.platformMatches.join("\n")
|
|
46
|
+
: "*"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setStatus(text) {
|
|
50
|
+
statusEl.textContent = text
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function send(message) {
|
|
54
|
+
return chrome.runtime.sendMessage(message)
|
|
55
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_BRIDGE_URL,
|
|
3
|
+
bridgeHeaders,
|
|
4
|
+
getSettings,
|
|
5
|
+
isPlatformUrl,
|
|
6
|
+
normalizeBaseUrl,
|
|
7
|
+
normalizeBridgeToken,
|
|
8
|
+
normalizePlatformMatches,
|
|
9
|
+
platformLabelForUrl,
|
|
10
|
+
} from "./settings.js"
|
|
11
|
+
|
|
12
|
+
export function createSessionManager() {
|
|
13
|
+
const sessionsByTab = new Map()
|
|
14
|
+
const pollers = new Map()
|
|
15
|
+
let cachedPlatformMatches = null
|
|
16
|
+
let toolRequestHandler = null
|
|
17
|
+
|
|
18
|
+
function getPlatformMatches() {
|
|
19
|
+
return cachedPlatformMatches
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function setToolRequestHandler(handler) {
|
|
23
|
+
toolRequestHandler = typeof handler === "function" ? handler : null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function forgetTab(tabId) {
|
|
27
|
+
sessionsByTab.delete(tabId)
|
|
28
|
+
pollers.get(tabId)?.abort()
|
|
29
|
+
pollers.delete(tabId)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function activateTab(tabId) {
|
|
33
|
+
const session = sessionsByTab.get(tabId)
|
|
34
|
+
if (!session) return
|
|
35
|
+
await postBridge("/sessions/activate", {
|
|
36
|
+
browserSessionId: session.browserSessionId,
|
|
37
|
+
userId: session.userId || "",
|
|
38
|
+
}).catch(() => {})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function handleMessage(message, sender) {
|
|
42
|
+
switch (message?.type) {
|
|
43
|
+
case "yunti_content_ready":
|
|
44
|
+
return registerContentSession(sender.tab, message.page)
|
|
45
|
+
case "yunti_is_supported_page":
|
|
46
|
+
return checkSupportedPage(message.page)
|
|
47
|
+
case "yunti_panel_get_state":
|
|
48
|
+
return getPanelState()
|
|
49
|
+
case "yunti_panel_save_settings":
|
|
50
|
+
await chrome.storage.local.set({
|
|
51
|
+
localUserName: "local",
|
|
52
|
+
localUserId: "local",
|
|
53
|
+
bridgeUrl: normalizeBaseUrl(
|
|
54
|
+
message.settings?.bridgeUrl || DEFAULT_BRIDGE_URL
|
|
55
|
+
),
|
|
56
|
+
bridgeToken: normalizeBridgeToken(message.settings?.bridgeToken || ""),
|
|
57
|
+
agentType: "browser_agent",
|
|
58
|
+
platformMatches: normalizePlatformMatches(
|
|
59
|
+
message.settings?.platformMatches
|
|
60
|
+
),
|
|
61
|
+
})
|
|
62
|
+
return getPanelState()
|
|
63
|
+
case "yunti_panel_refresh_active":
|
|
64
|
+
return refreshActiveTab()
|
|
65
|
+
default:
|
|
66
|
+
return { ok: false, error: `unknown message: ${message?.type}` }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function registerContentSession(tab, page) {
|
|
71
|
+
const settings = await getSettings()
|
|
72
|
+
cachedPlatformMatches = settings.platformMatches
|
|
73
|
+
if (!tab?.id || !isPlatformUrl(page?.url, settings)) {
|
|
74
|
+
return { ok: false, error: "not an Yunti tab" }
|
|
75
|
+
}
|
|
76
|
+
const browserSessionId =
|
|
77
|
+
sessionsByTab.get(tab.id)?.browserSessionId ||
|
|
78
|
+
`yunti-${tab.id}-${crypto.randomUUID ? crypto.randomUUID() : Date.now()}`
|
|
79
|
+
const session = {
|
|
80
|
+
browserSessionId,
|
|
81
|
+
userId: settings.localUserId || "",
|
|
82
|
+
displayName: settings.localUserName || "",
|
|
83
|
+
tabId: tab.id,
|
|
84
|
+
windowId: tab.windowId,
|
|
85
|
+
url: page.url,
|
|
86
|
+
title: page.title || tab.title || platformLabelForUrl(page.url),
|
|
87
|
+
client: normalizeClientInfo({
|
|
88
|
+
...getBackgroundClientInfo(),
|
|
89
|
+
...(page.client || {}),
|
|
90
|
+
}),
|
|
91
|
+
auth: normalizePageAuth(page.auth, "content_script_register"),
|
|
92
|
+
registeredAt: new Date().toISOString(),
|
|
93
|
+
}
|
|
94
|
+
sessionsByTab.set(tab.id, session)
|
|
95
|
+
await postBridge("/sessions/register", session).catch(() => null)
|
|
96
|
+
startPolling(tab.id)
|
|
97
|
+
return { ok: true, session, settings }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function checkSupportedPage(page) {
|
|
101
|
+
const settings = await getSettings()
|
|
102
|
+
cachedPlatformMatches = settings.platformMatches
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
supported: isPlatformUrl(page?.url, settings),
|
|
106
|
+
settings,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function startPolling(tabId) {
|
|
111
|
+
if (pollers.has(tabId)) return
|
|
112
|
+
const controller = new AbortController()
|
|
113
|
+
pollers.set(tabId, controller)
|
|
114
|
+
const loop = async () => {
|
|
115
|
+
while (!controller.signal.aborted) {
|
|
116
|
+
const session = sessionsByTab.get(tabId)
|
|
117
|
+
if (!session) break
|
|
118
|
+
try {
|
|
119
|
+
const settings = await getSettings()
|
|
120
|
+
cachedPlatformMatches = settings.platformMatches
|
|
121
|
+
await postBridge("/sessions/register", session).catch(() => null)
|
|
122
|
+
const url = `${settings.bridgeUrl}/extension/poll?browserSessionId=${encodeURIComponent(
|
|
123
|
+
session.browserSessionId
|
|
124
|
+
)}&timeoutMs=25000`
|
|
125
|
+
const response = await fetch(url, {
|
|
126
|
+
signal: controller.signal,
|
|
127
|
+
headers: bridgeHeaders(settings),
|
|
128
|
+
})
|
|
129
|
+
const event = await response.json()
|
|
130
|
+
if (event?.type === "tool_request" && toolRequestHandler) {
|
|
131
|
+
await toolRequestHandler(tabId, session, event)
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
if (!controller.signal.aborted) await delay(1500)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
pollers.delete(tabId)
|
|
138
|
+
}
|
|
139
|
+
void loop()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function forwardConsoleEvent(session, tabId, method, params = {}) {
|
|
143
|
+
let level = "log"
|
|
144
|
+
let text = ""
|
|
145
|
+
let source = "console-api"
|
|
146
|
+
let stackTrace = ""
|
|
147
|
+
let args = []
|
|
148
|
+
let url = ""
|
|
149
|
+
let lineNumber = null
|
|
150
|
+
let columnNumber = null
|
|
151
|
+
|
|
152
|
+
if (method === "Runtime.consoleAPICalled") {
|
|
153
|
+
source = "console-api"
|
|
154
|
+
level = String(params.type || "log").toLowerCase()
|
|
155
|
+
args = (params.args || []).map((arg) => {
|
|
156
|
+
if (arg.type === "string") return String(arg.value || "")
|
|
157
|
+
if (arg.type === "number" || arg.type === "boolean") {
|
|
158
|
+
return String(arg.value)
|
|
159
|
+
}
|
|
160
|
+
if (arg.type === "undefined") return "undefined"
|
|
161
|
+
if (arg.type === "null") return "null"
|
|
162
|
+
if (arg.type === "object" && arg.description) return arg.description
|
|
163
|
+
return arg.type || "unknown"
|
|
164
|
+
})
|
|
165
|
+
text = args.filter((arg) => typeof arg === "string").join(" ") || args.join(" ")
|
|
166
|
+
stackTrace = formatConsoleStackTrace(params.stackTrace)
|
|
167
|
+
} else if (method === "Log.entryAdded") {
|
|
168
|
+
source = "other"
|
|
169
|
+
const entry = params.entry || {}
|
|
170
|
+
level = String(entry.level || "verbose").toLowerCase()
|
|
171
|
+
text = String(entry.text || "").slice(0, 2000)
|
|
172
|
+
url = entry.url || ""
|
|
173
|
+
lineNumber = entry.lineNumber ?? null
|
|
174
|
+
columnNumber = entry.columnNumber ?? null
|
|
175
|
+
if (entry.stackTrace) stackTrace = formatConsoleStackTrace(entry.stackTrace)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await postBridge("/extension/console-event", {
|
|
179
|
+
browserSessionId: session.browserSessionId,
|
|
180
|
+
tabId,
|
|
181
|
+
level,
|
|
182
|
+
text: text.slice(0, 2000),
|
|
183
|
+
source,
|
|
184
|
+
stackTrace,
|
|
185
|
+
args,
|
|
186
|
+
url,
|
|
187
|
+
lineNumber,
|
|
188
|
+
columnNumber,
|
|
189
|
+
timestamp: new Date().toISOString(),
|
|
190
|
+
}).catch(() => {})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function postBridge(path, body) {
|
|
194
|
+
const settings = await getSettings()
|
|
195
|
+
cachedPlatformMatches = settings.platformMatches
|
|
196
|
+
const response = await fetch(`${settings.bridgeUrl}${path}`, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers: bridgeHeaders(settings),
|
|
199
|
+
body: JSON.stringify(body),
|
|
200
|
+
})
|
|
201
|
+
if (!response.ok) throw new Error(`bridge HTTP ${response.status}`)
|
|
202
|
+
return response.json()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function getPanelState() {
|
|
206
|
+
const settings = await getSettings()
|
|
207
|
+
cachedPlatformMatches = settings.platformMatches
|
|
208
|
+
const [activeTab] = await chrome.tabs.query({
|
|
209
|
+
active: true,
|
|
210
|
+
currentWindow: true,
|
|
211
|
+
})
|
|
212
|
+
const activeSession = activeTab?.id
|
|
213
|
+
? sessionsByTab.get(activeTab.id) || null
|
|
214
|
+
: null
|
|
215
|
+
let bridge = null
|
|
216
|
+
try {
|
|
217
|
+
const userId = encodeURIComponent(settings.localUserId || "")
|
|
218
|
+
const healthUrl = userId
|
|
219
|
+
? `${settings.bridgeUrl}/health?userId=${userId}`
|
|
220
|
+
: `${settings.bridgeUrl}/health`
|
|
221
|
+
const response = await fetch(healthUrl, {
|
|
222
|
+
headers: bridgeHeaders(settings),
|
|
223
|
+
})
|
|
224
|
+
bridge = await response.json()
|
|
225
|
+
} catch {
|
|
226
|
+
bridge = { ok: false }
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
settings,
|
|
231
|
+
activeTab: activeTab
|
|
232
|
+
? {
|
|
233
|
+
id: activeTab.id,
|
|
234
|
+
url: activeTab.url,
|
|
235
|
+
title: activeTab.title,
|
|
236
|
+
windowId: activeTab.windowId,
|
|
237
|
+
}
|
|
238
|
+
: null,
|
|
239
|
+
activeSession,
|
|
240
|
+
bridge,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function refreshActiveTab() {
|
|
245
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
|
|
246
|
+
if (!tab?.id) return { ok: false, error: "no active tab" }
|
|
247
|
+
await chrome.tabs
|
|
248
|
+
.sendMessage(tab.id, { type: "yunti_refresh_registration" })
|
|
249
|
+
.catch(() => null)
|
|
250
|
+
return getPanelState()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
sessionsByTab,
|
|
255
|
+
pollers,
|
|
256
|
+
activateTab,
|
|
257
|
+
forgetTab,
|
|
258
|
+
forwardConsoleEvent,
|
|
259
|
+
getPlatformMatches,
|
|
260
|
+
handleMessage,
|
|
261
|
+
postBridge,
|
|
262
|
+
setToolRequestHandler,
|
|
263
|
+
startPolling,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function formatConsoleStackTrace(stackTrace) {
|
|
268
|
+
if (!stackTrace?.callFrames) return ""
|
|
269
|
+
return stackTrace.callFrames
|
|
270
|
+
.map(
|
|
271
|
+
(frame) =>
|
|
272
|
+
`${frame.functionName || "(anonymous)"} (${frame.url || ""}:${frame.lineNumber}:${frame.columnNumber})`
|
|
273
|
+
)
|
|
274
|
+
.join("\n")
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizePageAuth(auth, source = "unknown") {
|
|
278
|
+
if (!auth || typeof auth !== "object") {
|
|
279
|
+
return {
|
|
280
|
+
state: "missing_from_content_script",
|
|
281
|
+
loggedIn: false,
|
|
282
|
+
reason: "Content script did not report page state; reload the extension and refresh the page",
|
|
283
|
+
checkedAt: new Date().toISOString(),
|
|
284
|
+
checkedBy: source,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
state: String(auth.state || "unknown"),
|
|
289
|
+
loggedIn: Boolean(auth.loggedIn),
|
|
290
|
+
reason: String(auth.reason || ""),
|
|
291
|
+
checkedAt: auth.checkedAt || new Date().toISOString(),
|
|
292
|
+
checkedBy: String(auth.checkedBy || source),
|
|
293
|
+
status: auth.status ?? null,
|
|
294
|
+
apiAuthError: auth.apiAuthError || "",
|
|
295
|
+
userName: auth.userName ? String(auth.userName).slice(0, 120) : "",
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function normalizeClientInfo(client) {
|
|
300
|
+
const value = client && typeof client === "object" ? client : {}
|
|
301
|
+
return {
|
|
302
|
+
family: normalizeBrowserFamily(value.family || value.userAgent),
|
|
303
|
+
userAgent: String(value.userAgent || "").slice(0, 500),
|
|
304
|
+
platform: String(value.platform || "").slice(0, 120),
|
|
305
|
+
language: String(value.language || "").slice(0, 80),
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getBackgroundClientInfo() {
|
|
310
|
+
return {
|
|
311
|
+
userAgent: String(globalThis.navigator?.userAgent || ""),
|
|
312
|
+
platform: String(globalThis.navigator?.platform || ""),
|
|
313
|
+
language: String(globalThis.navigator?.language || ""),
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeBrowserFamily(value) {
|
|
318
|
+
const text = String(value || "").toLowerCase()
|
|
319
|
+
if (text === "edge" || /edg\//i.test(text)) return "edge"
|
|
320
|
+
if (text === "chrome" || /chrome\//i.test(text) || /chromium\//i.test(text)) {
|
|
321
|
+
return "chrome"
|
|
322
|
+
}
|
|
323
|
+
if (text === "brave" || /brave\//i.test(text)) return "brave"
|
|
324
|
+
if (text === "opera" || /opr\//i.test(text)) return "opera"
|
|
325
|
+
if (text === "firefox" || /firefox\//i.test(text)) return "firefox"
|
|
326
|
+
if (text === "safari" || /safari\//i.test(text)) return "safari"
|
|
327
|
+
return "unknown"
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function delay(ms) {
|
|
331
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
332
|
+
}
|