* feat(docs): mirror every docs/ page in all 65 locales Extends the documentation mirrors from the 22-page core set (#13940) to every Markdown page under docs/: 152 sources x 65 locales = 9,880 mirrors (6,208 new), language bars rewritten for the full locale list, state adopted so the blocking drift gate now covers all 152 pages. run-translation.mjs: an oversized block made only of table rows or list items (PROVIDER_REFERENCE.md 244-row table, FREE_TIERS.md 71-item list) is cut at item boundaries and rejoined without a blank line — the single 16-40 KB request outlived the backend socket for verbose scripts. 48 older mirrors whose tables had lost rows were retranslated with --force. * docs(i18n): refresh mirrors for the sources the base changed since the branch cut Section-level retranslation of the 29 docs (and README.md) whose source or mirrors moved on release/v3.8.51 during the run, then state adoption; the drift gate is green again on the merged tree.
50 KiB
Gamification & Leaderboard System (Polski)
🌐 Languages: 🇺🇸 English · 🇪🇹 am · 🇸🇦 ar · 🇦🇿 az · 🇧🇬 bg · 🇧🇩 bn · 🇨🇿 cs · 🇩🇰 da · 🇩🇪 de · 🇬🇷 el · 🇪🇸 es · 🇪🇪 et · 🇮🇷 fa · 🇫🇮 fi · 🇫🇷 fr · 🇮🇪 ga · 🇮🇳 gu · 🇳🇬 ha · 🇮🇱 he · 🇮🇳 hi · 🇭🇷 hr · 🇭🇺 hu · 🇦🇲 hy · 🇮🇩 id · 🇳🇬 ig · 🇮🇹 it · 🇯🇵 ja · 🇬🇪 ka · 🇰🇭 km · 🇮🇳 kn · 🇰🇷 ko · 🇱🇹 lt · 🇱🇻 lv · 🇮🇳 ml · 🇮🇳 mr · 🇲🇾 ms · 🇲🇹 mt · 🇲🇲 my · 🇳🇵 ne · 🇳🇱 nl · 🇳🇴 no · 🇮🇳 or · 🇮🇳 pa · 🇵🇭 phi · 🇵🇹 pt · 🇧🇷 pt-BR · 🇷🇴 ro · 🇷🇺 ru · 🇱🇰 si · 🇸🇰 sk · 🇸🇮 sl · 🇷🇸 sr · 🇸🇪 sv · 🇰🇪 sw · 🇮🇳 ta · 🇮🇳 te · 🇹🇭 th · 🇹🇷 tr · 🇺🇦 uk-UA · 🇵🇰 ur · 🇺🇿 uz · 🇻🇳 vi · 🇳🇬 yo · 🇨🇳 zh-CN · 🇹🇼 zh-TW
title: "System grywalizacji i rankingów" version: 3.8.40 lastUpdated: 2026-06-28
System grywalizacji i rankingów
Source of truth:
src/lib/gamification/,src/lib/db/gamification.ts,src/app/api/gamification/Last updated: 2026-06-28 — v3.8.40
OmniRoute zawiera lokalną warstwę grywalizacji, która nagradza użytkowników za aktywność na platformie — wysyłanie zapytań, przełączanie providerów, tworzenie combo, udostępnianie tokenów i wkład w społeczność. Cały stan żyje w SQLite; federacja z serwerami społecznościowymi jest opcjonalna i oparta na push.
System jest zaprojektowany tak, by zapewniać zerowe opóźnienie na ścieżce gorącej — zdarzenia grywalizacji są wysyłane w trybie fire-and-forget z potoku żądań i nigdy nie blokują odpowiedzi LLM.
Przegląd
Cel
Zwiększyć zaangażowanie i retencję użytkowników przez widoczny postęp (XP, poziomy, odznaki), społeczny dowód (rankingi) oraz zachęty ekonomiczne (udostępnianie tokenów, nagrody za zaproszenia).
Zakres
| Feature | Description |
|---|---|
| XP & Levels | Zdobywaj XP za akcje; awansuj wzdłuż krzywej wielomianowej |
| Badges | 20+ osiągnięć w 5 kategoriach z 4 poziomami rzadkości |
| Streaks | Śledzenie codziennej aktywności z bieżącą/najdłuższą serią |
| Leaderboards | Zakresy: globalny, tygodniowy, miesięczny, udostępnianie tokenów i wkład |
| Token Sharing | Transfer kredytów między użytkownikami przez księgę double-entry |
| Invite & Redeem | Kody polecające z przechowywaniem hashowanym SHA-256 |
| Community Servers | Federacja z zewnętrznymi instancjami OmniRoute |
| Anti-Cheat | Punktacja po stronie serwera, rate limiting, detekcja anomalii z-score |
Zasady projektowe
- Local-first — cały stan w SQLite, bez wymaganych usług zewnętrznych.
- Non-blocking — zdarzenia są fire-and-forget; ścieżka odpowiedzi LLM nigdy nie jest opóźniana przez logikę grywalizacji.
- Server-authoritative — XP jest liczone wyłącznie po stronie serwera; klienci nie mogą zawyżać wyników.
- Privacy-respecting — udział w rankingu jest opcjonalny; użytkownicy mogą ukryć profil.
- Federation-ready — serwery społecznościowe mogą pushować wyniki przez podpisane API; synchronizacja jest nadpisująca, nie addytywna.
Architektura
Przepływ wysokiego poziomu
Żądanie klienta
→ /v1/chat/completions
→ handleChatCore() [open-sse/handlers/chatCore.ts]
→ ... (istniejący potok) ...
→ odpowiedź nadrzędna wysłana do klienta
→ setImmediate (uruchom i zapomnij):
→ emitGamificationEvent() [src/lib/gamification/events.ts]
→ awardXp() [src/lib/gamification/xp.ts]
→ updateStreak() [src/lib/gamification/streaks.ts]
→ evaluateBadges() [src/lib/gamification/badges.ts]
→ updateLeaderboard() [src/lib/gamification/leaderboard.ts]
→ checkAnomalies() [src/lib/gamification/antiCheat.ts]
Emiter zdarzeń stanowi pojedynczy punkt integracji. chatCore.ts wywołuje
emitGamificationEvent() po wysłaniu odpowiedzi; moduł zdarzeń przekazuje
ją do podsystemów XP, serii aktywności, odznak, tabeli wyników i ochrony przed oszustwami.
Graf zależności modułów
src/lib/gamification/
events.ts ← punkt wejścia (wywoływany z chatCore.ts)
├── xp.ts ← obliczanie XP i ustalanie poziomu
├── streaks.ts ← śledzenie codziennej serii aktywności
├── badges.ts ← ocena kryteriów odznak
├── leaderboard.ts ← obliczanie pozycji i rozgłaszanie przez SSE
├── antiCheat.ts ← ograniczanie częstotliwości i wykrywanie anomalii
├── sharing.ts ← rejestr transferów tokenów
├── invites.ts ← zarządzanie kodami zaproszeń i ich realizacją
├── servers.ts ← federacja serwerów społecznościowych
└── notifications.ts ← strumień powiadomień SSE
src/lib/db/
gamification.ts ← wszystkie operacje CRUD (8 tabel)
src/app/api/gamification/
leaderboard/ ← GET rankingi, POST ręczne odświeżenie
leaderboard/stream ← aktualizacje w czasie rzeczywistym przez SSE
transfer/ ← GET historia, POST wysyłanie tokenów
invite/ ← GET/POST kody, DELETE unieważnienie
invite/redeem/ ← POST realizacja kodu
servers/ ← GET/POST/DELETE serwery społecznościowe
federation/score/ ← POST przekazanie wyniku do serwera
federation/leaderboard/ ← GET pobranie tabeli wyników z serwera
notifications/ ← powiadomienia SSE o odznakach/awansach na wyższy poziom
anomalies/ ← GET raporty anomalii (administrator)
rotate/ ← POST rotacja sekretów tokenów zaproszeń
Warstwa danych
Tabele bazy danych
Wszystkie tabele żyją w głównej bazie SQLite OmniRoute, tworzonej przez migrację
060_create_gamification.sql. Journaling WAL jest dziedziczony z singletona
getDbInstance() w src/lib/db/core.ts.
┌─────────────────────────┐ ┌──────────────────────────┐
│ leaderboard │ │ user_levels │
├─────────────────────────┤ ├──────────────────────────┤
│ id TEXT PK │ │ api_key_id TEXT PK │
│ api_key_id TEXT │ │ xp INTEGER │
│ scope TEXT │ │ level INTEGER │
│ score INTEGER │ │ title TEXT │
│ period TEXT │ │ updated_at TEXT │
│ updated_at TEXT │ └──────────────────────────┘
└─────────────────────────┘
│
│ 1:N
▼
┌─────────────────────────┐ ┌──────────────────────────┐
│ user_badges │ │ badge_definitions │
├─────────────────────────┤ ├──────────────────────────┤
│ id TEXT PK │ │ id TEXT PK │
│ api_key_id TEXT │ │ name TEXT │
│ badge_id TEXT FK │ │ category TEXT │
│ earned_at TEXT │ │ rarity TEXT │
│ notified INTEGER │ │ criteria_type TEXT │
└─────────────────────────┘ │ criteria TEXT(JSON) │
│ description TEXT │
│ icon TEXT │
│ hidden INTEGER │
└──────────────────────────┘
┌─────────────────────────┐ ┌──────────────────────────┐
│ xp_audit_log │ │ token_ledger │
├─────────────────────────┤ ├──────────────────────────┤
│ id TEXT PK │ │ id TEXT PK │
│ api_key_id TEXT │ │ from_key_id TEXT │
│ action TEXT │ │ to_key_id TEXT │
│ xp_awarded INTEGER │ │ amount INTEGER │
│ metadata TEXT(JSON)│ │ idempotency_key TEXT UQ │
│ created_at TEXT │ │ created_at TEXT │
└─────────────────────────┘ └──────────────────────────┘
┌─────────────────────────┐ ┌──────────────────────────┐
│ invite_tokens │ │ community_servers │
├─────────────────────────┤ ├──────────────────────────┤
│ id TEXT PK │ │ id TEXT PK │
│ api_key_id TEXT │ │ name TEXT │
│ code TEXT UQ │ │ url TEXT │
│ token_hash TEXT │ │ token_hash TEXT │
│ uses INTEGER │ │ status TEXT │
│ max_uses INTEGER │ │ last_sync TEXT │
│ created_at TEXT │ │ created_at TEXT │
│ expires_at TEXT │ └──────────────────────────┘
└─────────────────────────┘
Moduł domenowy: src/lib/db/gamification.ts
Podąża za standardowym wzorcem OmniRoute — importuje getDbInstance() z
core.ts, eksportuje typowane funkcje CRUD. Bez surowego SQL w handlerach tras.
Kluczowe funkcje:
| Function | Description |
|---|---|
upsertLeaderboardEntry() |
Wstawienie lub aktualizacja wyniku dla (api_key_id, scope, period) |
getLeaderboard() |
Stronicowane rankingi dla danego scope/period |
getUserLevel() |
Pobranie lub utworzenie rekordu poziomu użytkownika |
updateUserLevel() |
Atomowe ustawienie XP, poziomu i tytułu |
getBadgeDefinitions() |
Wszystkie definicje odznak (opcjonalnie filtrowane) |
getUserBadges() |
Odznaki zdobyte przez użytkownika |
awardBadge() |
Wstawienie zdobycia odznaki (idempotentne na badge_id) |
logXpAction() |
Dopisanie do xp_audit_log |
getXpAuditLog() |
Stronicowana historia audytu użytkownika |
insertLedgerEntry() |
Transfer double-entry (w transakcji) |
getBalance() |
Suma otrzymanych minus wysłanych dla użytkownika |
getTransferHistory() |
Stronicowany dziennik transferów |
createInviteToken() |
Wstawienie kodu zaproszenia + zahashowanego tokenu |
redeemInviteToken() |
Wyszukanie po kodzie, walidacja, inkrementacja uses |
upsertCommunityServer() |
Rejestracja lub aktualizacja serwera federacji |
getCommunityServers() |
Lista serwerów użytkownika |
deleteCommunityServer() |
Usunięcie rejestracji serwera |
System XP / poziomów
Plik: src/lib/gamification/xp.ts
Krzywa poziomów
Liczba XP wymagana do osiągnięcia poziomu n jest określona krzywą wielomianową:
xp_for_level(n) = floor(100 * n^1.5)
| Poziom | XP do następnego poziomu | Łączne XP | Tytuł |
|---|---|---|---|
| 1 | 100 | 100 | Początkujący |
| 5 | 1,118 | 2,415 | Początkujący |
| 10 | 3,162 | 10,523 | Odkrywca |
| 25 | 12,500 | 86,024 | Odkrywca |
| 50 | 35,355 | 345,529 | Ekspert |
| 75 | 64,952 | 948,683 | Mistrz |
| 100 | 100,000 | 2,050,000 | Legenda |
Tytuły
| Zakres poziomów | Tytuł |
|---|---|
| 1 – 9 | Początkujący |
| 10 – 24 | Odkrywca |
| 25 – 49 | Ekspert |
| 50 – 74 | Mistrz |
| 75 – 100 | Legenda |
Nagrody XP
| Działanie | XP | Opis |
|---|---|---|
request |
1 | Za każde żądanie API kierowane przez OmniRoute |
provider_switch |
5 | Przełączenie na innego dostawcę |
model_switch |
3 | Przełączenie na inny model |
combo_create |
10 | Utworzenie nowego zestawu |
combo_use |
2 | Użycie zestawu dla żądania |
token_share |
1 | Za każde 1 000 tokenów udostępnionych innemu użytkownikowi |
invite_redeem |
50 | Wykorzystanie kodu zaproszenia |
daily_login |
5 | Codzienna aktywność (raz dziennie) |
streak_bonus |
2 | Za każdy kolejny dzień serii (pomnożone przez długość serii) |
badge_unlock |
10 | Odblokowanie odznaki |
Przebieg przyznawania
export async function awardXp(
apiKeyId: string,
action: XpAction,
metadata?: Record<string, unknown>
): Promise<{ xp: number; level: number; title: string; levelUp: boolean }>;
- Odczytaj
XP_REWARDS[action], aby uzyskać liczbę XP. - Przekaż przez
checkRateLimit()(ochrona przed oszustwami: maks. 1000 XP/min na klucz). - Otwórz transakcję:
- Odczytaj bieżący wiersz
user_levels. - Dodaj XP; ponownie oblicz poziom za pomocą
levelFromXp(totalXp). - Jeśli poziom się zmienił, ustaw
levelUp = true. - Zaktualizuj wiersz
user_levels. - Wstaw wpis do
xp_audit_log.
- Odczytaj bieżący wiersz
- Zwróć wynik. Kod wywołujący obsługuje powiadomienia.
Funkcja pomocnicza: levelFromXp(totalXp)
Iteruje po poziomach 1..100, sumując xp_for_level(n), aż łączna liczba XP
przekroczy totalXp. Zwraca najwyższy poziom, którego próg został osiągnięty.
Złożoność wynosi O(100) — jest to akceptowalne, ponieważ maksymalny poziom to 100.
System odznak
File: src/lib/gamification/badges.ts
Kategorie
| Category | Description | Example Badges |
|---|---|---|
usage |
Kamienie milowe oparte na wolumenie | First Request, 1K Requests, 100K |
sharing |
Udostępnianie tokenów i polecenia | First Share, Generous (10 shares) |
contribution |
Zaangażowanie społecznościowe | Combo Creator, Provider Explorer |
streak |
Konsekwencja w czasie | Week Warrior, Monthly Devoted |
rare |
Trudne lub ukryte osiągnięcia | Early Adopter, Bug Reporter |
Rzadkości
| Rarity | Color | Probability Hint |
|---|---|---|
common |
Gray | Większość użytkowników |
uncommon |
Green | Aktywni użytkownicy |
rare |
Blue | Zaangażowani użytkownicy |
legendary |
Gold | Top 1% |
Typy kryteriów
| Type | Field | Description |
|---|---|---|
action_count |
count |
Wykonaj akcję N razy (np. 1000 requestów) |
streak |
days |
Utrzymaj serię przez N kolejnych dni |
unique_count |
field, n |
Użyj N unikalnych wartości (np. 10 różnych modeli) |
rank |
scope, n |
Osiągnij rangę N w zakresie rankingu |
first |
— | Bądź pierwszym, który wykona akcję |
hidden |
(varies) | Kryteria niewidoczne do momentu zdobycia |
Definicje odznak są przechowywane w badge_definitions jako JSON criteria:
{
"type": "action_count",
"action": "request",
"count": 1000
}
Przepływ ewaluacji
emitGamificationEvent(event)
→ evaluateBadges(apiKeyId, event)
→ getBadgeDefinitions() # all definitions
→ getUserBadges(apiKeyId) # already earned (skip)
→ for each unearned badge:
→ matchesCriteria(badge, event, userState)
→ if match: awardBadge(apiKeyId, badgeId)
→ return notification payload
Ewaluacja jest sterowana zdarzeniami — uruchamia się po każdym zdarzeniu grywalizacji, ale
sprawdza tylko odznaki, których criteria.type pasuje do akcji zdarzenia. To
utrzymuje ewaluację szybką (< 5ms dla większości zdarzeń).
matchesCriteria(badge, event, userState)
| Criteria Type | Check |
|---|---|
action_count |
getActionCount(apiKeyId, action) >= count |
streak |
getCurrentStreak(apiKeyId) >= days |
unique_count |
getUniqueCount(apiKeyId, field) >= n |
rank |
getRank(apiKeyId, scope) <= n |
first |
Brak wcześniejszego wpisu xp_audit_log dla tego typu akcji |
hidden |
Deleguje do odpowiedniego pod-sprawdzenia |
Wbudowane odznaki (20+)
Pełna lista odznak
| Badge | Category | Rarity | Criteria |
|---|---|---|---|
| First Steps | usage | common | 1 request |
| Getting Warmed Up | usage | common | 100 requests |
| Power User | usage | uncommon | 1,000 requests |
| Centurion | usage | rare | 10,000 requests |
| OmniPower | usage | legendary | 100,000 requests |
| Provider Hopper | contribution | common | Use 5 different providers |
| Provider Master | contribution | uncommon | Use 20 different providers |
| Combo Architect | contribution | uncommon | Create 5 combos |
| Combo Grandmaster | contribution | rare | Create 25 combos |
| First Share | sharing | common | 1 token transfer |
| Generous | sharing | uncommon | 10 token transfers |
| Philanthropist | sharing | rare | Transfer 10,000 tokens total |
| Referrer | sharing | common | 1 successful referral |
| Network Builder | sharing | uncommon | 10 successful referrals |
| Week Warrior | streak | uncommon | 7-day streak |
| Monthly Devoted | streak | rare | 30-day streak |
| Unstoppable | streak | legendary | 365-day streak |
| Early Adopter | rare | legendary | Join during beta period |
| Compression Pioneer | rare | uncommon | Use compression 100 times |
| Skill Collector | rare | rare | Use 10 different skills |
| Model Explorer | contribution | uncommon | Use 15 different models |
Tracker serii (streak)
File: src/lib/gamification/streaks.ts
Model danych
Serie są przechowywane w tabeli key_value (współdzielona tabela narzędziowa) pod
kluczami w przestrzeni nazw:
| Key | Value | Description |
|---|---|---|
gamification:streak:{keyId} |
{current},{longest},{lastDate} |
Dane aktywnej serii |
Logika
export async function updateStreak(
apiKeyId: string
): Promise<{ current: number; longest: number; milestone: boolean }>;
- Odczytaj rekord serii z
key_value. - Sparsuj
{current},{longest},{lastDate}(ciąg daty ISO). - Jeśli
lastDate === today— bez zmian (już policzone dziś). - Jeśli
lastDate === yesterday— inkrementujcurrent; zaktualizujlongestw razie potrzeby. - Jeśli
lastDate < yesterday— zresetujcurrent = 1(seria przerwana). - Zapisz zaktualizowany rekord.
- Sprawdź kamienie milowe: 7, 14, 30, 60, 90, 180, 365 dni. Jeśli przekroczono, ustaw
milestone = true(caller przyznaje XP i sprawdza odznaki).
Przypadki brzegowe
- Timezone: serie używają dat UTC (
new Date().toISOString().slice(0, 10)). To zamierzone — jedna kanoniczna strefa czasowa zapobiega nadużyciom przez skakanie między strefami. - New users: brak rekordu serii; pierwsze żądanie tworzy go z
current=1, longest=1, lastDate=today. - Multiple requests per day: tylko pierwsze żądanie dnia UTC inkrementuje serię.
Ranking (Leaderboard)
File: src/lib/gamification/leaderboard.ts
Zakresy
| Scope | Period | Description |
|---|---|---|
global |
all |
Skumulowane XP wszech czasów |
weekly |
week |
XP zdobyte w bieżącym tygodniu UTC (pn–nd) |
monthly |
month |
XP zdobyte w bieżącym miesiącu UTC |
tokens_shared |
all |
Suma tokenów przekazanych innym |
contributions |
all |
Utworzone combo + użyte providery + użyte skille |
Obliczanie rangi
Rangi są liczone w momencie odczytu, nie przechowywane. To unika nieaktualnych danych rangi i eliminuje potrzebę okresowych zadań przeliczania rang.
export async function getLeaderboard(
scope: LeaderboardScope,
period: string,
limit: number,
offset: number
): Promise<{ entries: LeaderboardEntry[]; total: number }>;
Wzorzec zapytania:
SELECT api_key_id, score,
RANK() OVER (ORDER BY score DESC) as rank
FROM leaderboard
WHERE scope = ? AND period = ?
ORDER BY score DESC
LIMIT ? OFFSET ?
Rotacja okresów
Tygodniowe i miesięczne rankingi rotują automatycznie:
- Archive: na granicy okresu skopiuj bieżące wpisy do
leaderboard_archivez etykietą okresu. - Reset: usuń wpisy dla wygasłego okresu.
- Trigger: sprawdzane przy każdym wywołaniu
updateLeaderboard(); pierwsze żądanie nowego okresu uruchamia rotację.
To gwarantuje, że tablice tygodniowe resetują się w każdy poniedziałek o 00:00 UTC, a miesięczne
- dnia każdego miesiąca.
Aktualizacje SSE w czasie rzeczywistym
Endpoint: GET /api/gamification/stream
Client → GET /api/gamification/stream
→ SSE connection established
→ Server sends top-10 leaderboard snapshot immediately
→ Every 5 seconds: push updated top-10 if changed
→ Every 15 seconds: heartbeat comment (": heartbeat\n\n")
→ Client disconnects → cleanup (remove listener)
Format zdarzenia:
event: leaderboard
data: {"scope":"global","entries":[...]}
event: leaderboard
data: {"scope":"weekly","entries":[...]}
: heartbeat
Menedżer SSE śledzi podłączonych klientów per scope i wysyła aktualizacje tylko wtedy, gdy dane rankingu faktycznie się zmieniły od ostatniego pusha.
Udostępnianie tokenów
File: src/lib/gamification/sharing.ts
Księga double-entry
Każdy transfer tworzy dwa wiersze w token_ledger:
| Row | from_key_id |
to_key_id |
amount |
|---|---|---|---|
| Debit | sender | receiver | +amount |
| Credit | receiver | sender | -amount |
Czekaj — konwencja jest taka:
| Row | from_key_id |
to_key_id |
amount |
Meaning |
|---|---|---|---|---|
| Send | sender | receiver | +amount | Odpływ od nadawcy |
| Receive | receiver | sender | +amount | Dopływ do odbiorcy |
Saldo jest liczone jako:
SELECT
COALESCE(SUM(CASE WHEN to_key_id = ? THEN amount ELSE 0 END), 0)
- COALESCE(SUM(CASE WHEN from_key_id = ? THEN amount ELSE 0 END), 0)
AS balance
FROM token_ledger
WHERE from_key_id = ? OR to_key_id = ?
Przepływ transferu
export async function transferTokens(
fromKeyId: string,
toKeyId: string,
amount: number,
idempotencyKey: string
): Promise<{ success: boolean; balance: number }>;
- Validate:
amount > 0,fromKeyId !== toKeyId. - Idempotency: sprawdź, czy
idempotency_keyjuż istnieje w księdze. Jeśli tak, zwróć wynik z cache. - Transaction (pojedyncza transakcja SQLite):
a. Policz saldo nadawcy.
b. Jeśli
balance < amount, przerwij (niewystarczające środki). c. Wstaw wiersz send (from=sender, to=receiver, amount). d. Wstaw wiersz receive (from=receiver, to=sender, amount). - Rate limit: sprawdź limit transferów nadawcy (max 10 transferów/min).
- Event: wyemituj zdarzenie grywalizacji
token_sharedla XP + ewaluacji odznak. - Zwróć
{ success: true, balance: newBalance }.
Ograniczenia częstotliwości
- Max 10 transferów na minutę na klucz API.
- Max 10 000 tokenów na pojedynczy transfer.
- Max 100 000 tokenów przelanych dziennie na klucz API.
Tokeny zaproszeń i redeem
File: src/lib/gamification/invites.ts
Format kodu
- Code: 8-znakowy alfanumeryczny (np.
A3K9-X7M2), czytelny dla człowieka, wyświetlany użytkownikowi. - Token: 32-bajtowy losowy token, przechowywany jako hash SHA-256. Używany do programowego redeem (np. linki URL).
Przechowywanie
| Column | Value |
|---|---|
code |
A3K9X7M2 (unique, indexed) |
token_hash |
SHA-256(raw_token) |
Surowy token jest zwracany użytkownikowi dokładnie raz w momencie utworzenia. OmniRoute nigdy go potem nie przechowuje ani nie wyświetla — pozostaje tylko hash.
Zapobieganie self-referral
Gdy użytkownik redeemuje kod, system sprawdza:
- Kod należy do innego
api_key_id. - Redeemujący użytkownik nie redeemował wcześniej żadnego kodu od tego samego
polecającego (join na
invite_tokens+ dziennik redeemów).
Jeśli którekolwiek sprawdzenie zawiedzie, redeem jest odrzucany z jasnym komunikatem błędu.
Wygaśnięcie i limity
- Domyślne
max_uses: 10 (konfigurowalne przy tworzeniu). - Domyślne
expires_at: 30 dni od utworzenia. - Wygasłe lub wyczerpane kody zwracają HTTP 410 Gone.
Federacja serwerów społecznościowych
File: src/lib/gamification/servers.ts
Połączenie
Serwer społecznościowy jest rejestrowany przez token zaproszenia wystawiony przez zdalny serwer. Lokalna instancja:
- Odbiera token zaproszenia (np. wklejony w dashboard).
- Wywołuje
POST /api/gamification/federation/leaderboardna zdalnym serwerze, by zwalidować token i pobrać bieżący ranking. - Zapisuje rekord serwera ze
status: connected.
Model synchronizacji
Federacja używa overwrite sync, nie addytywnej:
Local Instance Community Server
│ │
├── push score ───────────────►│ POST /federation/score
│ { api_key_id, score } │ (server validates token hash)
│ │
├── pull leaderboard ─────────►│ GET /federation/leaderboard
│◄── top-N entries ────────────┤ (overwrites local cache)
│ │
└── health check ─────────────►│ GET /federation/health
(every 60s, timeout 5s) │
Auth
Żądania federacji zawierają:
Authorization: Bearer <raw_token>
X-Federation-Version: 1
Zdalny serwer haszuje token i wyszukuje pasujący
wiersz community_servers. To unika przesyłania przechowywanego hasha.
Monitorowanie zdrowia
Każdy rekord serwera śledzi:
| Field | Description |
|---|---|
status |
connected, degraded, unreachable |
last_sync |
Znacznik czasu ISO ostatniej udanej synchronizacji |
failures |
Kolejne nieudane health checki |
Po 5 kolejnych niepowodzeniach status zmienia się na unreachable i synchronizacja jest
wstrzymana do udanego ręcznego health checka.
Anti-Cheat
File: src/lib/gamification/antiCheat.ts
Punktacja po stronie serwera
Wszystkie obliczenia XP dzieją się w src/lib/gamification/xp.ts. Klienci nigdy
nie przesyłają wyniku — przesyłają akcje, a serwer liczy XP.
Kolumna leaderboard.score jest zapisywalna tylko przez kod serwerowy.
Rate limiting
| Limit | Value | Scope |
|---|---|---|
| Max XP per minute | 1,000 | Per API key |
| Max transfers per min | 10 | Per API key |
| Max transfer amount | 10,000 | Per transfer |
| Max daily transfers | 100,000 | Per API key |
Limity używają okna przesuwnego w pamięci (ten sam wzorzec co
RateLimitManager w open-sse/services/). Przy restarcie procesu fallbackiem są
liczniki oparte na SQLite.
Detekcja anomalii z-score
Dla każdego klucza API system utrzymuje rollingowe 7-dniowe okno XP zdobytego na godzinę. Przy każdym przyznaniu XP:
- Policz bieżącą godzinową stopę XP użytkownika.
- Policz średnią populacji i odchylenie standardowe.
- Oblicz
z = (user_rate - mean) / stddev. - Jeśli
z > 3.0(3 odchylenia standardowe), oznacz jako anomalię.
Anomalie są logowane do xp_audit_log z action = 'anomaly_detected'
i prezentowane na dashboardzie admina.
Ślad audytu
Każde przyznanie XP, transfer, zdobycie odznaki i detekcja anomalii są logowane do
xp_audit_log z:
| Field | Description |
|---|---|
api_key_id |
Kto |
action |
Co się stało (xp_award, transfer, anomaly, …) |
xp_awarded |
Kwota (0 dla zdarzeń nie-XP) |
metadata |
JSON z kontekstem (typ akcji, target, …) |
created_at |
Kiedy (ISO 8601) |
Admini mogą odpytać pełny ślad audytu przez GET /api/gamification/anomalies.
Trasy API
Wszystkie trasy podążają za standardowym wzorcem OmniRoute:
Route → CORS preflight → Body validation (Zod) → Auth (extractApiKey)
→ Handler
Endpointy
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/gamification/leaderboard |
Pobierz ranking (scope, period, stronicowanie) | Optional |
| POST | /api/gamification/leaderboard |
Wymuś odświeżenie cache rankingu | Required |
| GET | /api/gamification/stream |
Aktualizacje rankingu SSE w czasie rzeczywistym | Optional |
| GET | /api/gamification/transfer |
Historia transferów (stronicowanie) | Required |
| POST | /api/gamification/transfer |
Wyślij tokeny do innego użytkownika | Required |
| GET | /api/gamification/invite |
Lista moich kodów zaproszeń | Required |
| POST | /api/gamification/invite |
Wygeneruj nowy kod zaproszenia | Required |
| DELETE | /api/gamification/invite |
Unieważnij kod zaproszenia | Required |
| POST | /api/gamification/invite/redeem |
Redeemuj kod zaproszenia | Required |
| GET | /api/gamification/servers |
Lista serwerów społecznościowych | Required |
| POST | /api/gamification/servers |
Połącz z serwerem społecznościowym | Required |
| DELETE | /api/gamification/servers |
Rozłącz z serwerem społecznościowym | Required |
| POST | /api/gamification/federation/score |
Push wyniku na zdalny serwer | Federation |
| GET | /api/gamification/federation/leaderboard |
Pull rankingu ze zdalnego serwera | Federation |
| GET | /api/gamification/notifications |
Powiadomienia SSE o odznakach/level-up | Required |
| GET | /api/gamification/anomalies |
Raporty anomalii (admin) | Admin |
| POST | /api/gamification/rotate |
Rotacja sekretów tokenów zaproszeń | Required |
Przykłady request/response
POST /api/gamification/transfer
// Request
{
"to": "recipient-api-key-id",
"amount": 500,
"idempotencyKey": "uuid-v4"
}
// Response 200
{
"success": true,
"transfer": {
"id": "txn-uuid",
"from": "sender-api-key-id",
"to": "recipient-api-key-id",
"amount": 500,
"createdAt": "2026-05-19T12:00:00.000Z"
},
"balance": 2500
}
// Response 400 (insufficient funds)
{
"error": "Insufficient balance",
"balance": 200,
"requested": 500
}
GET /api/gamification/leaderboard?scope=weekly&limit=10
{
"scope": "weekly",
"period": "2026-W20",
"entries": [
{
"rank": 1,
"apiKeyId": "key-uuid",
"displayName": "User***1234",
"score": 15230,
"level": 42,
"title": "Expert"
}
],
"total": 847,
"updatedAt": "2026-05-19T12:00:00.000Z"
}
Narzędzia MCP (8)
Zarejestrowane w open-sse/mcp-server/ obok istniejących narzędzi. Objęte zakresem uprawnień
gamification.
| Narzędzie | Opis | Schemat danych wejściowych | |
|---|---|---|---|
gamification_leaderboard |
Pobierz tabelę wyników dla zakresu/okresu | { scope, period?, limit? } |
|
gamification_rank |
Pobierz pozycję wywołującego i jego sąsiadów | { scope } |
|
gamification_profile |
Pobierz podsumowanie XP, poziomu, tytułu i serii | {} |
|
gamification_badges |
Wyświetl zdobyte odznaki lub wszystkie definicje | { earned?: boolean } |
|
gamification_transfer |
Wyślij tokeny innemu użytkownikowi | { to, amount } |
|
gamification_invite |
Wygeneruj lub wyświetl kody zaproszeń | `{ action: "create" | "list" }` |
gamification_servers |
Wyświetl serwery społeczności lub połącz się z nimi | { action, token? } |
|
gamification_anomalies |
Wyświetl raporty anomalii (zakres administratora) | { limit?, since? } |
Strony dashboardu
/dashboard/leaderboard
- Wyświetlanie podium (top 3 z awatarami i XP).
- Selektor zakresu: Global / Weekly / Monthly / Tokens Shared / Contributions.
- Stronicowana tabela (25 na stronę) z rangą, nazwą, wynikiem, poziomem, tytułem.
- Aktualizacje SSE w czasie rzeczywistym — zmiany rang animują się.
- Bieżący użytkownik podświetlony w tabeli ze sticky wierszem „Your Rank”.
/dashboard/profile
- Pasek postępu XP z bieżącym poziomem i progiem następnego poziomu.
- Odznaka tytułu wyświetlana w sposób wyróżniony.
- Galeria odznak — zdobyte z datą, niezdobyte wyszarzone (ukryte odznaki pokazują „???” do momentu zdobycia).
- Licznik serii z ikoną płomienia; kalendarz serii (ostatnie 30 dni).
- Wykres historii XP (dzienne XP z ostatnich 30 dni).
/dashboard/tokens
- Saldo tokenów (wyróżnione, na górze strony).
- Formularz transferu: odbiorca, kwota, dialog potwierdzenia.
- Tabela historii transferów z filtrami (sent/received/all).
- Sekcja zaproszeń: aktywne kody, generowanie nowych, link do udostępnienia.
- Serwery społecznościowe: lista ze statusem zdrowia, connect/disconnect.
/dashboard/gamification/admin
- Lista anomalii z severity, użytkownikiem, znacznikiem czasu, z-score.
- Przeglądarka dziennika audytu z filtrami (typ akcji, użytkownik, zakres dat).
- Statystyki systemu: łączne przyznane XP, aktywni użytkownicy, stopy zdobywania odznak.
- Przegląd zdrowia serwerów federacji.
Integracja z potokiem
Punkt integracji
Grywalizacja podpina się do potoku żądań w jednym punkcie w
open-sse/handlers/chatCore.ts:
// After response is sent to client:
setImmediate(() => {
emitGamificationEvent({
type: "request.completed",
apiKeyId,
metadata: {
provider: selectedProvider,
model: selectedModel,
comboId: resolvedCombo?.id,
compressionUsed: compressionStats?.applied,
skillUsed: skillExecution?.name,
},
}).catch(() => {
// Fire-and-forget: log but never propagate to client
});
});
Typy zdarzeń
| Event Type | When Emitted |
|---|---|
request.completed |
Wysłano udaną odpowiedź LLM |
provider.switch |
Zmieniono providera (liczy się fallback combo) |
combo.created |
Zapisano nową konfigurację combo |
combo.used |
Udana obsługa targetu combo |
badge.earned |
Ewaluacja odznak znalazła dopasowanie |
streak.milestone |
Przekroczono próg serii |
transfer.sent |
Ukończono transfer tokenów |
referral.redeemed |
Udany redeem kodu zaproszenia |
compression.used |
Zastosowano kompresję promptu |
skill.executed |
Ukończono wykonanie skillu |
model.first_use |
Model nieużywany przez ostatnie 7 dni |
Gwarancja non-blocking
Wzorzec setImmediate + .catch(() => {}) gwarantuje:
- Odpowiedź jest w pełni wysłana, zanim uruchomi się grywalizacja.
- Błędy grywalizacji nigdy nie wychodzą do klienta.
- Przetwarzanie zdarzeń działa w następnym microtasku, nie inline.
Bezpieczeństwo
Model zagrożeń
| Threat | Mitigation |
|---|---|
| Score inflation | XP liczone tylko po stronie serwera; klienci wysyłają akcje, nie wyniki |
| Replay attacks | Klucze idempotencji na transferach; dedup dziennika audytu |
| Transfer fraud | Księga double-entry; atomowe transakcje; rate limits |
| Self-referral | Cross-check api_key_id przy redeemie |
| Leaderboard manipulation | Detekcja anomalii z-score; dashboard anomalii admina |
| Federation token theft | Przechowywanie hashowane SHA-256; surowy token pokazywany tylko raz |
| Brute force invite codes | Rate limiting na endpoincie redeem; entropia 8 znaków |
| XSS in display names | Sanityzacja display names; escapowanie wpisów rankingu |
| Timing attacks on hashes | crypto.timingSafeEqual przy porównaniu hashy tokenów |
Wymagania auth
- Public (bez auth):
GET /leaderboard,GET /stream(rankingi tylko do odczytu). - API key required: wszystkie operacje zapisu, profil, transfery, zaproszenia.
- Admin only: dashboard anomalii, przeglądarka dziennika audytu.
- Federation: osobna ścieżka auth z surowym tokenem w nagłówku
Authorization, walidowanym względem przechowywanego hasha SHA-256.
Testowanie
Pliki testowe
Wszystkie testy używają natywnego test runnera Node.js (node --import tsx/esm --test).
| Test File | Covers | Tests |
|---|---|---|
tests/unit/gamification/xp.test.ts |
Obliczanie XP, krzywa poziomów, tytuły | 8 |
tests/unit/gamification/badges.test.ts |
Dopasowanie kryteriów odznak, przyznawanie | 10 |
tests/unit/gamification/streaks.test.ts |
Logika serii, kamienie milowe, edge case'y | 7 |
tests/unit/gamification/leaderboard.test.ts |
Obliczanie rang, stronicowanie, rotacja | 8 |
tests/unit/gamification/sharing.test.ts |
Transfery, saldo, idempotencja | 9 |
tests/unit/gamification/invites.test.ts |
Tworzenie, redeem, wygaśnięcie, self-referral | 7 |
tests/unit/gamification/antiCheat.test.ts |
Rate limity, z-score, logowanie audytu | 6 |
tests/unit/gamification/events.test.ts |
Emisja zdarzeń, fan-out, obsługa błędów | 5 |
Uruchamianie testów
# All gamification tests
node --import tsx/esm --test tests/unit/gamification/*.test.ts
# Single test file
node --import tsx/esm --test tests/unit/gamification/xp.test.ts
Wymagania pokrycia
Zgodnie z CONTRIBUTING.md — wszystkie nowe moduły muszą mieć:
- Pokrycie gałęzi >= 80%.
- Każda publiczna funkcja przetestowana co najmniej raz.
- Przetestowane ścieżki błędów (niewystarczające saldo, wygasłe kody, rate limity).
Struktura plików
src/
lib/
db/
migrations/
060_create_gamification.sql # Wszystkie 8 tabel i indeksy
gamification.ts # Moduł CRUD domeny
gamification/
xp.ts # Obliczanie XP, krzywa poziomów, tytuły
badges.ts # Definicje odznak, kryteria, ocena
streaks.ts # Śledzenie codziennych serii
leaderboard.ts # Obliczanie pozycji, SSE, rotacja
antiCheat.ts # Ograniczanie częstotliwości, z-score, audyt
sharing.ts # Rejestr transferów tokenów
invites.ts # Kody zaproszeń i realizacji
servers.ts # Federacja serwerów społecznościowych
events.ts # Emiter zdarzeń (punkt integracji)
notifications.ts # Strumień powiadomień SSE
app/
api/
gamification/
leaderboard/route.ts # GET/POST rankingu
leaderboard/stream/route.ts # Aktualizacje w czasie rzeczywistym przez SSE
transfer/route.ts # GET/POST transferów
invite/route.ts # GET/POST/DELETE kodów zaproszeń
invite/redeem/route.ts # POST realizacji kodu
servers/route.ts # GET/POST/DELETE serwerów
federation/score/route.ts # POST przesyłania wyniku
federation/leaderboard/route.ts # GET pobierania rankingu
notifications/route.ts # Powiadomienia SSE
anomalies/route.ts # GET raportów anomalii
rotate/route.ts # POST rotacji sekretów
(dashboard)/
dashboard/
leaderboard/page.tsx # Strona rankingu
profile/page.tsx # Strona XP, odznak i serii
tokens/page.tsx # Strona salda, transferów i zaproszeń
gamification/admin/page.tsx # Monitorowanie anomalii przez administratora
shared/
constants/
gamification.ts # XP_REWARDS, TITLES, BADGE_DEFS, LIMITS
tests/
unit/
gamification/
xp.test.ts
badges.test.ts
streaks.test.ts
leaderboard.test.ts
sharing.test.ts
invites.test.ts
antiCheat.test.ts
events.test.ts
docs/
frameworks/
GAMIFICATION.md # Ten dokument
Strategia migracji
Faza 1: Backend core (PR 1)
- Migracja
060_create_gamification.sql(8 tabel). src/lib/db/gamification.ts(moduł domenowy).src/lib/gamification/xp.ts,streaks.ts,events.ts.- Punkt integracji w
chatCore.ts. - Testy jednostkowe XP, serii, zdarzeń.
Faza 2: Odznaki i ranking (PR 2)
src/lib/gamification/badges.ts,leaderboard.ts.- Definicje odznak w constants.
- Trasy API rankingu + strumień SSE.
- Testy jednostkowe odznak i rankingu.
Faza 3: Udostępnianie i zaproszenia (PR 3)
src/lib/gamification/sharing.ts,invites.ts,antiCheat.ts.- Trasy API transferów i zaproszeń.
- Testy jednostkowe sharing, invites, anti-cheat.
Faza 4: Federacja i dashboard (PR 4)
src/lib/gamification/servers.ts,notifications.ts.- Trasy API federacji.
- Strony dashboardu (leaderboard, profile, tokens, admin).
- Rejestracja narzędzi MCP.
Przyszłe rozważania
- Seasonal events: ograniczone czasowo zestawy odznak i sezony rankingów.
- Team leaderboards: grupowanie użytkowników według organizacji lub combo.
- XP multipliers: boost XP w okresach promocyjnych.
- Achievement sharing: generowanie udostępnialnych kart odznak (obrazy OpenGraph).
- Mobile push: powiadomienia webhook o zdarzeniach odznak/poziomów.
- Leaderboard API: publiczne API dla integracji zewnętrznych.