mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
chore: setup PM2 ecosystem, cleanup old bots, create hermes production guidelines skill and finalized plan
This commit is contained in:
committed by
diegosouzapw
parent
81fc56a369
commit
2d5e9fad45
20
.gitignore
vendored
20
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
142
CLAUDE.md
142
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`.
|
||||
<!-- CONTEXT BUDGET: this file loads in full on every session and after every compact.
|
||||
Keep it under 200 lines. Anything deeper goes to .claude/rules/ (path-scoped,
|
||||
loads only when Claude touches matching files) or .claude/skills/ (loads on demand).
|
||||
Do not re-inline detail here. -->
|
||||
|
||||
## 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 <N> --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-<feature>.md` |
|
||||
| Specs / design (`brainstorming`) | `docs/superpowers/specs/` | `_tasks/superpowers/specs/YYYY-MM-DD-<topic>-design.md` |
|
||||
| Research (`deep-research`, ad-hoc) | `docs/research/` | `_tasks/research/…` |
|
||||
| Hand-offs (`/handoff`) | — | `_tasks/hands-off/<YYYY-MM-DD>_<branch>_v<versão>_sess-<id>/` |
|
||||
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 <ref>:<path>` or `git diff <ref> -- <path>` 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 <N> --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: #<issue>` 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` |
|
||||
|
||||
57
ecosystem.config.cjs
Normal file
57
ecosystem.config.cjs
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
29
scripts/serpentos_logic/insert_openrouter.js
Normal file
29
scripts/serpentos_logic/insert_openrouter.js
Normal file
@@ -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.");
|
||||
}
|
||||
18
scripts/serpentos_logic/insert_openrouter.ts
Normal file
18
scripts/serpentos_logic/insert_openrouter.ts
Normal file
@@ -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);
|
||||
@@ -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)
|
||||
|
||||
129
scripts/serpentos_logic/tg-bot.cjs
Normal file
129
scripts/serpentos_logic/tg-bot.cjs
Normal file
@@ -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, "<b>OmniRoute Bot</b>\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<pre>${err.error?.message || response.data}</pre>`);
|
||||
}
|
||||
} 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);
|
||||
71
src/app/api/telegram/webhook/route.ts
Normal file
71
src/app/api/telegram/webhook/route.ts
Normal file
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user