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.
@@ -8,6 +8,10 @@
8
8
  // yr-ping 探测编辑器类型和查询按钮,返回结构化信息
9
9
  // yr-sql-set {sql} 注入 SQL(按探测到的编辑器类型选策略)
10
10
  // yr-query-click 找「查询」按钮并点击
11
+ // yr-source-switch {target} 点「切换数据源」按钮并在弹层中选中目标数据源,
12
+ // 以 URL hash 变化做成功验证(切源会切页面路由)
13
+ // yr-db-select {database} 打开库选择器的 antd Select 下拉,点选目标 schema,
14
+ // 以重读 meta 的 database 值做成功验证
11
15
 
12
16
  (function () {
13
17
  const TAG = "[terminal-bridge-yr]";
@@ -145,8 +149,18 @@
145
149
  }
146
150
 
147
151
  // ---------- monaco 编辑器内容读取(注入验证用,无 API 时从 DOM 读)----------
152
+ // 多 SQL tab 并存时 DOM 里有多个 monaco 实例,inactive tab 的编辑器不渲染
153
+ // view-lines——必须优先取"可见"的那个,否则注入/读回都落在旧隐藏 tab 上
154
+ function activeMonaco() {
155
+ return [...document.querySelectorAll(".monaco-editor")]
156
+ .find(m => m.offsetParent !== null)
157
+ || document.querySelector(".monaco-editor");
158
+ }
159
+
148
160
  function readMonacoText() {
149
- const lines = [...document.querySelectorAll(".monaco-editor .view-lines .view-line")]
161
+ const m = activeMonaco();
162
+ if (!m) return "";
163
+ const lines = [...m.querySelectorAll(".view-lines .view-line")]
150
164
  .map(l => l.textContent || "");
151
165
  return lines.join("\n");
152
166
  }
@@ -239,7 +253,395 @@
239
253
  return { ok: true, via: exact.text };
240
254
  }
241
255
 
242
- // ---------- 消息处理 ----------
256
+ // ---------- 归一化文本 / 等待 ----------
257
+ function normText(el) {
258
+ return (el.textContent || "").replace(/\s+/g, "").trim();
259
+ }
260
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
261
+ function pressEscape() {
262
+ // antd Modal/Select 对 keydown Escape 响应关闭弹层(含 keyCode 兼容旧版本)
263
+ document.dispatchEvent(new KeyboardEvent("keydown", {
264
+ key: "Escape", keyCode: 27, which: 27, bubbles: true, cancelable: true
265
+ }));
266
+ }
267
+
268
+ // ---------- 切换数据源(点「切换数据源」→ 弹层选目标 → hash 变化验证)----------
269
+ async function switchDataSource(targetName) {
270
+ const wanted = String(targetName || "").trim();
271
+ if (!wanted) return { ok: false, error: "missing target source name" };
272
+ const wantNorm = wanted.replace(/\s+/g, "");
273
+
274
+ const btns = [...document.querySelectorAll("button")]
275
+ .filter(b => b.offsetParent !== null && !b.disabled);
276
+ const entry = btns.find(b => normText(b).includes("切换数据源"));
277
+ if (!entry) {
278
+ return {
279
+ ok: false,
280
+ error: "「切换数据源」入口按钮未找到",
281
+ buttons: btns.map(b => normText(b)).filter(t => t && t.length <= 12).slice(0, 25),
282
+ };
283
+ }
284
+
285
+ const hashBefore = location.hash;
286
+ entry.click();
287
+
288
+ // 弹层形态未知,宽扫所有常见 antd overlay 容器里的可点击元素。
289
+ // 只保留"叶子"命中(内部不含其他命中元素),避免点到整个外层容器导致误击。
290
+ const grabMatches = () => {
291
+ const scopes = [...document.querySelectorAll(
292
+ ".ant-modal,.ant-drawer,.ant-dropdown,.ant-popover,[class*=drawer],[class*=modal]"
293
+ )].filter(el => el.offsetParent !== null);
294
+ const hits = [];
295
+ for (const sc of scopes) {
296
+ sc.querySelectorAll("li,[role=menuitem],[role=option],[class*=item],a,button,td").forEach(el => {
297
+ if (el.offsetParent === null) return;
298
+ const t = normText(el);
299
+ if (!t || t.length > 48 || !t.includes(wantNorm)) return;
300
+ hits.push({ el, t });
301
+ });
302
+ }
303
+ return hits.filter(h => !hits.some(o => o !== h && h.el.contains(o.el)));
304
+ };
305
+
306
+ const deadlineHit = Date.now() + 3000;
307
+ let leaves = [];
308
+ while (Date.now() < deadlineHit) {
309
+ leaves = grabMatches();
310
+ if (leaves.length > 0) break;
311
+ await sleep(150);
312
+ }
313
+ if (leaves.length === 0) {
314
+ pressEscape();
315
+ return { ok: false, error: "弹层中未找到目标数据源项", searched: wanted };
316
+ }
317
+ if (leaves.length > 1) {
318
+ const exact = leaves.find(l => l.t === wantNorm);
319
+ if (exact) leaves = [exact];
320
+ }
321
+ if (leaves.length > 1) {
322
+ pressEscape();
323
+ return { ok: false, error: "目标数据源命中多个候选", candidates: leaves.map(l => l.t).slice(0, 10) };
324
+ }
325
+ leaves[0].el.click();
326
+
327
+ // 验证:切源会切换页面路由,等 URL hash 变化(~6s)
328
+ const deadlineVer = Date.now() + 6000;
329
+ while (Date.now() < deadlineVer) {
330
+ await sleep(250);
331
+ if (location.hash !== hashBefore) {
332
+ return { ok: true, via: "source-switch", hash: location.hash };
333
+ }
334
+ }
335
+ return {
336
+ ok: false,
337
+ error: "已点击候选但路由未变化(hash 不变),切源可能失败",
338
+ clickedText: leaves[0].t,
339
+ hash: location.hash,
340
+ };
341
+ }
342
+
343
+ // ---------- 选数据库 schema(antd Select 下拉点选 → 重读 meta 验证)----------
344
+ function closestAntdSelect(node) {
345
+ let cur = node;
346
+ for (let i = 0; cur && i < 8; i++) {
347
+ if (cur.classList && cur.classList.contains("ant-select")) return cur;
348
+ cur = cur.parentElement;
349
+ }
350
+ return null;
351
+ }
352
+
353
+ function selectDumpLite(el) {
354
+ const r = el.getBoundingClientRect();
355
+ return {
356
+ cls: String(el.className || "").slice(0, 80),
357
+ inForm: !!el.closest("form"),
358
+ rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) },
359
+ hasInput: !!el.querySelector("input"),
360
+ visible: el.offsetParent !== null && r.width > 0 && r.height > 0,
361
+ };
362
+ }
363
+
364
+ function findDbTrigger() {
365
+ // 枚举所有 .ant-select,优先"可见 + 在 form 内 + 带 input"(可搜索 combobox);
366
+ // 新建 tab 后旧 select 会被隐藏(rect 全 0),必须过滤,否则点在 (0,0) 上
367
+ const all = [...document.querySelectorAll(".ant-select")];
368
+ const visible = all.filter(s => {
369
+ const r = s.getBoundingClientRect();
370
+ return s.offsetParent !== null && r.width > 0 && r.height > 0;
371
+ });
372
+ const score = (s) => (s.closest("form") ? 2 : 0) + (s.querySelector("input") ? 1 : 0);
373
+ const byScore = [...visible].sort((a, b) => score(b) - score(a));
374
+ // 主路径:DATABASE_XPATH 锚点(仍要求可见,防隐藏残留)
375
+ const anchor = xpathNode(DATABASE_XPATH);
376
+ const anchored = anchor ? closestAntdSelect(anchor) : null;
377
+ if (anchored && visible.includes(anchored)) return anchored;
378
+ return byScore[0] || null;
379
+ }
380
+
381
+ function visibleDbDropdown() {
382
+ // 页面会残留多个 dropdown portal(旧 select 隐藏后 portal 仍在 DOM)。
383
+ // antd 新 portal 渲染在 body 末尾、层级最高——取"最后一个可见"的才是
384
+ // 刚打开的那个;取第一个会拿到坐标错位的旧 portal(实测点 dk_shard
385
+ // 落到 information_schema 上)。
386
+ const visible = [...document.querySelectorAll(".ant-select-dropdown")]
387
+ .filter(d => d.offsetParent !== null && !/dropdown-hidden/.test(d.className));
388
+ return visible[visible.length - 1] || null;
389
+ }
390
+
391
+ async function selectDatabase(targetName) {
392
+ const wanted = String(targetName || "").trim();
393
+ if (!wanted) return { ok: false, error: "missing database name" };
394
+ const wantNorm = wanted.replace(/\s+/g, "");
395
+ const trigger = findDbTrigger();
396
+ if (!trigger) return { ok: false, error: "库选择器未找到(antd Select)" };
397
+
398
+ // rc-select 打开依赖 mousedown;click 序列作为部分版本的兜底重试
399
+ const openSeqs = [
400
+ () => trigger.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true })),
401
+ () => {
402
+ trigger.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
403
+ trigger.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
404
+ trigger.click();
405
+ },
406
+ ];
407
+ const readOptions = () => {
408
+ const dd = visibleDbDropdown();
409
+ if (!dd) return null;
410
+ const nodes = [...dd.querySelectorAll(".ant-select-item-option, [role=option]")];
411
+ if (nodes.length === 0) return null;
412
+ return nodes.map(o => {
413
+ const text = o.getAttribute("title") || o.textContent || "";
414
+ return { el: o, text, norm: text.replace(/\s+/g, ""), disabled: /disabled/.test(o.className) };
415
+ }).filter(o => o.norm);
416
+ };
417
+
418
+ let opts = null;
419
+ for (const open of openSeqs) {
420
+ open();
421
+ const ddl = Date.now() + 1200;
422
+ while (Date.now() < ddl) {
423
+ await sleep(120);
424
+ opts = readOptions();
425
+ if (opts) break;
426
+ }
427
+ if (opts) break;
428
+ }
429
+ if (!opts) return { ok: false, error: "下拉未能展开(mousedown/click 后均无选项出现)" };
430
+
431
+ const hit = opts.find(o => o.norm === wantNorm) || opts.find(o => o.norm.includes(wantNorm));
432
+ if (!hit) {
433
+ pressEscape();
434
+ return { ok: false, error: "下拉中无匹配的库", expect: wanted, options: opts.map(o => o.text.trim()).slice(0, 30) };
435
+ }
436
+ if (hit.disabled) {
437
+ pressEscape();
438
+ return { ok: false, error: "目标库存在但为禁用状态(无权限)", target: wanted };
439
+ }
440
+ // antd option 以 mouseup/click 完成选中
441
+ hit.el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
442
+ hit.el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
443
+ hit.el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
444
+
445
+ // 验证:重读 meta,确认 form 上实际显示的库名已变为目标
446
+ const vdl = Date.now() + 2000;
447
+ while (Date.now() < vdl) {
448
+ await sleep(200);
449
+ const meta = readYearningMeta();
450
+ if (meta.database && meta.database.replace(/\s+/g, "") === wantNorm) {
451
+ return { ok: true, via: "antd-select", database: meta.database };
452
+ }
453
+ }
454
+ const finalMeta = readYearningMeta();
455
+ return {
456
+ ok: false,
457
+ error: "已点击选项但验证失败:form 实际显示库名与目标不符",
458
+ expect: wanted,
459
+ actual: finalMeta.database,
460
+ options: opts.map(o => o.text.trim()).slice(0, 30),
461
+ };
462
+ }
463
+
464
+ // ---------- 只读定位/枚举(供 background 用 CDP 真实鼠标事件编排)----------
465
+ // 经验教训:antd Select 对合成 mousedown 不响应,必须 CDP Input.dispatchMouseEvent
466
+ // 在坐标上产生受信任点击;本脚本只负责"把元素找到并给出中心坐标"、"枚举下拉/弹层项"。
467
+ function rectCenterOf(el) {
468
+ el.scrollIntoView({ block: "center" });
469
+ const r = el.getBoundingClientRect();
470
+ return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2) };
471
+ }
472
+
473
+ function locateTarget(kind, arg) {
474
+ if (kind === "db-trigger") {
475
+ const el = findDbTrigger();
476
+ return el ? rectCenterOf(el) : null;
477
+ }
478
+ if (kind === "entry-button") {
479
+ // arg=按钮归一化文本片段,取文本最短的可命中项(通常是真正的入口小按钮)
480
+ const btn = [...document.querySelectorAll("button")]
481
+ .filter(b => b.offsetParent !== null && !b.disabled && normText(b).includes(String(arg || "")))
482
+ .sort((a, b) => normText(a).length - normText(b).length)[0];
483
+ return btn ? rectCenterOf(btn) : null;
484
+ }
485
+ return null;
486
+ }
487
+
488
+ // 容器内可点元素取叶子节点(内部不含其他命中元素),避免坐标落在整块外层容器上误击
489
+ function collectLeafItems(scopes, maxItems) {
490
+ const hits = [];
491
+ for (const sc of scopes) {
492
+ sc.querySelectorAll("li,[role=menuitem],[role=option],[class*=item],a,button,td").forEach(el => {
493
+ if (el.offsetParent === null) return;
494
+ const t = normText(el);
495
+ if (!t || t.length > 48) return;
496
+ hits.push({ el, t });
497
+ });
498
+ }
499
+ return hits
500
+ .filter(h => !hits.some(o => o !== h && h.el.contains(o.el)))
501
+ .slice(0, maxItems || 40)
502
+ .map(h => ({ text: h.t, rect: rectCenterOf(h.el) }));
503
+ }
504
+
505
+ function listDropdownOptionsWithRects() {
506
+ const dd = visibleDbDropdown();
507
+ if (!dd) {
508
+ // 诊断随行:下拉未出现时带回焦点元素与 listbox 节点是否存在于 DOM
509
+ const ae = document.activeElement;
510
+ return {
511
+ ready: false,
512
+ activeEl: ae ? { cls: String(ae.className || "").slice(0, 80), id: ae.id || "", tag: ae.tagName } : null,
513
+ listboxInDom: !!document.querySelector("[role=listbox]"),
514
+ listboxOptions: document.querySelectorAll("[role=listbox] .ant-select-item-option").length,
515
+ };
516
+ }
517
+ const nodes = [...dd.querySelectorAll(".ant-select-item-option, [role=option]")];
518
+ if (nodes.length === 0) return { ready: false };
519
+ // rc-virtual-list 会为每个选项渲染一份隐藏"测量行"(零尺寸),
520
+ // 其坐标漂移会导致点错行——过滤零尺寸后按文本去重,只留真实可见行
521
+ const real = nodes
522
+ .map(o => {
523
+ const r = o.getBoundingClientRect();
524
+ const text = o.getAttribute("title") || o.textContent || "";
525
+ return {
526
+ text: text.trim(),
527
+ disabled: /disabled/.test(o.className),
528
+ x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height),
529
+ };
530
+ })
531
+ .filter(o => o.text && o.w > 0 && o.h > 0);
532
+ const byText = new Map();
533
+ for (const o of real) if (!byText.has(o.text)) byText.set(o.text, o);
534
+ const options = [...byText.values()].map(o => ({
535
+ text: o.text, disabled: o.disabled,
536
+ rect: { x: Math.round(o.x + o.w / 2), y: Math.round(o.y + o.h / 2) },
537
+ }));
538
+ return { ready: options.length > 0, options };
539
+ }
540
+
541
+ function listOverlayItems() {
542
+ const scopes = [...document.querySelectorAll(
543
+ ".ant-modal,.ant-drawer,.ant-dropdown,.ant-popover,[class*=drawer],[class*=modal]"
544
+ )].filter(el => el.offsetParent !== null);
545
+ return { items: collectLeafItems(scopes, 40) };
546
+ }
547
+
548
+ // ---------- 切源弹层(元素级枚举/点击,不依赖坐标)----------
549
+ // modal/portal 场景 offsetParent 判可见不可靠(fixed 祖先/动画态返回 null),
550
+ // 用 getClientRects() 判"已布局";点击直接在元素上派发事件(React 合成事件可达)
551
+ function laidOut(el) {
552
+ try {
553
+ const r = el.getBoundingClientRect();
554
+ return el.getClientRects().length > 0 && r.width > 1 && r.height > 1;
555
+ } catch { return false; }
556
+ }
557
+
558
+ function overlayScopes() {
559
+ return [...document.querySelectorAll(
560
+ ".ant-modal,.ant-drawer,.ant-dropdown,.ant-popover,[class*=drawer],[class*=modal]"
561
+ )].filter(laidOut);
562
+ }
563
+
564
+ function overlayLeafElements() {
565
+ // scope 互相嵌套(mask⊃wrap⊃modal),同一元素会被重复收集;
566
+ // 且 el.contains(自身)===true,重复对象会在叶子过滤时互相"包含"而全军覆没
567
+ // (实测 rawCount=255 全有效、叶子过滤后=0 的根因)——先按元素去重再过滤
568
+ const seen = new Set();
569
+ const hits = [];
570
+ for (const sc of overlayScopes()) {
571
+ sc.querySelectorAll("li,[role=menuitem],[role=option],[class*=item],a,button,td").forEach(el => {
572
+ if (seen.has(el) || !laidOut(el)) return;
573
+ seen.add(el);
574
+ const t = normText(el);
575
+ if (!t || t.length > 48) return;
576
+ hits.push({ el, t });
577
+ });
578
+ }
579
+ return hits.filter(h => !hits.some(o => o.el !== h.el && h.el.contains(o.el)));
580
+ }
581
+
582
+ function listSourceModalItems() {
583
+ // 自诊断版:带回每一级过滤的计数与原始命中,定位"哪一步滤没了"
584
+ const scopes = overlayScopes();
585
+ let raw = [];
586
+ for (const sc of scopes) {
587
+ sc.querySelectorAll("li,[role=menuitem],[role=option],[class*=item],a,button,td").forEach(el => {
588
+ raw.push({ el, laidOut: laidOut(el), t: normText(el) });
589
+ });
590
+ }
591
+ const leaves = overlayLeafElements();
592
+ return {
593
+ items: leaves.map(h => h.t).slice(0, 40),
594
+ debug: {
595
+ scopeCount: scopes.length,
596
+ scopeCls: scopes.slice(0, 3).map(s => String(s.className).slice(0, 80)),
597
+ rawCount: raw.length,
598
+ rawLaidOut: raw.filter(r => r.laidOut).length,
599
+ rawTextOk: raw.filter(r => r.laidOut && r.t && r.t.length <= 48).length,
600
+ sample: raw.filter(r => r.laidOut && r.t).slice(0, 12).map(r => r.t.slice(0, 30)),
601
+ },
602
+ };
603
+ }
604
+
605
+ function clickSourceItem(target) {
606
+ const wanted = String(target || "").replace(/\s+/g, "");
607
+ const leaves = overlayLeafElements();
608
+ if (leaves.length === 0) return { ok: false, error: "弹层未打开或无可选项" };
609
+ const matches = leaves.filter(h => h.t.includes(wanted));
610
+ if (matches.length === 0) {
611
+ return { ok: false, error: "弹层中无匹配的数据源", candidates: leaves.map(h => h.t).slice(0, 30) };
612
+ }
613
+ if (matches.length > 1) {
614
+ const exact = matches.find(h => h.t === wanted);
615
+ if (!exact) {
616
+ return { ok: false, error: "目标命中多个候选,需精确名称", candidates: matches.map(h => h.t) };
617
+ }
618
+ matches.length = 0;
619
+ matches.push(exact);
620
+ }
621
+ ["mousedown", "mouseup", "click"].forEach(t =>
622
+ matches[0].el.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true, view: window })));
623
+ return { ok: true, clicked: matches[0].t };
624
+ }
625
+ // ---------- 点选下拉中的目标选项(在元素上直接派发事件,无坐标漂移)----------
626
+ function clickDropdownOption(target) {
627
+ const dd = visibleDbDropdown();
628
+ if (!dd) return { ok: false, error: "dropdown not open" };
629
+ const nodes = [...dd.querySelectorAll(".ant-select-item-option, [role=option]")]
630
+ .filter(o => { const r = o.getBoundingClientRect(); return r.width > 0 && r.height > 0; });
631
+ const norm = s => (s || "").replace(/\s+/g, "");
632
+ const textOf = o => (o.getAttribute("title") || o.textContent || "").trim();
633
+ const hit = nodes.find(o => norm(textOf(o)) === norm(target))
634
+ || nodes.find(o => norm(textOf(o)).includes(norm(target)));
635
+ if (!hit) {
636
+ return { ok: false, error: "option not found", options: nodes.map(textOf).slice(0, 30) };
637
+ }
638
+ if (/disabled/.test(hit.className)) return { ok: false, error: "目标库为禁用状态(无权限)", target };
639
+ ["mousedown", "mouseup", "click"].forEach(t =>
640
+ hit.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true, view: window })));
641
+ return { ok: true, clicked: textOf(hit) };
642
+ }
643
+
644
+ // ---------- 消息处理 ----------
243
645
  chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
244
646
  if (!msg || !msg.type) return;
245
647
 
@@ -249,10 +651,27 @@
249
651
  }
250
652
  if (msg.type === "yr-ping") {
251
653
  detectEditor().then(editor => {
654
+ // 切源入口 / Select 状态 / 当前路由,辅助诊断切库链路
655
+ const hasSourceEntry = [...document.querySelectorAll("button")]
656
+ .some(b => b.offsetParent !== null && normText(b).includes("切换数据源"));
657
+ const selects = [...document.querySelectorAll(".ant-select")]
658
+ .filter(s => s.offsetParent !== null).slice(0, 6)
659
+ .map(s => {
660
+ const valueEl = s.querySelector(".ant-select-selection-item");
661
+ const phEl = s.querySelector(".ant-select-selection-placeholder");
662
+ return {
663
+ value: valueEl ? (valueEl.getAttribute("title") || valueEl.textContent || "").trim() : "",
664
+ placeholder: phEl ? (phEl.textContent || "").trim() : "",
665
+ disabled: /disabled/.test(s.className),
666
+ };
667
+ });
252
668
  sendResponse({
253
669
  ok: true,
254
670
  editor,
255
671
  buttons: findQueryButtons().slice(0, 20),
672
+ sourceEntry: hasSourceEntry,
673
+ selects,
674
+ hash: location.hash,
256
675
  });
257
676
  });
258
677
  return true; // 异步响应
@@ -265,6 +684,123 @@
265
684
  sendResponse(clickQuery());
266
685
  return true;
267
686
  }
687
+ if (msg.type === "yr-source-switch") {
688
+ switchDataSource(msg.target).then(sendResponse);
689
+ return true;
690
+ }
691
+ if (msg.type === "yr-db-select") {
692
+ selectDatabase(msg.database).then(sendResponse);
693
+ return true;
694
+ }
695
+ if (msg.type === "yr-focus-db") {
696
+ // 程序化聚焦库选择器的搜索输入框(focus 无需受信任事件,随后键盘事件才有效)
697
+ const trigger = findDbTrigger();
698
+ const input = trigger?.querySelector("input");
699
+ if (!input) {
700
+ sendResponse({
701
+ ok: false,
702
+ error: "库选择器内未找到 input",
703
+ triggerCls: trigger ? String(trigger.className).slice(0, 100) : null,
704
+ selects: [...document.querySelectorAll(".ant-select")].map(selectDumpLite).slice(0, 10),
705
+ });
706
+ return true;
707
+ }
708
+ input.focus();
709
+ const ae = document.activeElement;
710
+ sendResponse({
711
+ ok: ae === input || (ae && input.contains(ae)) || ae === trigger,
712
+ focused: ae === input,
713
+ activeCls: String(ae?.className || "").slice(0, 80),
714
+ activeTag: ae?.tagName,
715
+ });
716
+ return true;
717
+ }
718
+ if (msg.type === "yr-db-click-option") {
719
+ sendResponse(clickDropdownOption(msg.database));
720
+ return true;
721
+ }
722
+ if (msg.type === "yr-source-items") {
723
+ sendResponse(listSourceModalItems());
724
+ return true;
725
+ }
726
+ if (msg.type === "yr-source-click") {
727
+ sendResponse(clickSourceItem(msg.target));
728
+ return true;
729
+ }
730
+ if (msg.type === "yr-locate") {
731
+ const rect = locateTarget(msg.kind, msg.arg);
732
+ if (rect) { sendResponse({ ok: true, rect }); return true; }
733
+ // 定位失败带回全量 select 候选(含可见性与尺寸),便于判读页面结构
734
+ sendResponse({
735
+ ok: false,
736
+ error: "target not found: " + (msg.kind || ""),
737
+ selects: [...document.querySelectorAll(".ant-select")].map(selectDumpLite).slice(0, 10),
738
+ });
739
+ return true;
740
+ }
741
+ if (msg.type === "yr-options") {
742
+ sendResponse(listDropdownOptionsWithRects());
743
+ return true;
744
+ }
745
+ if (msg.type === "yr-overlay-items") {
746
+ sendResponse(listOverlayItems());
747
+ return true;
748
+ }
749
+ if (msg.type === "yr-hash") {
750
+ sendResponse({ ok: true, hash: location.hash });
751
+ return true;
752
+ }
753
+ if (msg.type === "yr-active-element") {
754
+ const el = document.activeElement;
755
+ sendResponse(el ? { ok: true, cls: String(el.className || "").slice(0, 100), id: el.id || "", tag: el.tagName } : { ok: false });
756
+ return true;
757
+ }
758
+ if (msg.type === "yr-dom-probe") {
759
+ // 只读探测:抓 Select / 下拉层 / 弹窗的真实类名与文本,用于诊断未知 UI 结构
760
+ const dump = (el) => ({
761
+ cls: String(el.className || "").slice(0, 120),
762
+ text: normText(el).slice(0, 60),
763
+ visible: el.offsetParent !== null,
764
+ });
765
+ try {
766
+ const rectOf = (el) => {
767
+ const r = el.getBoundingClientRect();
768
+ return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) };
769
+ };
770
+ const selectDump = (el) => ({
771
+ cls: String(el.className || "").slice(0, 120),
772
+ text: normText(el).slice(0, 60),
773
+ visible: el.offsetParent !== null,
774
+ rect: rectOf(el),
775
+ inForm: !!el.closest("form"),
776
+ html: el.outerHTML.replace(/\s+/g, " ").slice(0, 500),
777
+ });
778
+ // 用与编排一致的定位逻辑回看"到底选中了哪个节点"
779
+ const trigEl = findDbTrigger();
780
+ sendResponse({
781
+ ok: true,
782
+ hash: location.hash,
783
+ selects: [...document.querySelectorAll(".ant-select")].map(selectDump),
784
+ triggerLocated: trigEl ? selectDump(trigEl) : null,
785
+ metaNow: readYearningMeta(),
786
+ dropdownsAny: [...document.querySelectorAll("[class*=dropdown]")]
787
+ .slice(0, 8)
788
+ .map(el => ({ cls: String(el.className || "").slice(0, 100), visible: el.offsetParent !== null, text: normText(el).slice(0, 60) })),
789
+ optionCount: document.querySelectorAll(".ant-select-item-option").length,
790
+ overlays: [...document.querySelectorAll(".ant-modal,.ant-drawer,.ant-popover")]
791
+ .filter(el => el.offsetParent !== null)
792
+ .map(el => ({
793
+ cls: String(el.className || "").slice(0, 100),
794
+ text: normText(el).slice(0, 100),
795
+ btns: [...el.querySelectorAll("button,li,[role=menuitem],[role=option]")]
796
+ .map(b => normText(b)).filter(t => t && t.length <= 30).slice(0, 15),
797
+ })),
798
+ });
799
+ } catch (e) {
800
+ sendResponse({ ok: false, error: e.message });
801
+ }
802
+ return true;
803
+ }
268
804
  if (msg.type === "yr-new-sql") {
269
805
  // 新建 SQL 窗口:点工具栏新建按钮,等新编辑器渲染。
270
806
  // 避免把 SQL 注入用户正在看/正在用的已有编辑器。
@@ -288,8 +824,8 @@
288
824
  return true;
289
825
  }
290
826
  if (msg.type === "yr-focus-editor") {
291
- // CDP 注入前置:聚焦 monaco 的 inputarea(Input.insertText 作用于焦点元素)
292
- const ta = document.querySelector(".monaco-editor textarea.inputarea")
827
+ // CDP 注入前置:聚焦当前可见 tabmonaco inputarea(Input.insertText 作用于焦点元素)
828
+ const ta = activeMonaco()?.querySelector("textarea.inputarea")
293
829
  || document.querySelector("textarea");
294
830
  if (!ta) { sendResponse({ ok: false, error: "no inputarea" }); return true; }
295
831
  ta.focus();
@@ -311,5 +847,5 @@
311
847
  }
312
848
  });
313
849
 
314
- console.log(TAG, "Yearning content script loaded at", location.href);
850
+ console.log(TAG, "Yearning content script v" + chrome.runtime.getManifest().version + " loaded at", location.href);
315
851
  })();
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "JumpServer 终端桥接",
4
- "version": "2.0.0",
5
- "description": "桥接 JumpServer Web 终端,让 Agent 能远程执行命令并获取输出",
4
+ "version": "3.3.0",
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"],
8
8
  "background": {
@@ -184,8 +184,8 @@
184
184
  </style>
185
185
  </head>
186
186
  <body>
187
- <h3>🔌 终端桥接</h3>
188
- <div class="subtitle">JumpServer · Arthas Web Console</div>
187
+ <h3>🔌 终端桥接 <span id="extVersion" style="font-size:10px;color:#bbb;font-weight:400;"></span></h3>
188
+ <div class="subtitle">JumpServer · Arthas Web Console · Yearning</div>
189
189
 
190
190
  <div class="section" style="margin-top:8px;padding-top:0;border:none;">
191
191
  <div class="status-line">
@@ -326,6 +326,9 @@ document.getElementById('btnCopyInstall').onclick = () => {
326
326
  setInterval(() => { refreshXterm(); refreshProxy(); refreshCsvList(); refreshYearningTabs(); }, 3000);
327
327
  refreshXterm();
328
328
  refreshProxy();
329
+ // 版本号显性化:与 manifest 一致,排查"加载的是不是最新代码"
330
+ try { document.getElementById('extVersion').textContent = 'v' + chrome.runtime.getManifest().version; } catch {}
331
+
329
332
  // 启动时主动探测一次 native host:发个 status,失败就显示安装提示
330
333
  chrome.runtime.sendMessage({ type: 'PROXY_STATUS' }, (res) => {
331
334
  if (isNativeHostMissing(res)) showInstallHint(true);