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.
- package/files/extension/background.js +71 -17
- package/files/extension/content.js +4 -4
- package/files/extension/manifest.json +1 -1
- package/files/native/host.js +1 -1
- package/files/proxy/package.json +2 -2
- package/files/proxy/server.js +1 -1
- package/files/skill/SKILL.md +5 -5
- package/files/skill/references/protocol.md +1 -1
- package/package.json +1 -1
|
@@ -288,7 +288,9 @@ function connectBridge() {
|
|
|
288
288
|
|
|
289
289
|
// ============== 手动扫描 xterm(popup 触发)==============
|
|
290
290
|
// 遍历所有 tab 的所有 frame,发 term-ping 探测有没有 xterm。
|
|
291
|
-
//
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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);
|
|
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('[
|
|
559
|
+
console.log('[Terminal Bridge] background 已启动');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
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 = "[
|
|
16
|
+
const TAG = "[terminal-bridge-cs]";
|
|
17
17
|
|
|
18
|
-
if (window.
|
|
19
|
-
window.
|
|
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
|
},
|
package/files/native/host.js
CHANGED
package/files/proxy/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "terminal-bridge-proxy",
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "
|
|
6
|
+
"description": "Terminal Bridge 代理:Agent <-> Chrome 插件,请求-响应配对",
|
|
7
7
|
"main": "server.js",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"start": "node server.js",
|
package/files/proxy/server.js
CHANGED
|
@@ -120,7 +120,7 @@ function maybeRunNext() {
|
|
|
120
120
|
// kitty 终端逐字符注入会重绘,把任何标记字符串(BEGIN/END/哨兵)打散,
|
|
121
121
|
// 标记方案不可靠。改用 shell prompt 作为命令完成的锚点(expect/pexpect 经典做法)。
|
|
122
122
|
//
|
|
123
|
-
//
|
|
123
|
+
// 典型 shell prompt: [root@host /path]# 或 [user@host ~]$
|
|
124
124
|
// 关键:prompt 是服务端 shell 在命令完成后输出的,不受 kitty 输入重绘影响。
|
|
125
125
|
//
|
|
126
126
|
// 流程:
|
package/files/skill/SKILL.md
CHANGED
|
@@ -91,17 +91,17 @@ Arthas 的能力不止于读,以下命令/用法一律禁止通过桥接执行
|
|
|
91
91
|
lsof -i:8787 | grep LISTEN
|
|
92
92
|
|
|
93
93
|
# 2. 没跑就启动(也可从插件 popup 的"启动代理"按钮启动)
|
|
94
|
-
cd
|
|
94
|
+
cd ~/.terminal-bridge/proxy && node server.js &
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
浏览器侧(任选其一或都开):
|
|
98
|
-
- **JumpServer**:
|
|
99
|
-
- **Arthas**:
|
|
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/` 刷新
|
|
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
|
|
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@
|
|
132
|
+
| JumpServer (shell) | `[root@host /path]#` 或 `]$` | `]\s*[#$]\s*$` |
|
|
133
133
|
| Arthas | `arthas@pid>` 或 `[arthas@...]` | `>\s*$` |
|
|
134
134
|
|
|
135
135
|
注意:单独的 `>` 较宽(命令输出里 `>` 偶尔出现),但配合"行尾 + ANSI 清理后 + 注入命令后才出现"三个条件,误判率可接受。
|
package/package.json
CHANGED