terminal-bridge-setup 2.3.0 → 2.5.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.
@@ -288,7 +288,9 @@ function connectBridge() {
288
288
 
289
289
  // ============== 手动扫描 xterm(popup 触发)==============
290
290
  // 遍历所有 tab 的所有 frame,发 term-ping 探测有没有 xterm。
291
- // 找到就登记到 termReadyTabs attach debugger,免去刷新页面的麻烦。
291
+ // 关键:reload 插件后已打开的页面里没有 content script,ping 无人响应。
292
+ // 所以 ping 失败时用 chrome.scripting 主动注入 content.js,再 ping 一次。
293
+ // 这样用户 reload 插件后不用刷新页面,点「捕捉终端」就能自动恢复。
292
294
  async function scanAllTabsForXterm() {
293
295
  const tabs = await chrome.tabs.query({});
294
296
  let found = 0;
@@ -306,26 +308,25 @@ async function scanAllTabsForXterm() {
306
308
  } catch { continue; }
307
309
  if (!frames) continue;
308
310
 
309
- // 对每个 frame 发 term-ping(必须指定 frameId 才能到 iframe)
310
311
  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
- });
312
+ // 第一次 ping(content script 可能已经在跑)
313
+ let res = await pingFrame(tab.id, frame.frameId);
314
+
315
+ // ping 失败:content script 没注入(reload 插件后常见)→ 主动注入再 ping
316
+ if (!res) {
317
+ console.log("[bg] ping 失败,尝试注入 content.js 到 tab", tab.id, "frame", frame.frameId);
318
+ await injectContentScript(tab.id, frame.frameId);
319
+ // 注入后等一下让 IIFE 执行 + term-ping 监听器注册
320
+ await new Promise(r => setTimeout(r, 200));
321
+ res = await pingFrame(tab.id, frame.frameId);
322
+ }
323
+
322
324
  if (res && res.ready) {
323
325
  found++;
324
326
  termReadyTabs.add(tab.id);
325
- termReadyFrames.set(tab.id, frame.frameId); // 记下 frame,注入时直接用
327
+ termReadyFrames.set(tab.id, frame.frameId);
326
328
  foundTabs.push({ tabId: tab.id, href: res.href });
327
329
  console.log("[bg] 扫描发现 xterm: tab", tab.id, "frame", frame.frameId, res.href);
328
- // 自动 attach
329
330
  attachDebugger(tab.id);
330
331
  break; // 一个 tab 找到一个就够
331
332
  }
@@ -341,6 +342,39 @@ async function scanAllTabsForXterm() {
341
342
  };
342
343
  }
343
344
 
345
+ // 向指定 frame 发 term-ping
346
+ function pingFrame(tabId, frameId) {
347
+ return new Promise((resolve) => {
348
+ try {
349
+ chrome.tabs.sendMessage(
350
+ tabId,
351
+ { type: "term-ping" },
352
+ { frameId },
353
+ (r) => {
354
+ if (chrome.runtime.lastError) { resolve(null); return; }
355
+ resolve(r);
356
+ }
357
+ );
358
+ } catch { resolve(null); }
359
+ });
360
+ }
361
+
362
+ // 用 chrome.scripting 主动注入 content.js 到指定 frame
363
+ // 解决 reload 插件后已打开页面没有 content script 的问题
364
+ async function injectContentScript(tabId, frameId) {
365
+ try {
366
+ await chrome.scripting.executeScript({
367
+ target: { tabId, frameIds: [frameId] },
368
+ files: ["content.js"],
369
+ });
370
+ return true;
371
+ } catch (err) {
372
+ // 有些 frame(如 about:blank)注入会失败,忽略
373
+ console.log("[bg] 注入 content.js 失败 tab", tabId, "frame", frameId, ":", err.message);
374
+ return false;
375
+ }
376
+ }
377
+
344
378
  function sendToBridge(obj) {
345
379
  if (bridgeWs && bridgeWs.readyState === WebSocket.OPEN) {
346
380
  try { bridgeWs.send(JSON.stringify(obj)); } catch {}
@@ -401,15 +435,35 @@ async function injectToXterm(text, reqId) {
401
435
  return;
402
436
  }
403
437
 
438
+ // 第一轮:ping 现有 content script
404
439
  for (const frame of frames) {
405
440
  const r = await sendToFrame(tabId, frame.frameId, text, reqId, true /*pingOnly*/);
406
441
  if (r) {
407
442
  targetFrameId = frame.frameId;
408
- termReadyFrames.set(tabId, targetFrameId); // 记下来,下次直接用
443
+ termReadyFrames.set(tabId, targetFrameId);
409
444
  ok = await sendToFrame(tabId, targetFrameId, text, reqId);
410
445
  break;
411
446
  }
412
447
  }
448
+
449
+ // 第二轮:第一轮全失败说明 content script 没注入(reload 插件后常见)
450
+ // 主动注入 content.js 到每个 frame,再 ping + 注入
451
+ if (!ok) {
452
+ console.log("[bg] 降级探测全失败,尝试主动注入 content.js");
453
+ for (const frame of frames) {
454
+ await injectContentScript(tabId, frame.frameId);
455
+ }
456
+ await new Promise(r => setTimeout(r, 200)); // 等 IIFE 执行
457
+ for (const frame of frames) {
458
+ const r = await sendToFrame(tabId, frame.frameId, text, reqId, true /*pingOnly*/);
459
+ if (r) {
460
+ targetFrameId = frame.frameId;
461
+ termReadyFrames.set(tabId, targetFrameId);
462
+ ok = await sendToFrame(tabId, targetFrameId, text, reqId);
463
+ break;
464
+ }
465
+ }
466
+ }
413
467
  }
414
468
 
415
469
  if (ok) {
@@ -502,4 +556,4 @@ chrome.alarms.onAlarm.addListener(() => {
502
556
 
503
557
  // ============== 启动 ==============
504
558
  connectBridge();
505
- console.log('[WS Sniffer] background 已启动');
559
+ console.log('[Terminal Bridge] background 已启动');
@@ -1,4 +1,4 @@
1
- // WS Sniffer - content script (ISOLATED world).
1
+ // Terminal Bridge - content script (ISOLATED world).
2
2
  //
3
3
  // 注:manifest 用 matches: <all_urls> + all_frames:true + run_at:document_start
4
4
  // 注入,是为了确保能进终端 frame(koko connect iframe / Arthas console 顶层文档等)。
@@ -13,10 +13,10 @@
13
13
  // observer 只监听 documentElement 的 childList,开销可忽略。
14
14
 
15
15
  (function () {
16
- const TAG = "[ws-sniffer-cs]";
16
+ const TAG = "[terminal-bridge-cs]";
17
17
 
18
- if (window.__wsSnifferContentLoaded) return;
19
- window.__wsSnifferContentLoaded = true;
18
+ if (window.__terminalBridgeContentLoaded) return;
19
+ window.__terminalBridgeContentLoaded = true;
20
20
 
21
21
  // ---------- xterm 定位 ----------
22
22
  function findXtermTextarea() {
@@ -4,7 +4,7 @@
4
4
  "version": "2.0.0",
5
5
  "description": "桥接 JumpServer Web 终端,让 Agent 能远程执行命令并获取输出",
6
6
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtsnqR6PcFUueZwYria79tVbstvjk+tM7PpvIXILm5xbd6bAdjDIhzg3lsnKioVfvxjfvT+s6vJsiOYa9ojVZyJMFc5m/05TYqr770ovYwQmz0e88fmiy6dUoSulbtKvBCSLbN6OOL7u+ul8ixLZ/HautxSmou/eNgAFPmhE+4UueE7wfCqcgMYvjLvEzlqTVumMW+5LKw9YsRk6WhHPghY1a3MVUn3eQOWXBtQTEUy3wBM3v4wHxLwDeinVOR4f/P87IlUNo84C5DeimoFit0qCj3K04hS8MIYCLCYZc3v9ftRJDJBkAoah6Eaqj7JajbS3KvLR1ctOYfDhubgmURQIDAQAB",
7
- "permissions": ["debugger", "tabs", "alarms", "webNavigation", "nativeMessaging"],
7
+ "permissions": ["debugger", "tabs", "alarms", "webNavigation", "nativeMessaging", "scripting"],
8
8
  "background": {
9
9
  "service_worker": "background.js"
10
10
  },
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // WS Sniffer Native Messaging Host
2
+ // Terminal Bridge Native Messaging Host
3
3
  //
4
4
  // Chrome 通过 native messaging 和这个脚本通信(stdin/stdout,4 字节小端长度前缀)。
5
5
  // 职责:接收插件命令,启动/停止/查询 proxy/server.js。
@@ -1,9 +1,9 @@
1
1
  {
2
- "name": "ws-sniffer-proxy",
2
+ "name": "terminal-bridge-proxy",
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "type": "module",
6
- "description": "JumpServer xterm 桥接代理:Agent <-> Chrome 插件,请求-响应配对",
6
+ "description": "Terminal Bridge 代理:Agent <-> Chrome 插件,请求-响应配对",
7
7
  "main": "server.js",
8
8
  "scripts": {
9
9
  "start": "node server.js",
@@ -120,7 +120,7 @@ function maybeRunNext() {
120
120
  // kitty 终端逐字符注入会重绘,把任何标记字符串(BEGIN/END/哨兵)打散,
121
121
  // 标记方案不可靠。改用 shell prompt 作为命令完成的锚点(expect/pexpect 经典做法)。
122
122
  //
123
- // 你的 PS1=[\u@\h \w]\$ 渲染成 [root@k8s-master-test /home/operation]#
123
+ // 典型 shell prompt: [root@host /path]# [user@host ~]$
124
124
  // 关键:prompt 是服务端 shell 在命令完成后输出的,不受 kitty 输入重绘影响。
125
125
  //
126
126
  // 流程:
@@ -91,17 +91,17 @@ Arthas 的能力不止于读,以下命令/用法一律禁止通过桥接执行
91
91
  lsof -i:8787 | grep LISTEN
92
92
 
93
93
  # 2. 没跑就启动(也可从插件 popup 的"启动代理"按钮启动)
94
- cd ~/Code/testwork/ws-sniffer/proxy && node server.js &
94
+ cd ~/.terminal-bridge/proxy && node server.js &
95
95
  ```
96
96
 
97
97
  浏览器侧(任选其一或都开):
98
- - **JumpServer**:WS Sniffer 插件已加载 + JumpServer 终端页面已打开 + 已连上一个资产(终端可见、能敲字)
99
- - **Arthas**:WS Sniffer 插件已加载 + Arthas Console 页面已打开 + 已 Connect 上目标 JVM
98
+ - **JumpServer**:Terminal Bridge 插件已加载 + JumpServer 终端页面已打开 + 已连上一个资产(终端可见、能敲字)
99
+ - **Arthas**:Terminal Bridge 插件已加载 + Arthas Console 页面已打开 + 已 Connect 上目标 JVM
100
100
 
101
101
  多终端场景:如果同时开了 JumpServer 和 Arthas 两个 tab,代理命令只会发给 popup 里"当前选中"的那个 tab。切换用 popup 的 tab 选择器,或让用户在 popup 点选。
102
102
 
103
103
  如果用户说"命令没反应"或"inject-failed":
104
- 1. `chrome://extensions/` 刷新 WS Sniffer 插件 ↻
104
+ 1. `chrome://extensions/` 刷新 Terminal Bridge 插件 ↻
105
105
  2. 让用户在终端页面按 F5 刷新(让 content script 重新识别 xterm)
106
106
  3. 让用户在 popup 点"捕捉 xterm"按钮(手动扫描,免刷新)
107
107
  4. 确认终端 tab 顶部有黄色调试条(说明 CDP 已 attach)
@@ -113,7 +113,7 @@ cd ~/Code/testwork/ws-sniffer/proxy && node server.js &
113
113
  最简方式(用示例客户端):
114
114
 
115
115
  ```bash
116
- cd ~/Code/testwork/ws-sniffer/proxy
116
+ cd ~/.terminal-bridge/proxy
117
117
 
118
118
  # JumpServer 场景
119
119
  node client-example.mjs "uname -a"
@@ -129,7 +129,7 @@ const PROMPT_RE = /\]\s*[#$]\s*$|>\s*$/;
129
129
 
130
130
  | 终端 | prompt 样式 | 匹配部分 |
131
131
  |------|------------|---------|
132
- | JumpServer (shell) | `[root@k8s-master /home/op]#` 或 `]$` | `]\s*[#$]\s*$` |
132
+ | JumpServer (shell) | `[root@host /path]#` 或 `]$` | `]\s*[#$]\s*$` |
133
133
  | Arthas | `arthas@pid>` 或 `[arthas@...]` | `>\s*$` |
134
134
 
135
135
  注意:单独的 `>` 较宽(命令输出里 `>` 偶尔出现),但配合"行尾 + ANSI 清理后 + 注入命令后才出现"三个条件,误判率可接受。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-bridge-setup",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "一次性安装器:释放终端桥接(JumpServer / Arthas)的本地代理 + Chrome 插件,并注册 native messaging host。让 Agent 能通过浏览器 xterm 终端执行命令并拿回输出。",
5
5
  "license": "MIT",
6
6
  "author": "encorearon",