Gamification & Leaderboard System (Русский)
Назначение
Заголовок раздела «Назначение»Повысить вовлечённость и удержание пользователей за счёт наглядного прогресса (XP, уровни, значки), социального подтверждения (таблицы лидеров) и экономических стимулов (передача токенов, награды за приглашения).
Область применения
Заголовок раздела «Область применения»| Функция | Описание |
|---|---|
| XP и уровни | Получение XP за каждое действие; повышение уровня по полиномиальной кривой |
| Значки | Более 20 достижений в 5 категориях с 4 уровнями редкости |
| Серии активности | Отслеживание ежедневной активности с текущей и самой длинной серией |
| Таблицы лидеров | Глобальные, еженедельные, ежемесячные рейтинги, а также рейтинги по передаче токенов и вкладу |
| Передача токенов | Перевод кредитов между пользователями через реестр с двойной записью |
| Приглашения и активация | Реферальные коды, хранящиеся в виде хешей SHA-256 |
| Серверы сообщества | Федерация с внешними экземплярами OmniRoute |
| Защита от мошенничества | Серверный подсчёт очков, ограничение частоты запросов, обнаружение аномалий по z-оценке |
Принципы проектирования
Заголовок раздела «Принципы проектирования»- Локальный подход — всё состояние хранится в SQLite, внешние сервисы не требуются.
- Неблокирующая работа — события обрабатываются по принципу «отправил и забыл»; путь ответа LLM никогда не задерживается логикой геймификации.
- Авторитетность сервера — XP рассчитывается только на стороне сервера; клиенты не могут искусственно завышать результаты.
- Уважение конфиденциальности — участие в таблице лидеров является добровольным; пользователи могут скрыть свой профиль.
- Готовность к федерации — серверы сообщества могут отправлять результаты через подписанный 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 │ └──────────────────────────┘└─────────────────────────┘Доменный модуль: src/lib/db/gamification.ts
Заголовок раздела «Доменный модуль: src/lib/db/gamification.ts»Следует стандартному шаблону 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() |
Удаляет регистрацию сервера |
Система XP / уровней
Заголовок раздела «Система XP / уровней»Файл: 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
Заголовок раздела «Награды XP»| Действие | 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 }>;- Получить количество XP из
XP_REWARDS[action]. - Выполнить проверку через
checkRateLimit()(защита от накрутки: не более 1000 XP/мин на ключ). - Открыть транзакцию:
- Прочитать текущую строку
user_levels. - Добавить XP; пересчитать уровень с помощью
levelFromXp(totalXp). - Если уровень изменился, установить
levelUp = true. - Обновить строку
user_levels. - Добавить запись в
xp_audit_log.
- Прочитать текущую строку
- Вернуть результат. Вызывающая сторона обрабатывает уведомления.
Вспомогательная функция: levelFromXp(totalXp)
Заголовок раздела «Вспомогательная функция: levelFromXp(totalXp)»Перебирает уровни от 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 мс для большинства событий).
matchesCriteria(badge, event, userState)
Заголовок раздела «matchesCriteria(badge, event, userState)»| Тип критерия | Проверка |
|---|---|
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 |
Делегирует соответствующей вложенной проверке |
Встроенные значки (20+)
Заголовок раздела «Встроенные значки (20+)»Полный список значков
| Значок | Категория | Редкость | Критерии |
|---|---|---|---|
| Первые шаги | использование | обычный | 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 }>;- Прочитать запись серии из
key_value. - Разобрать
{current},{longest},{lastDate}(строка даты в формате ISO). - Если
lastDate === today— ничего не менять (сегодня уже учтено). - Если
lastDate === yesterday— увеличитьcurrent; при необходимости обновитьlongest. - Если
lastDate < yesterday— сброситьcurrent = 1(серия прервана). - Записать обновлённую запись.
- Проверить рубежи: 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 rankFROM leaderboardWHERE scope = ? AND period = ?ORDER BY score DESCLIMIT ? OFFSET ?Смена периода
Заголовок раздела «Смена периода»Еженедельная и ежемесячная таблицы лидеров обновляются автоматически:
- Архивация: на границе периода текущие записи копируются в
leaderboard_archiveс меткой периода. - Сброс: записи за завершившийся период удаляются.
- Триггер: проверка выполняется при каждом вызове
updateLeaderboard(); первый запрос в новом периоде запускает смену периода.
Это гарантирует, что еженедельные таблицы сбрасываются каждый понедельник в 00:00 UTC, а ежемесячные — 1-го числа каждого месяца.
Обновления SSE в реальном времени
Заголовок раздела «Обновления SSE в реальном времени»Эндпоинт: GET /api/gamification/stream
Клиент → GET /api/gamification/stream → SSE-соединение установлено → Сервер немедленно отправляет снимок топ-10 таблицы лидеров → Каждые 5 секунд: отправка обновлённого топ-10, если он изменился → Каждые 15 секунд: комментарий пульса (": heartbeat\n\n") → Клиент отключается → очистка (удаление слушателя)Формат события:
event: leaderboarddata: {"scope":"global","entries":[...]}
event: leaderboarddata: {"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 balanceFROM token_ledgerWHERE from_key_id = ? OR to_key_id = ?Процесс передачи
Заголовок раздела «Процесс передачи»export async function transferTokens( fromKeyId: string, toKeyId: string, amount: number, idempotencyKey: string): Promise<{ success: boolean; balance: number }>;- Валидация:
amount > 0,fromKeyId !== toKeyId. - Идемпотентность: проверить, существует ли уже
idempotency_keyв реестре. Если да, вернуть кешированный результат. - Транзакция (единая транзакция 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 больше никогда не хранит и не отображает его — сохраняется только хеш.
Предотвращение саморефералов
Заголовок раздела «Предотвращение саморефералов»Когда пользователь активирует код, система проверяет:
- Код принадлежит другому
api_key_id. - Активирующий пользователь ранее не активировал ни одного кода от того же
реферера (объединение
invite_tokensс журналом активаций).
Если любая из проверок завершается неудачей, активация отклоняется с понятным сообщением об ошибке.
Срок действия и ограничения
Заголовок раздела «Срок действия и ограничения»- Значение
max_usesпо умолчанию: 10 (настраивается при создании). - Значение
expires_atпо умолчанию: 30 дней с момента создания. - Для просроченных или исчерпавших лимит кодов возвращается HTTP 410 Gone.
Федерация серверов сообщества
Заголовок раздела «Федерация серверов сообщества»Файл: src/lib/gamification/servers.ts
Подключение
Заголовок раздела «Подключение»Сервер сообщества регистрируется с помощью токена приглашения, выданного удалённым сервером. Локальный экземпляр:
- Получает токен приглашения (например, вставленный в панели управления).
- Вызывает
POST /api/gamification/federation/leaderboardна удалённом сервере, чтобы проверить токен и получить текущую таблицу лидеров. - Сохраняет запись сервера со значением
status: connected.
Модель синхронизации
Заголовок раздела «Модель синхронизации»Федерация использует синхронизацию с перезаписью, а не с накоплением:
Локальный экземпляр Сервер сообщества │ │ ├── отправка очков ───────────►│ POST /federation/score │ { api_key_id, score } │ (сервер проверяет хеш токена) │ │ ├── запрос таблицы лидеров ───►│ GET /federation/leaderboard │◄── первые N записей ─────────┤ (перезаписывают локальный кеш) │ │ └── проверка состояния ───────►│ GET /federation/health (каждые 60 с, тайм-аут 5 с)│Аутентификация
Заголовок раздела «Аутентификация»Запросы федерации содержат:
Authorization: Bearer <raw_token>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.
Обнаружение аномалий с помощью Z-оценки
Заголовок раздела «Обнаружение аномалий с помощью Z-оценки»Для каждого ключа API система поддерживает скользящее 7-дневное окно количества XP, заработанного за час. При каждом начислении XP:
- Вычисляется текущая почасовая скорость получения XP пользователем.
- Вычисляются среднее значение и стандартное отклонение по всей совокупности.
- Вычисляется
z = (user_rate - mean) / stddev. - Если
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.
Маршруты API
Заголовок раздела «Маршруты API»Все маршруты соответствуют стандартному шаблону 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"}Инструменты MCP (8)
Заголовок раздела «Инструменты MCP (8)»Зарегистрированы в 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? } |
Страницы панели управления
Заголовок раздела «Страницы панели управления»/dashboard/leaderboard
Заголовок раздела «/dashboard/leaderboard»- Отображение пьедестала (3 лучших участника с аватарами и XP).
- Выбор области: глобальная / за неделю / за месяц / переданные токены / вклады.
- Таблица с пагинацией (по 25 записей на страницу), содержащая место, имя, результат, уровень и титул.
- Обновления в реальном времени через SSE — изменения позиций анимируются.
- Текущий пользователь выделен в таблице закреплённой строкой «Ваше место».
/dashboard/profile
Заголовок раздела «/dashboard/profile»- Индикатор прогресса XP с текущим уровнем и порогом следующего уровня.
- Значок титула отображается на видном месте.
- Галерея значков — полученные значки с датой получения, неполученные значки отображаются серыми (скрытые значки отображаются как «???» до получения).
- Счётчик серии со значком пламени; календарь серии (последние 30 дней).
- График истории XP (ежедневный XP за последние 30 дней).
/dashboard/tokens
Заголовок раздела «/dashboard/tokens»- Баланс токенов (на видном месте в верхней части страницы).
- Форма перевода: получатель, сумма, диалог подтверждения.
- Таблица истории переводов с фильтрами (отправленные/полученные/все).
- Раздел приглашений: активные коды, создание новых, ссылка для отправки.
- Серверы сообщества: список с состоянием работоспособности, подключение/отключение.
/dashboard/gamification/admin
Заголовок раздела «/dashboard/gamification/admin»- Список аномалий с указанием серьёзности, пользователя, временной метки и 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(() => {}) гарантирует:
- Ответ полностью отправляется до запуска геймификации.
- Ошибки геймификации никогда не передаются клиенту.
- Обработка события выполняется в следующей микрозадаче, а не синхронно.
Безопасность
Заголовок раздела «Безопасность»Модель угроз
Заголовок раздела «Модель угроз»| Угроза | Мера защиты |
|---|---|
| Искусственное завышение очков | Расчёт 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 # Этот документСтратегия миграции
Заголовок раздела «Стратегия миграции»Этап 1: Серверное ядро (PR 1)
Заголовок раздела «Этап 1: Серверное ядро (PR 1)»- Миграция
060_create_gamification.sql(8 таблиц). src/lib/db/gamification.ts(доменный модуль).src/lib/gamification/xp.ts,streaks.ts,events.ts.- Точка интеграции в
chatCore.ts. - Модульные тесты для XP, серий и событий.
Этап 2: Значки и рейтинг (PR 2)
Заголовок раздела «Этап 2: Значки и рейтинг (PR 2)»src/lib/gamification/badges.ts,leaderboard.ts.- Определения значков в константах.
- Маршруты API рейтинга + поток SSE.
- Модульные тесты для значков и рейтинга.
Этап 3: Передача и приглашения (PR 3)
Заголовок раздела «Этап 3: Передача и приглашения (PR 3)»src/lib/gamification/sharing.ts,invites.ts,antiCheat.ts.- Маршруты API переводов и приглашений.
- Модульные тесты для передачи, приглашений и защиты от злоупотреблений.
Этап 4: Федерация и панель управления (PR 4)
Заголовок раздела «Этап 4: Федерация и панель управления (PR 4)»src/lib/gamification/servers.ts,notifications.ts.- Маршруты API федерации.
- Страницы панели управления (рейтинг, профиль, токены, администрирование).
- Регистрация инструментов MCP.
Планы на будущее
Заголовок раздела «Планы на будущее»- Сезонные события: ограниченные по времени наборы значков и сезоны таблицы лидеров.
- Командные таблицы лидеров: группировка пользователей по организации или комбо.
- Множители XP: увеличение XP в периоды проведения промоакций.
- Публикация достижений: создание карточек значков, которыми можно делиться (изображения OpenGraph).
- Мобильные push-уведомления: уведомления на основе вебхуков о получении значков и уровней.
- API таблицы лидеров: публичный API для сторонних интеграций.
HagiCode
HagiCode — агентная среда разработки со структурированными процессами, параллельным выполнением несколькими агентами и интерфейсами Hero Dungeon.
Превращайте идеи в полезное ПО с более умным, быстрым и увлекательным агентным рабочим процессом.

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