terminal-bridge-setup 2.9.0 → 2.9.1

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.
@@ -537,19 +537,32 @@ function cleanOutput(text, cmd) {
537
537
  .replace(/\r\n?/g, "\n");
538
538
 
539
539
  // 删除 prompt 行(命令回显行 = prompt + 命令文本,可能粘有 kitty 重绘碎片):
540
- // - 红帽系:[user@host /cwd]# —— 要求 ] 后紧跟 #$(不再允许中间隔任意内容)
541
- // - Ubuntu/Debian:user@host:~/path$ —— 要求出现在行首附近(≤40 字符前缀内,
542
- // 覆盖 kitty 碎片),且 prompt 段内无空格
540
+ // - 红帽系:[user@host /cwd]# —— user/host 段各限长 64(真实 prompt 远短于此)。
541
+ // 不限长时的实测反例(Case A):head -c 600 截断的 JSON 无闭合 ],
542
+ // "data":[ [ 落在前缀窗口内,[^\]]+ 跨过整段 JSON 匹配到 prompt 的 @…]
543
+ // → 647 字符合并行整行被误删。长度约束让这种跨吞匹配失败。
544
+ // - Ubuntu/Debian:user@host:~/path$ —— 同样限长
543
545
  // - Arthas:行尾 > 且行短(≤60 字符)
544
546
  // 前缀窗口 40 字符:正常 prompt 行 prompt 就在行首;碎片通常几到几十字符。
545
547
  out = out.split("\n").filter(line => {
546
- if (/^.{0,40}\[[^\]\n]+@[^\]\n]+\]\s*[#$]/.test(line)) return false;
547
- if (/^.{0,40}[\w.-]+@[\w.-]+:[^\s]*[$#]/.test(line)) return false;
548
+ if (/^.{0,40}\[[^\]\n]{1,64}@[^\]\n]{1,64}\]\s*[#$]/.test(line)) return false;
549
+ if (/^.{0,40}[\w.-]{1,64}@[\w.-]{1,64}:[^\s]{0,128}[$#]/.test(line)) return false;
548
550
  // Arthas / 通用 REPL prompt:行尾 > (允许前面有 arthas@xxx 等前缀)
549
551
  if (/>\s*$/.test(line) && line.trim().length <= 60) return false;
550
552
  return true;
551
553
  }).join("\n");
552
554
 
555
+ // 行尾 prompt 后缀剥离:无尾换行的输出(head -c N 截断)会与后续 prompt
556
+ // 合并成同一物理行。上面的整行过滤(带前缀窗口)会放过这种合并行——内容
557
+ // 保住了,但 prompt 碎片粘在输出尾巴上(如 JSON 尾 + [root@host dir]#),
558
+ // 污染 Agent 的后续解析。这里只剥离行尾的 prompt 形态后缀,内容原样保留。
559
+ out = out.split("\n").map(line => {
560
+ if (line.length <= 100) return line; // 短行已由整行过滤处理,避免误伤
561
+ return line
562
+ .replace(/\[[^\]\n]{1,64}@[^\]\n]{1,64}\]\s*[#$]\s*$/, "") // 红帽系后缀
563
+ .replace(/[\w.-]{1,64}@[\w.-]{1,64}:[^\s]{0,128}[$#]\s*$/, ""); // Ubuntu 后缀
564
+ }).join("\n");
565
+
553
566
  // 删除 koko 控制消息:koko 偶尔在终端流里发送 JSON 控制消息
554
567
  // (TERMINAL_RESIZE、PING 等),特征是以 {"id": 开头的 JSON 行(可能前面带 # )
555
568
  out = out.split("\n").filter(line => {
@@ -593,26 +606,35 @@ function cleanOutput(text, cmd) {
593
606
  }
594
607
 
595
608
  // ===================== 输出兜底(问题 1 的安全网)=====================
596
- // 清理后为空、但原始 WS 帧里有实际内容时,绝不能返回 ok:true + 空输出——
597
- // 那会让 Agent 误判"命令无输出/文件为空"。这里返回去 ANSI 的原始内容
598
- // (截断到 4KB)+ raw 字节数提示,把"被过滤"的事实显式暴露给调用方。
609
+ // 两层防御:
610
+ // 1. 清理后为空 + 原始内容 ≥512 字符 → 返回截断原始内容 + warning
611
+ // 2. 清理损失率 >80%(原始 ≥512 字符但清理后 <20%)→ 保留清理结果,
612
+ // 但附加 warning + 原始内容截断——任何未来的 filter 误杀都不再静默
613
+ // (Case A 教训:REDHAT 正则曾跨吞 647 字节合并行,靠这层兜底可暴露)
599
614
  const RAW_FALLBACK_THRESHOLD = 512; // 原始内容低于此值视为真无输出
600
615
  const RAW_FALLBACK_LIMIT = 4000; // 兜底输出截断长度
601
616
 
602
617
  function finalizeOutput(entry) {
603
- const cleaned = cleanOutput(entry.buffer, entry.echoCmd != null ? entry.echoCmd : entry.cmd);
604
- if (cleaned.length > 0) return cleaned;
618
+ const echoCmd = entry.echoCmd != null ? entry.echoCmd : entry.cmd;
619
+ const cleaned = cleanOutput(entry.buffer, echoCmd);
605
620
 
606
621
  const raw = stripAnsi(entry.buffer)
607
622
  .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, " ")
608
623
  .replace(/\r\n?/g, "\n")
609
624
  .trim();
610
- if (raw.length < RAW_FALLBACK_THRESHOLD) return "";
625
+ if (raw.length < RAW_FALLBACK_THRESHOLD) return cleaned;
626
+
627
+ const truncated = raw.length > RAW_FALLBACK_LIMIT
628
+ ? raw.slice(0, RAW_FALLBACK_LIMIT) + "\n...[truncated]"
629
+ : raw;
611
630
 
612
- const note = `[warning] output filtered to empty by cleaner; raw ${raw.length} chars shown (truncated)]`;
613
- return raw.length > RAW_FALLBACK_LIMIT
614
- ? raw.slice(0, RAW_FALLBACK_LIMIT) + "\n" + note
615
- : raw + "\n" + note;
631
+ if (cleaned.length === 0) {
632
+ return truncated + `\n[warning] output filtered to empty by cleaner; raw ${raw.length} chars shown]`;
633
+ }
634
+ if (cleaned.length < raw.length * 0.2) {
635
+ return cleaned + `\n[warning] cleaner dropped ${(100 - Math.round(cleaned.length / raw.length * 100))}% of output; raw tail:\n${truncated}`;
636
+ }
637
+ return cleaned;
616
638
  }
617
639
 
618
640
  // ===================== WebSocket 服务 =====================
@@ -63,9 +63,12 @@
63
63
  }
64
64
  ```
65
65
  - `output`:prompt 锚点出现前的所有 recv 帧拼接,已去 ANSI、`\r\n`→`\n`、
66
- 控制字符(NUL 等)替换为空格、删 prompt 行和命令回显行(折行感知)、首尾 trim。
67
- **空输出兜底**:若清理后为空但原始帧有内容(≥512 字符),返回截断的原始内容
68
- 并附 `[warning] output filtered to empty...` 提示——绝不返回 ok:true + 空输出。
66
+ 控制字符(NUL 等)替换为空格、删 prompt 行和命令回显行(折行感知)、
67
+ 剥离行尾 prompt 后缀(无尾换行输出与 prompt 合并形态)、首尾 trim。
68
+ prompt 段识别带长度约束(user/host ≤64 字符),防止截断 JSON `[`
69
+ 被当作 prompt 起点跨吞整行(Case A 根因)。
70
+ **双层兜底**:清理后为空或清理损失率 >80%(原始 ≥512 字符)时,附加
71
+ `[warning]` + 截断原始内容——绝不静默丢弃大段输出。
69
72
  - `unterminated-quote`:命令含未闭合引号,远端 shell 卡在 PS2 续行(`>` 提示)。
70
73
  代理检测到后 ~400ms 快速失败并自动 Ctrl+C 退出续行(终端可继续用),
71
74
  `suggest` 会指向 base64 通道。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-bridge-setup",
3
- "version": "2.9.0",
3
+ "version": "2.9.1",
4
4
  "description": "一次性安装器:释放终端桥接(JumpServer / Arthas)的本地代理 + Chrome 插件,并注册 native messaging host。让 Agent 能通过浏览器 xterm 终端执行命令并拿回输出。",
5
5
  "license": "MIT",
6
6
  "author": "encorearon",