terminal-bridge-setup 2.6.1 → 2.8.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/bin/setup.mjs CHANGED
@@ -131,16 +131,19 @@ function installProxyDeps() {
131
131
  function registerNativeHost() {
132
132
  step(3, STEPS, "注册 Native Messaging Host");
133
133
 
134
- // macOS / Linux 路径不同
135
134
  const plat = platform();
135
+ if (plat === "win32") {
136
+ return registerNativeHostWindows();
137
+ }
138
+
139
+ // macOS / Linux 路径不同
136
140
  let destDir;
137
141
  if (plat === "darwin") {
138
142
  destDir = join(homedir(), "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts");
139
143
  } else if (plat === "linux") {
140
144
  destDir = join(homedir(), ".config", "google-chrome", "NativeMessagingHosts");
141
145
  } else {
142
- console.log(c.yellow("Windows 平台需要手动配置 native host,跳过自动注册"));
143
- console.log(c.dim(" 参考:https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging"));
146
+ console.log(c.yellow(`不支持的平台 ${plat},跳过 native host 注册`));
144
147
  return false;
145
148
  }
146
149
 
@@ -165,6 +168,55 @@ function registerNativeHost() {
165
168
  return true;
166
169
  }
167
170
 
171
+ // Windows 注册:生成 host.bat + manifest,写注册表 HKCU
172
+ // Chrome 在 Windows 上通过注册表找 native host:
173
+ // HKCU\Software\Google\Chrome\NativeMessagingHosts\com.wssniffer.host
174
+ // 默认值 = manifest json 的绝对路径
175
+ function registerNativeHostWindows() {
176
+ console.log(c.dim(" Windows 平台:写注册表方式注册"));
177
+
178
+ const nativeDir = join(INSTALL_DIR, "native");
179
+
180
+ // 1. 生成 host.bat(等价于 macOS 的 host.sh,用绝对路径调 node)
181
+ // where node 拿 node 绝对路径
182
+ const whereRes = spawnSync("where", ["node"], { encoding: "utf8", shell: true });
183
+ const nodeBin = whereRes.status === 0 ? whereRes.stdout.trim().split(/\r?\n/)[0] : "node";
184
+ const hostBat = join(nativeDir, "host.bat");
185
+ // bat 里路径可能含空格(C:\Program Files\nodejs\node.exe),必须加引号
186
+ writeFileSync(hostBat, `@echo off\r\n"${nodeBin}" "${join(nativeDir, "host.js")}"\r\n`);
187
+ ok(`生成 host.bat → ${hostBat}`);
188
+
189
+ // 2. 生成 manifest json(allowed_origins 同 macOS/Linux)
190
+ const manifest = {
191
+ name: "com.wssniffer.host",
192
+ description: "Terminal bridge native messaging host",
193
+ path: hostBat,
194
+ type: "stdio",
195
+ allowed_origins: [`chrome-extension://${EXTENSION_ID}/`],
196
+ };
197
+ const manifestPath = join(nativeDir, "com.wssniffer.host.json");
198
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
199
+ ok(`生成 manifest → ${manifestPath}`);
200
+
201
+ // 3. 写注册表 HKCU(用户级,不需要管理员权限)
202
+ const regKey = "HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\com.wssniffer.host";
203
+ const regRes = spawnSync("reg", ["add", regKey, "/ve", "/t", "REG_SZ", "/d", manifestPath, "/f"],
204
+ { stdio: "pipe", encoding: "utf8", shell: true });
205
+ if (regRes.status !== 0) {
206
+ fail(`注册表写入失败:${regRes.stderr || regRes.stdout}`);
207
+ }
208
+ ok(`已写注册表 ${regKey}`);
209
+ console.log(c.dim(` → ${manifestPath}`));
210
+
211
+ // 4. 校验注册表
212
+ const verifyRes = spawnSync("reg", ["query", regKey, "/ve"], { encoding: "utf8", shell: true });
213
+ if (verifyRes.status !== 0 || !verifyRes.stdout.includes(manifestPath)) {
214
+ fail("注册表校验失败");
215
+ }
216
+ ok("注册表校验通过");
217
+ return true;
218
+ }
219
+
168
220
  // ===================== 步骤 4:安装 skill =====================
169
221
  // 把 skill 释放到 ~/.agents/skills/jumpserver-term-bridge/
170
222
  // 这样 Agent(如 ZCode)能自动发现并触发,知道怎么调用桥接、怎么处理各种错误。
@@ -211,6 +263,7 @@ function guideLoadExtension() {
211
263
 
212
264
  // ① 打开 chrome://extensions
213
265
  // macOS: open -a 指定 Chrome;open URL scheme 更可靠
266
+ // Windows: start 命令走默认关联程序(chrome:// 会由默认浏览器处理)
214
267
  // Linux: 直接给 google-chrome 传 URL
215
268
  let chromeOpened = false;
216
269
  try {
@@ -221,6 +274,9 @@ function guideLoadExtension() {
221
274
  spawnSync("open", ["chrome://extensions"], { stdio: "ignore" });
222
275
  }
223
276
  chromeOpened = true;
277
+ } else if (plat === "win32") {
278
+ spawnSync("cmd", ["/c", "start", "", "chrome://extensions"], { stdio: "ignore", shell: true });
279
+ chromeOpened = true;
224
280
  } else if (plat === "linux") {
225
281
  // 试几个常见的 Chrome 命令名
226
282
  for (const bin of ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"]) {
@@ -239,6 +295,9 @@ function guideLoadExtension() {
239
295
  // open <dir> 会用 Finder 打开,并选中该文件夹
240
296
  spawnSync("open", [extDir], { stdio: "ignore" });
241
297
  ok(`已在 Finder 打开 ${extDir}`);
298
+ } else if (plat === "win32") {
299
+ spawnSync("explorer", [extDir], { stdio: "ignore" });
300
+ ok(`已在资源管理器打开 ${extDir}`);
242
301
  } else if (plat === "linux") {
243
302
  spawnSync("xdg-open", [extDir], { stdio: "ignore" });
244
303
  ok(`已在文件管理器打开 ${extDir}`);
@@ -301,7 +360,10 @@ async function main() {
301
360
  console.log(` ${c.cyan("•")} 两个绿灯亮,即可通过 Agent 发命令`);
302
361
  console.log("");
303
362
  console.log(c.dim(`文件位置:${INSTALL_DIR}`));
304
- console.log(c.dim(`卸载:rm -rf ${INSTALL_DIR} && rm ~/Library/Application\\ Support/Google/Chrome/NativeMessagingHosts/com.wssniffer.host.json`));
363
+ const uninstallHint = platform() === "win32"
364
+ ? `卸载:rd /s /q ${INSTALL_DIR} && reg delete HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\com.wssniffer.host /f`
365
+ : `卸载:rm -rf ${INSTALL_DIR} && rm ~/Library/Application\\ Support/Google/Chrome/NativeMessagingHosts/com.wssniffer.host.json`;
366
+ console.log(c.dim(uninstallHint));
305
367
  console.log("");
306
368
  }
307
369
 
@@ -59,15 +59,13 @@ function isProxyListening() {
59
59
 
60
60
  function readPid() {
61
61
  try {
62
- const pid = parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
63
- return isNaN(pid) ? null : pid;
62
+ const raw = fs.readFileSync(PID_FILE, "utf8").trim();
63
+ if (!/^\d{1,10}$/.test(raw)) return null; // 纯数字白名单
64
+ const pid = Number(raw);
65
+ return pid > 0 ? pid : null;
64
66
  } catch { return null; }
65
67
  }
66
68
 
67
- function writePid(pid) {
68
- try { fs.writeFileSync(PID_FILE, String(pid)); } catch {}
69
- }
70
-
71
69
  function clearPid() {
72
70
  try { fs.unlinkSync(PID_FILE); } catch {}
73
71
  }
@@ -79,21 +77,6 @@ function isPidAlive(pid) {
79
77
  } catch { return false; }
80
78
  }
81
79
 
82
- // 通过端口找占用进程的 PID(PID 文件缺失时的回退方案)
83
- // 用 lsof -ti :port 拿监听该端口的进程 PID
84
- function findPidByPort(port) {
85
- return new Promise((resolve) => {
86
- const child = spawn("lsof", ["-ti", `-i:${port}`, "-sTCP:LISTEN"], { stdio: ["ignore", "pipe", "ignore"] });
87
- let out = "";
88
- child.stdout.on("data", (d) => { out += d.toString(); });
89
- child.on("error", () => resolve(null)); // lsof 不存在或没权限
90
- child.on("close", () => {
91
- const pid = parseInt(out.trim().split("\n")[0], 10);
92
- resolve(isNaN(pid) ? null : pid);
93
- });
94
- });
95
- }
96
-
97
80
  // ============== 命令处理 ==============
98
81
  async function startProxy() {
99
82
  // 已在运行?
@@ -117,7 +100,8 @@ async function startProxy() {
117
100
  });
118
101
  // unref 让父进程(host)可以不等子进程就退出
119
102
  child.unref();
120
- writePid(child.pid);
103
+ // 先落一份 spawn 返回的数字 PID(非外部输入);代理 listening 后会自写权威值
104
+ try { fs.writeFileSync(PID_FILE, String(child.pid)); } catch {}
121
105
  fs.writeFileSync(logFd, `\n[host] proxy started PID=${child.pid} at ${new Date().toISOString()}\n`);
122
106
 
123
107
  // 等一小段时间确认端口起来
@@ -135,40 +119,32 @@ async function startProxy() {
135
119
 
136
120
  function stopProxy() {
137
121
  return new Promise(async (resolve) => {
138
- let pid = readPid();
139
- let pidSource = pid ? "pid file" : null;
140
-
141
- // PID 文件缺失 → 回退用 lsof 按端口找
122
+ const pid = readPid();
142
123
  if (!pid) {
124
+ // 没有PID 文件。代理启动时会自己写 PID(server.js listening 后落盘),
125
+ // 走到这里说明代理未通过正常途径启动——用端口状态兜底判断。
143
126
  const up = await isProxyListening();
144
- if (!up) {
145
- resolve({ ok: true, status: "stopped", msg: "proxy was not running" });
146
- return;
147
- }
148
- pid = await findPidByPort(PROXY_PORT);
149
- pidSource = pid ? "port lookup" : null;
150
- if (!pid) {
151
- resolve({ ok: false, status: "running", msg: "无法停止:代理在监听但找不到 PID(lsof 失败),请手动 kill" });
152
- return;
153
- }
127
+ resolve(up
128
+ ? { ok: false, status: "running", msg: "无法停止:PID 文件缺失,请手动停止占用 8787 端口的进程" }
129
+ : { ok: true, status: "stopped", msg: "proxy was not running" });
130
+ return;
154
131
  }
155
132
 
156
133
  try {
157
134
  process.kill(pid, "SIGTERM");
158
- // 等 2s 确认退出
135
+ // 等 2s 确认退出(Windows 上 SIGTERM 即强制终止)
159
136
  setTimeout(async () => {
160
137
  if (isPidAlive(pid)) {
161
138
  try { process.kill(pid, "SIGKILL"); } catch {}
162
139
  }
163
140
  clearPid();
164
- // 再确认端口确实释放了
165
141
  const stillUp = await isProxyListening();
166
142
  resolve({
167
143
  ok: !stillUp,
168
144
  status: stillUp ? "running" : "stopped",
169
145
  msg: stillUp
170
- ? `SIGTERM+SIGKILL 已发(PID ${pid} via ${pidSource})但端口仍被占用,可能有其他进程`
171
- : `proxy stopped (PID ${pid} via ${pidSource})`,
146
+ ? "已发停止信号但端口仍被占用,可能有其他进程"
147
+ : "proxy stopped",
172
148
  });
173
149
  }, 2000);
174
150
  } catch (err) {
@@ -13,6 +13,11 @@
13
13
 
14
14
  import { WebSocketServer } from "ws";
15
15
  import { randomBytes } from "node:crypto";
16
+ import { writeFileSync, unlinkSync } from "node:fs";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ // PID 文件:native host「停止代理」时读取。代理自己在 listening 后写入。
20
+ const PID_FILE = fileURLToPath(new URL("./.proxy.pid", import.meta.url));
16
21
  import {
17
22
  auditArthasCommand,
18
23
  isArthasCommand,
@@ -138,6 +143,7 @@ function maybeRunNext() {
138
143
  resolve: (result) => {
139
144
  if (pending.has(job.reqId)) {
140
145
  clearTimeout(entry.timer);
146
+ if (entry.weakTimer != null) { clearTimeout(entry.weakTimer); entry.weakTimer = null; }
141
147
  pending.delete(job.reqId);
142
148
  try {
143
149
  job.ws.send(JSON.stringify({ type: "result", reqId: job.reqId, ...result }));
@@ -198,7 +204,7 @@ let terminalType = "unknown"; // "unknown" | "jumpserver" | "arthas"
198
204
 
199
205
  // 各类型的 prompt 正则
200
206
  const PROMPT_RE_BY_TYPE = {
201
- // JumpServer shell prompt[user@host /dir]# 或 ]$
207
+ // JumpServer shell prompt(强匹配):[user@host /dir]# 或 ]$
202
208
  // 注意:不匹配裸 >,避免输出内容里的 > 行误触发
203
209
  jumpserver: /\]\s*[#$]\s*$/,
204
210
  // Arthas prompt:arthas@pid>(带 arthas@ 前缀,不匹配裸 >)
@@ -207,16 +213,38 @@ const PROMPT_RE_BY_TYPE = {
207
213
  unknown: /\]\s*[#$]\s*$|>\s*$/,
208
214
  };
209
215
 
216
+ // 弱 prompt(无方括号的 bash 默认 PS1):user@host:~/path$ 或 root@host:~#
217
+ // Ubuntu/Debian 系资产的 PS1 没有方括号,强正则永远不匹配 → 全部超时。
218
+ // 弱匹配特征:行尾是 $ 或 #,且行内含 user@host: 或 ~/ 特征。
219
+ // 防误判(输出行恰好以 #/$ 结尾):不立即判定,等 WEAK_QUIET_MS 无新数据才确认。
220
+ const WEAK_PROMPT_LINE_RE = /[$#]$/;
221
+ const WEAK_PROMPT_CONTEXT_RE = /@[\w.-]+:|~\//;
222
+ const WEAK_QUIET_MS = 350;
223
+
210
224
  // 旧名保留(cleanOutput 等处仍引用),指向宽松兜底,仅用于"删 prompt 行"的清理逻辑
211
225
  const PROMPT_RE = PROMPT_RE_BY_TYPE.unknown;
212
226
 
227
+ // ANSI 清理(CSI + OSC),prompt 匹配统一在清理后的文本上做
228
+ function stripAnsi(s) {
229
+ return s
230
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
231
+ .replace(/\x1b\][^\x07\x1b]*(\x07|\x1b\\)/g, "");
232
+ }
233
+
234
+ // 取 ANSI 清理后文本的最后一个非空行(去尾部空白),prompt 判定用
235
+ function lastNonEmptyLine(s) {
236
+ const lines = stripAnsi(s).split("\n").map(l => l.replace(/\s+$/, ""));
237
+ for (let i = lines.length - 1; i >= 0; i--) {
238
+ if (lines[i].length > 0) return lines[i];
239
+ }
240
+ return "";
241
+ }
242
+
213
243
  // 探测终端类型:扫描 ANSI 清理后的文本,按 prompt 特征判定
214
244
  // 支持切换:如果已锁定类型 A,但检测到明确的类型 B 特征,则切换到 B
215
245
  // (用户在 popup 切 tab 从 Arthas 切到 JumpServer 时,代理靠这条路径纠正)
216
246
  function detectTerminalType(text) {
217
- const clean = text
218
- .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
219
- .replace(/\x1b\][^\x07\x1b]*(\x07|\x1b\\)/g, "");
247
+ const clean = stripAnsi(text);
220
248
 
221
249
  // Arthas prompt 特征:arthas@<pid>> (出现在任意行尾)
222
250
  if (/arthas@\S+>\s*$/m.test(clean)) {
@@ -227,7 +255,7 @@ function detectTerminalType(text) {
227
255
  return;
228
256
  }
229
257
 
230
- // JumpServer shell prompt 特征:[user@host /dir]# 或 ]$
258
+ // JumpServer shell prompt 特征(红帽系):[user@host /dir]# 或 ]$
231
259
  if (/\[[^\]\n]+@[^\]\n]+\][^\n]*[#$]\s*$/m.test(clean)) {
232
260
  if (terminalType !== "jumpserver") {
233
261
  terminalType = "jumpserver";
@@ -235,6 +263,16 @@ function detectTerminalType(text) {
235
263
  }
236
264
  return;
237
265
  }
266
+
267
+ // JumpServer shell prompt 特征(Ubuntu/Debian 默认 PS1,无方括号):
268
+ // user@host:~/path$ 或 root@host:~#
269
+ if (/[\w.-]+@[\w.-]+:\S*[$#]\s*$/m.test(clean)) {
270
+ if (terminalType !== "jumpserver") {
271
+ terminalType = "jumpserver";
272
+ console.log(TAG, "[probe] 终端类型切换 → jumpserver(检测到 user@host:path$ prompt,无方括号)");
273
+ }
274
+ return;
275
+ }
238
276
  // 未命中任一特征,保持当前类型不变
239
277
  }
240
278
 
@@ -345,6 +383,37 @@ function handleWsRecv(payload) {
345
383
  elapsedMs: Date.now() - entry.sentAt
346
384
  });
347
385
  }
386
+ } else if (entry.phase === 0 && (terminalType === "jumpserver" || terminalType === "unknown")) {
387
+ // ====== 弱 prompt 兜底(Ubuntu/Debian 默认 PS1 无方括号)======
388
+ // 强正则要求 ] 前缀;user@host:~/path$ 这种 prompt 永远匹配不上 → 全部超时。
389
+ // 弱匹配:最后一个非空行以 $/# 结尾且行内含 @ 或 : 特征。
390
+ // 防误判:不立即判定,等 WEAK_QUIET_MS 无新数据再确认
391
+ // (输出行恰好以 #/$ 结尾时,后续输出到达会取消定时器)。
392
+ const lastLine = lastNonEmptyLine(entry.buffer);
393
+ const weakHit = WEAK_PROMPT_LINE_RE.test(lastLine) && WEAK_PROMPT_CONTEXT_RE.test(lastLine);
394
+ if (weakHit) {
395
+ if (entry.weakTimer == null) {
396
+ const snapLen = entry.buffer.length;
397
+ entry.weakTimer = setTimeout(() => {
398
+ entry.weakTimer = null;
399
+ if (!pending.has(reqId) || entry.buffer.length !== snapLen) return;
400
+ const lineNow = lastNonEmptyLine(entry.buffer);
401
+ if (WEAK_PROMPT_LINE_RE.test(lineNow) && WEAK_PROMPT_CONTEXT_RE.test(lineNow)) {
402
+ console.log(TAG, `[${reqId}] 弱 prompt 确认完成(${WEAK_QUIET_MS}ms 静默): ${lineNow.slice(-60)}`);
403
+ entry.lastScanLen = entry.buffer.length;
404
+ entry.resolve({
405
+ ok: true,
406
+ output: cleanOutput(entry.buffer, entry.cmd),
407
+ elapsedMs: Date.now() - entry.sentAt
408
+ });
409
+ }
410
+ }, WEAK_QUIET_MS);
411
+ }
412
+ } else if (entry.weakTimer != null) {
413
+ // 新数据不再是弱 prompt 形态,取消待确认的判定
414
+ clearTimeout(entry.weakTimer);
415
+ entry.weakTimer = null;
416
+ }
348
417
  }
349
418
  }
350
419
  }
@@ -393,11 +462,14 @@ function cleanOutput(text, cmd) {
393
462
  .replace(/\r\n?/g, "\n");
394
463
 
395
464
  // 删除 prompt 行:
396
- // - shell 风格:行里出现 [user@host /cwd]# 或 ]$ → 整行删
465
+ // - shell 风格(红帽系):行里出现 [user@host /cwd]# 或 ]$ → 整行删
397
466
  // (删 "[root@host ~]# echo hello" 这种命令回显行,含粘在前面的碎片)
467
+ // - shell 风格(Ubuntu/Debian,无方括号):user@host:~/path$ 结尾的行
398
468
  // - Arthas 风格:行尾是 arthas@xxx> 或单独的 > → 整行删
399
469
  out = out.split("\n").filter(line => {
400
470
  if (/\[[^\]\n]+@[^\]\n]+\][^\n]*[#$]/.test(line)) return false;
471
+ // Ubuntu prompt 行(含粘在命令回显前的情况)
472
+ if (/[\w.-]+@[\w.-]+:\S*[$#]\s*$/.test(line)) return false;
401
473
  // Arthas / 通用 REPL prompt:行尾 > (允许前面有 arthas@xxx 等前缀)
402
474
  if (/>\s*$/.test(line) && line.trim().length <= 60) return false;
403
475
  return true;
@@ -515,10 +587,19 @@ wss.on("listening", () => {
515
587
  const { address, port } = wss.address();
516
588
  console.log(TAG, `监听 ws://${address}:${port}/ssh`);
517
589
  console.log(TAG, "等待插件连接(role: extension)和 Agent 连接(发 run 请求)");
590
+
591
+ // 写 PID 文件:native host 的「停止代理」靠它找到本进程。
592
+ // 由代理自己写(而不是 host 代写)保证准确性——手动 node server.js
593
+ // 启动时也有 PID 文件可停。进程自己写自己的 process.pid,无注入面。
594
+ try {
595
+ writeFileSync(PID_FILE, String(process.pid));
596
+ console.log(TAG, `PID ${process.pid} → ${PID_FILE}`);
597
+ } catch {}
518
598
  });
519
599
 
520
600
  function shutdown() {
521
601
  console.error(TAG, "shutting down");
602
+ try { unlinkSync(PID_FILE); } catch {}
522
603
  wss.clients.forEach((c) => c.close(1001, "shutting down"));
523
604
  for (const [, entry] of pending) {
524
605
  clearTimeout(entry.timer);
@@ -78,8 +78,8 @@ Arthas 的能力不止于读,以下命令/用法一律禁止通过桥接执行
78
78
  | 页面形态 | koko connect **iframe**(嵌在 luna 父页里) | **顶层文档**(无 iframe) |
79
79
  | WebSocket URL | `wss://.../koko/ws/...` | `wss://.../ws?method=connectArthas...` |
80
80
  | WS 帧格式 | **二进制帧 opcode=2**,payload 是 base64,解码后是 SSH PTY 明文 | **文本帧 opcode=1**,payload 直接是明文 ANSI |
81
- | prompt 样式 | shell 风格 `[root@host /dir]#` | Arthas 风格 `arthas@pid>` 或 `[arthas@...]` |
82
- | prompt 锚点正则 | `/\]\s*[#$]\s*$/` | `/>/(行尾)` |
81
+ | prompt 样式 | 红帽系 `[root@host /dir]#`;Ubuntu 资产 `user@host:~/path$`(无方括号) | Arthas 风格 `arthas@pid>` 或 `[arthas@...]` |
82
+ | prompt 锚点 | 强匹配 `]\s*[#$]\s*$`;无方括号 prompt 走弱兜底(行尾 `$`/`#` + `user@host:` 特征,350ms 静默确认) | `arthas@\S+>\s*$` |
83
83
  | sudo 检测 | 需要(cat/df 等可能被 alias 成 sudo) | 不适用(Java 诊断工具无 sudo 概念) |
84
84
 
85
85
  **桥接层已自动处理这些差异**:content script 按是否有 `.xterm` 元素识别终端 frame(不依赖 URL),proxy 按 opcode 自动决定是否 base64 解码,prompt 锚点同时匹配两种风格。Agent 侧调用方式完全一致——都是发 `run` 帧、收 `result` 帧。
@@ -129,8 +129,11 @@ const PROMPT_RE = /\]\s*[#$]\s*$|>\s*$/;
129
129
 
130
130
  | 终端 | prompt 样式 | 匹配部分 |
131
131
  |------|------------|---------|
132
- | JumpServer (shell) | `[root@host /path]#` 或 `]$` | `]\s*[#$]\s*$` |
133
- | Arthas | `arthas@pid>` `[arthas@...]` | `>\s*$` |
132
+ | JumpServer (shell, 红帽系) | `[root@host /path]#` 或 `]$` | 强匹配 `]\s*[#$]\s*$`(命中即完成) |
133
+ | JumpServer (shell, Ubuntu/Debian) | `user@host:~/path$`(无方括号) | 弱匹配兜底:行尾 `$`/`#` + `user@host:`/`~/` 特征,需 350ms 静默确认 |
134
+ | Arthas | `arthas@pid>` 或 `[arthas@...]` | `arthas@\S+>\s*$` |
135
+
136
+ 说明:Ubuntu/Debian 默认 PS1 没有方括号,强正则永远不命中会导致所有命令超时(曾出现在 Windows 同事的 Ubuntu 资产上)。弱兜底以 350ms 无新数据为确认条件,避免输出行恰好以 `#`/`$` 结尾时误判提前完成。
134
137
 
135
138
  注意:单独的 `>` 较宽(命令输出里 `>` 偶尔出现),但配合"行尾 + ANSI 清理后 + 注入命令后才出现"三个条件,误判率可接受。
136
139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-bridge-setup",
3
- "version": "2.6.1",
3
+ "version": "2.8.0",
4
4
  "description": "一次性安装器:释放终端桥接(JumpServer / Arthas)的本地代理 + Chrome 插件,并注册 native messaging host。让 Agent 能通过浏览器 xterm 终端执行命令并拿回输出。",
5
5
  "license": "MIT",
6
6
  "author": "encorearon",