chore: auto-commit (286 archivos)
- .claude/agents/fn-orquestador/SKILL.md - .claude/commands/fn_claude.md - .claude/rules/INDEX.md - .claude/rules/cpp_apps.md - .claude/rules/ids_naming.md - CHANGELOG.md - apps/dag_engine/README.md - apps/dag_engine/api.go - apps/dag_engine/dags_migrated/example.yaml - apps/dag_engine/dags_migrated/example_lineage_tracking.yaml - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
// cdp_pick_element_js - JS injected via CDP Runtime.evaluate.
|
||||
// Activates click-to-pick mode: hover highlights, click captures element selectors
|
||||
// and prints via console.log so caller (navegator_dashboard) reads via
|
||||
// Runtime.consoleAPICalled event.
|
||||
//
|
||||
// Inject as expression: see cdp_pick_element_js.md for the wrapping payload.
|
||||
// Idempotent: re-injecting cleans up previous overlays before reattaching.
|
||||
(function () {
|
||||
if (window.__fn_pick_active) {
|
||||
window.__fn_pick_cleanup && window.__fn_pick_cleanup();
|
||||
}
|
||||
window.__fn_pick_active = true;
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = '__fn_pick_overlay';
|
||||
Object.assign(overlay.style, {
|
||||
position: 'fixed', pointerEvents: 'none', zIndex: '2147483647',
|
||||
border: '2px solid #f44', background: 'rgba(255,80,80,0.10)',
|
||||
boxSizing: 'border-box', transition: 'all 30ms linear',
|
||||
left: '0px', top: '0px', width: '0px', height: '0px',
|
||||
});
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
function cssPath(el) {
|
||||
if (!(el instanceof Element)) return '';
|
||||
const path = [];
|
||||
while (el && el.nodeType === Node.ELEMENT_NODE && el !== document.body) {
|
||||
let seg = el.tagName.toLowerCase();
|
||||
if (el.id) { seg += '#' + CSS.escape(el.id); path.unshift(seg); break; }
|
||||
// Prefer class-based path when class is short and not utility-noise.
|
||||
if (el.className && typeof el.className === 'string') {
|
||||
const cls = el.className.trim().split(/\s+/).filter(c => c && c.length < 24 && !/^(active|hover|focus|selected|open|disabled|[0-9])/.test(c));
|
||||
if (cls.length) seg += '.' + cls.map(CSS.escape).join('.');
|
||||
}
|
||||
// Disambiguate by nth-of-type when siblings share tag.
|
||||
if (el.parentNode) {
|
||||
const siblings = [...el.parentNode.children].filter(c => c.tagName === el.tagName);
|
||||
if (siblings.length > 1) {
|
||||
const idx = siblings.indexOf(el) + 1;
|
||||
seg += `:nth-of-type(${idx})`;
|
||||
}
|
||||
}
|
||||
path.unshift(seg);
|
||||
el = el.parentNode;
|
||||
if (path.length > 6) break;
|
||||
}
|
||||
return path.join(' > ');
|
||||
}
|
||||
|
||||
function xpath(el) {
|
||||
if (!el) return '';
|
||||
if (el.id) return `//*[@id="${el.id}"]`;
|
||||
const parts = [];
|
||||
while (el && el.nodeType === Node.ELEMENT_NODE) {
|
||||
let ix = 0;
|
||||
for (const sib of el.parentNode ? el.parentNode.childNodes : []) {
|
||||
if (sib === el) { parts.unshift(`${el.tagName.toLowerCase()}[${ix + 1}]`); break; }
|
||||
if (sib.nodeType === 1 && sib.tagName === el.tagName) ix++;
|
||||
}
|
||||
el = el.parentNode;
|
||||
if (el === document.body) { parts.unshift('body'); break; }
|
||||
}
|
||||
return '/html/body/' + parts.join('/');
|
||||
}
|
||||
|
||||
function describe(el) {
|
||||
if (!el) return null;
|
||||
const r = el.getBoundingClientRect();
|
||||
const attrs = {};
|
||||
for (const a of el.attributes || []) attrs[a.name] = a.value;
|
||||
return {
|
||||
selector: cssPath(el),
|
||||
xpath: xpath(el),
|
||||
tag: el.tagName.toLowerCase(),
|
||||
text: (el.innerText || el.textContent || '').trim().slice(0, 200),
|
||||
attrs,
|
||||
rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) },
|
||||
};
|
||||
}
|
||||
|
||||
function onMove(e) {
|
||||
const el = e.target;
|
||||
if (!el || el === overlay) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
overlay.style.left = r.left + 'px';
|
||||
overlay.style.top = r.top + 'px';
|
||||
overlay.style.width = r.width + 'px';
|
||||
overlay.style.height = r.height + 'px';
|
||||
}
|
||||
|
||||
function onClick(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const info = describe(e.target);
|
||||
console.log('__fn_picked__', JSON.stringify(info));
|
||||
cleanup();
|
||||
}
|
||||
|
||||
function onKey(e) {
|
||||
if (e.key === 'Escape') {
|
||||
console.log('__fn_picked__', JSON.stringify({ cancelled: true }));
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
window.__fn_pick_active = false;
|
||||
document.removeEventListener('mousemove', onMove, true);
|
||||
document.removeEventListener('click', onClick, true);
|
||||
document.removeEventListener('keydown', onKey, true);
|
||||
overlay.remove();
|
||||
}
|
||||
window.__fn_pick_cleanup = cleanup;
|
||||
|
||||
document.addEventListener('mousemove', onMove, true);
|
||||
document.addEventListener('click', onClick, true);
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
|
||||
return 'pick mode active';
|
||||
})();
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: cdp_pick_element_js
|
||||
kind: function
|
||||
lang: js
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "(self-executing IIFE injected via CDP Runtime.evaluate)"
|
||||
description: "Snippet JS inyectable que activa modo click-to-pick en una pagina Chrome remota: hover muestra overlay rojo, click captura CSS selector + XPath + tag + texto + bbox y los emite por console.log con prefijo '__fn_picked__'. El caller (navegator_dashboard) lee via CDP Runtime.consoleAPICalled."
|
||||
tags: [navegator, cdp, browser, picker, scraping, js]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
file_path: "functions/browser/cdp_pick_element_js.js"
|
||||
params:
|
||||
- name: (none — IIFE, sin argumentos)
|
||||
desc: "Snippet stand-alone. Se inyecta el fichero entero como `expression` de Runtime.evaluate."
|
||||
output: "Mensaje en console.log con prefijo `__fn_picked__` seguido de JSON {selector,xpath,tag,text,attrs,rect}. Si user pulsa Escape: {cancelled: true}."
|
||||
example: |
|
||||
# En navegator_dashboard, al pulsar boton "Pick":
|
||||
# 1. Lee el archivo cdp_pick_element_js.js (string).
|
||||
# 2. Envia via WebSocket CDP al tab activo:
|
||||
# {"id": N, "method": "Runtime.evaluate", "params": {"expression": "<contenido del .js>"}}
|
||||
# 3. Escucha eventos "Runtime.consoleAPICalled" filtrando args[0].value == "__fn_picked__".
|
||||
# 4. Parsea args[1].value como JSON -> dict con selector/xpath/tag/text/attrs/rect.
|
||||
---
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando un user pulsa "Pick element" en un tab de Chrome remoto para capturar un selector robusto sin abrir DevTools. Salida usable en recipes YAML o tests.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Idempotente: re-inyectar limpia overlay anterior.
|
||||
- Path CSS truncado a 6 niveles para evitar selectores fragiles cross-rerender.
|
||||
- Filtra clases dinamicas comunes (`active`, `hover`, `selected`...) que rotan.
|
||||
- No funciona sobre iframes anidados — solo top frame.
|
||||
- Escape cancela y emite `{cancelled: true}`.
|
||||
- El listener captura events en fase capture para preceder a handlers de la pagina.
|
||||
|
||||
## Notas
|
||||
|
||||
Reutilizable en otras apps C++/Python que hablen CDP. La salida via `console.log` es preferible a `Runtime.evaluate` con `returnByValue=true` porque el pick es asincrono (espera click del user) y no encaja en una sola RPC.
|
||||
Reference in New Issue
Block a user