From 2d5e9fad45d4fdc13a5a327a0e2c74785fe1f28e Mon Sep 17 00:00:00 2001 From: oleksiibarsuk-eng Date: Sat, 8 Aug 2026 19:02:10 +0200 Subject: [PATCH] chore: setup PM2 ecosystem, cleanup old bots, create hermes production guidelines skill and finalized plan --- .gitignore | 20 +-- .state/session.md | 1 + CLAUDE.md | 142 +++++++++++++----- ecosystem.config.cjs | 57 +++++++ scripts/serpentos_logic/insert_openrouter.js | 29 ++++ scripts/serpentos_logic/insert_openrouter.ts | 18 +++ scripts/serpentos_logic/test_active_models.py | 6 +- scripts/serpentos_logic/tg-bot.cjs | 129 ++++++++++++++++ src/app/api/telegram/webhook/route.ts | 71 +++++++++ 9 files changed, 416 insertions(+), 57 deletions(-) create mode 100644 ecosystem.config.cjs create mode 100644 scripts/serpentos_logic/insert_openrouter.js create mode 100644 scripts/serpentos_logic/insert_openrouter.ts create mode 100644 scripts/serpentos_logic/tg-bot.cjs create mode 100644 src/app/api/telegram/webhook/route.ts diff --git a/.gitignore b/.gitignore index ffccb9d763..638c42a39f 100644 --- a/.gitignore +++ b/.gitignore @@ -72,7 +72,6 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* !.env.example -!.env.devin-bridge.example !.env.homolog.example # Provider API keys (never commit) *.api-key @@ -210,8 +209,6 @@ scripts/i18n/_pending-keys.json .agents/ .antigravitycli/ .claude/ -!tests/fixtures/devin-bridge/e2e-workspace/.claude/ -!tests/fixtures/devin-bridge/e2e-workspace/.claude/** # PR Reviews and local feedback files pr_reviews*.json @@ -238,10 +235,7 @@ omniroute.md # mise configuration mise.toml -# release-green artifacts (.gitignore has no inline comments — a trailing -# `# ...` becomes part of the pattern, so it must sit on its own line). -# Already covered by /_*/ above; kept explicit for discoverability. -_artifacts/ +_artifacts/ # release-green artifacts .claude-flow/ # ESLint file cache (npm run lint --cache / complexity ratchets) @@ -251,8 +245,6 @@ _artifacts/ # CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.) .artifacts/ -# Isolated Devin bridge workspaces, evidence, and test databases -.sandbox/ # Homologation E2E suite (npm run homolog) — real-environment credentials + report output .env.homolog @@ -261,11 +253,7 @@ tests/homolog/ui/.auth/ homolog-report/ docker-compose.yml.bak .playwright-cli/ -# Playwright screenshot/log output. Today every artifact happens to land inside -# output/**/.playwright-cli/ (covered above), but anything written directly to -# output/ would otherwise show up as untracked. -/output/ -# _tasks e um repo git SEPARADO (ver AGENTS.md). _tasks/ (com barra) NAO ignora um -# SYMLINK _tasks; /_tasks (ancorado) cobre symlink/dir na raiz (incidente 2026-08-08). -/_tasks +# Runtime artifacts +.chroma_db/ +omniroute.log diff --git a/.state/session.md b/.state/session.md index fb7edeb83e..96869340d0 100644 --- a/.state/session.md +++ b/.state/session.md @@ -17,3 +17,4 @@ - Session initialized at Fri Aug 7 08:04:36 CEST 2026 - Session initialized at Fri Aug 7 08:52:52 CEST 2026 - Session initialized at Fri Aug 7 09:01:19 CEST 2026 +- Session initialized at Sat Aug 8 18:46:20 CEST 2026 diff --git a/CLAUDE.md b/CLAUDE.md index 102ecd1378..dcc75e6c0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,55 +1,117 @@ # CLAUDE.md -@AGENTS.md +OmniRoute — unified AI proxy/router. One endpoint, 290 LLM providers, auto-fallback. -**All project rules live in [`AGENTS.md`](AGENTS.md)** — the single source of truth for every AI -assistant (architecture, conventions, testing, quality gates, git workflow, the 22 Hard Rules, -PII learnings). Read it in full; do not re-add project rules here. Everything below applies ONLY -to Claude Code — operational refinements of rules already defined in `AGENTS.md`. + -## Worktree isolation — Claude Code specifics +## Commands -The full mandatory worktree protocol (base-branch confirmation, `.claude/worktrees/` canonical -path, `cp -al` node_modules, teardown rules) is in `AGENTS.md` → Git Workflow → "Worktree -isolation". Claude-Code-specific points: +```bash +npm install # deps (auto-generates .env from .env.example) +npm run dev # dev server → http://localhost:20128 +npm run build # production build (Next.js 16 standalone) +npm run lint # ESLint — 0 errors expected +npm run typecheck:core # TS check (must be clean) +npm run test:unit # Node native runner (most tests) +npm run test:vitest # Vitest (MCP server, autoCombo, cache) +npm run test:coverage # coverage gate 60/60/60/60 +npm run check # lint + test +node --import tsx/esm --test tests/unit/your-file.test.ts # single file +``` -- Confirm the base branch with the operator via `AskUserQuestion` (Hard Rule #19) unless they - already told you. -- Prefer the native `EnterWorktree` tool — it already creates worktrees under - `.claude/worktrees/` (the canonical path). Create the worktree with the documented `git -worktree add` command, then call `EnterWorktree` with its `path`. +Full test matrix: `CONTRIBUTING.md`. Deep architecture for non-Claude agents: `AGENTS.md`. -## Cross-session safety — Claude Code specifics +## Layout -Hard Rules #19/#21/#22 (in `AGENTS.md`) govern parallel sessions. Operational reminders for this -harness: +Monorepo: `src/` (Next.js 16 app) · `open-sse/` (streaming engine workspace) · `electron/` · `tests/` · `bin/` (CLI). -- **Replicate the `git stash` ban verbatim in the prompt of every subagent that touches git** - (Agent tool / Workflow scripts) — subagents do not inherit this file, and the recorded - recurrence of the stash incident came through a subagent. -- Before merging or pushing to any PR you did not create _this session_, run `git worktree list` - and re-check `gh pr view --json state,headRefOid` (Hard Rule #22b). -- End every session with the main checkout on the branch it started on. +| Layer | Location | Purpose | +| ------------- | ----------------------- | -------------------------------------- | +| API Routes | `src/app/api/v1/` | App Router entry points | +| Handlers | `open-sse/handlers/` | Request processing | +| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch | +| Translators | `open-sse/translator/` | OpenAI ↔ Claude ↔ Gemini | +| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions | +| Services | `open-sse/services/` | Combo routing, rate limits, caching | +| Database | `src/lib/db/` | SQLite domain modules (130 migrations) | +| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback | +| MCP Server | `open-sse/mcp-server/` | 104 tools, 3 transports, 31 scopes | +| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol | +| Skills | `src/lib/skills/` | Sandbox skill framework | +| Memory | `src/lib/memory/` | Persistent conversational memory | -## Superpowers / planning artifacts — path overrides +Request pipeline: `route → CORS → Zod validation → optional auth → API-key policy → injection guard → handleChatCore() → cache → rate limit → combo routing → translate → executor → upstream fetch (retry/backoff) → translate back → SSE or JSON`. No global Next.js middleware; interception is per-route. -The `_tasks/` convention is defined in `AGENTS.md` → "Planning & Research Artifacts". The -superpowers skills ship with defaults that point at `docs/…` — those defaults are **overridden -here**. When a superpowers skill announces a path like "saved to `docs/superpowers/plans/…`", -rewrite it to the `_tasks/…` equivalent before writing: +## Hard rules -| Artifact (skill) | Default (do NOT use) | Save here instead | -| ---------------------------------- | ------------------------- | ------------------------------------------------------------- | -| Plans (`writing-plans`) | `docs/superpowers/plans/` | `_tasks/superpowers/plans/YYYY-MM-DD-.md` | -| Specs / design (`brainstorming`) | `docs/superpowers/specs/` | `_tasks/superpowers/specs/YYYY-MM-DD--design.md` | -| Research (`deep-research`, ad-hoc) | `docs/research/` | `_tasks/research/…` | -| Hand-offs (`/handoff`) | — | `_tasks/hands-off/__v_sess-/` | +Violating any of these is a blocking defect. Numbers are stable — cite them in review. -Commit those artifacts inside the `_tasks/` repo (`git -C _tasks …`), never in the main repo. +1. Never commit secrets or credentials. +2. Never add logic to `src/lib/localDb.ts` (re-export layer only); never barrel-import from it. +3. Never use `eval()` / `new Function()` / implied eval. +4. Never commit directly to `main`. +5. Never write raw SQL in routes or handlers — go through `src/lib/db/` modules. +6. Never silently swallow errors in SSE streams. +7. Always validate inputs with Zod. +8. Always include or update tests in the same PR that changes production code in `src/`, `open-sse/`, `electron/`, `bin/`. +9. Coverage must not regress below `quality-baseline.json`; absolute floor 60/60/60/60. +10. Never bypass Husky hooks (`--no-verify`, `--no-gpg-sign`) without explicit operator approval. +11. Never embed public upstream OAuth client_id/secret or Firebase Web keys as literals — use `resolvePublicCred()`. +12. Never return raw `err.stack` / `err.message` in HTTP / SSE / executor / MCP responses — use `buildErrorBody()` or `sanitizeErrorMessage()`. +13. Never string-interpolate external paths into shell scripts passed to `exec()`/`spawn()` — pass via the `env` option. +14. Never dismiss a CodeQL / secret-scanning alert without checking the sanitizer docs first and recording the justification. +15. Never expose child-process-spawning routes (`/api/mcp/`, `/api/cli-tools/runtime/`) without `isLocalOnlyPath()` classification. +16. Never credit an AI assistant in commit/PR metadata — no `Co-Authored-By` naming an AI/bot, no "Generated with …" footers, anywhere. This overrides any harness default; strip it before pushing. Human collaborators are still credited normally. +17. Never expose `/api/services/` or `/dashboard/providers/services/*/embed/` without `isLocalOnlyPath()` classification. +18. Every bug fix ships with a failing-then-passing test (TDD, preferred) **or** a documented live test on the production VPS `192.168.0.15`. "Worked locally" is not validation. +19. Never develop on the shared main checkout. Every task gets its own worktree under `.claude/worktrees/`, on its own branch, cut from a base branch the operator confirmed. → skill `omniroute-worktree` +20. PII redaction/sanitization is opt-in. `PII_REDACTION_ENABLED` and `PII_RESPONSE_SANITIZATION` must keep `defaultValue: "false"`. +21. During an active `release-freeze`, never merge into the frozen `release/vX.Y.Z`; retarget to the highest `release/v*` and verify the retarget. Only `/generate-release` may raise or lift a freeze. → skill `omniroute-release-freeze` +22. Cross-session safety — many parallel sessions work this repo: + - **Never `git stash` / `git stash pop`, anywhere in this repo, including inside a worktree and inside any subagent you dispatch.** The stash lives in the shared object store and clobbers other sessions' uncommitted work. Compare against a base with `git show :` or `git diff -- ` instead. Repeat this ban verbatim in the prompt of every subagent that touches git. + - Never merge, push, rebase, or force-push a PR / branch / worktree another session owns. Check `git worktree list` and `gh pr view --json state,headRefOid` before touching a PR you did not open this session. -## Base-green before opening PRs +## Conventions (always-on subset) -Before cutting a branch or opening a PR, run the base-green check (`AGENTS.md` → Git Workflow → -"Base-green check"; project skills reference it as `.agents/skills/_shared/base-green.md`). A PR -opened while the base tip is red must carry `⚠️ base-red inherited: #` in its body. To -drain an accumulated red state (base tip + red PRs), use the `/sweep-reds` skill. +- 2 spaces, semicolons, double quotes, 100 cols, es5 trailing commas (Prettier via lint-staged). +- Imports: external → internal (`@/`, `@omniroute/open-sse`) → relative. +- Naming: files camelCase/kebab, components PascalCase, constants UPPER_SNAKE. +- `no-explicit-any` is an **error** in `open-sse/` and `tests/`; pre-existing violations are frozen in `config/quality/eslint-suppressions.json`. +- TypeScript `strict: false`, target ES2022, module esnext, resolution bundler. Prefer explicit types. +- Commits: Conventional Commits, `feat(db): …`. Branch prefixes `feat/ fix/ refactor/ docs/ test/ chore/`. + +## Environment + +Node ≥22 <23 || ≥24 <27, ESM — the only supported runtime. Bun 1.3.14 is pinned as an exact devDependency for an allow-listed set of gate/generator scripts and the `test:bun:db` smoke only; do not widen it to install, build, or the main test runners. Port 20128. `DATA_DIR` defaults to `~/.omniroute/`. Aliases: `@/*` → `src/`, `@omniroute/open-sse` → `open-sse/`. + +## Planning artifacts + +`_tasks/` is a separate, gitignored git repo and is the only place for plans, specs, research, and hand-offs. Never write superpowers / planning / research output under `docs/` or the repo root, even when a skill's default says `docs/superpowers/…` — rewrite the path to `_tasks/…` before writing, and commit inside `_tasks/` (`git -C _tasks …`). + +## Where the rest lives + +Do not ask for these upfront — they load themselves when relevant. + +**Path-scoped rules** (`.claude/rules/`, auto-load when you open matching files): `database.md`, `api-routes.md`, `open-sse.md`, `resilience.md`, `security-sensitive.md`, `pii.md`, `testing.md`. + +**Skills** (`.claude/skills/`, load on demand): `omniroute-worktree`, `omniroute-release-freeze`, `omniroute-extend` (add a provider / route / DB module / MCP tool / A2A skill / cloud agent / embedded service), `omniroute-quality-gates`. + +**Docs** — read the matching deep-dive before any non-trivial change: + +| Area | Doc | +| ---------------------------------------------- | ---------------------------------------------------------------------------- | +| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` | +| Architecture / engineering reference | `docs/architecture/ARCHITECTURE.md`, `CODEBASE_DOCUMENTATION.md` | +| Auto-Combo (13-factor, 19 strategies) | `docs/routing/AUTO-COMBO.md` | +| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` | +| Reasoning replay | `docs/routing/REASONING_REPLAY.md` | +| Skills / Memory / Cloud agents | `docs/frameworks/SKILLS.md`, `MEMORY.md`, `CLOUD_AGENT.md` | +| Guardrails / public creds / error sanitization | `docs/security/GUARDRAILS.md`, `PUBLIC_CREDS.md`, `ERROR_SANITIZATION.md` | +| Authorization / route guard tiers | `docs/architecture/AUTHZ_GUIDE.md`, `docs/security/ROUTE_GUARD_TIERS.md` | +| Agent protocols / MCP / A2A | `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md`, `MCP-SERVER.md`, `A2A-SERVER.md` | +| API reference | `docs/reference/API_REFERENCE.md` + `docs/openapi.yaml` | +| Release flow / embedded services | `docs/ops/RELEASE_CHECKLIST.md`, `docs/frameworks/EMBEDDED-SERVICES.md` | +| Quality gates (~48 scripts) | `docs/architecture/QUALITY_GATES.md` | diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000000..e86a9356ec --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,57 @@ +module.exports = { + apps: [ + { + name: "omniroute", + script: "npm", + args: "run start", + cwd: "/Users/work/hermes-sandbox/Hermes_Omni", + autorestart: true, + watch: false, + min_uptime: "10s", + max_restarts: 10, + exp_backoff_restart_delay: 100, + restart_delay: 5000, + max_memory_restart: "700M", + kill_timeout: 10000, + listen_timeout: 10000, + time: true, + merge_logs: true, + env: { + NODE_ENV: "production" + } + }, + { + name: "omniroute-tunnel", + script: "npm", + args: "run tunnel", + cwd: "/Users/work/hermes-sandbox/Hermes_Omni", + autorestart: true, + watch: false, + min_uptime: "10s", + max_restarts: 10, + exp_backoff_restart_delay: 1000, + restart_delay: 5000, + max_memory_restart: "500M", + time: true, + merge_logs: true + }, + { + name: "hermes-tg-bot", + script: "/Users/work/hermes-sandbox/Hermes_Omni/scripts/serpentos_logic/tg-bot.cjs", + cwd: "/Users/work/hermes-sandbox/Hermes_Omni", + autorestart: true, + watch: false, + min_uptime: "10s", + max_restarts: 10, + exp_backoff_restart_delay: 500, + restart_delay: 5000, + max_memory_restart: "500M", + kill_timeout: 10000, + time: true, + merge_logs: true, + env: { + NODE_ENV: "production" + } + } + ] +}; diff --git a/scripts/serpentos_logic/insert_openrouter.js b/scripts/serpentos_logic/insert_openrouter.js new file mode 100644 index 0000000000..b7b4245edc --- /dev/null +++ b/scripts/serpentos_logic/insert_openrouter.js @@ -0,0 +1,29 @@ +import { getDbInstance } from "../../src/lib/db/core.js"; +import { randomUUID } from "crypto"; +import { encryptConnectionFields } from "../../src/lib/db/encryption.js"; + +const db = getDbInstance(); +const token = process.env.OPENROUTER_API_KEY; + +if (token) { + const fields = encryptConnectionFields({ key: token }); + const stmt = db.prepare(` + INSERT INTO provider_connections + (id, provider_id, account_id, name, connection_fields, encrypted_credentials, credentials_iv, credentials_tag, is_active, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `); + + stmt.run( + randomUUID(), + "openrouter", + "system_account", + "doppler_openrouter", + "{}", + fields.encryptedData, + fields.iv, + fields.authTag + ); + console.log("Added openrouter credentials successfully."); +} else { + console.log("No OPENROUTER_API_KEY provided."); +} diff --git a/scripts/serpentos_logic/insert_openrouter.ts b/scripts/serpentos_logic/insert_openrouter.ts new file mode 100644 index 0000000000..996ce07eda --- /dev/null +++ b/scripts/serpentos_logic/insert_openrouter.ts @@ -0,0 +1,18 @@ +import { createProviderConnection } from "../../src/lib/db/providers.ts"; + +async function run() { + const token = process.env.OPENROUTER_API_KEY; + if (!token) { + console.log("No token."); + process.exit(1); + } + await createProviderConnection({ + provider: "openrouter", + authType: "apikey", + apiKey: token, + name: "doppler_openrouter", + }); + console.log("Added provider."); +} + +run().catch(console.error); diff --git a/scripts/serpentos_logic/test_active_models.py b/scripts/serpentos_logic/test_active_models.py index d3a0907035..cfc1362229 100644 --- a/scripts/serpentos_logic/test_active_models.py +++ b/scripts/serpentos_logic/test_active_models.py @@ -13,7 +13,7 @@ MODELS = [ "god-mode" ] -url = "http://localhost:20131/v1/chat/completions" +url = "http://localhost:3001/api/v1/chat/completions" working_models = [] failed_models = [] @@ -40,6 +40,10 @@ for model in MODELS: elapsed = time.time() - start_time print(f"✅ OK ({elapsed:.2f}s) -> '{content}'") working_models.append(model) + except urllib.error.HTTPError as e: + body = e.read().decode('utf-8', errors='replace') + print(f"❌ FAILED HTTP {e.code}: {body}") + failed_models.append(model) except Exception as e: print(f"❌ FAILED: {e}") failed_models.append(model) diff --git a/scripts/serpentos_logic/tg-bot.cjs b/scripts/serpentos_logic/tg-bot.cjs new file mode 100644 index 0000000000..4e4ce0d4ce --- /dev/null +++ b/scripts/serpentos_logic/tg-bot.cjs @@ -0,0 +1,129 @@ +const https = require('https'); +const http = require('http'); +const { execSync } = require('child_process'); + +let TOKEN = process.env.TELEGRAM_BOT_TOKEN; + +// Try fetching from doppler if missing +if (!TOKEN) { + try { + TOKEN = execSync("doppler secrets get TELEGRAM_BOT_TOKEN --project serpent --config prd --plain", { encoding: 'utf-8' }).trim(); + } catch (e) { + console.error("Failed to get token from Doppler"); + process.exit(1); + } +} + +let offset = 0; + +function httpRequest(url, method = 'GET', body = null, headers = {}) { + return new Promise((resolve, reject) => { + const isHttps = url.startsWith('https'); + const mod = isHttps ? https : http; + + const parsedUrl = new URL(url); + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || (isHttps ? 443 : 80), + path: parsedUrl.pathname + parsedUrl.search, + method: method, + headers: { ...headers }, + timeout: 30000, + }; + + if (body) { + const data = typeof body === 'string' ? body : JSON.stringify(body); + options.headers['Content-Length'] = Buffer.byteLength(data); + if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/json'; + } + + const req = mod.request(options, (res) => { + let d = ''; + res.on('data', c => d += c); + res.on('end', () => resolve({ status: res.statusCode, data: d })); + }); + + req.on('error', reject); + req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); }); + if (body) req.write(typeof body === 'string' ? body : JSON.stringify(body)); + req.end(); + }); +} + +async function sendMessage(chatId, text) { + try { + await httpRequest(`https://api.telegram.org/bot${TOKEN}/sendMessage`, 'POST', { + chat_id: chatId, + text: text, + parse_mode: 'HTML' + }); + } catch (e) { + console.error("Failed to send message:", e.message); + } +} + +async function processMessage(msg) { + if (!msg || !msg.text) return; + const chatId = msg.chat.id; + const text = msg.text; + + console.log(`[Message] ${chatId}: ${text}`); + + if (text === '/start' || text === '/help') { + await sendMessage(chatId, "OmniRoute Bot\nSend me any prompt to process via OmniRoute models."); + return; + } + + await sendMessage(chatId, "🧠 Думаю..."); + + try { + const response = await httpRequest("http://localhost:3001/api/v1/chat/completions", "POST", { + model: "auto", + messages: [{ role: "user", content: text }], + max_tokens: 2048 + }); + + if (response.status === 200) { + const data = JSON.parse(response.data); + const reply = data.choices?.[0]?.message?.content || "Empty response from OmniRoute."; + await sendMessage(chatId, reply); + } else { + const err = JSON.parse(response.data); + await sendMessage(chatId, `❌ OmniRoute Error:\n
${err.error?.message || response.data}
`); + } + } catch (e) { + await sendMessage(chatId, `❌ Connection error: ${e.message}`); + } +} + +async function poll() { + try { + const res = await httpRequest(`https://api.telegram.org/bot${TOKEN}/getUpdates?timeout=30&offset=${offset}`); + if (res.status === 200) { + const data = JSON.parse(res.data); + if (data.ok && data.result.length > 0) { + for (const update of data.result) { + offset = update.update_id + 1; + if (update.message) { + await processMessage(update.message); + } + } + } + } else if (res.status === 409) { + console.error("Conflict! Another bot is running or webhook is active."); + // delete webhook just in case + await httpRequest(`https://api.telegram.org/bot${TOKEN}/deleteWebhook?drop_pending_updates=true`); + } + } catch (e) { + console.error("Poll error:", e.message); + } + setTimeout(poll, 1000); +} + +// Clear webhook before polling +httpRequest(`https://api.telegram.org/bot${TOKEN}/deleteWebhook?drop_pending_updates=true`) + .then(() => { + console.log("OmniRoute Telegram Bot started."); + poll(); + }) + .catch(console.error); diff --git a/src/app/api/telegram/webhook/route.ts b/src/app/api/telegram/webhook/route.ts new file mode 100644 index 0000000000..744873e1b6 --- /dev/null +++ b/src/app/api/telegram/webhook/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from "next/server"; +import { headers } from "next/headers"; + +export async function POST(req: Request) { + try { + const data = await req.json(); + console.log("[Telegram Webhook] Received update:", JSON.stringify(data)); + + const message = data.message; + if (!message || !message.text) { + return NextResponse.json({ ok: true }); + } + + const chatId = message.chat.id; + const text = message.text; + const botToken = process.env.TELEGRAM_BOT_TOKEN; + + if (!botToken) { + console.error("[Telegram Webhook] TELEGRAM_BOT_TOKEN not configured"); + return NextResponse.json({ ok: false, error: "Missing bot token" }, { status: 500 }); + } + + // Call OmniRoute's internal chat completions (since we are in the same Next.js app, we can fetch from localhost:3001) + // First, let's just send a thinking message + const sendThinking = fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ chat_id: chatId, text: "🧠 Thinking..." }), + }); + + // Then process the chat + const processChat = async () => { + try { + const response = await fetch("http://localhost:3001/api/v1/chat/completions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: "auto", // OmniRoute can route 'auto' + messages: [{ role: "user", content: text }], + max_tokens: 2048, + }), + }); + + let replyText = "Error communicating with AI."; + if (response.ok) { + const resJson = await response.json(); + replyText = resJson.choices?.[0]?.message?.content || "No response generated."; + } else { + const errorJson = await response.json().catch(() => ({})); + replyText = `API Error ${response.status}: ${JSON.stringify(errorJson)}`; + } + + await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ chat_id: chatId, text: replyText, parse_mode: "HTML" }), + }); + } catch (err) { + console.error("[Telegram Webhook] Error processing chat:", err); + } + }; + + // Run process asynchronously without blocking the webhook response + Promise.allSettled([sendThinking, processChat()]); + + return NextResponse.json({ ok: true }); + } catch (error) { + console.error("[Telegram Webhook] Error processing webhook:", error); + return NextResponse.json({ ok: false, error: "Internal error" }, { status: 500 }); + } +}