terminal-bridge-setup 3.3.4 → 3.3.6

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.
@@ -846,6 +846,19 @@ async function yrSqlSetViaCDP(tabId, sql, reqId, skipNew = false) {
846
846
  await new Promise(resolve => attachDebugger(tabId) ?? resolve());
847
847
  await new Promise(r => setTimeout(r, 400)); // 等 Network.enable 完成
848
848
  }
849
+ // 把 Yearning 标签页带到前台:后台标签页的 Vue/monaco 渲染被 rAF 节流,
850
+ // 新建查询 tab 点了也不渲染 → SQL 会被追加到已有编辑器后面(严重 bug,实测)
851
+ try {
852
+ const yrTab = await chrome.tabs.get(tabId);
853
+ if (!yrTab.active) {
854
+ await chrome.tabs.update(tabId, { active: true });
855
+ if (yrTab.windowId != null) await chrome.windows.update(yrTab.windowId, { focused: true });
856
+ await sleepMs(500); // 等页面从节流中恢复渲染
857
+ console.log("[bg] Yearning 标签页已带到前台(原为后台)");
858
+ }
859
+ } catch (err) {
860
+ console.warn("[bg] 激活 Yearning 标签页失败(继续注入):", err?.message);
861
+ }
849
862
  // 新建 SQL 窗口:避免把 SQL 注入用户正在使用的已有编辑器。
850
863
  // 编排模式(代理已显式建过 tab 并选好库)由 skipNew 跳过。
851
864
  if (!skipNew) {
@@ -853,6 +866,18 @@ async function yrSqlSetViaCDP(tabId, sql, reqId, skipNew = false) {
853
866
  if (!newWin || !newWin.ok) {
854
867
  console.warn("[bg] 新建 SQL 窗口失败(继续在当前编辑器注入):", newWin?.error);
855
868
  }
869
+ // 安全护栏:新建失败且现有编辑器非空时绝不注入——宁可失败也不能把 SQL
870
+ // 追加到用户已有语句后面(后台节流场景实测踩过)
871
+ const cur = await sendFrameMessage(tabId, { type: "yr-sql-get" }, 0);
872
+ if (cur?.ok && (cur.sql || "").trim().length > 0) {
873
+ sendToBridge({
874
+ type: "yr-result", reqId, tabId, ok: false,
875
+ error: "editor-not-empty",
876
+ message: "新建查询 tab 未生效且当前编辑器已有内容,为避免把 SQL 追加到你现有语句后面已中止。请切到 Yearning 页面确认后重试(或先清空/关闭已有 SQL tab)。",
877
+ editorText: (cur.sql || "").slice(0, 120),
878
+ });
879
+ return;
880
+ }
856
881
  }
857
882
  // 前置校验:数据库未选择时 Yearning 查询必报错,提前失败给明确提示
858
883
  const meta = await sendFrameMessage(tabId, { type: "yr-meta" }, 0);
@@ -777,9 +777,22 @@ function clickDropdownOption(target) {
777
777
  });
778
778
  // 用与编排一致的定位逻辑回看"到底选中了哪个节点"
779
779
  const trigEl = findDbTrigger();
780
+ // 编辑器现场取证:焦点在哪、每个 monaco 的可见性与内容长度
781
+ const ae = document.activeElement;
782
+ const editors = [...document.querySelectorAll(".monaco-editor")].map(m => {
783
+ const r = m.getBoundingClientRect();
784
+ return {
785
+ visible: m.offsetParent !== null && r.width > 0 && r.height > 0,
786
+ rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) },
787
+ textLen: (m.querySelector(".view-lines")?.textContent || "").length,
788
+ inputareaFocused: document.activeElement === m.querySelector("textarea.inputarea"),
789
+ };
790
+ });
780
791
  sendResponse({
781
792
  ok: true,
782
793
  hash: location.hash,
794
+ activeElement: ae ? { cls: String(ae.className || "").slice(0, 80), id: ae.id || "", tag: ae.tagName } : null,
795
+ editors,
783
796
  selects: [...document.querySelectorAll(".ant-select")].map(selectDump),
784
797
  triggerLocated: trigEl ? selectDump(trigEl) : null,
785
798
  metaNow: readYearningMeta(),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "JumpServer 终端桥接",
4
- "version": "3.3.4",
4
+ "version": "3.3.6",
5
5
  "description": "终端桥接:JumpServer / Arthas / Yearning,让 Agent 能通过浏览器执行命令、收结果与查数据",
6
6
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtsnqR6PcFUueZwYria79tVbstvjk+tM7PpvIXILm5xbd6bAdjDIhzg3lsnKioVfvxjfvT+s6vJsiOYa9ojVZyJMFc5m/05TYqr770ovYwQmz0e88fmiy6dUoSulbtKvBCSLbN6OOL7u+ul8ixLZ/HautxSmou/eNgAFPmhE+4UueE7wfCqcgMYvjLvEzlqTVumMW+5LKw9YsRk6WhHPghY1a3MVUn3eQOWXBtQTEUy3wBM3v4wHxLwDeinVOR4f/P87IlUNo84C5DeimoFit0qCj3K04hS8MIYCLCYZc3v9ftRJDJBkAoah6Eaqj7JajbS3KvLR1ctOYfDhubgmURQIDAQAB",
7
7
  "permissions": ["debugger", "tabs", "alarms", "webNavigation", "nativeMessaging", "scripting", "downloads", "storage"],
@@ -166,7 +166,12 @@ function feedYearningWaiters(payload) {
166
166
  try {
167
167
  obj = msgpackDecode(Buffer.from(data, "base64"));
168
168
  } catch { return; }
169
- if (!obj || obj.results == null) return; // 心跳帧忽略
169
+ if (!obj) return;
170
+ // 两类有效帧:正常结果(results 非空)/ SQL 执行报错(error 非空,results 为空)。
171
+ // 错误帧不接住的话 waiter 只能干等超时,Agent 无从得知 SQL 写错了
172
+ const hasResults = obj.results != null;
173
+ const hasError = typeof obj.error === "string" && obj.error.trim().length > 0;
174
+ if (!hasResults && !hasError) return; // 心跳等噪音帧
170
175
 
171
176
  let consumed = false;
172
177
  if (yrRunWaiters.size > 0) {
@@ -181,8 +186,8 @@ function feedYearningWaiters(payload) {
181
186
  }
182
187
 
183
188
  // 未被 yr-run 消费的结果帧 = 手动查询(或 yr-run 已完成后的重复帧),
184
- // 也生成 CSV 记录。同一帧 yr-run 路径已经发过 export,不重复。
185
- if (!consumed) {
189
+ // 也生成 CSV 记录。错误帧没有数据,跳过导出。
190
+ if (!consumed && hasResults) {
186
191
  const rows = Array.isArray(obj.results)
187
192
  ? obj.results.map(t => (t && t.data ? t.data.length : 0)).reduce((a, b) => a + b, 0)
188
193
  : 0;
@@ -251,6 +256,18 @@ async function handleYrRun(ws, msg) {
251
256
  settled = true;
252
257
  yrRunWaiters.delete(runEntry);
253
258
  clearTimeout(timer);
259
+ // SQL 执行报错帧:把数据库错误原文带回给 Agent(不导 CSV,无数据可导)
260
+ const sqlError = typeof obj.error === "string" ? obj.error.trim() : "";
261
+ if (sqlError) {
262
+ console.log(TAG, `[yr-run ${reqId}] SQL 执行报错: ${sqlError.slice(0, 200)}`);
263
+ ws.send(JSON.stringify({
264
+ type: "result", reqId, ok: false, error: "sql-error",
265
+ message: "SQL 被数据库拒绝:" + sqlError,
266
+ output: JSON.stringify(obj),
267
+ elapsedMs: Date.now() - sentAt,
268
+ }));
269
+ return true;
270
+ }
254
271
  console.log(TAG, `[yr-run ${reqId}] 结果帧到达(query_time=${obj.query_time ?? "?"})`);
255
272
  // 同步发给插件:浏览器侧生成 CSV 落下载(popup 可见、可重新下载)
256
273
  sendToExtension({
@@ -602,6 +619,25 @@ function detectTerminalType(text) {
602
619
  }
603
620
 
604
621
  // 插件上报的 ws-recv 帧
622
+ // OrcaTerm(腾讯云 TKE 容器终端等)用 JSON 文本帧包裹终端流:
623
+ // {"type":"data","data":"<带 ANSI 的真实终端明文>","sessionId":"..."}
624
+ // 且一帧内可能拼接多个 JSON 对象。解包取 data 字段;其他类型帧(心跳等)丢弃。
625
+ function extractOrcaTermData(text) {
626
+ if (typeof text !== "string" || text.indexOf('{"type"') === -1) return text;
627
+ let out = "";
628
+ let buf = "";
629
+ for (const ch of text) {
630
+ buf += ch;
631
+ if (ch !== "}") continue;
632
+ try {
633
+ const obj = JSON.parse(buf);
634
+ buf = "";
635
+ if (obj && obj.type === "data" && typeof obj.data === "string") out += obj.data;
636
+ } catch { /* 还没凑齐一个完整 JSON,继续累积 */ }
637
+ }
638
+ return out;
639
+ }
640
+
605
641
  function handleWsRecv(payload) {
606
642
  const data = payload && payload.data;
607
643
  const opcode = payload && payload.opcode;
@@ -630,6 +666,9 @@ function handleWsRecv(payload) {
630
666
  text = typeof data === "string" ? data : String(data);
631
667
  }
632
668
 
669
+ // OrcaTerm JSON 包裹帧解包(koko 无此包裹,indexOf 快速跳过零开销)
670
+ text = extractOrcaTermData(text);
671
+
633
672
  // 给当前 pending(同时只会有一个)喂帧,按 prompt 锚点状态机处理
634
673
  for (const [reqId, entry] of pending) {
635
674
  entry.buffer += text;
@@ -1045,7 +1084,10 @@ wss.on("connection", (ws, req) => {
1045
1084
  // 防止 tap 页面(Yearning JSON 帧)污染终端命令的输出配对。
1046
1085
  broadcastTap(msg.payload);
1047
1086
  const frameUrl = (msg.payload && msg.payload.url) || "";
1048
- const isTerminalUrl = !frameUrl || /\/koko\/ws|connectArthas/i.test(frameUrl);
1087
+ // 终端白名单:koko(JumpServer)/ Arthas / 腾讯云 OrcaTerm(TKE 容器终端,
1088
+ // JSON 文本帧由 extractOrcaTermData 解包)。空 URL 也接受(attach 前建立的
1089
+ // 连接拿不到 webSocketCreated,帧没有 url)
1090
+ const isTerminalUrl = !frameUrl || /\/koko\/ws|connectArthas|orcaterm/i.test(frameUrl);
1049
1091
  if (isTerminalUrl) handleWsRecv(msg.payload);
1050
1092
  return;
1051
1093
  }
@@ -334,6 +334,8 @@ Agent → 代理:
334
334
  | 现象 | 原因 | 处理 |
335
335
  |------|------|------|
336
336
  | `error: extension not connected` | 代理没收到插件 hello | 检查插件是否启用、service worker 日志、代理是否在跑 |
337
+ | `error: write-forbidden` | Yearning 只读白名单拦截(非 SELECT/SHOW/DESC/EXPLAIN) | 修正为只读语句重试;不要试图混淆绕过 |
338
+ | `error: sql-error` | **SQL 被数据库拒绝**(message 带原文,如 `Table 'xxx' doesn't exist`、权限不足) | 按 message 修正 SQL 后重试;秒回不是超时 |
337
339
  | `error: sudo-required` | 命令触发 sudo 密码提示(alias 劫持,仅 JumpServer) | 按"sudo 自动重试"流程问用户是否切 root,确认后切 root 重发原命令 |
338
340
  | `error: arthas-forbidden` | Arthas 高风险命令(retransform/profiler/stop/reset)被禁用 | 告知用户去浏览器 Arthas 终端手动执行,不要重试或绕过 |
339
341
  | `error: arthas-needs-limit` | 中风险命令(trace/watch/stack/monitor)缺 -n/#cost(严格模式) | 按 suggest 补参数重发,或加 `-n 1` |
@@ -50,6 +50,10 @@ WebSocket 结果帧。Yearning 结果帧是 opcode=2 二进制帧,解码后通
50
50
  SELECT/SHOW/DESC/EXPLAIN;命中 INTO OUTFILE/DUMPFILE/FOR UPDATE 等副作用子句
51
51
  同样拒绝。失败返回 `ok:false, error:"write-forbidden"`。yr-set 路径同受约束。
52
52
 
53
+ **SQL 执行报错回传**:数据库拒绝 SQL 时(表不存在/权限不足等),Yearning 的
54
+ 错误帧会被捕获并立即返回 `ok:false, error:"sql-error", message:"SQL 被数据库拒绝:<原文>"`,
55
+ 不会等到超时。Agent 应按 message 修正 SQL 重试。
56
+
53
57
  prepare 模式示例:
54
58
 
55
59
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-bridge-setup",
3
- "version": "3.3.4",
3
+ "version": "3.3.6",
4
4
  "description": "一次性安装器:释放终端桥接(JumpServer / Arthas / Yearning)的本地代理 + Chrome 插件,并注册 native messaging host。让 Agent 能通过浏览器 xterm 终端执行命令并拿回输出。",
5
5
  "license": "MIT",
6
6
  "author": "encorearon",