feat(enrichers): dispatcher multi-lang go|python|bash (issue 0033 fase A)
Extiende el sistema de enrichers para soportar varios lenguajes en el
mismo registro. El manifest gana dos campos opcionales:
lang: python|go|bash (default: python — retrocompat con los 5
enrichers existentes que no lo declaran)
exec: run (basename del script o binario; default "run")
EnricherSpec ahora lleva `lang`, `exec_basename`, `disabled` y
`disabled_reason`. parse_manifest lee los nuevos campos y aplica
defaults; resolve_run_path busca <dir>/<exec>{.py|.sh|.exe|<vacio>}
segun lang + plataforma. Si el ejecutable no existe (binario Go sin
compilar, script ausente), el spec queda en el registro pero
disabled — enrichers_for_type lo oculta del menu y jobs.cpp aborta
con mensaje claro si llega un job para uno disabled.
run_subprocess (POSIX y Windows) ramifica argv segun lang:
- go -> execv del binario directamente, sin python ni wsl.exe
- bash -> /bin/bash <run_path> (en Windows: wsl.exe -- bash ...)
- python -> python3 <run_path> (default)
El call site en jobs.cpp resuelve run_path y lang via
ge::enricher_by_id() en lugar del hardcode "run.py". Los 5 enrichers
existentes siguen funcionando sin cambios — heredan lang: python por
default.
Tests pytest (22/22 verde):
- 16 regresion: los 5 enrichers actuales siguen pasando.
- 6 nuevos en test_dispatcher_lang.py: parser default a python,
parser lee lang: bash, wire protocol identico para python y
bash, enricher Go sin binario queda disabled, enricher real
sigue funcionando tras el cambio.
NO incluye: runtime Python embebido (fase B) ni badges de lang en
la UI (fase C). El issue 0033 sigue abierto hasta cerrar las dos
fases restantes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+84
-6
@@ -64,10 +64,13 @@ std::vector<std::string> parse_inline_list(const std::string& v) {
|
||||
// name: "Fetch web page"
|
||||
// description: "..."
|
||||
// applies_to: [Webpage, Url]
|
||||
// lang: python <- issue 0033: go|python|bash (default python)
|
||||
// exec: run <- basename del binario/script (default "run")
|
||||
// params: <- v1 ignora bloque
|
||||
// - { name: timeout_s, ... }
|
||||
//
|
||||
// Las claves anidadas bajo `params:` se ignoran (saltamos lineas indentadas).
|
||||
// Las claves anidadas bajo `params:` (y otros bloques con valor vacio
|
||||
// seguido de lineas indentadas) se ignoran.
|
||||
bool parse_manifest(const std::string& path, EnricherSpec* out) {
|
||||
std::ifstream f(path);
|
||||
if (!f) return false;
|
||||
@@ -96,12 +99,74 @@ bool parse_manifest(const std::string& path, EnricherSpec* out) {
|
||||
else if (key == "name") out->name = strip_quotes(val);
|
||||
else if (key == "description") out->description = strip_quotes(val);
|
||||
else if (key == "applies_to") out->applies_to = parse_inline_list(val);
|
||||
else if (key == "params" && val.empty()) in_skip_block = true;
|
||||
// emits/relations los ignoramos en v1 (solo informativos).
|
||||
else if (key == "lang") out->lang = lower(strip_quotes(val));
|
||||
else if (key == "exec") out->exec_basename = strip_quotes(val);
|
||||
else if (key == "params" && val.empty()) in_skip_block = true;
|
||||
else if (key == "emits" && val.empty()) in_skip_block = true;
|
||||
else if (key == "relations" && val.empty()) in_skip_block = true;
|
||||
}
|
||||
|
||||
// Defaults — preservan retrocompat con manifests existentes que no
|
||||
// declaran lang/exec.
|
||||
if (out->lang.empty()) out->lang = "python";
|
||||
if (out->exec_basename.empty()) out->exec_basename = "run";
|
||||
|
||||
// Validar lang reconocido. Manifests con lang invalido se cargan
|
||||
// pero quedan disabled — asi la UI puede informar y el usuario
|
||||
// arregla el manifest.
|
||||
if (out->lang != "python" && out->lang != "go" && out->lang != "bash") {
|
||||
out->disabled = true;
|
||||
out->disabled_reason = "lang invalido: '" + out->lang + "'";
|
||||
}
|
||||
|
||||
return !out->id.empty();
|
||||
}
|
||||
|
||||
// Resuelve el path al ejecutable/script segun lang + plataforma.
|
||||
// Devuelve "" si no encuentra el archivo y rellena `reason`.
|
||||
std::string resolve_run_path(const std::string& dir,
|
||||
const EnricherSpec& spec,
|
||||
std::string* reason) {
|
||||
#ifdef _WIN32
|
||||
const char sep = '\\';
|
||||
const char* go_ext = ".exe";
|
||||
#else
|
||||
const char sep = '/';
|
||||
const char* go_ext = "";
|
||||
#endif
|
||||
auto exists = [](const std::string& p) {
|
||||
struct stat st{};
|
||||
return stat(p.c_str(), &st) == 0 && !S_ISDIR(st.st_mode);
|
||||
};
|
||||
|
||||
std::string base = dir + sep + spec.exec_basename;
|
||||
|
||||
if (spec.lang == "python") {
|
||||
std::string p = base + ".py";
|
||||
if (exists(p)) return p;
|
||||
if (reason) *reason = "no existe " + p;
|
||||
return "";
|
||||
}
|
||||
if (spec.lang == "bash") {
|
||||
std::string p = base + ".sh";
|
||||
if (exists(p)) return p;
|
||||
if (reason) *reason = "no existe " + p;
|
||||
return "";
|
||||
}
|
||||
if (spec.lang == "go") {
|
||||
// En Windows: <base>.exe. En Linux: <base> (sin extension).
|
||||
std::string p = base + go_ext;
|
||||
if (exists(p)) return p;
|
||||
if (reason) {
|
||||
*reason = "binario Go no compilado: " + p
|
||||
+ " (corre el build script del enricher)";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
if (reason) *reason = "lang no soportado";
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int enrichers_load(const char* enrichers_dir) {
|
||||
@@ -135,16 +200,28 @@ int enrichers_load(const char* enrichers_dir) {
|
||||
if (stat(sub.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) continue;
|
||||
|
||||
std::string manifest = sub + sep + "manifest.yaml";
|
||||
std::string runpy = sub + sep + "run.py";
|
||||
if (stat(manifest.c_str(), &st) != 0) continue;
|
||||
if (stat(runpy.c_str(), &st) != 0) continue;
|
||||
|
||||
EnricherSpec spec;
|
||||
if (!parse_manifest(manifest, &spec)) {
|
||||
std::fprintf(stderr, "[enrichers] parse failed: %s\n", manifest.c_str());
|
||||
continue;
|
||||
}
|
||||
spec.run_path = runpy;
|
||||
|
||||
// Resolver el ejecutable segun lang. Si falla (binario Go no
|
||||
// compilado, script ausente, etc.) registramos el spec como
|
||||
// disabled — sigue apareciendo en `enrichers_all()` para que
|
||||
// la UI pueda mostrar warning, pero `enrichers_for_type` lo
|
||||
// oculta del menu de ejecucion.
|
||||
std::string reason;
|
||||
std::string run_path = resolve_run_path(sub, spec, &reason);
|
||||
if (run_path.empty()) {
|
||||
spec.disabled = true;
|
||||
if (spec.disabled_reason.empty()) spec.disabled_reason = reason;
|
||||
std::fprintf(stderr, "[enrichers] %s deshabilitado: %s\n",
|
||||
spec.id.c_str(), spec.disabled_reason.c_str());
|
||||
}
|
||||
spec.run_path = run_path;
|
||||
g_enrichers.push_back(std::move(spec));
|
||||
}
|
||||
closedir(d);
|
||||
@@ -165,6 +242,7 @@ std::vector<EnricherSpec> enrichers_for_type(const char* type_ref) {
|
||||
if (!type_ref || !*type_ref) return out;
|
||||
std::string want = lower(type_ref);
|
||||
for (const auto& e : g_enrichers) {
|
||||
if (e.disabled) continue; // no ofrecer enrichers no resueltos
|
||||
if (e.applies_to.empty()) {
|
||||
out.push_back(e);
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user