О проекте
Команда: Tovarish.Sanechka
Кейс HackAlem.ai: Финансы — Freedom
Стадия: Доведённый продукт
Задача кейса: «Граф денег»: восстановить структуру организованной группы по сети транзакций (AML)
Суть
AML-система: по трём Parquet-файлам строит граф транзакций, определяет роли, сообщества и приоритеты узлов. Интерфейс на React/Cytoscape: маршруты, проверка хронологии, досье, AI-ассистент.
Как устроено
Python-пакет (starter.py и scoring.py) валидирует Parquet и строит DiGraph. Считаются потоки, достижимость от seed, дневные признаки, PageRank и betweenness, Louvain на ненаправленной проекции. Роли назначаются правилами с порогами из config.toml, приоритет независим от роли: 35% охват seed, 40% структура, 25% оборот. Дополнительно строятся кратчайшие пути, сценарии устойчивости (исключение top-N) и чувствительность ролей при порогах ±10%. Результат экспортируется в 3 CSV и analysis.json. React-клиент ищет общих участников и проверяет хронологию дат по путям. Ассистент через Alem GPT OSS только переводит вопрос в 1–3 разрешённые операции.
Ключевые решения
- NetworkX
- Louvain
- PageRank
- betweenness
- reachability
- shortest paths
- temporal path validation
- sensitivity analysis
- rule-based roles
- intent parsing LLM
Особенности
Хронология путей проверяется: сначала ищется строго возрастающая последовательность дат, потом неубывающая, с поиском альтернативного маршрута при конфликте. Healthcheck сверяет SHA-256 опубликованных файлов. LLM не считает факты, только выбирает разрешённую команду. CSV побайтово воспроизводимы.
Стек
Python 3.12 · pandas · PyArrow · NetworkX · SciPy · React 19 · TypeScript · Vite · Cytoscape.js · Docker Compose · uv · pytest · Playwright
Модели
GPT OSS via Alem
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-5fc9e139-tovarish-sanechka.
README
README.mdГраф денег · HackAlem
1. Краткое описание
«Граф денег» — локальная система анализа транзакционной сети, которая помогает AML-аналитику определить, каких участников проверять первыми и почему. Она преобразует три Parquet-файла в направленный граф, рассчитывает признаки, гипотезы ролей, сообщества и приоритеты, затем публикует согласованные CSV-выгрузки и данные для браузера. Решение рассчитано на пакетную обработку обезличенной выборки: 2 248 узлов, 3 119 связей и 4 840 операций за июль 2026.
Роли — проверяемые гипотезы. Баллы не являются вероятностью нарушения или доказательством виновности.
Быстрый запуск из корня репозитория: docker compose up -d --build --wait --wait-timeout 360, затем открыть http://localhost:8080. Для основных функций .env и API-ключ не нужны; первая сборка требует сети. Готовые CSV и схема решения лежат в submission/, сценарий демонстрации — в DEMO.md.
2. Технический стек
| Категория | Технологии | Ответственность |
|---|---|---|
| Core | Python 3.12.13; React 19.3, TypeScript 7.0, Vite 8.3 | Пакетный расчёт, HTTP-сервер с API ассистента, браузерный клиент |
| Data | Parquet, pandas 3.0.6, PyArrow 25.0.1, NumPy 2.5.3 | Валидация, агрегация, признаки и сериализация |
| Графовая аналитика / AI/ML | NetworkX 3.7, SciPy 1.18.1 | PageRank, betweenness, достижимость, Louvain, объяснимые правила ролей; обучаемых моделей для ролей нет; опциональный GPT OSS через Alem распознаёт вопросы |
| Визуализация | Cytoscape.js 3.34.3 | Направленные подграфы и маршруты из результатов расчёта |
| Infra | Docker, Docker Compose, uv 0.11.21 | Multi-stage build, фиксированное окружение, запуск и healthcheck |
| Проверки | pytest 9.1.1, Playwright 1.63.0, TypeScript | Аналитические инварианты, контракт данных, браузерные сценарии и проверка типов |
Python-зависимости зафиксированы в pyproject.toml и uv.lock, frontend — в package.json и package-lock.json. Базовые образы в Dockerfile закреплены версиями и digest. PostgreSQL, Redis и отдельный API-сервис не используются.
3. Архитектура и поток данных
Системная схема
flowchart TB
INPUT["nodes / edges / transactions.parquet"] --> VALIDATE["Проверка схем и согласованности операций"]
CONFIG["config.toml: пороги, веса, random_seed"] --> ANALYSIS
subgraph APP["Docker Compose: сервис app"]
subgraph ANALYSIS["Python: пакетный расчёт"]
VALIDATE --> GRAPH["DiGraph: все узлы, включая изоляты"]
GRAPH --> FEATURES["Потоки, структура, достижимость, дневные признаки"]
FEATURES --> CLUSTERS["Louvain и признаки соседних сообществ"]
CLUSTERS --> SCORING["Правила ролей и независимый приоритет"]
GRAPH --> PATHS["Кратчайшие направленные пути"]
SCORING --> RESILIENCE["Исключение топ-N и контрольные сценарии"]
SCORING --> EXPORT["Проверка выходного контракта и запись"]
PATHS --> EXPORT
RESILIENCE --> EXPORT
end
EXPORT --> FILES["3 CSV + analysis.json v3"]
FILES --> HTTP["run.py: файлы + API ассистента"]
ASSETS["React build из Docker build-стадии"] --> HTTP
HEALTH["healthcheck.py: run_id, SHA-256, схемы"] --> HTTP
end
HTTP --> BROWSER["Браузер: роли, маршруты по датам, досье"]
BROWSER --> QUESTION["Вопрос + идентификаторы контекста"]
QUESTION --> API["assistant.py: проверка запросов"]
API --> ALEM["Alem GPT OSS: распознавание намерения"]
ALEM --> API
API --> VALID["Проверенный JSON-запрос"]
VALID --> BROWSER
SCORING --> SENS["Три сценария порогов"]
SENS --> EXPORT
BROWSER --> SEARCH["Общий поиск и проверка дат выбранных путей"]
BROWSER --> URL["URL: контекст расследования"]
BROWSER --> LOCAL["localStorage: решения и заметки"]
Границы компонентов
| Компонент | Что делает | Почему отдельно |
|---|---|---|
| starter.py | Читает и проверяет Parquet; строит граф, признаки, сообщества, пути и сценарии устойчивости; формирует результаты | Единый расчёт для CSV и браузера исключает расхождение ролей и рейтингов |
| scoring.py | Применяет правила ролей и нормализует компоненты приоритета | Правила можно проверить по признакам и конфигурации без запуска интерфейса |
| run.py | Запускает расчёт; после успешного завершения обслуживает каталог результата, frontend и два endpoint ассистента | HTTP не открывается до проверки данных; Parquet и исходный код не публикуются |
| assistant.py | Читает серверные настройки Alem, распознаёт вопрос через GPT OSS и проверяет допустимость команды | Модель не вычисляет факты; ключ и полный граф не передаются браузеру и провайдеру соответственно |
| healthcheck.py | Сравнивает локальный и HTTP-манифесты запуска, хеши файлов, схемы и число строк | Готовность означает доступность согласованных результатов, а не только открытый порт |
| web/src | Загружает и валидирует analysis.json, выполняет интерактивные обходы графа и проверку дат | Роли и приоритеты не пересчитываются в браузере; запросы аналитика не требуют нового Python-процесса |
Входящие и исходящие суммы сохраняют направление переводов. Для Louvain строится отдельная ненаправленная проекция: встречные потоки суммируются, самопереводы исключаются из кластеризации. Изолированные узлы остаются в полном графе и получают собственные кластеры. При фиксированных входных данных и конфигурации повторный расчёт даёт побайтово одинаковые CSV и JSON; идентификатор запуска run_id при этом меняется.
Объяснимая аналитика
Пороговые правила находятся в config.toml; обоснование порогов и формулы — в ANALYTICS.md. Исходный клиент (seed) — один из 81 участников, от которых собиралась выгрузка.
| Роль | Условия при конфигурации по умолчанию |
|---|---|
consolidator | ≥3 плательщиков; достижимость от ≥2 исходных клиентов; доля крупнейшего плательщика ≤80% |
distributor | ≥5 получателей; доля крупнейшего получателя ≤80% |
transit | Не исходный клиент; есть плательщики и получатели, отличные от самого узла; суммарные вход и выход ≥164 528 ₸; отношение выхода ко входу 0,7–1,4; доля выхода с поступлением в предшествующие 0–2 дня ≥69% |
terminal | Не исходный клиент и не обрыв на depth=4 без внешних получателей; есть другой плательщик; суммарный вход ≥164 528 ₸; отношение выхода ко входу ≤10% |
coordinator | Достижимость от ≥9 исходных клиентов; ≥2 внешних соседних кластеров; betweenness ≥0,0001553 |
peripheral | Остальные правила не выполнены |
Выбирается роль с максимальным допустимым баллом; конкурирующая гипотеза снижает итоговый role_score. При равенстве применяется role_order из конфигурации. Приоритет рассчитывается независимо от выбранной роли: 35% охвата исходных клиентов + 40% структуры + 25% оборота, с логарифмической нормализацией признаков. Каждый вклад сохраняется отдельно, равенства разрешаются по числовому gid.
Браузерный поиск пересекает множества узлов, достижимых от 2–5 выбранных исходных клиентов, и исключает уже известных участников. Для каждого кандидата строятся кратчайшие направленные пути. Проверка дат сначала ищет строго возрастающую последовательность операций, затем неубывающую; результат различает согласованный порядок, неопределённость внутри дня и конфликт дат. Для конфликта или неизвестного порядка внутри дня доступен поиск альтернативы: сначала строго возрастающие даты, затем неубывающие; в каждом режиме выбирается путь с минимумом шагов. Исходный маршрут сохраняется для сравнения.
AI-ассистент через Alem переводит вопросы в одну–три разрешённые операции: карточка узла, общие участники, маршрут, чувствительность роли. Python проверяет команду, браузер выполняет её теми же функциями, что обычный интерфейс. Ответы — карточки с рассчитанными числами и переходами, без свободного пересказа модели. Промпт находится в assistant.py; агентного цикла нет.
Карточка сравнивает роли при шести порогах −10%, базовых и +10%. Приоритет остаётся базовым. «Сформировать досье» открывает документ одного участника: пути и операции, основания и ограничения, три сценария, решение и заметка; печать позволяет сохранить PDF. Все эти функции работают без AI.
Контракт, публикация и хранение
| Результат | Контракт |
|---|---|
nodes_roles.csv | Все 2 248 узлов: gid, role, role_score, cluster_id, priority_score, evidence и дополнительные признаки; evidence ≤200 символов |
clusters.csv | cluster_id, n_nodes, n_seed, sum_kzt_internal, top_gids, hypothesis; на текущих данных 88 кластеров |
top_nodes.csv | Все узлы по убыванию приоритета: rank, gid, role, priority_score, why |
analysis.json | schema_version=3; метаданные, конфигурация, узлы с тремя сценариями роли, связи с датами и исходными операциями, кластеры, рейтинг и сценарии связности |
ready.json | Манифест текущего HTTP-запуска: run_id, число узлов и SHA-256 опубликованных файлов |
gid передаётся в JSON строкой: 64-битные идентификаторы нельзя безопасно преобразовывать в JavaScript number. В обязательных CSV идентификаторы записаны десятичными целыми; при импорте в электронную таблицу выбирайте текстовый тип столбца. top_gids и seed_path_gids в CSV — JSON-массивы строк; неопределённые дополнительные отношения представлены пустым значением в CSV и null в JSON. Обязательные поля заполнены, скоры в файлах лежат в диапазоне 0–1.
Результаты сначала проверяются и записываются во временный каталог, затем каждый файл заменяется через os.replace. Это атомарная замена отдельных файлов, не транзакция всего набора. Согласованность при обслуживании обеспечивается порядком запуска и эксклюзивной блокировкой каталога: блокировка удерживается до остановки HTTP-сервера. При ошибке расчёта новый сервер не запускается.
В Compose не настроены volumes: /app/out находится в файловом слое контейнера, при пересоздании результаты рассчитываются заново. Контекст расследования хранится в URL, а решения и заметки — в localStorage браузера с привязкой к периоду и gid. Сервер не принимает эти записи; передача решений выполняется CSV-выгрузкой, синхронизации между устройствами нет.
Архитектурные решения — ADR
- Пакетный расчёт и статическая публикация. Вход — конечная выгрузка, обновления в реальном времени не требуются. Один процесс расчёта и файловый контракт упрощают воспроизводимость и позволяют обходиться без БД, очередей и отдельного backend-сервиса. Компромисс: новые данные требуют полного пересчёта.
- Правила и графовые метрики вместо обученной модели. В датасете нет размеченных ролей. Явные пороги позволяют объяснить каждую гипотезу и проверить чувствительность результата; оценка accuracy без разметки не заявляется.
- Вычислительное ядро в Python, интерактивные запросы в браузере. Python формирует единую модель анализа, React работает с её снимком. Основные функции работают локально, внешний API нужен только для распознавания вопросов; компромисс — весь JSON загружается в память клиента и подходит текущему объёму, а не миллиону узлов.
Ограничения и масштабирование
Выборка включает только внутрибанковские операции ≥5 000 ₸ за июль 2026 и четыре шага исходящего обхода. Входящие исходных клиентов неполны; дальнейшие переводы 444 граничных узлов неизвестны. Поэтому отсутствие исходящих не используется как достаточное основание для terminal на границе. Достижимость и согласованные даты не доказывают движение одной суммы; оборот повторно учитывает деньги вдоль цепочек. Проверка устойчивости моделирует связность наблюдаемого графа, а не эффект реального блокирования счетов.
При росте до ~1 млн узлов потребуется заменить полный граф NetworkX и точную betweenness компактным графовым представлением и приближёнными метриками, выполнять агрегации пакетно, а подграфы и списки выдавать порционно через API. Браузеру следует передавать выбранную область сети вместо полного JSON. Эти изменения описывают направление развития и не входят в текущую реализацию.
Описание распределений — DATA_PROFILE.md, результаты проверок — VERIFICATION.md, навигация по документации — docs/README.md.
4. Установка, запуск и проверка
Docker Compose
Нужны Docker Engine с Compose и браузер. Команды выполняются из корня репозитория. Конфигурация находится в compose.yaml; в ней один сервис app. Файла docker-compose.yml и сервиса базы данных нет. Используется CLI docker compose.
-
Проверьте конфигурацию. Аналитические параметры задаются в
config.toml, исходные файлы лежат вcase/data/. Для основных функций.envне нужен. Для AI задайте три переменные Alem, описанные ниже; Compose передаёт их только серверному процессу.docker compose config --quiet -
Соберите и запустите приложение. Docker собирает frontend, устанавливает Python-зависимости и запускает расчёт автоматически.
docker compose up -d --build --wait --wait-timeout 360--waitожидает готовности сервиса; параметры описаны в официальной документации Compose. Первая сборка требует доступа к реестрам образов и пакетов. После сборки основные функции работают без интернета; AI требует доступа к Alem; лимит ТЗ в пять минут относится к пересчёту данных, а не установке зависимостей. -
Проверьте статус и откройте приложение.
docker compose ps docker compose exec -T app python healthcheck.pyАдрес: http://localhost:8080. Контейнер слушает
0.0.0.0:8080, на хосте опубликован только127.0.0.1:8080. Процесс работает от пользователяappс UID 10001. Приложение предназначено для локального использования: аутентификация и TLS не реализованы.
Для просмотра логов и остановки:
docker compose logs -f app
docker compose down
Если 8080 занят, замените публикацию в compose.yaml на 127.0.0.1:8081:8080 и откройте http://localhost:8081. После изменения Parquet или config.toml повторите команду запуска с --build: данные и конфигурация копируются в образ. Обычный restart пересчитывает данные, уже находящиеся в образе.
Настройка AI-ассистента
AI включается только при непустых всех трёх настройках ниже. Если .env отсутствует и переменные не заданы в окружении либо не хватает хотя бы одной настройки, расчёт и интерфейс запускаются как обычно. В панели ассистента появляется «AI не настроен. Основные функции доступны», отправка вопроса отключена. Роли, граф, общий поиск, маршруты по датам, досье и CSV доступны без AI.
В корневом .env или окружении серверного процесса задайте:
ALEM_API_KEY=ваш_ключ
ALEM_CHAT_URL=https://llm.alem.ai/v1/chat/completions
ALEM_MODEL=gpt-oss
Используйте полный рабочий URL из вашего сервиса Alem: инструкция указывает /v1/chat/completions, а примеры — /chat/completions. Приложение отправляет запрос ровно по ALEM_CHAT_URL, без автоматического переключения. Существующий .env не перезаписывайте.
При нативном запуске окружение процесса имеет приоритет над .env, в том числе пустое значение. Для запуска без AI задайте ALEM_API_KEY=. После изменения настроек перезапустите процесс; для Compose выполните docker compose up -d --force-recreate --wait.
Ключ остаётся на сервере; .env исключён из Git и Docker image. Провайдер получает вопрос, выбранный gid, до пяти исходных gid и до шести предыдущих вопросов. Полный граф, транзакции и заметки не отправляются. История сохраняется только в памяти вкладки. Отмена прекращает ожидание браузера; уже отправленный запрос провайдеру может завершиться до серверного тайм-аута.
Без настройки, сети или при ошибке Alem интерфейс показывает причину; ручной поиск, сценарии порогов и досье остаются доступны. Контракт и ограничения — ASSISTANT.md.
Проверить состояние без вывода ключа: открыть локальный статус ассистента. enabled=true подтверждает только наличие настроек; доступность Alem и действительность ключа проверяются при отправке вопроса. Ошибки чтения .env и проверки URL, обработанные приложением, отключают только AI. Ошибка синтаксиса .env на этапе чтения самим Compose может помешать запуску контейнера.
Сохранение CSV
В submission/ уже сохранены три CSV, рассчитанные на исходных данных с текущим config.toml, а также схема решения. Запуск пересчитывает файлы в /app/out контейнера или нативном out/; каталог submission/ автоматически не обновляется. После изменения данных или порогов используйте новые выгрузки.
Для сохранения результатов до удаления контейнера:
mkdir -p exports
docker compose cp app:/app/out/nodes_roles.csv ./exports/nodes_roles.csv
docker compose cp app:/app/out/clusters.csv ./exports/clusters.csv
docker compose cp app:/app/out/top_nodes.csv ./exports/top_nodes.csv
Разработка без Docker
Для проверенного окружения: Python 3.12.13, Node.js 24.20.0 и uv 0.11.21. Команды также выполняются из корня репозитория; нативный запуск использует POSIX-блокировку fcntl, на Windows используйте Docker или WSL.
uv sync --locked
npm --prefix web ci
npm --prefix web run build
uv run python run.py --data case/data --out out --serve
Без --serve выполняется только расчёт, сборка frontend не требуется. Параметры run.py:
| Параметр | Значение по умолчанию | Назначение |
|---|---|---|
--data | <корень проекта>/case/data | Три входных Parquet-файла |
--out | <корень проекта>/out | Каталог публикации |
--config | <корень проекта>/config.toml | Пороги и веса |
--ui | <корень проекта>/web/dist | Собранный frontend |
--serve | Выключен | После расчёта запустить HTTP-сервер; без флага процесс завершится после экспорта |
--host | 127.0.0.1 | Адрес HTTP при нативном запуске |
--port | 8080 | Порт HTTP |
Каталог --out нельзя пересчитывать, пока его обслуживает другой процесс.
Для отдельного пробного расчёта выберите другой каталог через --out.
Проверки
uv run pytest -q
npm --prefix web run build
cd web
npx playwright install chromium
npm run test:e2e
Перед E2E запустите приложение отдельно на http://127.0.0.1:8080. При необходимости задайте переменные только для тестового процесса:
| Переменная | Значение по умолчанию | Назначение |
|---|---|---|
APP_URL | http://127.0.0.1:8080 | Адрес приложения для Playwright |
CHROMIUM_PATH | Не задано | При отсутствии используется браузер Playwright; иначе путь к установленному Chromium |
APP_OUTPUT | /app/out | Каталог манифеста только для healthcheck.py; не меняет каталог расчёта |
Проверено: 70 Python-тестов, 27 Playwright-сценариев, TypeScript/build и Docker healthcheck. Полный расчёт в контейнере — 6,506 с. Условия и ограничения проверки зафиксированы в VERIFICATION.md.
Авторы коммитов
История коммитов
- 2026-09-23 17:34Present documentation as completed projecthhhhhffd
- 2026-09-23 17:33Clarify submission docs, analytics and operation without AIhhhhhffd
- 2026-09-23 17:15Document investigation workflow, Alem setup and verified demohhhhhffd
- 2026-09-23 17:13Keep base application available with invalid AI settingshhhhhffd
- 2026-09-23 17:12Complete temporal investigation, dossier and assistant workspacehhhhhffd
- 2026-09-23 17:04Add validated Alem query interpretation APIhhhhhffd
- 2026-09-23 16:56Add role sensitivity scenarios and transaction evidence schemahhhhhffd
- 2026-09-23 16:42Add automated reviewer prompts in source commentshhhhhffd
Все коммиты (30)
- 2026-09-23 16:35Refresh architecture documentation and archive obsolete planshhhhhffd
- 2026-09-23 16:28Complete analyst workflow and resolve interface review findingshhhhhffd
- 2026-09-23 15:58Record chronology and consolidation release verificationhhhhhffd
- 2026-09-23 15:56Add investigation view for new consolidation candidateshhhhhffd
- 2026-09-23 15:51Validate transaction chronology in observed pathshhhhhffd
- 2026-09-23 15:37Record verification of updated local servicehhhhhffd
- 2026-09-23 15:35Add common-seed investigation to analyst UIhhhhhffd
- 2026-09-23 15:11Lead demo with discovered non-seed priority nodehhhhhffd
- 2026-09-23 15:03Prepare competition submission artifacts and verified demohhhhhffd
- 2026-09-23 14:53Show seed paths and resilience in analyst UIhhhhhffd
- 2026-09-23 14:44Model observed network resilience with controlshhhhhffd
- 2026-09-23 14:43Explain cluster hypotheses with observed flowshhhhhffd
- 2026-09-23 14:39Add reproducible seed paths and follow-up checkshhhhhffd
- 2026-09-23 14:16Deliver automatic Docker Compose runtime and verified demo documentationhhhhhffd
- 2026-09-23 14:07Build interactive graph analysis interface with browser coveragehhhhhffd
- 2026-09-23 13:58Implement explainable roles priorities and validated analysis exportshhhhhffd
- 2026-09-23 13:52Compute seed reachability daily flow features and stable communitieshhhhhffd
- 2026-09-23 13:50Validate transaction data and preserve isolated graph nodeshhhhhffd
- 2026-09-23 13:45micro pravkahhhhhffd
- 2026-09-23 13:44Require Docker Compose delivery and add coding agent prompthhhhhffd
- 2026-09-23 13:40Document technical stack and implementation handoffhhhhhffd
- 2026-09-23 13:36Add case materials and initial implementation planhhhhhffd
Структура репозитория (69 файлов)
.dockerignore .gitignore Dockerfile README.md assistant.py case/HackAlem AI_ Граф денег_ восстановление финансовой структуры организованной группы по транзакционной сети.pdf case/README.md case/data/edges.parquet case/data/nodes.parquet case/data/transactions.parquet case/starter/README.md case/starter/requirements.txt case/starter/scoring.py case/starter/starter.py compose.yaml config.toml docs/ANALYTICS.md docs/ASSISTANT.md docs/DATA_PROFILE.md docs/DEMO.md docs/README.md docs/VERIFICATION.md docs/archive/CODER_PROMPT.md docs/archive/PLAN.md docs/archive/README.md docs/archive/STARTER_README.md docs/archive/TECH_HANDOFF.md docs/archive/VERIFICATION_HISTORY.md docs/frontend/DESIGN.md docs/frontend/UX_FIXES.md healthcheck.py pyproject.toml run.py submission/clusters.csv submission/nodes_roles.csv submission/overview.svg submission/top_nodes.csv tests/test_assistant.py tests/test_scoring.py tests/test_starter.py uv.lock web/e2e/app.spec.ts web/e2e/assistant.spec.ts web/e2e/chronology.spec.ts web/e2e/consolidation.spec.ts web/e2e/dossier.spec.ts web/e2e/temporal.spec.ts web/e2e/workspace.spec.ts web/index.html web/package-lock.json web/package.json web/playwright.config.ts web/src/App.tsx web/src/Assistant.tsx web/src/ConvergenceSearch.tsx web/src/Dossier.tsx web/src/GraphView.tsx web/src/Pagination.tsx web/src/PathChronology.tsx web/src/RoleRules.tsx web/src/Sensitivity.tsx web/src/TemporalRoute.tsx web/src/graphQueries.ts web/src/main.tsx web/src/style.css web/src/types.ts web/src/vite-env.d.ts web/src/workspace.ts web/tsconfig.json
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.