terminal-bridge-setup 3.2.0 → 3.3.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.
@@ -9,6 +9,8 @@
9
9
  // content script:在 koko connect iframe 里运行(见 manifest content_scripts)
10
10
 
11
11
  // ================= 状态 =================
12
+ // 版本显性化:service worker 每次启动打印,便于确认 Chrome 加载的是最新构建
13
+ console.log("[bg] Terminal Bridge v" + chrome.runtime.getManifest().version + " service worker 启动");
12
14
  const attached = {}; // 标记哪些 tab 当前已 attach debugger
13
15
  const BRIDGE_WS = "ws://127.0.0.1:8787/ssh";
14
16
  let bridgeWs = null;
@@ -376,7 +378,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
376
378
  }
377
379
  }
378
380
  else if (method === 'Network.webSocketFrameReceived') {
379
- // recv 帧:终端激活 tab 的进代理配对;tap tab 的进代理 tap 通道(都带 url)
381
+ // recv 帧(下行):终端激活 tab 的进代理配对;tap tab 的进代理 tap 通道(都带 url)
380
382
  sendToBridge({
381
383
  type: 'ws-recv',
382
384
  payload: {
@@ -384,11 +386,14 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
384
386
  opcode: params.response && params.response.opcode,
385
387
  url: wsUrls.get(params.requestId) || "",
386
388
  tabId,
389
+ dir: 'recv',
387
390
  t: Date.now()
388
391
  }
389
392
  });
390
393
  }
391
394
  else if (method === 'Network.webSocketFrameSent') {
395
+ // sent 帧(上行):终端 tab 走调试用 ws-send;tap 页面的上行帧(如 Yearning
396
+ // 点「查 询」发出的 msgpack 请求)也走 tap 通道转发,dir=sent 供探针观测
392
397
  if (isActive) {
393
398
  sendToBridge({
394
399
  type: 'ws-send',
@@ -397,6 +402,19 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
397
402
  opcode: params.response && params.response.opcode,
398
403
  url: wsUrls.get(params.requestId) || "",
399
404
  tabId,
405
+ dir: 'sent',
406
+ t: Date.now()
407
+ }
408
+ });
409
+ } else if (isTap) {
410
+ sendToBridge({
411
+ type: 'ws-recv',
412
+ payload: {
413
+ data: extractPayloadData(params.response),
414
+ opcode: params.response && params.response.opcode,
415
+ url: wsUrls.get(params.requestId) || "",
416
+ tabId,
417
+ dir: 'sent',
400
418
  t: Date.now()
401
419
  }
402
420
  });
@@ -559,10 +577,11 @@ function handleBridgeCommand(frame) {
559
577
  return;
560
578
  }
561
579
 
562
- // { type: "yr-cmd", sub: "ping"|"sql-set"|"query-click", sql, reqId }
580
+ // { type: "yr-cmd", sub, sql, reqId, tabId, skipNew }
563
581
  // Yearning 自动化:代理编排(yr-run),本插件转发到 tap tab 的 content script。
564
- // sql-set CDP Input.insertText(浏览器信任层级,monaco 等任何编辑器都接受);
565
- // 合成 paste/execCommand monaco 无效(isTrusted=false 被忽略,实测)。
582
+ // sub: ping | sql-set | query-click | new-sql | db-select | source-switch
583
+ // 编排模式下代理已显式建过查询 tab 时,sql-set skipNew=true 不再重复新建。
584
+ // 非 sql-set 子命令里 sql 字段承载目标参数(db-select 库名 / source-switch 源名)。
566
585
  if (frame.type === "yr-cmd") {
567
586
  const tabId = frame.tabId != null ? frame.tabId : activeTapTabId;
568
587
  if (tabId == null) {
@@ -570,11 +589,31 @@ function handleBridgeCommand(frame) {
570
589
  return;
571
590
  }
572
591
  if (frame.sub === "sql-set") {
573
- yrSqlSetViaCDP(tabId, frame.sql || "", frame.reqId);
592
+ yrSqlSetViaCDP(tabId, frame.sql || "", frame.reqId, !!frame.skipNew);
593
+ return;
594
+ }
595
+ // 选库/切源由 bg 用 CDP 真实鼠标事件编排(合成事件对 antd 无效),不走 content 转发
596
+ if (frame.sub === "db-select") {
597
+ yearningSelectDatabase(tabId, frame.sql || "", frame.reqId);
598
+ return;
599
+ }
600
+ if (frame.sub === "source-switch") {
601
+ yearningSwitchSource(tabId, frame.sql || "", frame.reqId);
602
+ return;
603
+ }
604
+ const yrSubPayloads = {
605
+ ping: { type: "yr-ping" },
606
+ "query-click": { type: "yr-query-click" },
607
+ "new-sql": { type: "yr-new-sql" },
608
+ "db-select": { type: "yr-db-select", database: frame.sql },
609
+ "source-switch": { type: "yr-source-switch", target: frame.sql },
610
+ "dom-probe": { type: "yr-dom-probe", selector: frame.sql },
611
+ };
612
+ const payload = yrSubPayloads[frame.sub];
613
+ if (!payload) {
614
+ sendToBridge({ type: "yr-result", reqId: frame.reqId, tabId, ok: false, error: "unknown yr-cmd sub: " + frame.sub });
574
615
  return;
575
616
  }
576
- const payload = frame.sub === "ping" ? { type: "yr-ping" }
577
- : { type: "yr-query-click" };
578
617
  chrome.tabs.sendMessage(tabId, payload, { frameId: 0 }, (res) => {
579
618
  if (chrome.runtime.lastError) {
580
619
  sendToBridge({ type: "yr-result", reqId: frame.reqId, tabId, ok: false, error: "content script 无响应: " + chrome.runtime.lastError.message });
@@ -599,18 +638,221 @@ function handleBridgeCommand(frame) {
599
638
  }
600
639
  }
601
640
 
641
+ // ===== Yearning 选库/切源:CDP 真实鼠标点击编排 =====
642
+ // antd Select / 弹层对 content script 的合成 mousedown 不响应(实测下拉展不开),
643
+ // 改用 CDP Input.dispatchMouseEvent 产生与真人一致的受信任点击:
644
+ // content script 只读定位元素坐标 / 枚举下拉、弹层选项;bg 负责按坐标真实点击。
645
+ const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
646
+
647
+ function replyYr(reqId, tabId, payload) {
648
+ sendToBridge({ type: "yr-result", reqId, tabId, ...payload });
649
+ }
650
+
651
+ async function cdpMouseClick(tabId, x, y) {
652
+ const opts = { x, y, button: "left", clickCount: 1 };
653
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchMouseEvent", { type: "mouseMoved", ...opts });
654
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchMouseEvent", { type: "mousePressed", ...opts });
655
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchMouseEvent", { type: "mouseReleased", ...opts });
656
+ }
657
+
658
+ async function cdpPressKey(tabId, key, code, keyCode) {
659
+ const base = { key, code, windowsVirtualKeyCode: keyCode, nativeVirtualKeyCode: keyCode };
660
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchKeyEvent", { type: "keyDown", ...base });
661
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchKeyEvent", { type: "keyUp", ...base });
662
+ }
663
+
664
+ async function ensureYearningAttach(tabId) {
665
+ if (!attached[tabId]) {
666
+ await new Promise(resolve => attachDebugger(tabId) ?? resolve());
667
+ await sleepMs(400); // 等 Network.enable 完成
668
+ }
669
+ }
670
+
671
+ // 选库:点开 Select 下拉 → 枚举选项 → 点目标 → 重读 meta 验证
672
+ async function yearningSelectDatabase(tabId, database, reqId) {
673
+ try {
674
+ await ensureYearningAttach(tabId);
675
+ const wanted = String(database || "").trim().replace(/\s+/g, "");
676
+ const loc = await sendFrameMessage(tabId, { type: "yr-locate", kind: "db-trigger" }, 0);
677
+ if (!loc?.ok || !loc.rect) {
678
+ replyYr(reqId, tabId, { ok: false, error: loc?.error || "库选择器未定位到" });
679
+ return;
680
+ }
681
+ // 分级展开尝试:①程序化 focus + ArrowDown(combobox 标准开法)
682
+ // ②真实鼠标点击(antd 本应响应点击)③Alt+Down
683
+ const stages = [];
684
+ const focusRes = await sendFrameMessage(tabId, { type: "yr-focus-db" }, 0);
685
+ stages.push(`focus:${focusRes?.focused ? "hit" : (focusRes?.ok ? "miss" : "fail")}`);
686
+ let opts = null;
687
+ const pollOpen = async (times = 16) => {
688
+ for (let i = 0; i < times; i++) {
689
+ await sleepMs(150);
690
+ opts = await sendFrameMessage(tabId, { type: "yr-options" }, 0);
691
+ if (opts?.ready) return true;
692
+ }
693
+ return false;
694
+ };
695
+
696
+ if (focusRes?.ok) {
697
+ stages.push("arrowdown");
698
+ await cdpPressKey(tabId, "ArrowDown", "ArrowDown", 40);
699
+ if (!(await pollOpen())) {
700
+ // 关了?重新聚焦再来一次方向键
701
+ await sendFrameMessage(tabId, { type: "yr-focus-db" }, 0);
702
+ await cdpPressKey(tabId, "ArrowDown", "ArrowDown", 40);
703
+ if (!(await pollOpen())) {
704
+ stages.pop(); stages.push("arrowdown×2");
705
+ }
706
+ }
707
+ }
708
+ if (!opts?.ready) {
709
+ stages.push("click");
710
+ await cdpMouseClick(tabId, loc.rect.x, loc.rect.y);
711
+ if (!(await pollOpen())) {
712
+ // 点击后补一发方向键(此时焦点多半已被点击带到位)
713
+ await cdpPressKey(tabId, "ArrowDown", "ArrowDown", 40);
714
+ if (!(await pollOpen())) stages[stages.length - 1] = "click+down";
715
+ }
716
+ }
717
+ if (!opts?.ready) {
718
+ stages.push("alt-arrowdown");
719
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchKeyEvent", {
720
+ type: "keyDown", key: "ArrowDown", code: "ArrowDown",
721
+ windowsVirtualKeyCode: 40, nativeVirtualKeyCode: 40, modifiers: 1,
722
+ });
723
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchKeyEvent", {
724
+ type: "keyUp", key: "ArrowDown", code: "ArrowDown",
725
+ windowsVirtualKeyCode: 40, nativeVirtualKeyCode: 40, modifiers: 1,
726
+ });
727
+ if (!(await pollOpen())) stages[stages.length - 1] = "alt-arrowdown×";
728
+ }
729
+ if (!opts?.ready) {
730
+ const activeEl = await sendFrameMessage(tabId, { type: "yr-active-element" }, 0);
731
+ replyYr(reqId, tabId, {
732
+ ok: false,
733
+ error: "全部展开方式均失败",
734
+ rect: loc.rect,
735
+ activeElement: activeEl || null,
736
+ listboxInDom: opts?.listboxInDom,
737
+ stages,
738
+ });
739
+ return;
740
+ }
741
+ const hit = opts.options.find(o => o.text.replace(/\s+/g, "") === wanted)
742
+ || opts.options.find(o => o.text.replace(/\s+/g, "").includes(wanted));
743
+ if (!hit) {
744
+ replyYr(reqId, tabId, { ok: false, error: "下拉中无匹配的库", options: opts.options.map(o => o.text).slice(0, 30) });
745
+ return;
746
+ }
747
+ if (hit.disabled) {
748
+ replyYr(reqId, tabId, { ok: false, error: "目标库为禁用状态(无权限)", target: database });
749
+ return;
750
+ }
751
+ // 点选项:content script 在元素上直接派发鼠标事件(无坐标漂移;
752
+ // React onClick 监听根节点,合成冒泡事件有效)
753
+ const clicked = await sendFrameMessage(tabId, { type: "yr-db-click-option", database: wanted }, 0);
754
+ if (!clicked?.ok) {
755
+ replyYr(reqId, tabId, { ok: false, error: clicked?.error || "点击选项失败", options: clicked?.options });
756
+ return;
757
+ }
758
+ for (let i = 0; i < 12; i++) {
759
+ await sleepMs(200);
760
+ const meta = await sendFrameMessage(tabId, { type: "yr-meta" }, 0);
761
+ if (meta?.ok && meta.database && meta.database.replace(/\s+/g, "") === wanted) {
762
+ replyYr(reqId, tabId, { ok: true, via: "cdp-mouse", database: meta.database });
763
+ return;
764
+ }
765
+ }
766
+ const finalMeta = await sendFrameMessage(tabId, { type: "yr-meta" }, 0);
767
+ replyYr(reqId, tabId, {
768
+ ok: false,
769
+ error: "已点击选项但验证失败:form 实际库名与目标不符",
770
+ expect: database,
771
+ actual: finalMeta?.database || "",
772
+ options: opts.options.map(o => o.text).slice(0, 30),
773
+ });
774
+ } catch (err) {
775
+ replyYr(reqId, tabId, { ok: false, error: "db-select 异常: " + err.message });
776
+ }
777
+ }
778
+
779
+ // 切源:点「切换数据源」入口 → 弹层叶子项匹配目标 → 点击后以 hash 变化验证
780
+ async function yearningSwitchSource(tabId, sourceName, reqId) {
781
+ try {
782
+ await ensureYearningAttach(tabId);
783
+ const wanted = String(sourceName || "").trim().replace(/\s+/g, "");
784
+ // 幂等:已在目标源上(hash 的 source 参数一致)直接返回成功,避免
785
+ // 重复点击后 hash 不变被误判为切换失败
786
+ const curHash0 = (await sendFrameMessage(tabId, { type: "yr-hash" }, 0))?.hash || "";
787
+ if (curHash0.includes("source=" + wanted)) {
788
+ replyYr(reqId, tabId, { ok: true, via: "already-on-source", hash: curHash0 });
789
+ return;
790
+ }
791
+ // 先 Escape 关掉可能残留的弹层(上次未命中的 modal 会挡住入口按钮坐标)
792
+ await cdpPressKey(tabId, "Escape", "Escape", 27);
793
+ await sleepMs(300);
794
+ const beforeHash = (await sendFrameMessage(tabId, { type: "yr-hash" }, 0))?.hash;
795
+ const loc = await sendFrameMessage(tabId, { type: "yr-locate", kind: "entry-button", arg: "切换数据源" }, 0);
796
+ if (!loc?.ok || !loc.rect) {
797
+ replyYr(reqId, tabId, { ok: false, error: "「切换数据源」入口按钮未找到" });
798
+ return;
799
+ }
800
+ await cdpMouseClick(tabId, loc.rect.x, loc.rect.y);
801
+ // 枚举弹层项(元素级,不依赖坐标可见性),命中后元素级派发点击
802
+ let srcItems = null;
803
+ let items = [];
804
+ for (let i = 0; i < 40; i++) {
805
+ await sleepMs(150);
806
+ srcItems = await sendFrameMessage(tabId, { type: "yr-source-items" }, 0);
807
+ items = srcItems?.items || [];
808
+ if (items.length > 0) break;
809
+ }
810
+ if (items.length === 0) {
811
+ await cdpPressKey(tabId, "Escape", "Escape", 27);
812
+ replyYr(reqId, tabId, {
813
+ ok: false,
814
+ error: "弹层未出现或无可选数据源项",
815
+ itemsDebug: srcItems?.debug || "no-response",
816
+ });
817
+ return;
818
+ }
819
+ const clicked = await sendFrameMessage(tabId, { type: "yr-source-click", target: wanted }, 0);
820
+ if (!clicked?.ok) {
821
+ await cdpPressKey(tabId, "Escape", "Escape", 27);
822
+ replyYr(reqId, tabId, { ok: false, error: clicked?.error || "点击数据源项失败", candidates: clicked?.candidates || items.slice(0, 30) });
823
+ return;
824
+ }
825
+ for (let i = 0; i < 24; i++) {
826
+ await sleepMs(250);
827
+ const nowHash = (await sendFrameMessage(tabId, { type: "yr-hash" }, 0))?.hash;
828
+ if (nowHash !== undefined && nowHash !== beforeHash) {
829
+ replyYr(reqId, tabId, { ok: true, via: "element-dispatch", clicked: clicked.clicked, hash: nowHash });
830
+ return;
831
+ }
832
+ }
833
+ const nowHash = (await sendFrameMessage(tabId, { type: "yr-hash" }, 0))?.hash;
834
+ await cdpPressKey(tabId, "Escape", "Escape", 27);
835
+ replyYr(reqId, tabId, { ok: false, error: "已点击候选但路由未变化(hash 不变)", clickedText: clicked.clicked, hash: nowHash });
836
+ } catch (err) {
837
+ replyYr(reqId, tabId, { ok: false, error: "source-switch 异常: " + err.message });
838
+ }
839
+ }
840
+
602
841
  // Yearning SQL 通过 CDP Input 注入(monaco 等编辑器接受浏览器信任层级事件)
603
- async function yrSqlSetViaCDP(tabId, sql, reqId) {
842
+ async function yrSqlSetViaCDP(tabId, sql, reqId, skipNew = false) {
604
843
  try {
605
844
  // 未 attach 的 tab 上 Input.* 命令会静默无效 → 注入校验失败,先确保 attach
606
845
  if (!attached[tabId]) {
607
846
  await new Promise(resolve => attachDebugger(tabId) ?? resolve());
608
847
  await new Promise(r => setTimeout(r, 400)); // 等 Network.enable 完成
609
848
  }
610
- // 新建 SQL 窗口:避免把 SQL 注入用户正在使用的已有编辑器
611
- const newWin = await sendFrameMessage(tabId, { type: "yr-new-sql" }, 0);
612
- if (!newWin || !newWin.ok) {
613
- console.warn("[bg] 新建 SQL 窗口失败(继续在当前编辑器注入):", newWin?.error);
849
+ // 新建 SQL 窗口:避免把 SQL 注入用户正在使用的已有编辑器。
850
+ // 编排模式(代理已显式建过 tab 并选好库)由 skipNew 跳过。
851
+ if (!skipNew) {
852
+ const newWin = await sendFrameMessage(tabId, { type: "yr-new-sql" }, 0);
853
+ if (!newWin || !newWin.ok) {
854
+ console.warn("[bg] 新建 SQL 窗口失败(继续在当前编辑器注入):", newWin?.error);
855
+ }
614
856
  }
615
857
  // 前置校验:数据库未选择时 Yearning 查询必报错,提前失败给明确提示
616
858
  const meta = await sendFrameMessage(tabId, { type: "yr-meta" }, 0);