From 3c9cb21cca443b8caef5aa180827a6989e258a95 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Wed, 19 Aug 2026 15:14:55 -0300 Subject: [PATCH] feat(ad-hoc): add mesh interaction scripts for Discord integration --- scripts/ad-hoc/discord-en.json | 212 +++++++++++++++++++++++++++++ scripts/ad-hoc/mesh-run.mjs | 93 +++++++++++++ scripts/ad-hoc/mesh-send.mjs | 42 ++++++ scripts/ad-hoc/verify-coverage.mjs | 36 +++++ 4 files changed, 383 insertions(+) create mode 100644 scripts/ad-hoc/discord-en.json create mode 100644 scripts/ad-hoc/mesh-run.mjs create mode 100644 scripts/ad-hoc/mesh-send.mjs create mode 100644 scripts/ad-hoc/verify-coverage.mjs diff --git a/scripts/ad-hoc/discord-en.json b/scripts/ad-hoc/discord-en.json new file mode 100644 index 0000000000..c0d668592b --- /dev/null +++ b/scripts/ad-hoc/discord-en.json @@ -0,0 +1,212 @@ +[ + { + "bucket": "A", + "match": "failed to load external module playwright", + "text": "That error means the Playwright install shipped with OmniRoute is broken, not that you misconfigured anything. Reinstall with npm i -g omniroute and run npx playwright install chromium on the same host. See open-sse/executors/gemini-web.ts." + }, + { + "bucket": "A", + "match": "duckduckgo ai chat error", + "text": "That ERR_BAD_REQUEST usually means the model you picked is retired or unknown in Duck.ai's lineup, or a reasoningEffort setting the lineup doesn't accept. Try a current model like gpt-5.4-mini. See open-sse/executors/duckduckgo-web.ts." + }, + { + "bucket": "A", + "match": "what does endpoints do", + "text": "Endpoints are the OpenAI-compatible surface OmniRoute exposes. You point any client at base http://localhost:20128/v1 with your API key and it behaves like a normal provider. For opencode there is a dedicated guide at docs/frameworks/OPENCODE.md." + }, + { + "bucket": "A", + "match": "setup omniroute in opencode", + "text": "You don't need /connect. Run 'omniroute config opencode --base-url http://localhost:20128 --api-key YOUR_KEY' and point opencode at it. The most common bug is ending with /v1/v1, so keep a single /v1. See docs/frameworks/OPENCODE.md." + }, + { + "bucket": "A", + "match": "best way to integrate jules", + "text": "Use the Cloud Agents API: POST /api/v1/agents/tasks with providerId jules and OmniRoute spins a remote agent for that task. Selection is manual per task and control is via REST or the dashboard. See docs/frameworks/CLOUD_AGENT.md." + }, + { + "bucket": "A", + "match": "codex cloud and devin", + "text": "Same API, just swap the providerId: jules, devin, codex-cloud or cursor-cloud. Antigravity and Qwen are chat providers, not cloud agents, so they stay on chat routes. The choice is manual per task. See docs/frameworks/CLOUD_AGENT.md." + }, + { + "bucket": "A", + "match": "handle everything from claude", + "text": "Not quite. Cloud agents are controlled through the REST API and the dashboard, not through Claude Code or MCP. So keep them as separate tooling that talks to OmniRoute. See docs/frameworks/CLOUD_AGENT.md." + }, + { + "bucket": "A", + "match": "huggingchat returned http 500", + "text": "A 500 is a passthrough from the upstream HuggingChat endpoint (huggingface.co/chat), not something in your config. Just retry; if it keeps failing the service itself is likely having trouble. See open-sse/executors/huggingchat.ts." + }, + { + "bucket": "A", + "match": "use this on termux", + "text": "In Termux run 'pkg install nodejs' and then 'npx -y omniroute' to start the server. Your phone browser opens the dashboard over localhost afterwards. Walkthrough at docs/guides/TERMUX_GUIDE.md." + }, + { + "bucket": "A", + "match": "run the entire thing im on android", + "text": "You run everything in Termux with no root: pkg install nodejs, then npx -y omniroute starts the server. The dashboard opens in your phone's browser and all of it stays on the device." + }, + { + "bucket": "A", + "match": "i dont have omniroute", + "text": "Quick start: npm i -g omniroute on any machine with Node. Start it, open http://localhost:20128, and the auto model already answers so you don't even need an API key to try it." + }, + { + "bucket": "A", + "match": "api endpoints allowed", + "text": "Endpoints are their own API surface: anyone with a valid API key can call them. To lock it down, set REQUIRE_API_KEY=true so only the keys you issue get access. See docs/getting-started/QUICK-START.md." + }, + { + "bucket": "A", + "match": "need which host", + "text": "The host is wherever you run the server, localhost:20128 by default. Clients just need the base URL (http://host:20128/v1) plus an API key, so a VPS or Fly instance works the same." + }, + { + "bucket": "A", + "match": "hosting web in cpanel", + "text": "Self-host anywhere Node runs: a VPS, Docker or Fly.io. cPanel usually can't keep a long-running Node process alive, so prefer a real server or container. See docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md." + }, + { + "bucket": "A", + "match": "run fly.io docker file", + "text": "Use the repo's fly.toml: fly launch and then fly deploy, and the Dockerfile builds the image. Full steps and env vars are in docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md." + }, + { + "bucket": "A", + "match": "website https://fly.io", + "text": "Yes, the site runs on Fly.io, that's the host. From the repo run fly launch and fly deploy, and the app gets a public URL on a domain you own." + }, + { + "bucket": "A", + "match": "github are down", + "text": "You don't need GitHub to run OmniRoute. It installs straight from npm and you host it anywhere you want, a VPS, Docker, or Fly. GitHub matters only if you build from source." + }, + { + "bucket": "A", + "match": "2000 models is there a better way", + "text": "With that many models, Auto-Combo is the way: set the model to auto, auto/coding, auto/fast or auto/cheap and OmniRoute scores every option per request. The 14-factor scorer is in docs/routing/AUTO-COMBO.md." + }, + { + "bucket": "A", + "match": "is there a combo already", + "text": "Yes, there is a ready one for exactly this: auto/coding. It picks a good free coding model with no setup. The other auto strategies are explained in docs/routing/AUTO-COMBO.md." + }, + { + "bucket": "A", + "match": "where do i put auto", + "text": "You set it as the model field on your client exactly like a model name: auto/coding, or auto/fast and auto/cheap for other strategies. Their differences are in docs/routing/AUTO-COMBO.md." + }, + { + "bucket": "A", + "match": "dont see my combos as models", + "text": "Only auto/ combos are advertised in /v1/models. Custom combos are internal destinations that never appear in the list, so call them directly by the combo id you set up." + }, + { + "bucket": "A", + "match": "where is my circuit breaker", + "text": "It lives in the dashboard Health tab, in the circuit breaker states section, one status per provider. The closed, open, half-open model is in docs/architecture/RESILIENCE_GUIDE.md." + }, + { + "bucket": "A", + "match": "with claude desktop app", + "text": "Two ways: Claude Code pointed at OmniRoute via ANTHROPIC_BASE_URL plus setup-claude, or the Claude Desktop app as an MCP client via omniroute --mcp. Both are in docs/guides/CLAUDE-CODE-CONFIGURATION.md." + }, + { + "bucket": "A", + "match": "retrying in 30s", + "text": "That is a 429 rate limit from the provider, so retrying is expected. OmniRoute applies the cooldown and can fall back to another key or model automatically, so you don't need to touch anything." + }, + { + "bucket": "A", + "match": "cliproxyapi is configured", + "text": "It is informative, not an error. CLIProxyAPI is an upstream proxy layer, managed at runtime in the CLI Tools and toggled per provider between native, cliproxyapi and fallback modes. See docs/ops/PROXY_GUIDE.md." + }, + { + "bucket": "A", + "match": "getaddrinfo enotfound", + "text": "That is a doubled URL in the proxy registry: the host field carries the scheme. Use type=http, host=127.0.0.1 with no scheme, and port=20130. Steps are in docs/ops/PROXY_GUIDE.md." + }, + { + "bucket": "A", + "match": "proxy connection failed", + "text": "The registry expects type, host and port as separate fields, not one combined URL. Set host to 127.0.0.1 with no scheme and port to 20130, and the connection error clears. Same recipe in docs/ops/PROXY_GUIDE.md." + }, + { + "bucket": "B", + "match": "only 14 providers out of the 50", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "music play when i enable modal", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "do you mean the global proxy", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "provider's limits from docs", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "combine deepseek", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "store limits within the app", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "manually write these limits", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "official omniroute doesn't support", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "set limits in omniroute for a provider", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "create a compact prompt", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "continue your answer from where you left off", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "i am android that sorry", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "B", + "match": "yes", + "text": "I don't know this one yet - I've flagged it for someone who does, they'll get back to you soon :)" + }, + { + "bucket": "C", + "match": "no such tool available: bash", + "text": "Here I only help with OmniRoute questions :)" + }, + { + "bucket": "C", + "match": "interupt the code", + "text": "Here I only help with OmniRoute questions :)" + } +] diff --git a/scripts/ad-hoc/mesh-run.mjs b/scripts/ad-hoc/mesh-run.mjs new file mode 100644 index 0000000000..95c45efd7d --- /dev/null +++ b/scripts/ad-hoc/mesh-run.mjs @@ -0,0 +1,93 @@ +// Runner generico para a mesh. Recebe um arquivo JSON de plano: +// [ +// { "bucket": "A"|"B"|"C", "match": "", "text": "" } +// ] +// Fases: A=reply (answered), B=notice mode:note (fica pending), C=recusa note + mark ignored (por ultimo). +// Envs: BOT_URL, BOT_TOKEN. Uso: node mesh-run.mjs +import { readFileSync } from "node:fs"; +import { env } from "node:process"; + +const BOT_URL = env.BOT_URL; +const BOT_TOKEN = env.BOT_TOKEN; +const FILTER = "platform=discord&language=en&direct=only"; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +async function api(path, opts = {}) { + const res = await fetch(BOT_URL + path, { + ...opts, + headers: { + Authorization: "Bearer " + BOT_TOKEN, + "Content-Type": "application/json", + ...(opts.headers || {}), + }, + }); + return { status: res.status, json: await res.json().catch(() => null) }; +} + +const planPath = process.argv[2]; +const plan = JSON.parse(readFileSync(planPath, "utf8")); + +async function main() { + console.log("Fetching pendentes (" + FILTER + ")..."); + const { json } = await api("/internal/bridge/questions?" + FILTER); + const pending = (json && json.data) || []; + console.log("-> " + pending.length + " pendentes"); + + const out = { A: [], B: [], C: [], U: [] }; + + // fase 1-2: A (reply) e B (notice) + for (const msg of pending) { + const t = (msg.text || "").toLowerCase(); + const hit = plan.find((e) => t.includes(e.match.toLowerCase())); + if (!hit) { + out.U.push(msg.id + " :: " + (msg.text || "").slice(0, 60)); + continue; + } + if (hit.bucket === "A") { + const r = await api("/internal/bridge/reply", { + method: "POST", + body: JSON.stringify({ messageId: msg.id, text: hit.text }), + }); + out.A.push(r.status + " " + msg.id); + } else if (hit.bucket === "B") { + const r = await api("/internal/bridge/reply", { + method: "POST", + body: JSON.stringify({ messageId: msg.id, text: hit.text, mode: "note" }), + }); + out.B.push(r.status + " " + msg.id); + } else if (hit.bucket === "C") { + out.C.push(msg.id); + } + await sleep(1000); + } + + // fase 3: bucket C — nota de recusa + mark ignored (por último) + for (const id of out.C) { + const msg = pending.find((m) => m.id === id); + const t = (msg.text || "").toLowerCase(); + const hit = plan.find((e) => e.bucket === "C" && t.includes(e.match.toLowerCase())); + if (!hit) continue; + const note = await api("/internal/bridge/reply", { + method: "POST", + body: JSON.stringify({ messageId: id, text: hit.text, mode: "note" }), + }); + const mark = await api("/internal/bridge/mark", { + method: "POST", + body: JSON.stringify({ messageIds: [id], status: "ignored", ref: "auto-declined" }), + }); + out.C[out.C.indexOf(id)] = + "note:" + note.status + " mark:" + (mark.json && mark.json.updated) + " " + id; + await sleep(1000); + } + + console.log("\n=== RESUMO ==="); + console.log("A (respondidas):", out.A); + console.log("B (notices, pending):", out.B); + console.log("C (recusa+ignoradas):", out.C); + console.log("U (nao classif., relatar):", out.U); +} + +main().catch((e) => { + console.error("ERRO:", e); + process.exit(1); +}); diff --git a/scripts/ad-hoc/mesh-send.mjs b/scripts/ad-hoc/mesh-send.mjs new file mode 100644 index 0000000000..8c6feb6de6 --- /dev/null +++ b/scripts/ad-hoc/mesh-send.mjs @@ -0,0 +1,42 @@ +// Helper único para enviar replies/notes no bridge do bot da mesh. +// Lê BOT_URL e BOT_TOKEN do ambiente (nunca embutidos). +// Uso: BOT_URL=... BOT_TOKEN=... node scripts/ad-hoc/mesh-send.mjs +// cmd: reply | note +import { readFileSync } from "node:fs"; + +const [cmd, path] = process.argv.slice(2); +const BOT_URL = process.env.BOT_URL; +const BOT_TOKEN = process.env.BOT_TOKEN; + +const input = path === "-" ? readFileSync(0, "utf8") : readFileSync(path, "utf8"); +const items = JSON.parse(input); + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +async function send(item) { + // endpoint de reply; mode presente => note + const body = { + messageId: item.id, + text: item.text, + ...(cmd === "note" ? { mode: "note" } : {}), + }; + const res = await fetch(`${BOT_URL}/internal/bridge/reply`, { + method: "POST", + headers: { + Authorization: `Bearer ${BOT_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + const txt = await res.text(); + console.log(`[${cmd}] ${item.id.slice(0, 12)} → ${res.status} ${txt.slice(0, 80)}`); +} + +for (const item of items) { + try { + await send(item); + } catch (e) { + console.log(`[${cmd}] ${item.id.slice(0, 12)} → ERRO ${e.message}`); + } + await sleep(1000); // pace ~1s +} diff --git a/scripts/ad-hoc/verify-coverage.mjs b/scripts/ad-hoc/verify-coverage.mjs new file mode 100644 index 0000000000..05b05384d9 --- /dev/null +++ b/scripts/ad-hoc/verify-coverage.mjs @@ -0,0 +1,36 @@ +// Verificacao de cobertura do plano da mesh. +// Envs: BOT_URL, BOT_TOKEN. Uso: node verify-coverage.mjs +import { readFileSync } from "node:fs"; +import { env } from "node:process"; + +const BOT_URL = env.BOT_URL; +const BOT_TOKEN = env.BOT_TOKEN; +const FILTER = "platform=discord&language=en&direct=only"; + +const plan = JSON.parse(readFileSync(process.argv[2], "utf8")); + +const res = await fetch(BOT_URL + "/internal/bridge/questions?" + FILTER, { + headers: { Authorization: "Bearer " + BOT_TOKEN }, +}); +const json = await res.json(); +const pending = json.data || []; + +const gaps = []; +const amb = []; + +for (const m of pending) { + const t = (m.text || "").toLowerCase(); + const hits = plan.filter((e) => t.includes(e.match.toLowerCase())); + if (hits.length === 0) { + gaps.push(m.id + " :: " + t.slice(0, 80)); + } else if (hits.length > 1) { + const names = hits.map((h) => h.bucket + ":" + h.match).join(" | "); + amb.push(m.id + " :: " + names + " :: " + t.slice(0, 50)); + } +} + +console.log("pendentes:", pending.length, "| plano:", plan.length); +console.log("\n[GAPS] sem match (" + gaps.length + "):"); +for (const g of gaps) console.log(" -", g); +console.log("\n[AMB] >1 match (" + amb.length + "):"); +for (const a of amb) console.log(" -", a);