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.
- package/files/extension/background.js +254 -12
- package/files/extension/content-yearning.js +541 -5
- package/files/extension/manifest.json +2 -2
- package/files/extension/popup.html +2 -2
- package/files/extension/popup.js +3 -0
- package/files/proxy/server.js +133 -36
- package/files/proxy/yr-example.mjs +41 -5
- package/files/proxy/yr-sql-guard.js +43 -0
- package/files/skill/SKILL.md +73 -0
- package/files/skill/references/protocol.md +27 -0
- package/package.json +2 -2
|
@@ -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
|
|
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
|
|
580
|
+
// { type: "yr-cmd", sub, sql, reqId, tabId, skipNew }
|
|
563
581
|
// Yearning 自动化:代理编排(yr-run),本插件转发到 tap tab 的 content script。
|
|
564
|
-
// sql-set
|
|
565
|
-
//
|
|
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
|
-
|
|
612
|
-
if (!
|
|
613
|
-
|
|
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);
|