О проекте
Команда: ХАХАТОНШИКИ
Кейс HackAlem.ai: Финансы — Freedom
Стадия: Доведённый продукт
Задача кейса: «Граф денег»: восстановить структуру организованной группы по сети транзакций (AML)
Суть
Локальный прототип AML-анализа транзакционной сети: роли участников, ранжирование узлов, кластеры и веб-интерфейс аналитика с картой сообществ и ассистентом на tool calling.
Как устроено
run.py загружает nodes/edges/transactions.parquet со строгой валидацией схемы и сверкой сумм, строит направленный граф и считает метрики: число контрагентов, входящие/исходящие суммы и их отношение, seed_reach, betweenness (выборка 128 узлов), fast_ratio по временному сопоставлению, взвешенный PageRank степенной итерацией на NumPy; по ним назначаются роли и приоритет, выгружаются CSV. LLM через NVIDIA API Catalog или Brev выбирает 1–3 разрешённые функции (node_details, find_path, common_recipients и др.), Python выполняет их и формирует ответ с точными числами; свободный текст модели фактом не считается.
Ключевые решения
- PageRank
- betweenness centrality
- временное сопоставление потоков
- кластеризация сообществ
- ролевые правила
- function calling tool_choice=required
Особенности
Ассистент, у которого модель выбирает только функции, а ответ целиком собирает код; в LLM не отправляется граф и результаты функций. Загрузка ZIP с parquet прямо в интерфейсе с автоматическим пересчётом.
Стек
Python · NumPy · parquet · локальный HTTP-сервер · vanilla JS · NVIDIA API Catalog · NVIDIA Brev · GitHub Actions
Модели
qwen/qwen3.5-122b-a10b · Qwen2.5-7B
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-0eeb6870-team.
README
README.mdГраф денег
Локальный прототип для кейса HackAlem AI. Восстанавливает гипотезы о ролях участников транзакционной сети, ранжирует узлы и показывает проверяемые основания. Графовые расчёты работают без LLM. Модель через NVIDIA API или Brev выбирает функции по вопросу на естественном языке; приложение формирует ответ из проверенных результатов.
Версия адаптирована к выданным файлам HackAlem: 2 248 узлов, 3 119 рёбер, 4 840 транзакций. Файлы организаторов нужно положить в data/; результаты появятся в results/. Эти папки не включены в Git. Синтетическое демо оставлено отдельно для тестирования.
Дополнительные материалы организаторов сохранены без изменений в reference/. Стартовые метрики расширены ролями, временными признаками, кластеризацией и интерфейсом. Разбор фактического набора: DATASET_REPORT.md.
Интерфейс аналитика: светлое рабочее пространство с боковой навигацией, картой сообществ, карточкой участника и таблицей приоритетов. Принципы дизайна и запуск превью описаны в DESIGN.md; источники и лицензия локальных иконок Streamline — в THIRD_PARTY_NOTICES.md.
Быстрый запуск
Нужен Python 3.10–3.12; проверено на Python 3.12. Установите зависимости один раз. Интернет нужен при установке пакетов и для необязательных вызовов NVIDIA. Интерфейс, анализ и CSV работают офлайн; внешних шрифтов, CDN и телеметрии нет.
Windows PowerShell, из папки проекта:
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\python.exe run.py --data data
macOS / Linux:
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python run.py --data data
Откройте http://127.0.0.1:8765. При >500 узлах стартовый экран показывает окружение наиболее приоритетного кандидата в консолидаторы: входящие слева, исходящие справа. Кнопка «Показать всю сеть» открывает обзор. Полный рейтинг и выгрузки всегда рассчитываются по всей сети. Для синтетического демо запустите python run.py --demo. Сервер слушает только loopback. Остановка — Ctrl+C. Для другого порта добавьте --port 8766.
Реальный датасет
Положите файлы организаторов в data/:
data/
nodes.parquet
edges.parquet
transactions.parquet
Один запуск от сырых Parquet до всех результатов и интерфейса:
.\.venv\Scripts\python.exe run.py --data data --out results
Только пакетный расчёт, без запуска сервера:
.\.venv\Scripts\python.exe run.py --data data --out results --no-serve
В Linux замените путь к интерпретатору на .venv/bin/python.
В уже открытом интерфейсе можно нажать «Загрузить ZIP». В архиве должны находиться ровно по одному файлу nodes.parquet, edges.parquet, transactions.parquet; вложенная папка допустима. Загрузка автоматически пересчитывает данные и обновляет выгрузки. При ошибке предыдущий результат сохраняется. Лимиты загрузки прототипа: ZIP 50 MB, каждый распакованный Parquet 50 MB, до 250 000 строк в таблице, до 10 000 узлов. Это ограничения интерфейса, не обещание SLA для 10 000 узлов.
Схема входных файлов
| Таблица | Обязательные поля |
|---|---|
| nodes | gid int64, depth целое 0–4, is_seed bool или 0/1 |
| edges | src, dst int64, sum_kzt положительное число, n_tx положительное целое, depth целое 1–4 |
| transactions | src, dst int64, date ISO-дата / timestamp, sum_kzt положительное число |
Дополнительные поля игнорируются. gid уникальны. Все ссылки должны присутствовать в nodes. is_seed согласован с depth=0. edges содержит одну строку на направленную пару. Суммы и числа транзакций проверяются против transactions (абсолютный допуск 0,02 KZT или относительный 1e-9). Дубли транзакций автоматически не удаляются: без transaction_id одинаковые строки могут быть разными переводами. Пустые edges и transactions допустимы при непустой nodes.
На хакатоне сначала сопоставьте эту схему с README организаторов. Нельзя молча заменять отсутствующие поля выдуманными значениями. Данные ниже порога 5 000 KZT сохраняются, но отмечаются в отчёте качества.
NVIDIA Brev
Если ваши кредиты находятся в Brev, используется модель на GPU-инстансе и SSH-туннель. Инструкция: BREV.md, конфигурация: .env.brev.example. Поддерживается настраиваемый NVIDIA_BASE_URL; API Catalog и его ключ для этого варианта не нужны.
NVIDIA API Catalog
Скопируйте .env.example в .env и заполните ключ локально:
NVIDIA_API_KEY=ваш_ключ
NVIDIA_MODEL=qwen/qwen3.5-122b-a10b
NVIDIA_MAX_CALLS=40
Перезапустите сервер. В панели ассистента включите переключатель LLM. Ключ не вводится в браузер, не попадает в ответы сервера и не включается в репозиторий. Переменные окружения имеют приоритет над .env.
Для API Catalog по умолчанию используется модель qwen/qwen3.5-122b-a10b. Можно указать другую доступную в вашем аккаунте модель, поддерживающую OpenAI-compatible tool calling и tool_choice="required". Доступность конкретной модели и списание кредитов зависят от аккаунта NVIDIA. Соединение с API Catalog не проверено без ключа; отдельно проверяется собственный Qwen2.5-7B на Brev через SSH-туннель (см. VALIDATION.md).
Официальный контракт API: NVIDIA Qwen3.5 chat completions. Запросы отправляются на https://integrate.api.nvidia.com/v1/chat/completions.
Как отвечает ассистент
- LLM получает вопрос, gid и кластер выбранного узла и ограниченную историю диалога; выбирает 1–3 разрешённые функции.
- Python валидирует аргументы и выполняет расчёт локально.
- Python формирует ответ из результатов функций: точные числа, условия роли, ограничения и ссылки
[gid:...]. Свободный текст модели не используется как аналитический вывод. - Интерфейс показывает ответ и раскрываемые результаты функций для проверки.
Доступные функции:
| Функция | Что возвращает |
|---|---|
node_details(gid) | Метрики, гипотезу роли, до 20 входящих и 20 исходящих связей, до 30 транзакций, путь от seed, ограничения |
top_nodes(limit, role) | До 20 приоритетных узлов, с необязательным фильтром роли |
common_recipients(gids) | Прямых получателей от каждого из 2–10 указанных клиентов и суммы именно этих рёбер; до 20 результатов |
find_path(source, target) | Кратчайший направленный путь, максимум 8 переходов |
cluster_details(cluster_id) | Назначение кластера, потоки, ведущие узлы и ограничения |
node_period(gid, start, end) | Наблюдаемые операции узла за включительный период дат ISO, без пересчёта месячной роли |
На вопрос расходуется не больше одного API-запроса. Кэш учитывает вопрос, выбранный узел и историю текущего набора. NVIDIA_MAX_CALLS ограничивает общее число запросов за сессию сервера, включая конкурентные запросы и смену датасета; это не денежный лимит. Таймаут — 90 секунд, автоматических повторов нет. При отсутствии ключа, ошибке API, неверных функциях, неизвестных gid или исчерпании лимита включается локальный ответ. Пайплайн не зависит от сети.
В API отправляются вопрос, выбранный gid, номер его кластера и до шести коротких сообщений истории (до 1000 символов каждое, суммарно до 2400); история может содержать ранее показанные суммы и gid. Результаты текущих инструментов и полный граф в модель не отправляются. Это исключает переполнение контекста большими выдачами функций. История используется для понимания вопроса, а не как источник фактов. Выход LLM не меняет роли, скоры или CSV. Идентификаторы в вызовах должны быть упомянуты в контексте или выбраны в карточке; даты периода должны быть явно указаны пользователем в формате YYYY-MM-DD. Неподтверждённые аргументы отклоняются. Модель всё ещё может выбрать неподходящий инструмент: его имя, аргументы и результат раскрываются в интерфейсе. Числа и объяснение правил формируются кодом, поэтому свободная галлюцинация модели не выдаётся за факт.
Локальный режим поддерживает команды:
Справка по 1020
Общие получатели 1000 1001
Путь от 1000 до 1030
Кого смотреть первым?
Идентификаторы в этих примерах относятся только к синтетическому демо. Для реальных данных используйте gid из таблицы или карточки: они состоят из 18 цифр. Не открывайте CSV в Excel как обычные числа без настройки типа столбца: Excel может округлить длинные идентификаторы. Импортируйте gid/src/dst как текст; приложение и сами CSV сохраняют все цифры.
Методика
Граф и метрики
edges.depth — колено обнаружения ребра (1–4); это не глубина его отправителя. nodes.depth — минимальная глубина клиента (0–4). Эти поля не смешиваются.
Сначала добавляются все узлы из nodes, затем направленные рёбра. Изолированные seed не теряются. Самопереводы сохраняются в исходном графе, выгрузке связей и обороте, но исключаются из числа контрагентов, входящих/исходящих объёмов для ролей и временного сопоставления.
Для узла используются:
I,O: число различных отправителей и получателей без самого узла;S_in,S_out: наблюдаемые входящие и исходящие суммы;R = S_out / S_in, еслиS_in > 0, иначе null;seed_reach: число разных seed, от которых существует направленный путь длиной ≤4; сам узел не считается собственным seed-предком;neighbor_clusters: число различных кластеров среди входящих/исходящих соседей, включая свой кластер, если в нём есть сосед;betweenness: нормированная посредническая центральность направленного невзвешенного графа. При >128 узлах используется выборка 128 стартовых узлов, seed=42. Сумма перевода не трактуется как расстояние;fast_ratio: временно сопоставленная сумма / наблюдаемый входящий объём, от 0 до 1;in_tx,out_tx: число входящих и исходящих переводов; количество операций даёт отдельный вклад в приоритет;pagerank: PageRank с весами сумм и alpha=0,85 на графе узлов с рёбрами, как в starter. Изолированным узлам присваивается 0. Вычисляется степенной итерацией (L1-допуск n×1e-10, до 1000 итераций) через NumPy, без SciPy. Это дополнительная диагностическая метрика, а не доказательство роли;in_deg,out_deg,in_kzt,out_kzt,pass_through,truncated_by_depth: имена, совместимые со starter. При отсутствии входящих pass_through пуст в CSV (не определён). Обязательные поля всегда заполнены.
Сообщества
Louvain, resolution=1, random seed=42. Для кластеризации граф превращается в неориентированный: встречные суммы складываются. Изолированные узлы получают отдельные кластеры. ID кластеров стабилизируются сортировкой по убыванию размера и минимальному gid. Внутренний оборот — сумма направленных рёбер с обоими концами внутри кластера, а не сумма объёмов узлов, которая посчитала бы рёбра дважды.
Гипотеза назначения кластера строится по наблюдаемым внутренним и внешним потокам, концентрации и распределению ролей. Она сопровождается численными основаниями; при недостатке признаков функция сообщества отмечается как неопределённая. Алгоритм не утверждает, что кластер является одной преступной группой.
Роли и порядок правил
Первое подходящее правило назначает основную роль. Поэтому узел с признаками нескольких ролей получает одну роль по явно заданному приоритету. Пороговые значения — стартовые эвристики, не обученная модель; корректировать их следует в едином источнике role_rules, который используют расчёт и объяснения.
| Порядок | Роль | Критерий | role_score |
|---|---|---|---|
| 1 | peripheral + truncated_by_depth | depth=4 и O=0 | 0; функция узла неизвестна |
| 2 | coordinator | neighbor_clusters≥3, seed_reach≥3, I≥2, O≥2 | min(0,85; 0,5 + 0,05×neighbor_clusters + 0,02×min(seed_reach;10)) |
| 3 | distributor | O≥8 и O≥2×max(I;1) | min(0,95; 0,6 + 0,35×min(O/30;1)) |
| 4 | consolidator | не seed; I≥4; R≤0,35 | min(0,9; 0,55 + 0,2×min(I/12;1) + 0,15×(1−R)) |
| 5 | transit | не seed; I>0; O>0; 0,8≤R≤1,2 | min(0,95; 0,6 + 0,2×(1−abs(1−R)/0,2) + 0,15×fast_ratio) |
| 6 | terminal | не seed; I>0; O=0; depth<4 | 0,5 |
| 7 | peripheral | остальные | 0,2; слабая поддержка определённой роли |
В CSV используются строго шесть ролей из starter: consolidator, transit, distributor, terminal, coordinator, peripheral. Для обрезанного узла записываются role=peripheral, role_score=0, truncated_by_depth=True и численное объяснение о четвёртом колене. В интерфейсе он отдельно помечен «Граница наблюдения». Это неизвестная финансовая функция, а не утверждение о низкой значимости.
role_score — эвристическая выраженность признаков выбранной роли, не калиброванная вероятность, не accuracy и не вероятность виновности. У seed R отображается для прозрачности, но не используется для назначения consolidator, transit или terminal. При других ролях seed может иметь высокий приоритет по структуре сети.
Каждая карточка и ответ инструмента содержат role_rule: идентификатор правила, его порядок и условия с фактическими значениями. Эти условия берутся из того же источника, что назначает роль. Дополнительные подходящие роли отображаются отдельно (secondary_roles); основная роль для CSV остаётся одной. temporal_support отдельно сообщает, наблюдалось ли сопоставление переводов во времени. Месячный коэффициент транзита не означает доказанную скорость движения средств.
Приоритет проверки
Для каждого признака рассчитывается средний порядковый ранг среди строго положительных значений, делённый на их число. Совпадающие значения имеют одинаковый ранг. Нулевые значения всегда получают 0. Обозначим это P.
role_signal = role_score для coordinator/distributor/consolidator/transit/terminal
role_signal = 0 для peripheral (включая границу)
priority_score = 0.25 × P(S_in + S_out)
+ 0.25 × P(seed_reach)
+ 0.10 × P(in_tx + out_tx)
+ 0.20 × P(betweenness)
+ 0.20 × role_signal
Вклад каждой части виден в карточке узла. При равном приоритете порядок задаётся gid. Top CSV содержит все узлы, поэтому при ≥20 узлах требование топ-20 автоматически выполнено. Приоритет относителен текущему набору: изменение выборки может изменить ранги.
meta.priority_sensitivity проверяет десять сценариев: изменение каждого веса на ±20% с перенормировкой остальных долей и сравнение состава топ-20. Это диагностика чувствительности к весам, а не проверка истинности ролей. Чувствительность порогов правил, выбора seed и Louvain отдельно не измеряется.
Временные признаки
FIFO: входящая сумма сопоставляется только с более поздним исходящим переводом в пределах 48 часов. Расходование частичное; одна входящая сумма не используется повторно. Для одинаковой временной метки исходящие обрабатываются раньше входящих. При данных только по дням переводы в тот же день намеренно не сопоставляются: их порядок неизвестен. Временные метки с часовым поясом приводятся к UTC; naive timestamps условно считаются UTC, сохраняя интервалы внутри набора.
Это дополнительный аргумент для транзита, не доказательство движения именно тех же денежных средств. Остатки на счёте и операции за пределами выборки неизвестны.
Ограничения
- depth=4 с нулевым исходящим потоком — граница наблюдения, а не доказанный получатель.
- Входящие неполны у всей сети, особенно у seed. Отправил больше полученного — не самостоятельная аномалия.
- Порог 5 000 KZT скрывает мелкие операции. Межбанковские переводы и остатки неизвестны.
- Terminal до границы — тоже предварительная гипотеза: деньги могли уйти вне банка или периода.
- Louvain и коэффициенты приоритета задают один объяснимый вариант, не единственное правильное решение.
- Метрики не обучались на ground truth. Утверждения о точности на реальных ролях не делаются.
- Из-за seed-based отбора выборка не репрезентативна всей клиентской базе.
- HTTP-сервер предназначен для локального демо, не многопользовательского продакшена.
Результаты
Готовые обязательные CSV текущего конкурсного набора приложены в submission/ вместе с manifest.json (хеш набора, проверки и SHA-256 файлов). Воспроизведение пакета: python scripts/package_submission.py --data data --out submission. Скрипт не копирует сырые транзакции. Исходный датасет остаётся отдельно; CSV предназначены для сдачи в рамках хакатона.
В results/:
| Файл | Содержание |
|---|---|
| nodes_roles.csv | gid, role, role_score, cluster_id, priority_score, evidence плюс диагностические поля starter; строка на каждый узел, evidence содержит числа и ≤200 символов |
| clusters.csv | cluster_id, n_nodes, n_seed, sum_kzt_internal, top_gids, hypothesis |
| top_nodes.csv | rank, gid, role, priority_score, why; все узлы по приоритету |
| analysis.json | Метрики, граф, координаты для интерфейса и транзакции |
| quality.json | Размеры, компоненты, предупреждения и хеш набора |
CSV используют UTF-8 BOM для корректного открытия кириллицы в Excel. Идентификаторы в HTTP JSON передаются строками, чтобы JavaScript не округлял int64. В CSV gid остаётся десятичным целым.
Проверки
.\.venv\Scripts\python.exe -m unittest discover -s tests -v
Тесты проверяют сохранение изолированных seed, границу глубины, все правила ролей, FIFO без двойного расходования и обратного времени, детерминизм при перестановке строк, целостность трёх таблиц, экспорты, int64, общих получателей и направление путей, цикл NVIDIA tool calling через тестовый транспорт, кэш, fallback, запрет неизвестных функций, лимит запросов, HTTP, ZIP-пересчёт и защиту локального API от постороннего Origin.
Регрессии аудита дополнительно проверяют точные идентификаторы без float, глубину BFS, канонические условия ролей, гипотезы кластеров, неверные ответы модели, общий бюджет при замене набора, повторную загрузку ZIP и ограничения Parquet до декодирования. UI-обработчики проверяются командой node tests/ui_regressions.cjs. GitHub Actions запускает проверки на Windows и Linux и генерирует демо-CSV. Шесть тестов реального набора пропускаются без файлов организаторов; для полной проверки поместите их в data/ и повторите unittest.
Нагрузочная проверка на синтетическом наборе размера кейса:
.\.venv\Scripts\python.exe benchmark.py
Это проверка производительности, не качества выделения реальных ролей. Итоги локальной проверки записаны в VALIDATION.md.
Демонстрация за 5 минут
- Запустите
python run.py --data data, покажите 2 248 узлов и три готовые CSV. - Найдите
100000003701161100: 4 отправителя, 13 входящих переводов; наблюдаемая доля перевода дальше 22.9%. Откройте окружение и основания консолидации. - Найдите
100000003635170100: гипотеза транзита. Покажите отношение потоков, число операций и временное сопоставление. - Найдите
100000008175078100: граница четвёртого колена. Объясните, почему это не доказанный конечный получатель. - Нажмите «Составить справку», раскройте результаты инструмента, перейдите по gid.
- Покажите вклад каждого признака в приоритет, отчёт качества и README. Подчеркните: 88 кластеров всего, из них 8 с несколькими seed; 16 компонент со связями плюс 19 изолированных узлов.
Эти примеры взяты из текущего расчёта и не используются алгоритмом для назначения ролей. После смены данных выбирайте новые примеры из рейтинга. Расширенный разбор — DATASET_REPORT.md.
Схема для слайда: architecture.svg. Исходники: moneygraph/analysis.py, assistant.py, server.py, static/. Синтетический генератор отделён в demo.py.
Рост до миллиона узлов
Потребуются изменения архитектуры, а не просто больше памяти: колоночная агрегация Parquet через DuckDB/Polars; хранение графа CSR/CSC и расчёты в igraph/NetworKit; распределённая/приближённая кластеризация при необходимости. Полную посредническую центральность следует заменить выборочной оценкой или более дешёвыми локальными признаками. Достижимость от seed — через битовые множества/батчи, не BFS от каждого seed при их большом числе. Расчёт становится пакетным фоновым заданием с версионированными результатами.
В браузер отдавать агрегаты сообществ и подграф выбранного узла, а не миллион вершин. Координаты рассчитывать и кэшировать отдельно. Инструменты LLM должны сохранять ограничение объёма выдачи; полный граф в промпт не передавать. Добавить БД результатов, авторизацию, аудит запросов, контроль доступа и безопасное хранение ключей для многопользовательской эксплуатации. Реализация масштаба в миллион узлов в этот прототип не входит.
Авторы коммитов
История коммитов
- 2026-09-23 17:59merge: keep main audit updates and remove requested reportswaytowin
- 2026-09-23 17:59chore: remove temporary dataset and QA reportswaytowin
- 2026-09-23 17:44Merge remote-tracking branch 'origin/main' into review/main-quick-auditkrein63
- 2026-09-23 17:41docs: record integrated design and local QA resultswaytowin
- 2026-09-23 17:40fix: validate transaction amounts and record main reviewkrein63
- 2026-09-23 17:38fix(qa): ISSUE-002 normalize CSV bytes and repair submission hasheswaytowin
- 2026-09-23 17:38test(qa): regression coverage for ISSUE-002 CSV reproducibilitywaytowin
- 2026-09-23 17:37fix(qa): ISSUE-001 keep graph sizing stable across viewport changeswaytowin
Все коммиты (19)
- 2026-09-23 17:35merge: integrate latest main audit fixes with dashboard designwaytowin
- 2026-09-23 17:33feat: add offline analyst dashboard designwaytowin
- 2026-09-23 17:28docs: record GitHub Actions billing blockertorch883
- 2026-09-23 17:26fix: resolve audit findings and verify graph assistant outputstorch883
- 2026-09-23 16:59docs: add usage and dataset referencestorch883
- 2026-09-23 16:40feat: add MoneyGraph analysis UI and Brev assistanttorch883
- 2026-09-23 16:11chore: add validation and Brev toolingtorch883
- 2026-09-23 15:23feat: add analysis API and assistant UItorch883
- 2026-09-23 14:35feat: add transaction graph analysistorch883
- 2026-09-23 14:02init:FIRTSwaytowin
- 2026-09-23 13:47init: bootstrap MoneyGraph projectwaytowin
Структура репозитория (49 файлов)
.env.brev.example .env.example .gitattributes .github/workflows/test.yml .gitignore .gstack/qa-reports/baseline.json AUDIT_FIXES.md BREV.md DESIGN.md QUICK_REVIEW.md README.md THIRD_PARTY_NOTICES.md VALIDATION.md architecture.svg benchmark.py demo-data/DEMO.txt demo-data/edges.parquet demo-data/nodes.parquet demo-data/transactions.parquet main.py moneygraph/__init__.py moneygraph/analysis.py moneygraph/assistant.py moneygraph/demo.py moneygraph/server.py moneygraph/static/app.js moneygraph/static/icons.js moneygraph/static/index.html moneygraph/static/style.css reference/dataset_README.md reference/starter_README.md reference/starter_requirements.txt reference/starter_starter.py requirements.txt run.py scripts/package_submission.py start-brev-tunnel.ps1 submission/clusters.csv submission/manifest.json submission/nodes_roles.csv submission/top_nodes.csv tests/test_analysis_regressions.py tests/test_assistant_regressions.py tests/test_brev.py tests/test_core.py tests/test_dataset.py tests/test_export_regressions.py tests/test_server_regressions.py tests/ui_regressions.cjs
Ветки (2)
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.