diff --git a/01_content/lib/client.js b/01_content/lib/client.js index b53522f..62ebf8a 100644 --- a/01_content/lib/client.js +++ b/01_content/lib/client.js @@ -16869,8 +16869,14 @@ function windowLabelOf(row, index) { if (c) { try { if (c.kind === "iframe" && c.url) label += "\uFF08\u7F51\u9875 " + new URL(c.url).hostname + "\uFF09"; - else if (c.kind === "builtin") label += "\uFF08\u5185\u7F6E\xB7" + c.type + "\uFF09"; - else if (c.kind === "file") label += "\uFF08\u6587\u4EF6\uFF09"; + else if (c.kind === "builtin") { + label += "\uFF08\u5185\u7F6E\xB7" + c.type; + try { + if (c.url) label += " " + new URL(c.url).hostname; + } catch { + } + label += "\uFF09"; + } else if (c.kind === "file") label += "\uFF08\u6587\u4EF6\uFF09"; } catch { } } @@ -16899,59 +16905,108 @@ function boxPayload(x0, y0, x1, y1) { const cx = (L + R) / 2; const cy = (T2 + B) / 2; let primary = null; - let textParent = null; - try { - const pd = document; - const pos = pd.caretPositionFromPoint ? pd.caretPositionFromPoint(cx, cy) : pd.caretRangeFromPoint ? pd.caretRangeFromPoint(cx, cy) : null; - const textNode = pos ? pos.offsetNode ?? pos.startContainer ?? null : null; - if (textNode) { - const parent = textNode.parentElement; - if (parent) { - textParent = parent; - const text2 = (textNode.textContent || "").trim().replace(/\s+/g, " ").slice(0, 60); - if (text2) { - const cs = getComputedStyle(parent); - primary = { text: text2, fontSize: cs.fontSize, lineHeight: cs.lineHeight, color: cs.color }; + let line = ""; + let candidates = []; + let limited = false; + let src = ""; + const readDoc = (doc, ox, oy) => { + let ok = false; + let textParent = null; + try { + const pd = doc; + const px = cx - ox; + const py = cy - oy; + let pos = null; + try { + if (pd.caretPositionFromPoint) pos = pd.caretPositionFromPoint(px, py); + } catch { + } + if (!pos) { + try { + if (pd.caretRangeFromPoint) pos = pd.caretRangeFromPoint(px, py); + } catch { } } - } - } catch { - } - let line = ""; - if (textParent) { - try { - let el = textParent; - while (el && el !== document.body) { - const r = el.getBoundingClientRect(); - if (r.height >= 8 && r.height <= 44 && r.width > 0) { - const t = (el.innerText || "").replace(/\s+/g, " ").trim(); - if (t) { - line = t.slice(0, 120); - break; + const textNode = pos ? pos.offsetNode ?? pos.startContainer ?? null : null; + if (textNode) { + const parent = textNode.parentElement; + if (parent) { + textParent = parent; + const text2 = (textNode.textContent || "").trim().replace(/\s+/g, " ").slice(0, 60); + if (text2) { + const cs2 = getComputedStyle(parent); + primary = { text: text2, fontSize: cs2.fontSize, lineHeight: cs2.lineHeight, color: cs2.color }; + ok = true; } } - el = el.parentElement; + } + } catch { + } + if (textParent) { + try { + let el = textParent; + while (el && el !== doc.body) { + const r = el.getBoundingClientRect(); + if (r.height >= 8 && r.height <= 44 && r.width > 0) { + const t = (el.innerText || "").replace(/\s+/g, " ").trim(); + if (t) { + line = t.slice(0, 120); + break; + } + } + el = el.parentElement; + } + } catch { + } + } + const seen = /* @__PURE__ */ new Set(); + if (primary) seen.add(primary.text); + const cs = []; + try { + for (const el of Array.from(doc.querySelectorAll("*"))) { + const tag = el.tagName.toLowerCase(); + if (tag === "script" || tag === "style" || tag === "svg" || tag === "path" || tag === "br" || tag === "template" || tag === "iframe") continue; + const rect = el.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) continue; + if (rect.right + ox < L || rect.left + ox > R || rect.bottom + oy < T2 || rect.top + oy > B) continue; + const text2 = (el.textContent || "").trim().replace(/\s+/g, " "); + if (text2.length < 1 || text2.length > 30) continue; + if (seen.has(text2)) continue; + seen.add(text2); + cs.push(text2); + if (cs.length >= 4) break; + } + } catch { + } + if (ok || cs.length > 0) candidates = cs; + return ok; + }; + readDoc(document, 0, 0); + if (!primary) { + try { + for (const iframe of Array.from(document.querySelectorAll("iframe"))) { + try { + const r = iframe.getBoundingClientRect(); + if (r.width <= 0 || r.height <= 0) continue; + if (cx < r.left || cx > r.right || cy < r.top || cy > r.bottom) continue; + src = iframe.getAttribute("src") ?? ""; + let inner = null; + try { + inner = iframe.contentDocument; + } catch { + inner = null; + } + if (inner) readDoc(inner, r.left, r.top); + else limited = true; + break; + } catch { + continue; + } } } catch { } } - const seen = /* @__PURE__ */ new Set(); - if (primary) seen.add(primary.text); - const candidates = []; - for (const el of document.querySelectorAll("*")) { - const tag = el.tagName.toLowerCase(); - if (tag === "script" || tag === "style" || tag === "svg" || tag === "path" || tag === "br" || tag === "template" || tag === "iframe") continue; - const rect = el.getBoundingClientRect(); - if (rect.width <= 0 || rect.height <= 0) continue; - if (rect.right < L || rect.left > R || rect.bottom < T2 || rect.top > B) continue; - const text2 = (el.textContent || "").trim().replace(/\s+/g, " "); - if (text2.length < 1 || text2.length > 30) continue; - if (seen.has(text2)) continue; - seen.add(text2); - candidates.push(text2); - if (candidates.length >= 4) break; - } - return { primary, line, candidates }; + return { primary, line, candidates, limited, src }; } function fillHostInput(text2) { try { @@ -16974,7 +17029,7 @@ function fillHostInput(text2) { } function confirmAnnot() { const s = annotState; - const text2 = "\u{1F4CC} \u6807\u6CE8-" + s.resp + "\n" + (s.stat || "") + "\n\u8981\u6C42\uFF1A" + (s.draft.trim() || "\uFF08\u672A\u586B\u5199\uFF09") + "\n\u3010\u5DE5\u4F5C\u53F0\u6807\u6CE8\xB7\u5DF2\u5B9A\u4F4D\uFF0C\u76F4\u63A5\u4F5C\u7B54\uFF0C\u65E0\u9700\u518D\u95EE\uFF1B\u4EC5\u5F53\u786E\u5B9E\u65E0\u6CD5\u4ECE\u6807\u6CE8\u5B9A\u4F4D\u65F6\u624D\u95EE\u4E00\u6B21\u3011"; + const text2 = "\u{1F4CC} \u6807\u6CE8-" + s.resp + "\n" + (s.stat || "") + "\n\u8981\u6C42\uFF1A" + (s.draft.trim() || "\uFF08\u672A\u586B\u5199\uFF09") + "\n\u3010\u5DE5\u4F5C\u53F0\u6807\u6CE8\xB7\u5DF2\u5B9A\u4F4D\uFF0C\u76F4\u63A5\u4F5C\u7B54\uFF0C\u65E0\u9700\u518D\u95EE\uFF1B\u4EC5\u5F53\u786E\u5B9E\u65E0\u6CD5\u4ECE\u6807\u6CE8\u5B9A\u4F4D\u65F6\u624D\u95EE\u4E00\u6B21\u3002\u6570\u636E\u7F3A\u5931\u6216\u6807\u6CE8\u6807\u6CE8\u8BFB\u53D6\u53D7\u9650\u65F6\uFF0C\u5982\u5B9E\u8BF4\u660E\u65E0\u6CD5\u786E\u5B9A\uFF0C\u7981\u6B62\u7F16\u9020\u6570\u503C\uFF1B\u53EF\u5EFA\u8BAE\u63D0\u4F9B\u622A\u56FE\u6216\u4F7F\u7528\u89C6\u89C9\u6A21\u578B\u4F1A\u8BDD\u3011"; const ok = fillHostInput(text2); if (!ok) { try { @@ -17043,6 +17098,7 @@ function AnnotationOverlay() { const hit = boxPayload(L, T2, R, B); if (hit.primary) stat += "\uFF0C\u4E3B\u76EE\u6807\uFF1A" + hit.primary.text + "\uFF08\u5B57\u53F7 " + hit.primary.fontSize + "\uFF0C\u884C\u9AD8 " + hit.primary.lineHeight + "\uFF09"; if (hit.line) stat += "\uFF1B\u6574\u884C\uFF1A" + hit.line; + if (hit.limited) stat += "\uFF1B\u8BFB\u53D6\u53D7\u9650\uFF1A\u8DE8\u57DF\u9875\u9762\u5185\u5BB9\u4E0D\u53EF\u89C1\uFF08\u6D4F\u89C8\u5668\u5B89\u5168\u9650\u5236\uFF09\uFF0C\u7A97\u53E3\u8EAB\u4EFD\u89C1\u4E0A\uFF08" + (hit.src ? hit.src : "\u65E0URL") + "\uFF09"; if (hit.candidates.length > 0) stat += "\uFF1B\u5019\u9009\uFF1A" + hit.candidates.map((t, i) => "[" + (i + 1) + "]" + t).join(" "); } else { stat = "\u5C4F\u5E55\u5750\u6807 (" + Math.round(e.clientX) + ", " + Math.round(e.clientY) + ")"; diff --git a/01_content/src/client/split.tsx b/01_content/src/client/split.tsx index cc81b46..749551a 100644 --- a/01_content/src/client/split.tsx +++ b/01_content/src/client/split.tsx @@ -358,7 +358,11 @@ function windowLabelOf(row: PaneRow, index: number): string { if (c) { try { if (c.kind === 'iframe' && c.url) label += '(网页 ' + new URL(c.url).hostname + ')' - else if (c.kind === 'builtin') label += '(内置·' + c.type + ')' + else if (c.kind === 'builtin') { + label += '(内置·' + c.type + try { if (c.url) label += ' ' + new URL(c.url).hostname } catch {} + label += ')' + } else if (c.kind === 'file') label += '(文件)' } catch {} } @@ -381,7 +385,7 @@ function ctxOf(t: EventTarget | null): string { /** 框选 payload v3.1:主目标 = 框中心点【光标下的字符】所在的 Text 节点(caretPositionFromPoint, * 标签即使不是独立元素也能取到真正的词/段);样式取该 Text 节点的父元素(真实渲染值); * 整行 = 最近的高度≤44px 祖先的内容(供「这行字是什么」类问题);候选 = 相交的独立短文本。 */ -function boxPayload(x0: number, y0: number, x1: number, y1: number): { primary: { text: string; fontSize: string; lineHeight: string; color: string } | null; line: string; candidates: string[] } { +function boxPayload(x0: number, y0: number, x1: number, y1: number): { primary: { text: string; fontSize: string; lineHeight: string; color: string } | null; line: string; candidates: string[]; limited: boolean; src: string } { const L = Math.min(x0, x1) const T = Math.min(y0, y1) const R = Math.max(x0, x1) @@ -389,55 +393,92 @@ function boxPayload(x0: number, y0: number, x1: number, y1: number): { primary: const cx = (L + R) / 2 const cy = (T + B) / 2 let primary: { text: string; fontSize: string; lineHeight: string; color: string } | null = null - let textParent: HTMLElement | null = null - try { - const pd = document as any - const pos = pd.caretPositionFromPoint ? pd.caretPositionFromPoint(cx, cy) : (pd.caretRangeFromPoint ? pd.caretRangeFromPoint(cx, cy) : null) - const textNode: Node | null = pos ? (pos.offsetNode ?? pos.startContainer ?? null) : null - if (textNode) { - const parent = textNode.parentElement - if (parent) { - textParent = parent - const text = (textNode.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 60) - if (text) { - const cs = getComputedStyle(parent) - primary = { text, fontSize: cs.fontSize, lineHeight: cs.lineHeight, color: cs.color } + let line = '' + let candidates: string[] = [] + let limited = false + let src = '' + // 在某 document 内取字(rect = 该 doc 视口在顶层页面的偏移) + const readDoc = (doc: Document, ox: number, oy: number): boolean => { + let ok = false + let textParent: HTMLElement | null = null + try { + const pd = doc as any + const px = cx - ox + const py = cy - oy + let pos: any = null + try { if (pd.caretPositionFromPoint) pos = pd.caretPositionFromPoint(px, py) } catch {} + if (!pos) { try { if (pd.caretRangeFromPoint) pos = pd.caretRangeFromPoint(px, py) } catch {} } + const textNode: Node | null = pos ? (pos.offsetNode ?? pos.startContainer ?? null) : null + if (textNode) { + const parent = textNode.parentElement + if (parent) { + textParent = parent + const text = (textNode.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 60) + if (text) { + const cs = getComputedStyle(parent) + primary = { text, fontSize: cs.fontSize, lineHeight: cs.lineHeight, color: cs.color } + ok = true + } } } - } - } catch {} - // 整行:从文本父元素向上找最近的高度 ≤44px 的祖先 - let line = '' - if (textParent) { - try { - let el: HTMLElement | null = textParent - while (el && el !== document.body) { - const r = el.getBoundingClientRect() - if (r.height >= 8 && r.height <= 44 && r.width > 0) { - const t = (el.innerText || '').replace(/\s+/g, ' ').trim() - if (t) { line = t.slice(0, 120); break } + } catch {} + // 整行 + if (textParent) { + try { + let el: HTMLElement | null = textParent + while (el && el !== doc.body) { + const r = el.getBoundingClientRect() + if (r.height >= 8 && r.height <= 44 && r.width > 0) { + const t = (el.innerText || '').replace(/\s+/g, ' ').trim() + if (t) { line = t.slice(0, 120); break } + } + el = el.parentElement } - el = el.parentElement + } catch {} + } + // 候选 + const seen = new Set() + if (primary) seen.add(primary.text) + const cs: string[] = [] + try { + for (const el of Array.from(doc.querySelectorAll('*'))) { + const tag = el.tagName.toLowerCase() + if (tag === 'script' || tag === 'style' || tag === 'svg' || tag === 'path' || tag === 'br' || tag === 'template' || tag === 'iframe') continue + const rect = el.getBoundingClientRect() + if (rect.width <= 0 || rect.height <= 0) continue + if (rect.right + ox < L || rect.left + ox > R || rect.bottom + oy < T || rect.top + oy > B) continue + const text = (el.textContent || '').trim().replace(/\s+/g, ' ') + if (text.length < 1 || text.length > 30) continue + if (seen.has(text)) continue + seen.add(text) + cs.push(text) + if (cs.length >= 4) break + } + } catch {} + if (ok || cs.length > 0) candidates = cs + return ok + } + // 1) 顶层文档 + readDoc(document, 0, 0) + // 2) 顶层取不到 → 中心点所在 iframe 下钻(同源可读;跨域标记受限于 src) + if (!primary) { + try { + for (const iframe of Array.from(document.querySelectorAll('iframe'))) { + try { + const r = iframe.getBoundingClientRect() + if (r.width <= 0 || r.height <= 0) continue + if (cx < r.left || cx > r.right || cy < r.top || cy > r.bottom) continue + src = iframe.getAttribute('src') ?? '' + let inner: Document | null = null + try { inner = iframe.contentDocument } catch { inner = null } + if (inner) readDoc(inner, r.left, r.top) + else limited = true + break + } catch { continue } } } catch {} } - const seen = new Set() - if (primary) seen.add(primary.text) - const candidates: string[] = [] - for (const el of document.querySelectorAll('*')) { - const tag = el.tagName.toLowerCase() - if (tag === 'script' || tag === 'style' || tag === 'svg' || tag === 'path' || tag === 'br' || tag === 'template' || tag === 'iframe') continue - const rect = el.getBoundingClientRect() - if (rect.width <= 0 || rect.height <= 0) continue - if (rect.right < L || rect.left > R || rect.bottom < T || rect.top > B) continue - const text = (el.textContent || '').trim().replace(/\s+/g, ' ') - if (text.length < 1 || text.length > 30) continue - if (seen.has(text)) continue - seen.add(text) - candidates.push(text) - if (candidates.length >= 4) break - } - return { primary, line, candidates } + return { primary, line, candidates, limited, src } } /** 注入宿主对话框输入框(不发送);失败返回 false */ @@ -465,7 +506,7 @@ function fillHostInput(text: string): boolean { function confirmAnnot() { const s = annotState const text = '📌 标注-' + s.resp + '\n' + (s.stat || '') + '\n要求:' + (s.draft.trim() || '(未填写)') - + '\n【工作台标注·已定位,直接作答,无需再问;仅当确实无法从标注定位时才问一次】' + + '\n【工作台标注·已定位,直接作答,无需再问;仅当确实无法从标注定位时才问一次。数据缺失或标注标注读取受限时,如实说明无法确定,禁止编造数值;可建议提供截图或使用视觉模型会话】' const ok = fillHostInput(text) if (!ok) { try { @@ -529,6 +570,7 @@ function AnnotationOverlay() { const hit = boxPayload(L, T, R, B) if (hit.primary) stat += ',主目标:' + hit.primary.text + '(字号 ' + hit.primary.fontSize + ',行高 ' + hit.primary.lineHeight + ')' if (hit.line) stat += ';整行:' + hit.line + if (hit.limited) stat += ';读取受限:跨域页面内容不可见(浏览器安全限制),窗口身份见上(' + (hit.src ? hit.src : '无URL') + ')' if (hit.candidates.length > 0) stat += ';候选:' + hit.candidates.map((t, i) => '[' + (i + 1) + ']' + t).join(' ') } else { stat = '屏幕坐标 (' + Math.round(e.clientX) + ', ' + Math.round(e.clientY) + ')'