Перейти к содержимому
OmniRoute source

Gamification & Leaderboard System (Русский)

Повысить вовлечённость и удержание пользователей за счёт наглядного прогресса (XP, уровни, значки), социального подтверждения (таблицы лидеров) и экономических стимулов (передача токенов, награды за приглашения).

Функция Описание
XP и уровни Получение XP за каждое действие; повышение уровня по полиномиальной кривой
Значки Более 20 достижений в 5 категориях с 4 уровнями редкости
Серии активности Отслеживание ежедневной активности с текущей и самой длинной серией
Таблицы лидеров Глобальные, еженедельные, ежемесячные рейтинги, а также рейтинги по передаче токенов и вкладу
Передача токенов Перевод кредитов между пользователями через реестр с двойной записью
Приглашения и активация Реферальные коды, хранящиеся в виде хешей SHA-256
Серверы сообщества Федерация с внешними экземплярами OmniRoute
Защита от мошенничества Серверный подсчёт очков, ограничение частоты запросов, обнаружение аномалий по z-оценке
  1. Локальный подход — всё состояние хранится в SQLite, внешние сервисы не требуются.
  2. Неблокирующая работа — события обрабатываются по принципу «отправил и забыл»; путь ответа LLM никогда не задерживается логикой геймификации.
  3. Авторитетность сервера — XP рассчитывается только на стороне сервера; клиенты не могут искусственно завышать результаты.
  4. Уважение конфиденциальности — участие в таблице лидеров является добровольным; пользователи могут скрыть свой профиль.
  5. Готовность к федерации — серверы сообщества могут отправлять результаты через подписанный API; синхронизация выполняется с перезаписью, а не с суммированием.

Запрос клиента
→ /v1/chat/completions
→ handleChatCore() [open-sse/handlers/chatCore.ts]
→ ... (существующий конвейер) ...
→ ответ вышестоящего сервера отправлен клиенту
→ setImmediate (по принципу «отправил и забыл»):
→ 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]

Эмиттер событий является единой точкой интеграции. chatCore.ts вызывает emitGamificationEvent() после отправки ответа; модуль событий распределяет обработку между подсистемами XP, серий активности, значков, таблиц лидеров и защиты от мошенничества.

src/lib/gamification/
events.ts ← точка входа (вызывается из chatCore.ts)
├── xp.ts ← расчёт XP и определение уровня
├── streaks.ts ← отслеживание ежедневных серий активности
├── badges.ts ← проверка критериев получения значков
├── leaderboard.ts ← расчёт рейтинга и трансляция через SSE
├── antiCheat.ts ← ограничение частоты запросов и обнаружение аномалий
├── sharing.ts ← реестр передачи токенов
├── invites.ts ← управление кодами приглашений и активации
├── servers.ts ← федерация серверов сообщества
└── notifications.ts ← поток уведомлений SSE
src/lib/db/
gamification.ts ← все операции CRUD (8 таблиц)
src/app/api/gamification/
leaderboard/ ← GET рейтингов, POST ручного обновления
leaderboard/stream ← обновления в реальном времени через SSE
transfer/ ← GET истории, POST отправки токенов
invite/ ← GET/POST кодов, DELETE для отзыва
invite/redeem/ ← POST для активации кода
servers/ ← GET/POST/DELETE серверов сообщества
federation/score/ ← POST для отправки результата на сервер
federation/leaderboard/ ← GET для получения таблицы лидеров с сервера
notifications/ ← SSE-уведомления о значках и повышении уровня
anomalies/ ← GET отчётов об аномалиях (для администратора)
rotate/ ← POST для ротации секретов токенов приглашений

Все таблицы находятся в основной базе данных OmniRoute SQLite и создаются миграцией 060_create_gamification.sql. Журналирование WAL наследуется от синглтона getDbInstance() в 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 │ └──────────────────────────┘
└─────────────────────────┘

Следует стандартному шаблону OmniRoute — импортирует getDbInstance() из core.ts и экспортирует типизированные CRUD-функции. В обработчиках маршрутов нет необработанного SQL.

Ключевые функции:

Функция Описание
upsertLeaderboardEntry() Добавляет или обновляет результат для (api_key_id, scope, period)
getLeaderboard() Возвращает постраничный рейтинг для указанных области и периода
getUserLevel() Получает или создаёт запись об уровне пользователя
updateUserLevel() Атомарно задаёт XP, уровень и звание
getBadgeDefinitions() Возвращает все определения значков (с необязательной фильтрацией)
getUserBadges() Возвращает значки, полученные пользователем
awardBadge() Добавляет получение значка (идемпотентно по badge_id)
logXpAction() Добавляет запись в xp_audit_log
getXpAuditLog() Возвращает постраничную историю аудита пользователя
insertLedgerEntry() Выполняет перевод с двойной записью (в транзакции)
getBalance() Вычисляет для пользователя сумму полученного за вычетом отправленного
getTransferHistory() Возвращает постраничный журнал переводов
createInviteToken() Добавляет код приглашения и хешированный токен
redeemInviteToken() Выполняет поиск по коду, проверяет его и увеличивает счётчик использований
upsertCommunityServer() Регистрирует или обновляет сервер федерации
getCommunityServers() Возвращает список серверов пользователя
deleteCommunityServer() Удаляет регистрацию сервера

Файл: src/lib/gamification/xp.ts

Количество XP, необходимое для достижения уровня n, рассчитывается по полиномиальной кривой:

xp_for_level(n) = floor(100 * n^1.5)
Уровень XP до следующего уровня Суммарный XP Звание
1 100 100 Новичок
5 1,118 2,415 Новичок
10 3,162 10,523 Исследователь
25 12,500 86,024 Исследователь
50 35,355 345,529 Эксперт
75 64,952 948,683 Мастер
100 100,000 2,050,000 Легенда
Диапазон уровней Звание
1 – 9 Новичок
10 – 24 Исследователь
25 – 49 Эксперт
50 – 74 Мастер
75 – 100 Легенда
Действие XP Описание
request 1 За каждый API-запрос, маршрутизированный через OmniRoute
provider_switch 5 Переключение на другого провайдера
model_switch 3 Переключение на другую модель
combo_create 10 Создание новой комбинации
combo_use 2 Использование комбинации для запроса
token_share 1 За каждую 1 000 токенов, переданных другому пользователю
invite_redeem 50 Активация кода приглашения
daily_login 5 Ежедневная активность (один раз в день)
streak_bonus 2 За каждый день серии подряд (умножается на длительность серии)
badge_unlock 10 Разблокировка значка
export async function awardXp(
apiKeyId: string,
action: XpAction,
metadata?: Record<string, unknown>
): Promise<{ xp: number; level: number; title: string; levelUp: boolean }>;
  1. Получить количество XP из XP_REWARDS[action].
  2. Выполнить проверку через checkRateLimit() (защита от накрутки: не более 1000 XP/мин на ключ).
  3. Открыть транзакцию:
    • Прочитать текущую строку user_levels.
    • Добавить XP; пересчитать уровень с помощью levelFromXp(totalXp).
    • Если уровень изменился, установить levelUp = true.
    • Обновить строку user_levels.
    • Добавить запись в xp_audit_log.
  4. Вернуть результат. Вызывающая сторона обрабатывает уведомления.

Перебирает уровни от 1 до 100, суммируя xp_for_level(n), пока накопленный XP не превысит totalXp. Возвращает наивысший уровень, порог которого достигнут. Сложность составляет O(100), что допустимо, поскольку максимальный уровень — 100.


Файл: src/lib/gamification/badges.ts

Категория Описание Примеры значков
usage Количественные достижения Первый запрос, 1 тыс. запросов, 100 тыс.
sharing Передача токенов и приглашения Первая передача, Щедрый (10 передач)
contribution Участие в жизни сообщества Создатель комбинаций, Исследователь провайдеров
streak Постоянство с течением времени Недельный воин, Преданный на месяц
rare Труднодоступные или скрытые достижения Ранний пользователь, Автор отчёта об ошибке
Редкость Цвет Ориентировочная вероятность
common Серый Большинство пользователей
uncommon Зелёный Активные пользователи
rare Синий Преданные пользователи
legendary Золотой Лучший 1%
Тип Поле Описание
action_count count Выполнить действие N раз (например, 1000 запросов)
streak days Поддерживать серию в течение N дней подряд
unique_count field, n Использовать N уникальных значений (например, 10 разных моделей)
rank scope, n Достичь позиции N в определённом рейтинге
first — Первым выполнить действие
hidden (различается) Критерии не отображаются до получения

Определения значков хранятся в badge_definitions в виде JSON-поля criteria:

{
"type": "action_count",
"action": "request",
"count": 1000
}
emitGamificationEvent(event)
→ evaluateBadges(apiKeyId, event)
→ getBadgeDefinitions() # все определения
→ getUserBadges(apiKeyId) # уже полученные (пропустить)
→ для каждого неполученного значка:
→ matchesCriteria(badge, event, userState)
→ при совпадении: awardBadge(apiKeyId, badgeId)
→ вернуть данные уведомления

Проверка выполняется по событиям — она запускается после каждого события геймификации, но проверяет только значки, чей criteria.type соответствует действию события. Благодаря этому проверка выполняется быстро (< 5 мс для большинства событий).

Тип критерия Проверка
action_count getActionCount(apiKeyId, action) >= count
streak getCurrentStreak(apiKeyId) >= days
unique_count getUniqueCount(apiKeyId, field) >= n
rank getRank(apiKeyId, scope) <= n
first Отсутствует предшествующая запись xp_audit_log для этого типа действия
hidden Делегирует соответствующей вложенной проверке
Полный список значков
Значок Категория Редкость Критерии
Первые шаги использование обычный 1 запрос
Разминка использование обычный 100 запросов
Опытный пользователь использование необычный 1 000 запросов
Центурион использование редкий 10 000 запросов
Всемогущество использование легендарный 100 000 запросов
Переключатель провайдеров вклад обычный Использовать 5 разных провайдеров
Мастер провайдеров вклад необычный Использовать 20 разных провайдеров
Архитектор комбинаций вклад необычный Создать 5 комбинаций
Гроссмейстер комбинаций вклад редкий Создать 25 комбинаций
Первый перевод обмен обычный 1 перевод токенов
Щедрый обмен необычный 10 переводов токенов
Филантроп обмен редкий Перевести в общей сложности 10 000 токенов
Реферер обмен обычный 1 успешная рекомендация
Создатель сети обмен необычный 10 успешных рекомендаций
Воин недели серия необычный Серия продолжительностью 7 дней
Преданный на месяц серия редкий Серия продолжительностью 30 дней
Неудержимый серия легендарный Серия продолжительностью 365 дней
Ранний пользователь редкий легендарный Присоединиться в период бета-тестирования
Пионер сжатия редкий необычный Использовать сжатие 100 раз
Коллекционер навыков редкий редкий Использовать 10 разных навыков
Исследователь моделей вклад необычный Использовать 15 разных моделей

Файл: src/lib/gamification/streaks.ts

Серии хранятся в таблице key_value (общая служебная таблица) под ключами с пространством имён:

Ключ Значение Описание
gamification:streak:{keyId} {current},{longest},{lastDate} Данные активной серии
export async function updateStreak(
apiKeyId: string
): Promise<{ current: number; longest: number; milestone: boolean }>;
  1. Прочитать запись серии из key_value.
  2. Разобрать {current}, {longest}, {lastDate} (строка даты в формате ISO).
  3. Если lastDate === today — ничего не менять (сегодня уже учтено).
  4. Если lastDate === yesterday — увеличить current; при необходимости обновить longest.
  5. Если lastDate < yesterday — сбросить current = 1 (серия прервана).
  6. Записать обновлённую запись.
  7. Проверить рубежи: 7, 14, 30, 60, 90, 180, 365 дней. Если рубеж достигнут, установить milestone = true (вызывающая сторона начисляет XP и проверяет значки).
  • Часовой пояс: для серий используются даты UTC (new Date().toISOString().slice(0, 10)). Это сделано намеренно — единый канонический часовой пояс предотвращает манипуляции путём переключения между часовыми поясами.
  • Новые пользователи: запись серии отсутствует; первый запрос создаёт её со значениями current=1, longest=1, lastDate=today.
  • Несколько запросов в день: серию увеличивает только первый запрос за день по UTC.

Файл: src/lib/gamification/leaderboard.ts

Область Период Описание
global all Суммарное количество XP за всё время
weekly week XP, заработанные за текущую неделю UTC (пн–вс)
monthly month XP, заработанные за текущий месяц UTC
tokens_shared all Общее количество токенов, переданных другим
contributions all Созданные комбинации + использованные провайдеры + использованные навыки

Позиции в рейтинге вычисляются во время чтения, а не сохраняются. Это позволяет избежать устаревших данных о позициях и устраняет необходимость в периодических задачах по их пересчёту.

export async function getLeaderboard(
scope: LeaderboardScope,
period: string,
limit: number,
offset: number
): Promise<{ entries: LeaderboardEntry[]; total: number }>;

Шаблон запроса:

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 ?

Еженедельная и ежемесячная таблицы лидеров обновляются автоматически:

  1. Архивация: на границе периода текущие записи копируются в leaderboard_archive с меткой периода.
  2. Сброс: записи за завершившийся период удаляются.
  3. Триггер: проверка выполняется при каждом вызове updateLeaderboard(); первый запрос в новом периоде запускает смену периода.

Это гарантирует, что еженедельные таблицы сбрасываются каждый понедельник в 00:00 UTC, а ежемесячные — 1-го числа каждого месяца.

Эндпоинт: GET /api/gamification/stream

Клиент → GET /api/gamification/stream
→ SSE-соединение установлено
→ Сервер немедленно отправляет снимок топ-10 таблицы лидеров
→ Каждые 5 секунд: отправка обновлённого топ-10, если он изменился
→ Каждые 15 секунд: комментарий пульса (": heartbeat\n\n")
→ Клиент отключается → очистка (удаление слушателя)

Формат события:

event: leaderboard
data: {"scope":"global","entries":[...]}
event: leaderboard
data: {"scope":"weekly","entries":[...]}
: heartbeat

Менеджер SSE отслеживает подключённых клиентов по каждой области и отправляет обновления только в том случае, если данные таблицы лидеров действительно изменились с момента последней отправки.


Файл: src/lib/gamification/sharing.ts

Каждая передача создаёт две строки в token_ledger:

Строка from_key_id to_key_id amount
Дебет отправитель получатель +сумма
Кредит получатель отправитель -сумма

Стоп — используется следующее соглашение:

Строка from_key_id to_key_id amount Значение
Отправка отправитель получатель +сумма Списание у отправителя
Получение получатель отправитель +сумма Зачисление получателю

Баланс вычисляется следующим образом:

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 = ?
export async function transferTokens(
fromKeyId: string,
toKeyId: string,
amount: number,
idempotencyKey: string
): Promise<{ success: boolean; balance: number }>;
  1. Валидация: amount > 0, fromKeyId !== toKeyId.
  2. Идемпотентность: проверить, существует ли уже idempotency_key в реестре. Если да, вернуть кешированный результат.
  3. Транзакция (единая транзакция SQLite): a. Вычислить баланс отправителя. b. Если balance < amount, прервать операцию (недостаточно средств). c. Вставить строку отправки (from=sender,.
  • Не более 10 передач в минуту для каждого API-ключа.
  • Не более 10 000 токенов за одну передачу.
  • Не более 100 000 переданных токенов в день для каждого API-ключа.

Файл: src/lib/gamification/invites.ts

  • Код: 8-символьный буквенно-цифровой код (например, A3K9-X7M2), удобный для чтения, отображается пользователю.
  • Токен: случайный токен размером 32 байта, хранящийся в виде хеша SHA-256. Используется для программной активации (например, через URL-ссылки).
Столбец Значение
code A3K9X7M2 (уникальный, индексированный)
token_hash SHA-256(raw_token)

Необработанный токен возвращается пользователю только один раз при создании. OmniRoute больше никогда не хранит и не отображает его — сохраняется только хеш.

Когда пользователь активирует код, система проверяет:

  1. Код принадлежит другому api_key_id.
  2. Активирующий пользователь ранее не активировал ни одного кода от того же реферера (объединение invite_tokens с журналом активаций).

Если любая из проверок завершается неудачей, активация отклоняется с понятным сообщением об ошибке.

  • Значение max_uses по умолчанию: 10 (настраивается при создании).
  • Значение expires_at по умолчанию: 30 дней с момента создания.
  • Для просроченных или исчерпавших лимит кодов возвращается HTTP 410 Gone.

Файл: src/lib/gamification/servers.ts

Сервер сообщества регистрируется с помощью токена приглашения, выданного удалённым сервером. Локальный экземпляр:

  1. Получает токен приглашения (например, вставленный в панели управления).
  2. Вызывает POST /api/gamification/federation/leaderboard на удалённом сервере, чтобы проверить токен и получить текущую таблицу лидеров.
  3. Сохраняет запись сервера со значением status: connected.

Федерация использует синхронизацию с перезаписью, а не с накоплением:

Локальный экземпляр Сервер сообщества
│ │
├── отправка очков ───────────►│ POST /federation/score
│ { api_key_id, score } │ (сервер проверяет хеш токена)
│ │
├── запрос таблицы лидеров ───►│ GET /federation/leaderboard
│◄── первые N записей ─────────┤ (перезаписывают локальный кеш)
│ │
└── проверка состояния ───────►│ GET /federation/health
(каждые 60 с, тайм-аут 5 с)│

Запросы федерации содержат:

Authorization: Bearer &lt;raw_token&gt;
X-Federation-Version: 1

Удалённый сервер хеширует токен и находит соответствующую строку community_servers. Это позволяет не передавать сохранённый хеш.

В каждой записи сервера отслеживаются:

Поле Описание
status connected, degraded, unreachable
last_sync Временная метка ISO последней успешной синхронизации
failures Число последовательных неудачных проверок состояния

После 5 последовательных неудач статус меняется на unreachable, а синхронизация приостанавливается до успешного завершения ручной проверки состояния.


Файл: src/lib/gamification/antiCheat.ts

Все вычисления XP выполняются в src/lib/gamification/xp.ts. Клиенты никогда не отправляют количество очков — они отправляют действия, а сервер вычисляет XP. Столбец leaderboard.score доступен для записи только серверному коду.

Ограничение Значение Область действия
Максимум XP в минуту 1,000 На ключ API
Максимум переводов в минуту 10 На ключ API
Максимальная сумма перевода 10,000 На перевод
Максимум переводов за сутки 100,000 На ключ API

Для ограничения частоты используется скользящее окно в памяти (по тому же шаблону, что и RateLimitManager в open-sse/services/). Если процесс перезапускается, используются счётчики на основе SQLite.

Для каждого ключа API система поддерживает скользящее 7-дневное окно количества XP, заработанного за час. При каждом начислении XP:

  1. Вычисляется текущая почасовая скорость получения XP пользователем.
  2. Вычисляются среднее значение и стандартное отклонение по всей совокупности.
  3. Вычисляется z = (user_rate - mean) / stddev.
  4. Если z > 3.0 (3 стандартных отклонения), событие помечается как аномалия.

Аномалии записываются в xp_audit_log со значением action = 'anomaly_detected' и отображаются в панели администратора.

Каждое начисление XP, перевод, получение значка и обнаружение аномалии записывается в xp_audit_log со следующими полями:

Поле Описание
api_key_id Кто
action Что произошло (xp_award, transfer, anomaly, …)
xp_awarded Количество (0 для событий, не связанных с XP)
metadata JSON с контекстом (тип действия, цель, …)
created_at Когда (ISO 8601)

Администраторы могут запросить полный журнал аудита через GET /api/gamification/anomalies.


Все маршруты соответствуют стандартному шаблону OmniRoute:

Маршрут → Предварительный запрос CORS → Валидация тела запроса (Zod) → Аутентификация (extractApiKey)
→ Обработчик
Метод Путь Описание Аутентификация
GET /api/gamification/leaderboard Получить таблицу лидеров (область, период, пагинация) Необязательная
POST /api/gamification/leaderboard Принудительно обновить кеш таблицы лидеров Обязательная
GET /api/gamification/stream Обновления таблицы лидеров в реальном времени через SSE Необязательная
GET /api/gamification/transfer Получить историю переводов (с пагинацией) Обязательная
POST /api/gamification/transfer Отправить токены другому пользователю Обязательная
GET /api/gamification/invite Получить список моих кодов приглашений Обязательная
POST /api/gamification/invite Сгенерировать новый код приглашения Обязательная
DELETE /api/gamification/invite Отозвать код приглашения Обязательная
POST /api/gamification/invite/redeem Активировать код приглашения Обязательная
GET /api/gamification/servers Получить список серверов сообщества Обязательная
POST /api/gamification/servers Подключиться к серверу сообщества Обязательная
DELETE /api/gamification/servers Отключиться от сервера сообщества Обязательная
POST /api/gamification/federation/score Отправить результат на удалённый сервер Федеративная
GET /api/gamification/federation/leaderboard Получить таблицу лидеров с удалённого сервера Федеративная
GET /api/gamification/notifications SSE-уведомления о значках и повышении уровня Обязательная
GET /api/gamification/anomalies Просмотреть отчёты об аномалиях (для администратора) Администратор
POST /api/gamification/rotate Выполнить ротацию секретов токенов приглашений Обязательная

POST /api/gamification/transfer

// Запрос
{
"to": "recipient-api-key-id",
"amount": 500,
"idempotencyKey": "uuid-v4"
}
// Ответ 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
}
// Ответ 400 (недостаточно средств)
{
"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"
}

Зарегистрированы в open-sse/mcp-server/ вместе с существующими инструментами. Ограничены областью разрешений gamification.

Инструмент Описание Схема входных данных
gamification_leaderboard Получить таблицу лидеров для области/периода { scope, period?, limit? }
gamification_rank Получить место вызывающего и его соседей { scope }
gamification_profile Получить сводку по XP, уровню, титулу и серии {}
gamification_badges Вывести полученные значки или все определения { earned?: boolean }
gamification_transfer Отправить токены другому пользователю { to, amount }
gamification_invite Создать или вывести коды приглашений `{ action: “create” “list” }`
gamification_servers Вывести или подключить серверы сообщества { action, token? }
gamification_anomalies Просмотреть отчёты об аномалиях (область администратора) { limit?, since? }

  • Отображение пьедестала (3 лучших участника с аватарами и XP).
  • Выбор области: глобальная / за неделю / за месяц / переданные токены / вклады.
  • Таблица с пагинацией (по 25 записей на страницу), содержащая место, имя, результат, уровень и титул.
  • Обновления в реальном времени через SSE — изменения позиций анимируются.
  • Текущий пользователь выделен в таблице закреплённой строкой «Ваше место».
  • Индикатор прогресса XP с текущим уровнем и порогом следующего уровня.
  • Значок титула отображается на видном месте.
  • Галерея значков — полученные значки с датой получения, неполученные значки отображаются серыми (скрытые значки отображаются как «???» до получения).
  • Счётчик серии со значком пламени; календарь серии (последние 30 дней).
  • График истории XP (ежедневный XP за последние 30 дней).
  • Баланс токенов (на видном месте в верхней части страницы).
  • Форма перевода: получатель, сумма, диалог подтверждения.
  • Таблица истории переводов с фильтрами (отправленные/полученные/все).
  • Раздел приглашений: активные коды, создание новых, ссылка для отправки.
  • Серверы сообщества: список с состоянием работоспособности, подключение/отключение.
  • Список аномалий с указанием серьёзности, пользователя, временной метки и z-оценки.
  • Средство просмотра журнала аудита с фильтрами (тип действия, пользователь, диапазон дат).
  • Системная статистика: всего начислено XP, активные пользователи, частота получения значков.
  • Обзор состояния серверов федерации.

Геймификация подключается к конвейеру обработки запросов в единственной точке open-sse/handlers/chatCore.ts:

// После отправки ответа клиенту:
setImmediate(() => {
emitGamificationEvent({
type: "request.completed",
apiKeyId,
metadata: {
provider: selectedProvider,
model: selectedModel,
comboId: resolvedCombo?.id,
compressionUsed: compressionStats?.applied,
skillUsed: skillExecution?.name,
},
}).catch(() => {
// Запуск без ожидания результата: регистрировать в журнале, но никогда не передавать клиенту
});
});
Тип события Когда создаётся
request.completed Успешный ответ LLM отправлен
provider.switch Провайдер изменён (включая резервное переключение комбинации)
combo.created Новая конфигурация комбинации сохранена
combo.used Цель комбинации успешно достигнута
badge.earned При проверке значков найдено совпадение
streak.milestone Преодолён порог серии
transfer.sent Перевод токенов завершён
referral.redeemed Код приглашения успешно активирован
compression.used Применено сжатие промпта
skill.executed Выполнение навыка завершено
model.first_use Модель не использовалась в течение последних 7 дней

Шаблон setImmediate + .catch(() => {}) гарантирует:

  1. Ответ полностью отправляется до запуска геймификации.
  2. Ошибки геймификации никогда не передаются клиенту.
  3. Обработка события выполняется в следующей микрозадаче, а не синхронно.

Угроза Мера защиты
Искусственное завышение очков Расчёт XP только на сервере; клиенты отправляют действия, а не очки
Атаки повторного воспроизведения Ключи идемпотентности для переводов; дедупликация журнала аудита
Мошенничество с переводами Бухгалтерская книга с двойной записью; атомарные транзакции; ограничения частоты запросов
Самореферальная регистрация Перекрёстная проверка api_key_id при активации
Манипуляции с таблицей лидеров Обнаружение аномалий по Z-оценке; панель аномалий для администратора
Кража токена федерации Хранение хеша SHA-256; исходный токен показывается только один раз
Перебор кодов приглашений Ограничение частоты запросов к конечной точке активации; энтропия 8 символов
XSS в отображаемых именах Отображаемые имена санитизируются; записи таблицы лидеров экранируются
Атаки по времени на хеши crypto.timingSafeEqual для сравнения хешей токенов
  • Публичный доступ (без аутентификации): GET /leaderboard, GET /stream (таблицы лидеров только для чтения).
  • Требуется ключ API: все операции записи, профиль, переводы, приглашения.
  • Только для администратора: панель аномалий, средство просмотра журнала аудита.
  • Федерация: отдельный путь аутентификации с использованием исходного токена в заголовке Authorization, проверяемого по сохранённому хешу SHA-256.

Все тесты используют встроенное средство запуска тестов Node.js (node --import tsx/esm --test).

Файл теста Что проверяется Тесты
tests/unit/gamification/xp.test.ts Расчёт XP, кривая уровней, звания 8
tests/unit/gamification/badges.test.ts Соответствие критериям значков, присвоение 10
tests/unit/gamification/streaks.test.ts Логика серий, этапы, пограничные случаи 7
tests/unit/gamification/leaderboard.test.ts Расчёт рейтинга, пагинация, ротация 8
tests/unit/gamification/sharing.test.ts Переводы, баланс, идемпотентность 9
tests/unit/gamification/invites.test.ts Создание, активация, истечение срока, саморефералы 7
tests/unit/gamification/antiCheat.test.ts Ограничения частоты, Z-оценка, ведение журнала аудита 6
tests/unit/gamification/events.test.ts Генерация событий, рассылка, обработка ошибок 5
Окно терминала
# Все тесты геймификации
node --import tsx/esm --test tests/unit/gamification/*.test.ts
# Один файл тестов
node --import tsx/esm --test tests/unit/gamification/xp.test.ts

Согласно CONTRIBUTING.md, все новые модули должны соответствовать следующим требованиям:

  • Покрытие ветвей >= 80%.
  • Каждая публичная функция протестирована как минимум один раз.
  • Протестированы пути обработки ошибок (недостаточный баланс, истёкшие коды, ограничения частоты запросов).

src/
lib/
db/
migrations/
060_create_gamification.sql # Все 8 таблиц + индексы
gamification.ts # Доменный CRUD-модуль
gamification/
xp.ts # Расчёт XP, кривая уровней, звания
badges.ts # Определения значков, критерии, оценка
streaks.ts # Отслеживание ежедневных серий
leaderboard.ts # Расчёт рейтинга, SSE, ротация
antiCheat.ts # Ограничение частоты запросов, z-оценка, аудит
sharing.ts # Реестр передачи токенов
invites.ts # Коды приглашений/активации
servers.ts # Федерация серверов сообщества
events.ts # Эмиттер событий (точка интеграции)
notifications.ts # Поток SSE-уведомлений
app/
api/
gamification/
leaderboard/route.ts # GET/POST рейтинга
leaderboard/stream/route.ts # SSE-обновления в реальном времени
transfer/route.ts # GET/POST переводов
invite/route.ts # GET/POST/DELETE кодов приглашений
invite/redeem/route.ts # POST активации кода
servers/route.ts # GET/POST/DELETE серверов
federation/score/route.ts # POST отправки результата
federation/leaderboard/route.ts # GET получения рейтинга
notifications/route.ts # SSE-уведомления
anomalies/route.ts # GET отчётов об аномалиях
rotate/route.ts # POST ротации секретов
(dashboard)/
dashboard/
leaderboard/page.tsx # Страница рейтинга
profile/page.tsx # Страница XP/значков/серий
tokens/page.tsx # Страница баланса/переводов/приглашений
gamification/admin/page.tsx # Административный мониторинг аномалий
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 # Этот документ

  • Миграция 060_create_gamification.sql (8 таблиц).
  • src/lib/db/gamification.ts (доменный модуль).
  • src/lib/gamification/xp.ts, streaks.ts, events.ts.
  • Точка интеграции в chatCore.ts.
  • Модульные тесты для XP, серий и событий.
  • src/lib/gamification/badges.ts, leaderboard.ts.
  • Определения значков в константах.
  • Маршруты API рейтинга + поток SSE.
  • Модульные тесты для значков и рейтинга.
  • src/lib/gamification/sharing.ts, invites.ts, antiCheat.ts.
  • Маршруты API переводов и приглашений.
  • Модульные тесты для передачи, приглашений и защиты от злоупотреблений.
  • src/lib/gamification/servers.ts, notifications.ts.
  • Маршруты API федерации.
  • Страницы панели управления (рейтинг, профиль, токены, администрирование).
  • Регистрация инструментов MCP.

  • Сезонные события: ограниченные по времени наборы значков и сезоны таблицы лидеров.
  • Командные таблицы лидеров: группировка пользователей по организации или комбо.
  • Множители XP: увеличение XP в периоды проведения промоакций.
  • Публикация достижений: создание карточек значков, которыми можно делиться (изображения OpenGraph).
  • Мобильные push-уведомления: уведомления на основе вебхуков о получении значков и уровней.
  • API таблицы лидеров: публичный API для сторонних интеграций.

Исходный код OmniRoute (a58000c7685f)

HagiCode

HagiCode — агентная среда разработки со структурированными процессами, параллельным выполнением несколькими агентами и интерфейсами Hero Dungeon.

Превращайте идеи в полезное ПО с более умным, быстрым и увлекательным агентным рабочим процессом.

Главный экран HagiCode в светлой теме
  • SmartСтруктурированные процессы превращают намерение в исполнимый путь от идеи до готового изменения.
  • EfficientМультиагентные процессы параллельно продвигают исследование, реализацию и проверку.
  • FunHero Dungeon делает длительную совместную разработку наглядной и увлекательной.
Перейти на HagiCode