Files
navegator_dashboard/picker_state.cpp
T
egutierrez 8357774b86 chore: auto-commit (11 archivos)
- CMakeLists.txt
- app.md
- main.cpp
- panels.cpp
- appicon.ico
- autoextract_panel.cpp
- picker_state.cpp
- picker_state.h
- py_subprocess.cpp
- py_subprocess.h
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:33:25 +02:00

198 lines
6.4 KiB
C++

#include "picker_state.h"
#include "cdp_ws.h"
#include "py_subprocess.h" // for py_resolve_registry_root
// crude_json del vendor imgui-node-editor (ya linkado por CMakeLists.txt).
#include "crude_json.h"
#include <atomic>
#include <cstdio>
#include <fstream>
#include <mutex>
#include <sstream>
#include <thread>
namespace navegator {
namespace {
std::mutex g_mu;
std::unique_ptr<CdpWs> g_ws;
std::atomic<bool> g_active{false};
std::atomic<bool> g_stop_pump{false};
std::thread g_pump_thread;
PickedElement g_last;
std::string slurp(const std::string& path) {
std::ifstream f(path, std::ios::binary);
if (!f) return "";
std::ostringstream ss; ss << f.rdbuf();
return ss.str();
}
// JS-string-literal escape (NO JSON encoding — el JSON wrapper se hace al construir params).
std::string js_str_escape(const std::string& s) {
std::string out; out.reserve(s.size() + 8);
for (char c : s) {
switch (c) {
case '\\': out += "\\\\"; break;
case '"': out += "\\\""; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if ((unsigned char)c < 0x20) {
char buf[8]; std::snprintf(buf, sizeof(buf), "\\u%04x", (unsigned)c);
out += buf;
} else out += c;
}
}
return out;
}
void pump_loop() {
while (!g_stop_pump.load()) {
std::vector<std::string> msgs;
{
std::lock_guard<std::mutex> lk(g_mu);
if (!g_ws || !g_ws->is_connected()) break;
msgs = g_ws->drain(64);
}
for (const auto& m : msgs) {
crude_json::value v = crude_json::value::parse(m);
if (!v.is_object()) continue;
// Filtrar method=Runtime.consoleAPICalled, args[0].value=="__fn_picked__"
if (!v.contains("method")) continue;
const auto& method = v["method"];
if (!method.is_string()) continue;
if (method.get<std::string>() != "Runtime.consoleAPICalled") continue;
if (!v.contains("params")) continue;
const auto& params = v["params"];
if (!params.is_object() || !params.contains("args")) continue;
const auto& args = params["args"];
if (!args.is_array()) continue;
const auto& args_arr = args.get<crude_json::array>();
if (args_arr.size() < 2) continue;
const auto& a0 = args_arr[0];
if (!a0.is_object() || !a0.contains("value")) continue;
const auto& v0 = a0["value"];
if (!v0.is_string() || v0.get<std::string>() != "__fn_picked__") continue;
const auto& a1 = args_arr[1];
if (!a1.is_object() || !a1.contains("value")) continue;
const auto& v1 = a1["value"];
// v1.value puede ser string (JSON serializado) o un objeto. El JS
// hace console.log("__fn_picked__", JSON.stringify(payload)).
std::string payload;
if (v1.is_string()) payload = v1.get<std::string>();
else payload = v1.dump();
crude_json::value p = crude_json::value::parse(payload);
if (!p.is_object()) continue;
PickedElement el;
auto get = [&](const char* k) -> std::string {
if (!p.contains(k)) return "";
const auto& x = p[k];
if (x.is_string()) return x.get<std::string>();
if (x.is_null()) return "";
return x.dump();
};
el.selector = get("selector");
el.xpath = get("xpath");
el.tag = get("tag");
el.text = get("text");
if (p.contains("rect")) {
el.rect_str = p["rect"].dump();
}
el.valid = true;
{
std::lock_guard<std::mutex> lk(g_mu);
g_last = std::move(el);
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(80));
}
g_active.store(false);
}
} // anon
std::string picker_load_js() {
std::string root = py_resolve_registry_root();
if (root.empty()) return "";
#ifdef _WIN32
std::string path = root + "\\functions\\browser\\cdp_pick_element_js.js";
#else
std::string path = root + "/functions/browser/cdp_pick_element_js.js";
#endif
return slurp(path);
}
std::string picker_start(int /*port*/, const std::string& /*tab_id*/, const std::string& ws_url) {
if (ws_url.empty()) return "no ws_url";
picker_stop();
std::string js = picker_load_js();
if (js.empty()) return "could not load cdp_pick_element_js.js (set FN_REGISTRY_ROOT)";
std::string host, path;
int p = 0;
if (!CdpWs::parse_ws_url(ws_url, host, p, path)) return "invalid ws_url";
auto ws = std::make_unique<CdpWs>();
CdpWsConfig cfg;
cfg.host = host;
cfg.port = p;
cfg.path = path;
cfg.timeout_ms = 5000;
std::string err;
if (!ws->connect(cfg, &err)) return "ws connect failed: " + err;
// Enable Runtime (necesario para consoleAPICalled).
ws->send_command("Runtime.enable", "");
// Inyectar el JS via Runtime.evaluate. expression es el codigo.
// El payload del JS termina con un IIFE; envolvemos en wrapper sin returnByValue.
std::ostringstream params;
params << "{\"expression\":\"" << js_str_escape(js)
<< "\",\"includeCommandLineAPI\":true"
<< ",\"awaitPromise\":false"
<< ",\"returnByValue\":false}";
ws->send_command("Runtime.evaluate", params.str());
{
std::lock_guard<std::mutex> lk(g_mu);
g_ws = std::move(ws);
}
g_stop_pump.store(false);
g_active.store(true);
g_pump_thread = std::thread(pump_loop);
return "";
}
void picker_stop() {
g_stop_pump.store(true);
{
std::lock_guard<std::mutex> lk(g_mu);
if (g_ws) g_ws->close();
}
if (g_pump_thread.joinable()) g_pump_thread.join();
{
std::lock_guard<std::mutex> lk(g_mu);
g_ws.reset();
}
g_active.store(false);
}
bool picker_is_active() { return g_active.load(); }
PickedElement picker_last() {
std::lock_guard<std::mutex> lk(g_mu);
return g_last;
}
void picker_clear_last() {
std::lock_guard<std::mutex> lk(g_mu);
g_last = PickedElement{};
}
} // namespace navegator