terminal-bridge-setup 2.0.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.
@@ -0,0 +1,505 @@
1
+ // JumpServer 终端桥接 - background service worker
2
+ //
3
+ // 职责:桥接本地代理和 JumpServer 页面的 xterm 终端。
4
+ // - 代理下发命令 → 转发给 activeTabId 的 content script 注入 xterm
5
+ // - CDP 抓到 activeTabId 的 WS recv 帧 → 回传给代理做请求-响应配对
6
+ // - native messaging:让 popup 能启动/停止本地代理
7
+ //
8
+ // 代理连接:ws://127.0.0.1:8787/ssh(断线 2s 重连)
9
+ // content script:在 koko connect iframe 里运行(见 manifest content_scripts)
10
+
11
+ // ================= 状态 =================
12
+ const attached = {}; // 标记哪些 tab 当前已 attach debugger
13
+ const BRIDGE_WS = "ws://127.0.0.1:8787/ssh";
14
+ let bridgeWs = null;
15
+ let bridgeConnected = false;
16
+ const termReadyTabs = new Set(); // 哪些 tab 的 content script 上报了 term-ready
17
+ const termReadyFrames = new Map(); // tabId -> frameId(终端所在的 frame,注入时直接用)
18
+ let activeTabId = null; // 当前激活的终端 tab(命令只发它,WS 帧只收它的)
19
+
20
+ // ============== 消息处理(来自 popup / content script)==============
21
+ // 注意:listener 不能是 async——async 函数返回 Promise 而非 true,
22
+ // Chrome 会在 listener 返回后立即关闭消息通道,导致异步 sendResponse 失效。
23
+ // 需要异步处理的分支必须 return true(同步),在内部 then/catch 里调 sendResponse。
24
+ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
25
+
26
+ // --- 来自 content script 的上报 ---
27
+ if (msg.type === "term-ready") {
28
+ // term-ready 不需要异步,直接同步处理
29
+ const tabId = sender.tab && sender.tab.id;
30
+ if (tabId != null) {
31
+ termReadyTabs.add(tabId);
32
+ // 记下终端所在的 frameId,injectToXterm 直接用它(不用再按 URL 找)
33
+ const frameId = sender.frameId;
34
+ if (frameId != null) termReadyFrames.set(tabId, frameId);
35
+ console.log("[bg] term-ready from tab", tabId, "frame", frameId, msg.payload && msg.payload.href);
36
+ attachDebugger(tabId);
37
+ if (activeTabId === null) {
38
+ activeTabId = tabId;
39
+ console.log("[bg] 自动设置 activeTabId =", tabId);
40
+ }
41
+ }
42
+ sendResponse({ ok: true });
43
+ return false; // 同步,不需要保持通道
44
+ }
45
+
46
+ // --- 来自 popup 的代理控制(native messaging)---
47
+ if (msg.type === "PROXY_START" || msg.type === "PROXY_STOP" || msg.type === "PROXY_STATUS") {
48
+ const cmd = msg.type === "PROXY_START" ? "start" :
49
+ msg.type === "PROXY_STOP" ? "stop" : "status";
50
+ sendNative({ cmd }).then(sendResponse);
51
+ return true; // 保持通道直到 sendResponse 被调用
52
+ }
53
+
54
+ // --- 来自 popup 的 xterm 状态查询(需要异步查 tab 标题)---
55
+ if (msg.type === "XTERM_STATUS") {
56
+ buildXtermStatus().then(sendResponse);
57
+ return true;
58
+ }
59
+
60
+ // --- 来自 popup 的切换激活 tab ---
61
+ if (msg.type === "XTERM_SELECT") {
62
+ const tabId = msg.tabId;
63
+ if (termReadyTabs.has(tabId)) {
64
+ activeTabId = tabId;
65
+ console.log("[bg] 切换 activeTabId =", tabId);
66
+ sendResponse({ ok: true, activeTabId: tabId });
67
+ } else {
68
+ sendResponse({ ok: false, msg: "该 tab 未捕获 xterm" });
69
+ }
70
+ return false;
71
+ }
72
+
73
+ // --- 来自 popup 的手动扫描 xterm ---
74
+ if (msg.type === "XTERM_SCAN") {
75
+ scanAllTabsForXterm().then((res) => {
76
+ if (activeTabId === null && res.found > 0) {
77
+ activeTabId = res.tabs[0].tabId;
78
+ console.log("[bg] 扫描后自动设置 activeTabId =", activeTabId);
79
+ }
80
+ sendResponse(res);
81
+ });
82
+ return true;
83
+ }
84
+
85
+ return false;
86
+ });
87
+
88
+ // 构建 xterm 状态响应(异步,因为要查 tab 标题)
89
+ async function buildXtermStatus() {
90
+ // 查当前浏览器激活的 tab(用户正在看的那个),用于在 popup 里标注"当前窗口"
91
+ let currentTabId = null;
92
+ try {
93
+ const [ct] = await chrome.tabs.query({ active: true, currentWindow: true });
94
+ if (ct) currentTabId = ct.id;
95
+ } catch {}
96
+
97
+ const tabsInfo = [];
98
+ for (const tabId of termReadyTabs) {
99
+ let title = String(tabId);
100
+ let url = "";
101
+ try {
102
+ const tab = await chrome.tabs.get(tabId);
103
+ title = tab.title || tab.url || String(tabId);
104
+ url = tab.url || "";
105
+ } catch {}
106
+ tabsInfo.push({
107
+ tabId,
108
+ title: title.slice(0, 40),
109
+ host: hostFromUrl(url),
110
+ active: tabId === activeTabId, // 桥接选中的 tab(命令发到这个)
111
+ isCurrent: tabId === currentTabId, // 用户当前正在看的浏览器 tab
112
+ });
113
+ }
114
+ return {
115
+ ok: true,
116
+ ready: termReadyTabs.size > 0,
117
+ tabCount: termReadyTabs.size,
118
+ activeTabId,
119
+ attachedCount: Object.keys(attached).filter(k => attached[k]).length,
120
+ tabs: tabsInfo
121
+ };
122
+ }
123
+
124
+ // 从 URL 提取 host(用于 tab 列表区分同名终端)
125
+ function hostFromUrl(url) {
126
+ try {
127
+ const u = new URL(url);
128
+ return u.hostname;
129
+ } catch {
130
+ return "";
131
+ }
132
+ }
133
+
134
+ // ============== Native Messaging(启动/停止代理)==============
135
+ const NATIVE_HOST = "com.wssniffer.host";
136
+
137
+ // 通过 native messaging 发消息给 host,返回 host 的响应
138
+ function sendNative(msg) {
139
+ return new Promise((resolve) => {
140
+ console.log("[bg] sendNative:", JSON.stringify(msg), "→ host:", NATIVE_HOST);
141
+ let port;
142
+ try {
143
+ port = chrome.runtime.connectNative(NATIVE_HOST);
144
+ console.log("[bg] connectNative 成功");
145
+ } catch (err) {
146
+ console.error("[bg] connectNative 抛异常:", err.message || err);
147
+ resolve({ ok: false, msg: "无法连接 native host: " + (err.message || err) +
148
+ "。请运行 native/install.sh 安装。" });
149
+ return;
150
+ }
151
+
152
+ let resolved = false;
153
+ const done = (res) => {
154
+ if (resolved) return;
155
+ resolved = true;
156
+ console.log("[bg] native 响应:", JSON.stringify(res));
157
+ try { port.disconnect(); } catch {}
158
+ resolve(res);
159
+ };
160
+
161
+ port.onMessage.addListener((response) => {
162
+ done(response);
163
+ });
164
+ port.onDisconnect.addListener(() => {
165
+ const err = chrome.runtime.lastError;
166
+ if (!resolved) {
167
+ console.error("[bg] native 断开, lastError:", err);
168
+ done({ ok: false, msg: err ? err.message : "native host 连接失败(可能未安装或权限不足)" });
169
+ }
170
+ });
171
+ setTimeout(() => done({ ok: false, msg: "native host 响应超时" }), 8000);
172
+
173
+ try {
174
+ port.postMessage(msg);
175
+ console.log("[bg] postMessage 已发送");
176
+ } catch (err) {
177
+ console.error("[bg] postMessage 失败:", err.message);
178
+ done({ ok: false, msg: "postMessage 失败: " + err.message });
179
+ }
180
+ });
181
+ }
182
+
183
+
184
+ // ============== Debugger attach / detach(原有)==============
185
+ function attachDebugger(tabId) {
186
+ if (attached[tabId]) return;
187
+ chrome.debugger.attach({ tabId }, '1.3', () => {
188
+ if (chrome.runtime.lastError) {
189
+ console.error('[WS] attach 失败:', chrome.runtime.lastError.message);
190
+ return;
191
+ }
192
+ attached[tabId] = true;
193
+ chrome.debugger.sendCommand({ tabId }, 'Network.enable', {}, () => {
194
+ console.log('[WS] 已 attach tab', tabId, '并开启 Network');
195
+ });
196
+ });
197
+ }
198
+
199
+ // ============== CDP 事件监听 ==============
200
+ // 抓 activeTabId 的 WebSocket 帧,发给代理做请求-响应配对。
201
+ // 这是 bridge 的核心数据来源——代理靠这些帧判断命令何时完成。
202
+ chrome.debugger.onEvent.addListener((source, method, params) => {
203
+ const tabId = source.tabId;
204
+ const isActive = tabId === activeTabId;
205
+
206
+ if (method === 'Network.webSocketCreated') {
207
+ if (isActive) {
208
+ console.log('[WS] 新连接:', params.url, '(active tab', tabId, ')');
209
+ sendToBridge({ type: 'ws-open', payload: { url: params.url, requestId: params.requestId } });
210
+ }
211
+ }
212
+ else if (method === 'Network.webSocketFrameReceived') {
213
+ // recv 帧是命令输出,只有 activeTabId 的才上送给代理配对
214
+ if (isActive) {
215
+ sendToBridge({
216
+ type: 'ws-recv',
217
+ payload: {
218
+ data: extractPayloadData(params.response),
219
+ opcode: params.response && params.response.opcode,
220
+ t: Date.now()
221
+ }
222
+ });
223
+ }
224
+ }
225
+ else if (method === 'Network.webSocketFrameSent') {
226
+ if (isActive) {
227
+ sendToBridge({
228
+ type: 'ws-send',
229
+ payload: {
230
+ data: extractPayloadData(params.response),
231
+ opcode: params.response && params.response.opcode,
232
+ t: Date.now()
233
+ }
234
+ });
235
+ }
236
+ }
237
+ else if (method === 'Network.webSocketClosed') {
238
+ if (isActive) sendToBridge({ type: 'ws-close', payload: { requestId: params.requestId } });
239
+ }
240
+ else if (method === 'Network.webSocketFrameError') {
241
+ console.error('[WS] 帧错误:', params.errorMessage);
242
+ }
243
+ });
244
+
245
+ // 提取 CDP frame 的 payloadData
246
+ // 文本帧(opcode=1)是 string;二进制帧(opcode=2)是 base64 string。代理按 opcode 解析。
247
+ function extractPayloadData(response) {
248
+ if (!response) return "";
249
+ return response.payloadData || "";
250
+ }
251
+
252
+ // ============== 本地代理连接 ==============
253
+ function connectBridge() {
254
+ if (bridgeWs && (bridgeWs.readyState === WebSocket.OPEN ||
255
+ bridgeWs.readyState === WebSocket.CONNECTING)) {
256
+ return;
257
+ }
258
+ try {
259
+ bridgeWs = new WebSocket(BRIDGE_WS);
260
+ } catch (err) {
261
+ console.warn("[bg] 无法连接代理:", err.message);
262
+ setTimeout(connectBridge, 2000);
263
+ return;
264
+ }
265
+
266
+ bridgeWs.addEventListener("open", () => {
267
+ bridgeConnected = true;
268
+ console.log("[bg] 已连上本地代理", BRIDGE_WS);
269
+ sendToBridge({ type: "hello", payload: { role: "extension" } });
270
+ });
271
+
272
+ bridgeWs.addEventListener("message", (event) => {
273
+ let frame;
274
+ try { frame = JSON.parse(event.data); } catch { return; }
275
+ if (!frame) return;
276
+ handleBridgeCommand(frame);
277
+ });
278
+
279
+ bridgeWs.addEventListener("close", () => {
280
+ bridgeConnected = false;
281
+ bridgeWs = null;
282
+ console.log("[bg] 代理连接断开,2s 后重连");
283
+ setTimeout(connectBridge, 2000);
284
+ });
285
+
286
+ bridgeWs.addEventListener("error", () => {});
287
+ }
288
+
289
+ // ============== 手动扫描 xterm(popup 触发)==============
290
+ // 遍历所有 tab 的所有 frame,发 term-ping 探测有没有 xterm。
291
+ // 找到就登记到 termReadyTabs 并 attach debugger,免去刷新页面的麻烦。
292
+ async function scanAllTabsForXterm() {
293
+ const tabs = await chrome.tabs.query({});
294
+ let found = 0;
295
+ const foundTabs = [];
296
+
297
+ for (const tab of tabs) {
298
+ if (!tab.id || !tab.url) continue;
299
+ // 跳过 chrome:// 等内部页面
300
+ if (!/^https?:/.test(tab.url)) continue;
301
+
302
+ // 列出该 tab 所有 frame
303
+ let frames;
304
+ try {
305
+ frames = await chrome.webNavigation.getAllFrames({ tabId: tab.id });
306
+ } catch { continue; }
307
+ if (!frames) continue;
308
+
309
+ // 对每个 frame 发 term-ping(必须指定 frameId 才能到 iframe)
310
+ for (const frame of frames) {
311
+ const res = await new Promise((resolve) => {
312
+ chrome.tabs.sendMessage(
313
+ tab.id,
314
+ { type: "term-ping" },
315
+ { frameId: frame.frameId },
316
+ (r) => {
317
+ if (chrome.runtime.lastError) { resolve(null); return; }
318
+ resolve(r);
319
+ }
320
+ );
321
+ });
322
+ if (res && res.ready) {
323
+ found++;
324
+ termReadyTabs.add(tab.id);
325
+ termReadyFrames.set(tab.id, frame.frameId); // 记下 frame,注入时直接用
326
+ foundTabs.push({ tabId: tab.id, href: res.href });
327
+ console.log("[bg] 扫描发现 xterm: tab", tab.id, "frame", frame.frameId, res.href);
328
+ // 自动 attach
329
+ attachDebugger(tab.id);
330
+ break; // 一个 tab 找到一个就够
331
+ }
332
+ }
333
+ }
334
+
335
+ return {
336
+ ok: true,
337
+ found,
338
+ tabs: foundTabs,
339
+ ready: termReadyTabs.size > 0,
340
+ tabCount: termReadyTabs.size
341
+ };
342
+ }
343
+
344
+ function sendToBridge(obj) {
345
+ if (bridgeWs && bridgeWs.readyState === WebSocket.OPEN) {
346
+ try { bridgeWs.send(JSON.stringify(obj)); } catch {}
347
+ }
348
+ }
349
+
350
+ // 处理代理下发的命令
351
+ function handleBridgeCommand(frame) {
352
+ // { type: "run-cmd", text, reqId }
353
+ // 代理已经把命令包成 `cmd; printf 哨兵\r`,我们只需把 text 注入 xterm
354
+ if (frame.type === "run-cmd") {
355
+ injectToXterm(frame.text, frame.reqId);
356
+ return;
357
+ }
358
+
359
+ // { type: "ping" }
360
+ if (frame.type === "ping") {
361
+ sendToBridge({ type: "pong", payload: { readyTabs: [...termReadyTabs] } });
362
+ return;
363
+ }
364
+ }
365
+
366
+ // 把命令注入到 activeTabId 的终端 frame。
367
+ // 多终端场景下只注入用户选中的 tab,避免串扰。
368
+ //
369
+ // frame 定位策略(从快到慢):
370
+ // 1. 优先用 termReadyFrames 里记下的 frameId(term-ready 上报时存的,最准)
371
+ // 2. 降级:逐 frame 发 term-ping 探测哪个有 xterm(覆盖 term-ready 没存上的情况,
372
+ // 比如 Arthas 页面是顶层文档不是 iframe,frameId=0)
373
+ async function injectToXterm(text, reqId) {
374
+ const tabId = activeTabId;
375
+
376
+ if (tabId === null) {
377
+ sendToBridge({
378
+ type: "inject-failed",
379
+ payload: { reqId, error: "no active tab selected; use '捕捉 xterm' in popup" }
380
+ });
381
+ return;
382
+ }
383
+
384
+ // 策略 1:用记下的 frameId
385
+ let targetFrameId = termReadyFrames.get(tabId);
386
+ let ok = false;
387
+
388
+ if (targetFrameId != null) {
389
+ ok = await sendToFrame(tabId, targetFrameId, text, reqId);
390
+ }
391
+
392
+ // 策略 2:降级探测。targetFrameId 没存上,或投递失败(frame 可能已刷新),
393
+ // 就遍历所有 frame 找有 xterm 的那个。
394
+ if (!ok) {
395
+ const frames = await getAllFramesSafe(tabId);
396
+ if (!frames) {
397
+ sendToBridge({
398
+ type: "inject-failed",
399
+ payload: { reqId, error: `cannot list frames for tab ${tabId}` }
400
+ });
401
+ return;
402
+ }
403
+
404
+ for (const frame of frames) {
405
+ const r = await sendToFrame(tabId, frame.frameId, text, reqId, true /*pingOnly*/);
406
+ if (r) {
407
+ targetFrameId = frame.frameId;
408
+ termReadyFrames.set(tabId, targetFrameId); // 记下来,下次直接用
409
+ ok = await sendToFrame(tabId, targetFrameId, text, reqId);
410
+ break;
411
+ }
412
+ }
413
+ }
414
+
415
+ if (ok) {
416
+ console.log("[bg] 注入成功 tab", tabId, "frame", targetFrameId, "reqId", reqId);
417
+ } else {
418
+ sendToBridge({
419
+ type: "inject-failed",
420
+ payload: { reqId, error: "no terminal frame with xterm in active tab" }
421
+ });
422
+ }
423
+ }
424
+
425
+ // 向指定 frame 发消息。pingOnly=true 时只探测不注入(用 term-ping 问有没有 xterm)。
426
+ // 返回:pingOnly 时返回 boolean(是否 ready);注入时返回 boolean(是否成功)。
427
+ function sendToFrame(tabId, frameId, text, reqId, pingOnly = false) {
428
+ const payload = pingOnly
429
+ ? { type: "term-ping" }
430
+ : { type: "term-write", text, reqId };
431
+ return new Promise((resolve) => {
432
+ try {
433
+ chrome.tabs.sendMessage(tabId, payload, { frameId }, (res) => {
434
+ if (chrome.runtime.lastError) { resolve(false); return; }
435
+ if (pingOnly) resolve(!!(res && res.ready));
436
+ else resolve(!!(res && res.ok));
437
+ });
438
+ } catch {
439
+ resolve(false);
440
+ }
441
+ });
442
+ }
443
+
444
+ // 包装 chrome.webNavigation.getAllFrames 成 Promise,失败/无权限返回 null
445
+ function getAllFramesSafe(tabId) {
446
+ return new Promise((resolve) => {
447
+ try {
448
+ chrome.webNavigation.getAllFrames({ tabId }, (frames) => {
449
+ if (chrome.runtime.lastError) {
450
+ resolve(null);
451
+ return;
452
+ }
453
+ resolve(frames || []);
454
+ });
455
+ } catch {
456
+ resolve(null);
457
+ }
458
+ });
459
+ }
460
+
461
+ // ============== 页面刷新/关闭处理 ==============
462
+ chrome.tabs.onUpdated.addListener((tabId, change) => {
463
+ // 刷新后 content script 重新加载,清掉旧的 ready 标记,等它重新上报
464
+ if (change.status === 'loading') {
465
+ termReadyTabs.delete(tabId);
466
+ termReadyFrames.delete(tabId);
467
+ // 刷新的是 activeTabId 就清空(重新上报后会自动重新选)
468
+ if (activeTabId === tabId) activeTabId = null;
469
+ }
470
+ // 页面刷新完成后,如果之前 attach 过这个 tab,重新 attach
471
+ // (CDP attach 在页面刷新后会失效)
472
+ if (change.status === 'complete' && attached[tabId]) {
473
+ delete attached[tabId];
474
+ setTimeout(() => attachDebugger(tabId), 500);
475
+ }
476
+ });
477
+
478
+ chrome.tabs.onRemoved.addListener((tabId) => {
479
+ delete attached[tabId];
480
+ termReadyTabs.delete(tabId);
481
+ termReadyFrames.delete(tabId);
482
+ // 关闭的是 activeTabId 就清空,让下次自动选或用户重选
483
+ if (activeTabId === tabId) {
484
+ activeTabId = null;
485
+ // 如果还有其他终端 tab,自动选一个
486
+ if (termReadyTabs.size > 0) {
487
+ activeTabId = [...termReadyTabs][0];
488
+ console.log("[bg] activeTabId 关闭,自动切换到", activeTabId);
489
+ }
490
+ }
491
+ });
492
+
493
+ // ============== Service Worker 保活(新增)==============
494
+ // MV3 SW 会被 Chrome 休眠。代理连接和 CDP 事件会唤醒它,但保险起见
495
+ // 用 alarms 周期性检查连接。
496
+ chrome.alarms.create("keepalive", { periodInMinutes: 0.5 });
497
+ chrome.alarms.onAlarm.addListener(() => {
498
+ if (!bridgeWs || bridgeWs.readyState === WebSocket.CLOSED) {
499
+ connectBridge();
500
+ }
501
+ });
502
+
503
+ // ============== 启动 ==============
504
+ connectBridge();
505
+ console.log('[WS Sniffer] background 已启动');
@@ -0,0 +1,134 @@
1
+ // WS Sniffer - content script (ISOLATED world).
2
+ //
3
+ // 注:manifest 用 matches: <all_urls> + all_frames:true + run_at:document_start
4
+ // 注入,是为了确保能进终端 frame(koko connect iframe / Arthas console 顶层文档等)。
5
+ //
6
+ // 时序问题:脚本在 document_start 运行,此时 .xterm 元素还没渲染出来
7
+ // (koko 的 xterm 在 iframe 加载后渲染,Arthas 的 xterm 要等 Vue 应用挂载)。
8
+ // 所以不能在 document_start 立即判断"是不是终端 frame"然后 return——
9
+ // 那样所有终端 frame 都会被错过。
10
+ //
11
+ // 正确做法:所有 frame 都注册一个轻量 MutationObserver 等 .xterm 出现;
12
+ // 出现了才真正激活(上报 ready + 注册消息监听)。.xterm 永远不出现的 frame,
13
+ // observer 只监听 documentElement 的 childList,开销可忽略。
14
+
15
+ (function () {
16
+ const TAG = "[ws-sniffer-cs]";
17
+
18
+ if (window.__wsSnifferContentLoaded) return;
19
+ window.__wsSnifferContentLoaded = true;
20
+
21
+ // ---------- xterm 定位 ----------
22
+ function findXtermTextarea() {
23
+ const xtermEl = document.querySelector(".xterm");
24
+ if (!xtermEl) return null;
25
+ return xtermEl.querySelector("textarea") || null;
26
+ }
27
+
28
+ // 终端就绪后只上报一次,让 background 知道这个 frame 可以接命令了
29
+ let readyReported = false;
30
+ function reportReady() {
31
+ if (readyReported) return;
32
+ readyReported = true;
33
+ try {
34
+ chrome.runtime.sendMessage({
35
+ type: "term-ready",
36
+ payload: { href: location.href, frame: "terminal" }
37
+ });
38
+ } catch {}
39
+ console.log(TAG, "terminal ready at", location.href);
40
+ }
41
+
42
+ // ---------- 等待 .xterm 出现,出现后才激活 ----------
43
+ // 激活前:只挂这个 observer,不注册 onMessage(避免非终端 frame 白跑监听)。
44
+ // 激活后:注销 observer,注册 onMessage,上报 ready。
45
+ let activated = false;
46
+ function tryActivate() {
47
+ if (activated) return;
48
+ if (!findXtermTextarea()) return; // .xterm 还没出现,或出现了但 textarea 还没有
49
+ activated = true;
50
+ if (mo) mo.disconnect();
51
+ console.log(TAG, "terminal frame 激活 at", location.href);
52
+ activateTerminal();
53
+ reportReady();
54
+ }
55
+
56
+ const mo = new MutationObserver(tryActivate);
57
+ mo.observe(document.documentElement, { childList: true, subtree: true });
58
+ // 立即检查一次(万一 .xterm 已经在 DOM 里了——比如脚本注入晚了)
59
+ tryActivate();
60
+
61
+ // ---------- term-ping 探测(始终响应,激活前也要能被 ping 到)----------
62
+ // background 的 scanAllTabs 和 injectToXterm 降级路径都靠 term-ping 找终端 frame。
63
+ // 所以这个监听器必须在脚本一加载就注册,不能等 .xterm 出现。
64
+ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
65
+ if (!msg || msg.type !== "term-ping") return;
66
+ sendResponse({
67
+ ok: true,
68
+ ready: !!findXtermTextarea(),
69
+ href: location.href
70
+ });
71
+ return true;
72
+ });
73
+
74
+ // ---------- 终端激活后才注册的逻辑 ----------
75
+ function activateTerminal() {
76
+ // 性能优化:批量注入而非逐字符。
77
+ // 之前逐字符 dispatch(每个字符一次 InputEvent),命令文本越长越慢。
78
+ // 现在:把整段文本一次性塞进 textarea.value,只派发一次 input 事件,
79
+ // xterm 会一次性处理整段文本(类似快速输入),大幅减少往返次数。
80
+ // 末尾的 \r 单独用 Enter keydown 触发执行。
81
+ function dispatchInput(text) {
82
+ const ta = findXtermTextarea();
83
+ if (!ta) return false;
84
+ ta.focus();
85
+
86
+ // 分离末尾的回车(\r 或 \n),单独处理
87
+ let body = text;
88
+ let trailingCr = "";
89
+ if (text.endsWith("\r")) { body = text.slice(0, -1); trailingCr = "\r"; }
90
+ else if (text.endsWith("\n")) { body = text.slice(0, -1); trailingCr = "\n"; }
91
+
92
+ // 批量注入命令文本(一次性)
93
+ if (body.length > 0) {
94
+ ta.value = body;
95
+ ta.dispatchEvent(new InputEvent("input", {
96
+ inputType: "insertText",
97
+ data: body,
98
+ bubbles: true,
99
+ cancelable: true
100
+ }));
101
+ }
102
+
103
+ // 末尾回车单独派发,触发命令执行
104
+ if (trailingCr) {
105
+ ta.value = "";
106
+ ta.dispatchEvent(new KeyboardEvent("keydown", {
107
+ key: "Enter",
108
+ code: "Enter",
109
+ keyCode: 13,
110
+ which: 13,
111
+ charCode: 0,
112
+ bubbles: true,
113
+ cancelable: true
114
+ }));
115
+ }
116
+ return true;
117
+ }
118
+
119
+ // ---------- 接收 background 的注入命令 ----------
120
+ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
121
+ if (!msg || msg.type !== "term-write") return;
122
+ // background 转发来的注入请求:{ type, text, reqId }
123
+ // 如果这个 frame 没有 xterm(比如终端还没加载好),明确返回 no-xterm
124
+ const ta = findXtermTextarea();
125
+ if (!ta) {
126
+ sendResponse({ ok: false, reason: "no-xterm", reqId: msg.reqId });
127
+ return true;
128
+ }
129
+ const ok = dispatchInput(msg.text || "");
130
+ sendResponse({ ok, reqId: msg.reqId });
131
+ return true;
132
+ });
133
+ } // end activateTerminal
134
+ })();
@@ -0,0 +1,24 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "JumpServer 终端桥接",
4
+ "version": "2.0.0",
5
+ "description": "桥接 JumpServer Web 终端,让 Agent 能远程执行命令并获取输出",
6
+ "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtsnqR6PcFUueZwYria79tVbstvjk+tM7PpvIXILm5xbd6bAdjDIhzg3lsnKioVfvxjfvT+s6vJsiOYa9ojVZyJMFc5m/05TYqr770ovYwQmz0e88fmiy6dUoSulbtKvBCSLbN6OOL7u+ul8ixLZ/HautxSmou/eNgAFPmhE+4UueE7wfCqcgMYvjLvEzlqTVumMW+5LKw9YsRk6WhHPghY1a3MVUn3eQOWXBtQTEUy3wBM3v4wHxLwDeinVOR4f/P87IlUNo84C5DeimoFit0qCj3K04hS8MIYCLCYZc3v9ftRJDJBkAoah6Eaqj7JajbS3KvLR1ctOYfDhubgmURQIDAQAB",
7
+ "permissions": ["debugger", "tabs", "alarms", "webNavigation", "nativeMessaging"],
8
+ "background": {
9
+ "service_worker": "background.js"
10
+ },
11
+ "action": {
12
+ "default_popup": "popup.html",
13
+ "default_title": "JumpServer 终端桥接"
14
+ },
15
+ "content_scripts": [
16
+ {
17
+ "matches": ["<all_urls>"],
18
+ "js": ["content.js"],
19
+ "run_at": "document_start",
20
+ "all_frames": true
21
+ }
22
+ ],
23
+ "host_permissions": ["<all_urls>"]
24
+ }