@glittering0717/ai-browser 2.1.10 → 2.1.12
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/package.json +1 -1
- package/src/preload/extractor.cjs +42 -0
- package/src/shared/version.js +1 -1
- package/src/shared/wrapper_hint.js +25 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glittering0717/ai-browser",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.12",
|
|
4
4
|
"description": "AI-native browser — renders pixels for humans, structured semantic-tree API for agents. No screenshots, no OCR, no coordinate guessing.",
|
|
5
5
|
"main": "src/main/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -309,6 +309,40 @@ function extractTree() {
|
|
|
309
309
|
function hasIconInside(el) {
|
|
310
310
|
try { return !!(el.querySelector && el.querySelector("svg,img,i[class],[class*=icon]")); } catch (e) { return false; }
|
|
311
311
|
}
|
|
312
|
+
|
|
313
|
+
// ---- label 是否"由子树文字拼出来"(agg 标记的判据)-------------------------
|
|
314
|
+
//
|
|
315
|
+
// 为什么需要(2026-10-25 知乎私信活动链接):列表行 `<div class="ChatUserListItem" onclick>`
|
|
316
|
+
// 在树里是 `generic`,label 却是整段预览文字的拼接:
|
|
317
|
+
// "创作者小助手 10-21 ☄️最新一期「创作打卡挑战赛」活动来喽!「创作打卡挑战赛第五十四期 」将于…"
|
|
318
|
+
// 于是"按文字找链接"必定先命中这个容器(点它只选中会话),而真链接的 label 只有
|
|
319
|
+
// 那 11 个字。用户看到的现象是"树里有、点不动",且完全无从判断为什么。
|
|
320
|
+
//
|
|
321
|
+
// 判据是 DOM 事实,不猜:**自身没有直接文字**(own text 为空)却有子元素、
|
|
322
|
+
// 而 label 又够长 —— 这段 label 只可能是从下面拼上来的,打 agg:1 让读树的人
|
|
323
|
+
// 一眼看出"文字不归它"。可交互角色(a/button/input…)即使包着 span,文字语义
|
|
324
|
+
// 也归它自己,不标;aria-label/title 是作者写的名字,也不是拼接,同样不标。
|
|
325
|
+
var CONTAINER_ROLES = {
|
|
326
|
+
generic: 1, group: 1, list: 1, listitem: 1, table: 1, row: 1, cell: 1, gridcell: 1,
|
|
327
|
+
article: 1, region: 1, main: 1, navigation: 1, section: 1, paragraph: 1, none: 1
|
|
328
|
+
};
|
|
329
|
+
function ownTextOf(el) {
|
|
330
|
+
var s = "";
|
|
331
|
+
try {
|
|
332
|
+
for (var i = 0; i < el.childNodes.length; i++) {
|
|
333
|
+
var c = el.childNodes[i];
|
|
334
|
+
if (c.nodeType === 3) s += c.textContent;
|
|
335
|
+
}
|
|
336
|
+
} catch (e) {}
|
|
337
|
+
return String(s).replace(/\s+/g, " ").trim();
|
|
338
|
+
}
|
|
339
|
+
function isAggregatedLabel(el, role, label, labelMeta) {
|
|
340
|
+
if (!label || label.length < 8) return false;
|
|
341
|
+
if (!CONTAINER_ROLES[role]) return false;
|
|
342
|
+
if (labelMeta && labelMeta.kind && labelMeta.kind !== "text") return false;
|
|
343
|
+
if (!el.children || !el.children.length) return false;
|
|
344
|
+
return ownTextOf(el).length < 3;
|
|
345
|
+
}
|
|
312
346
|
function isNamed(el) {
|
|
313
347
|
return !!elementName(el).trim() || hasIconInside(el);
|
|
314
348
|
}
|
|
@@ -821,6 +855,14 @@ function extractTree() {
|
|
|
821
855
|
// 把 aria-label / alt / title / placeholder 排除掉(它们不在 innerText 里)。
|
|
822
856
|
// 正常节点不写,省 token。
|
|
823
857
|
if (labelMeta.kind && labelMeta.kind !== "text") node.label_kind = labelMeta.kind;
|
|
858
|
+
// label 是**子树文字的拼接**时打 agg:1 —— 读树的人必须能分辨"这段文字是我的"
|
|
859
|
+
// 和"这段文字在我下面",否则按文字找链接会命中列表行容器,点了什么都不会发生。
|
|
860
|
+
// 2026-10-25 知乎私信:`<div class="ChatUserListItem" onclick>` 的 label 成了
|
|
861
|
+
// "创作者小助手 10-21 ☄️最新一期「创作打卡挑战赛」活动来喽!…",agent 点它
|
|
862
|
+
// 只选中会话,真链接(<a>创作打卡挑战赛第五十四期</a>)在会话面板里。
|
|
863
|
+
// 判据用 DOM 事实而不是猜:自身没有直接文字(own text 为空)却有子元素、
|
|
864
|
+
// label 还够长 —— 那这段 label 只可能是从下面拼上来的。
|
|
865
|
+
if (isAggregatedLabel(el, role, label, labelMeta)) node.agg = 1;
|
|
824
866
|
// Attach current value for input/textarea/contenteditable.
|
|
825
867
|
// 截断走共享契约(src/shared/value_contract.cjs):按 Unicode code point
|
|
826
868
|
// 计数,截断时带 value_truncated / value_full_length / value_fetch_ref,
|
package/src/shared/version.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
// Kept in JS (not imported from package.json) to avoid Node import-attribute
|
|
3
3
|
// (JSON module) compatibility issues inside Electron. package.json "version"
|
|
4
4
|
// is manually synced to this value; tests assert they match.
|
|
5
|
-
export const VERSION = '2.1.
|
|
5
|
+
export const VERSION = '2.1.12';
|
|
@@ -54,20 +54,22 @@ export function wrapperHintExpr(aiId) {
|
|
|
54
54
|
// 而这恰恰就是要找的那个链接。退一步用"8 字窗口命中":只要候选文字里有连续
|
|
55
55
|
// 8 个字出现在容器文字里,就算相关 —— 中文 8 个字已经足够特异。
|
|
56
56
|
function related(aggText, t) {
|
|
57
|
-
if (aggText.indexOf(t) >= 0) return
|
|
58
|
-
if (t.length < 8) return
|
|
57
|
+
if (aggText.indexOf(t) >= 0) return 2; // 整串命中:最可信
|
|
58
|
+
if (t.length < 8) return 0;
|
|
59
59
|
for (var k = 0; k + 8 <= t.length; k++) {
|
|
60
|
-
if (aggText.indexOf(t.slice(k, k + 8)) >= 0) return
|
|
60
|
+
if (aggText.indexOf(t.slice(k, k + 8)) >= 0) return 1;
|
|
61
61
|
}
|
|
62
|
-
return
|
|
62
|
+
return 0;
|
|
63
63
|
}
|
|
64
|
+
var viewH = window.innerHeight || 800;
|
|
64
65
|
for (var i = 0; i < limit; i++) {
|
|
65
66
|
var c = cands[i];
|
|
66
67
|
if (c === el) continue;
|
|
67
68
|
if (c.contains(el)) continue; // 目标的祖先:点它等于又点一次外层
|
|
68
69
|
var t = ownText(c);
|
|
69
70
|
if (t.length < 6) continue;
|
|
70
|
-
|
|
71
|
+
var match = related(agg, t);
|
|
72
|
+
if (!match) continue;
|
|
71
73
|
var r = c.getBoundingClientRect();
|
|
72
74
|
if (r.width < 4 || r.height < 4) continue;
|
|
73
75
|
var cs = getComputedStyle(c);
|
|
@@ -76,8 +78,23 @@ export function wrapperHintExpr(aiId) {
|
|
|
76
78
|
var role = String((c.getAttribute && c.getAttribute('role')) || '');
|
|
77
79
|
var clickable = (tag === 'A' || tag === 'BUTTON' || role === 'link' || role === 'button' || cs.cursor === 'pointer');
|
|
78
80
|
var area = Math.max(1, Math.round(r.width) * Math.round(r.height));
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
+
// 排序必须按"相关性"而不是"谁小":
|
|
82
|
+
// ① 整串命中优先于 8 字窗口命中 —— 知乎那边同一条私信里躺着第一期到第五十四期
|
|
83
|
+
// 五个链接,它们的 8 字窗口全都命中"创作打卡挑战赛",只按面积排会挑中
|
|
84
|
+
// 面积最小的那一期(实测挑成了第五十期,还滚在视口外 4500px);
|
|
85
|
+
// ② 视口内优先 —— 用户看到并想点的是屏幕上那个;
|
|
86
|
+
// ③ 能点的优先;
|
|
87
|
+
// ④ 最后才用面积(越贴近那段文字越精确)。
|
|
88
|
+
var inView = (r.y + r.height > 0 && r.y < viewH) ? 0 : 1;
|
|
89
|
+
// 更长的那段文字更具体:同一段预览里"创作打卡挑战赛"和"创作打卡挑战赛第五十四期"
|
|
90
|
+
// 都是整串命中,但后者才是用户想点的那一期。
|
|
91
|
+
var specificity = 200 - Math.min(199, t.length);
|
|
92
|
+
// 排序:整串命中 > 能点 > 更具体(文字更长)> 视口内 > 面积小。
|
|
93
|
+
// 为什么"更具体"排在"视口内"前面:同一段预览里的第一期…第五十四期五个链接,
|
|
94
|
+
// 整串命中的只有对的那一期,即使它在视口外也值得点(点击前会 scrollIntoView);
|
|
95
|
+
// 反过来点一个"看得见但期数错了"的链接,是把人带到错误页面。
|
|
96
|
+
var score = (2 - match) * 1e15 + (clickable ? 0 : 1e13)
|
|
97
|
+
+ specificity * 1e9 + inView * 1e8 + Math.min(area, 99999);
|
|
81
98
|
if (!best || score < best.score) {
|
|
82
99
|
best = {
|
|
83
100
|
score: score,
|
|
@@ -87,6 +104,7 @@ export function wrapperHintExpr(aiId) {
|
|
|
87
104
|
text: t.slice(0, 60),
|
|
88
105
|
box: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) },
|
|
89
106
|
clickable: clickable,
|
|
107
|
+
exact: match === 2,
|
|
90
108
|
};
|
|
91
109
|
}
|
|
92
110
|
}
|