Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69db8bb786 | ||
|
|
eaf398372c | ||
|
|
762e073cab | ||
|
|
58cce2e83c | ||
|
|
96aec0a3da | ||
|
|
2058aba483 | ||
|
|
04eccff819 | ||
|
|
fec6fa166c | ||
|
|
dec7ed2509 | ||
|
|
b628bfcd82 |
10
.env.example
Normal file
10
.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# Конфигурация для разработки (пример)
|
||||
# Скопируй в .env.local и заполни реальными значениями
|
||||
|
||||
# Sentry DSN для сбора crash reports
|
||||
# Получи из: https://umbrix-dj.sentry.io/ → Projects → umbrix-app → Settings → Client Keys
|
||||
# Формат: https://публичный_ключ@o123.ingest.sentry.io/456
|
||||
SENTRY_DSN=
|
||||
|
||||
# Пример (ЗАМЕНИ на свой):
|
||||
# SENTRY_DSN=https://abc123def456@o4510744763170816.ingest.sentry.io/789456
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -59,5 +59,9 @@ app.*.map.json
|
||||
/data
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/lib/core/telegram_config.dart
|
||||
.fvm/
|
||||
|
||||
# Secrets
|
||||
lib/core/telegram_config.dart
|
||||
android/key.properties
|
||||
.env.local
|
||||
|
||||
359
BUG_REPORT_INTEGRATION.md
Normal file
359
BUG_REPORT_INTEGRATION.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# 🔗 Интеграция системы багрепортов с существующими логами
|
||||
|
||||
## Архитектура логирования в Umbrix
|
||||
|
||||
### Файлы логов
|
||||
|
||||
```
|
||||
~/.local/share/umbrix/logs/ (или другая директория на основе платформы)
|
||||
├── box.log # Логи ядра (singbox core)
|
||||
└── app.log # Логи приложения (Flutter)
|
||||
```
|
||||
|
||||
### Ротация логов
|
||||
|
||||
Файлы автоматически ротируются при достижении 5MB:
|
||||
- `box.log` → `box.log.1` → `box.log.2`
|
||||
- Максимум 2 backup файла
|
||||
|
||||
## Как подключиться к логам
|
||||
|
||||
### 1. LogRepository (Основной источник)
|
||||
|
||||
```dart
|
||||
// Получить репозиторий логов
|
||||
final logRepository = await ref.read(logRepositoryProvider.future);
|
||||
|
||||
// Подписаться на поток логов в реальном времени
|
||||
logRepository.watchLogs().listen((either) {
|
||||
either.fold(
|
||||
(failure) => print('Error: $failure'),
|
||||
(logs) {
|
||||
for (final log in logs) {
|
||||
print('${log.time} [${log.level}] ${log.message}');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. LogPathResolver (Пути к файлам)
|
||||
|
||||
```dart
|
||||
// Получить пути к файлам логов
|
||||
final logPathResolver = ref.read(logPathResolverProvider);
|
||||
|
||||
// Core logs
|
||||
final coreLogFile = logPathResolver.coreFile();
|
||||
print('Core logs: ${coreLogFile.path}');
|
||||
|
||||
// App logs
|
||||
final appLogFile = logPathResolver.appFile();
|
||||
print('App logs: ${appLogFile.path}');
|
||||
|
||||
// Директория логов
|
||||
final logsDir = logPathResolver.directory;
|
||||
print('Logs directory: ${logsDir.path}');
|
||||
```
|
||||
|
||||
### 3. Чтение файлов напрямую
|
||||
|
||||
```dart
|
||||
import 'dart:io';
|
||||
|
||||
// Прочитать все логи
|
||||
final coreLogFile = logPathResolver.coreFile();
|
||||
if (await coreLogFile.exists()) {
|
||||
final content = await coreLogFile.readAsString();
|
||||
print(content);
|
||||
}
|
||||
|
||||
// Прочитать последние N строк
|
||||
Future<String> readLastLines(File file, int maxLines) async {
|
||||
final lines = await file.readAsLines();
|
||||
final lastLines = lines.length > maxLines
|
||||
? lines.sublist(lines.length - maxLines)
|
||||
: lines;
|
||||
return lastLines.join('\n');
|
||||
}
|
||||
|
||||
final last100Lines = await readLastLines(coreLogFile, 100);
|
||||
```
|
||||
|
||||
## Что собирает BugReportService
|
||||
|
||||
### Диагностическая информация
|
||||
|
||||
```dart
|
||||
class DiagnosticInfo {
|
||||
// 1. Информация об устройстве (анонимно)
|
||||
final String deviceInfo; // "Android 12" / "iOS 15" / "Windows"
|
||||
|
||||
// 2. Статус подключения
|
||||
final String connectionStatus; // "CONNECTED" / "DISCONNECTED" / etc
|
||||
final String? connectionError; // Текст ошибки если есть
|
||||
|
||||
// 3. Информация о прокси
|
||||
final String? activeProxyInfo; // "Тип: vmess, Тег: server-1, Пинг: 120ms"
|
||||
final int? pingDelay; // Задержка в мс (120, 450, etc)
|
||||
|
||||
// 4. Логи (последние 100 строк)
|
||||
final String coreLogsPreview; // Из box.log
|
||||
final String appLogsPreview; // Из app.log
|
||||
|
||||
// 5. Метаданные
|
||||
final DateTime timestamp; // Время создания отчёта
|
||||
}
|
||||
```
|
||||
|
||||
### Источники данных
|
||||
|
||||
| Данные | Источник | Provider/Service |
|
||||
|--------|----------|------------------|
|
||||
| **Device Info** | `Platform.operatingSystem` | `TelegramLogger.getAnonymousDeviceInfo()` |
|
||||
| **Connection Status** | `ConnectionNotifier` | `connectionNotifierProvider` |
|
||||
| **Active Proxy** | `ActiveProxyNotifier` | `activeProxyNotifierProvider` |
|
||||
| **Ping Delay** | `ProxyItemEntity.urlTestDelay` | Из активного прокси |
|
||||
| **Core Logs** | `box.log` файл | `logPathResolver.coreFile()` |
|
||||
| **App Logs** | `app.log` файл | `logPathResolver.appFile()` |
|
||||
|
||||
## Как добавить дополнительную информацию
|
||||
|
||||
### 1. Расширить DiagnosticInfo
|
||||
|
||||
```dart
|
||||
// В bug_report_service.dart
|
||||
class DiagnosticInfo {
|
||||
// Добавьте новое поле
|
||||
final String? vpnStatus;
|
||||
final int? memoryUsage;
|
||||
final String? networkType;
|
||||
|
||||
// ... остальное
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Обновить метод collectDiagnostics()
|
||||
|
||||
```dart
|
||||
Future<DiagnosticInfo> collectDiagnostics() async {
|
||||
// ... существующий код
|
||||
|
||||
// Добавьте новый сбор данных
|
||||
String? vpnStatus;
|
||||
try {
|
||||
// Ваша логика получения VPN статуса
|
||||
vpnStatus = await getVpnStatus();
|
||||
} catch (e) {
|
||||
vpnStatus = 'Ошибка: $e';
|
||||
}
|
||||
|
||||
return DiagnosticInfo(
|
||||
// ... существующие поля
|
||||
vpnStatus: vpnStatus,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Обновить форматирование
|
||||
|
||||
```dart
|
||||
String _formatBugReport(...) {
|
||||
// ... существующий код
|
||||
|
||||
// Добавьте в отчёт
|
||||
if (diagnostics.vpnStatus != null) {
|
||||
buffer.writeln('📡 VPN:');
|
||||
buffer.writeln(diagnostics.vpnStatus);
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
// ... остальное
|
||||
}
|
||||
```
|
||||
|
||||
## Подключение к метрикам Singbox
|
||||
|
||||
### Получить статистику трафика
|
||||
|
||||
```dart
|
||||
// Singbox предоставляет метрики
|
||||
final singbox = ref.read(singboxServiceProvider);
|
||||
|
||||
// В будущем можно добавить:
|
||||
// - Общий трафик (upload/download)
|
||||
// - Количество подключений
|
||||
// - Ошибки DNS
|
||||
// - etc
|
||||
```
|
||||
|
||||
### Пример расширенной диагностики
|
||||
|
||||
```dart
|
||||
Future<DiagnosticInfo> collectDiagnostics() async {
|
||||
// ... существующий код
|
||||
|
||||
// Дополнительные данные из singbox
|
||||
String? trafficInfo;
|
||||
try {
|
||||
// Если singbox предоставляет API для статистики
|
||||
final stats = await singbox.getStats();
|
||||
trafficInfo = 'Upload: ${stats.upload}, Download: ${stats.download}';
|
||||
} catch (e) {
|
||||
trafficInfo = 'Недоступно';
|
||||
}
|
||||
|
||||
return DiagnosticInfo(
|
||||
// ... остальные поля
|
||||
trafficInfo: trafficInfo,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Логирование в приложении
|
||||
|
||||
### Использование логгера
|
||||
|
||||
```dart
|
||||
import 'package:umbrix/utils/custom_loggers.dart';
|
||||
|
||||
class MyService with InfraLogger { // или AppLogger, CoreLogger
|
||||
void someMethod() {
|
||||
loggy.debug('Debug message');
|
||||
loggy.info('Info message');
|
||||
loggy.warning('Warning message');
|
||||
loggy.error('Error message', error, stackTrace);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Типы логгеров
|
||||
|
||||
- `InfraLogger` — инфраструктурные логи (network, IO, etc)
|
||||
- `AppLogger` — логи приложения (UI, бизнес-логика)
|
||||
- `CoreLogger` — логи ядра (singbox)
|
||||
|
||||
Все они пишутся в соответствующие файлы через `LogRepository`.
|
||||
|
||||
## Расширенная интеграция с Telegram
|
||||
|
||||
### Форматирование для Telegram
|
||||
|
||||
```dart
|
||||
// Telegram поддерживает HTML разметку
|
||||
final message = '''
|
||||
<b>🐛 БАГРЕПОРТ</b>
|
||||
<i>Устройство:</i> $deviceInfo
|
||||
<code>Статус: $connectionStatus</code>
|
||||
|
||||
<pre>
|
||||
Логи:
|
||||
$logs
|
||||
</pre>
|
||||
''';
|
||||
|
||||
await telegramLogger.sendLogsAsText(message);
|
||||
```
|
||||
|
||||
### Отправка нескольких файлов
|
||||
|
||||
```dart
|
||||
// Можно отправить Core и App логи отдельно
|
||||
await telegramLogger.sendLogsAsFile(
|
||||
logPathResolver.coreFile(),
|
||||
deviceInfo: 'Core Logs - $deviceInfo',
|
||||
);
|
||||
|
||||
await telegramLogger.sendLogsAsFile(
|
||||
logPathResolver.appFile(),
|
||||
deviceInfo: 'App Logs - $deviceInfo',
|
||||
);
|
||||
```
|
||||
|
||||
## Дебаг и тестирование
|
||||
|
||||
### Посмотреть текущие логи
|
||||
|
||||
```dart
|
||||
// В debug режиме можно вывести текущие логи
|
||||
void debugPrintLogs() async {
|
||||
final logPathResolver = ref.read(logPathResolverProvider);
|
||||
|
||||
print('=== CORE LOGS ===');
|
||||
final coreFile = logPathResolver.coreFile();
|
||||
if (await coreFile.exists()) {
|
||||
final lines = await coreFile.readAsLines();
|
||||
lines.take(10).forEach(print); // Первые 10 строк
|
||||
}
|
||||
|
||||
print('=== APP LOGS ===');
|
||||
final appFile = logPathResolver.appFile();
|
||||
if (await appFile.exists()) {
|
||||
final lines = await appFile.readAsLines();
|
||||
lines.take(10).forEach(print); // Первые 10 строк
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Тест диагностики
|
||||
|
||||
```dart
|
||||
void testDiagnostics() async {
|
||||
final service = ref.read(bugReportServiceProvider);
|
||||
final diagnostics = await service.collectDiagnostics();
|
||||
|
||||
print('Device: ${diagnostics.deviceInfo}');
|
||||
print('Connection: ${diagnostics.connectionStatus}');
|
||||
print('Proxy: ${diagnostics.activeProxyInfo}');
|
||||
print('Ping: ${diagnostics.pingDelay}ms');
|
||||
print('Core logs (first 5 lines):');
|
||||
print(diagnostics.coreLogsPreview.split('\n').take(5).join('\n'));
|
||||
}
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: Где физически хранятся логи?
|
||||
**A:** Зависит от платформы:
|
||||
- Android: `/data/data/com.hiddify.umbrix/files/logs/`
|
||||
- iOS: `Application Support/logs/`
|
||||
- Desktop: `~/.local/share/umbrix/logs/` (Linux), `~/Library/Application Support/umbrix/logs/` (macOS)
|
||||
|
||||
### Q: Как очистить логи?
|
||||
**A:**
|
||||
```dart
|
||||
final logRepository = await ref.read(logRepositoryProvider.future);
|
||||
await logRepository.clearLogs();
|
||||
```
|
||||
|
||||
### Q: Можно ли собирать логи без отправки?
|
||||
**A:** Да:
|
||||
```dart
|
||||
final service = ref.read(bugReportServiceProvider);
|
||||
final diagnostics = await service.collectDiagnostics();
|
||||
// diagnostics теперь содержит всю информацию
|
||||
```
|
||||
|
||||
### Q: Как добавить скриншот к багрепорту?
|
||||
**A:** Нужно расширить `TelegramLogger`:
|
||||
```dart
|
||||
Future<bool> sendPhotoWithCaption(File photo, String caption) async {
|
||||
// Используйте Telegram Bot API endpoint sendPhoto
|
||||
final formData = FormData.fromMap({
|
||||
'chat_id': TelegramConfig.chatId,
|
||||
'photo': await MultipartFile.fromFile(photo.path),
|
||||
'caption': caption,
|
||||
});
|
||||
|
||||
final response = await _dio.post(
|
||||
'https://api.telegram.org/bot${TelegramConfig.botToken}/sendPhoto',
|
||||
data: formData,
|
||||
);
|
||||
|
||||
return response.statusCode == 200;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Система готова и полностью интегрирована!** 🎉
|
||||
225
BUG_REPORT_SYSTEM.md
Normal file
225
BUG_REPORT_SYSTEM.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# 🐛 Система отправки багрепортов в техподдержку
|
||||
|
||||
## 📋 Обзор
|
||||
|
||||
Реализована полноценная система для сбора и отправки багрепортов в Telegram с автоматическим сбором диагностической информации.
|
||||
|
||||
## ✨ Что реализовано
|
||||
|
||||
### 1. **Автоматический сбор диагностики** (`BugReportService`)
|
||||
|
||||
Система автоматически собирает:
|
||||
- ✅ **Информация об устройстве** (анонимно): OS, версия
|
||||
- ✅ **Статус подключения**: connected/disconnected/connecting
|
||||
- ✅ **Ошибки подключения**: если есть
|
||||
- ✅ **Активный прокси**: тип протокола, тег сервера
|
||||
- ✅ **Пинг/задержка**: в миллисекундах + оценка качества
|
||||
- ✅ **Логи**: последние 100 строк из `core.log` и `app.log`
|
||||
|
||||
### 2. **UI для отправки багрепортов** (`BugReportDialog`)
|
||||
|
||||
Диалоговое окно с:
|
||||
- Поле для описания проблемы пользователем
|
||||
- Чекбокс "Включить логи" (по умолчанию включён)
|
||||
- Информация о конфиденциальности
|
||||
- Кнопка "Отправить" с индикатором загрузки
|
||||
|
||||
### 3. **Интеграция с Telegram**
|
||||
|
||||
Использует существующий `TelegramLogger`:
|
||||
- Отправка как текст (если помещается в 4KB)
|
||||
- Или как файл (если логи большие)
|
||||
- Красиво форматированный отчёт с эмодзи
|
||||
|
||||
## 📍 Где находится
|
||||
|
||||
### Код:
|
||||
```
|
||||
lib/features/bug_report/
|
||||
├── data/
|
||||
│ └── bug_report_service.dart # Сервис сбора и отправки
|
||||
└── widget/
|
||||
└── bug_report_dialog.dart # UI диалога
|
||||
```
|
||||
|
||||
### Кнопки в UI:
|
||||
1. **Настройки → Логи и отладка → "Сообщить о проблеме"**
|
||||
- Всегда доступна
|
||||
- Основной способ отправки багрепорта
|
||||
|
||||
## 🔧 Как использовать
|
||||
|
||||
### Для пользователя:
|
||||
|
||||
1. Откройте **Настройки**
|
||||
2. Раздел **"Логи и отладка"**
|
||||
3. Нажмите **"Сообщить о проблеме"** 🐛
|
||||
4. Опишите проблему
|
||||
5. Нажмите **"Отправить"**
|
||||
|
||||
### Для разработчика:
|
||||
|
||||
```dart
|
||||
// Показать диалог программно
|
||||
await BugReportDialog.show(context);
|
||||
|
||||
// Или получить сервис напрямую
|
||||
final service = ref.read(bugReportServiceProvider);
|
||||
final result = await service.sendBugReport(
|
||||
userDescription: 'Описание проблемы',
|
||||
includeLogs: true,
|
||||
);
|
||||
```
|
||||
|
||||
## 📊 Формат отчёта
|
||||
|
||||
Отчёт выглядит так:
|
||||
|
||||
```
|
||||
🐛 БАГРЕПОРТ UMBRIX
|
||||
═══════════════════════════════
|
||||
|
||||
📝 ОПИСАНИЕ ПРОБЛЕМЫ:
|
||||
Не могу подключиться к серверу, таймаут
|
||||
|
||||
═══════════════════════════════
|
||||
💻 УСТРОЙСТВО:
|
||||
Android 12
|
||||
|
||||
🔌 СТАТУС ПОДКЛЮЧЕНИЯ:
|
||||
DISCONNECTED
|
||||
Ошибка: Connection timeout
|
||||
|
||||
🌐 ПРОКСИ:
|
||||
Тип: vmess, Тег: server-1, Пинг: 450ms
|
||||
Задержка: 450ms (🟠 Медленно)
|
||||
|
||||
🕐 ВРЕМЯ:
|
||||
2026-01-22T15:30:00.000Z
|
||||
|
||||
═══════════════════════════════
|
||||
📋 ЛОГИ (ПОСЛЕДНИЕ 20 СТРОК):
|
||||
|
||||
Core:
|
||||
[INFO] Starting connection...
|
||||
[ERROR] Connection failed: timeout
|
||||
...
|
||||
|
||||
App:
|
||||
[DEBUG] User clicked connect
|
||||
[ERROR] Failed to establish connection
|
||||
...
|
||||
```
|
||||
|
||||
## 🔐 Безопасность и конфиденциальность
|
||||
|
||||
✅ **Мы НЕ собираем:**
|
||||
- Личные данные
|
||||
- IP адреса
|
||||
- Конфигурацию серверов (URL, пароли)
|
||||
- Историю посещений
|
||||
|
||||
✅ **Мы собираем ТОЛЬКО:**
|
||||
- Тип ОС (Android/iOS/Windows...)
|
||||
- Статус подключения
|
||||
- Задержку пинга
|
||||
- Технические логи (без личных данных)
|
||||
|
||||
## 🎯 Как это предотвращает негативные отзывы в Play Store
|
||||
|
||||
### Проблема:
|
||||
Пользователи оставляют плохие отзывы когда:
|
||||
- Не могут подключиться
|
||||
- Приложение "не работает"
|
||||
- Не понимают, что делать
|
||||
|
||||
### Решение:
|
||||
1. **Перехватываем недовольство** — кнопка "Сообщить о проблеме" даёт альтернативу отзыву
|
||||
2. **Собираем контекст** — автоматически видим ЧТО именно не работает
|
||||
3. **Быстро реагируем** — получаем отчёт в Telegram → можем помочь
|
||||
4. **Показываем заботу** — пользователь видит, что есть поддержка
|
||||
|
||||
## 📱 Дополнительные места размещения (опционально)
|
||||
|
||||
Можно добавить кнопку в:
|
||||
|
||||
### 1. Экран ошибки подключения
|
||||
```dart
|
||||
// В connection_failure_screen.dart
|
||||
FilledButton.icon(
|
||||
onPressed: () => BugReportDialog.show(context),
|
||||
icon: const Icon(FluentIcons.bug_20_regular),
|
||||
label: const Text('Сообщить о проблеме'),
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Меню приложения (три точки)
|
||||
```dart
|
||||
// В app_bar_actions.dart
|
||||
PopupMenuItem(
|
||||
child: const Text('Сообщить о проблеме'),
|
||||
onTap: () => BugReportDialog.show(context),
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Диалог обновления (при ошибке)
|
||||
```dart
|
||||
// Если обновление провалилось
|
||||
if (updateFailed) {
|
||||
TextButton(
|
||||
child: const Text('Сообщить о проблеме'),
|
||||
onPressed: () => BugReportDialog.show(context),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Настройка Telegram
|
||||
|
||||
1. Создайте бота через @BotFather
|
||||
2. Получите токен
|
||||
3. Создайте приватный канал/группу
|
||||
4. Получите Chat ID
|
||||
5. Настройте в `lib/core/telegram_config.dart`:
|
||||
|
||||
```dart
|
||||
class TelegramConfig {
|
||||
static const String botToken = 'YOUR_BOT_TOKEN';
|
||||
static const String chatId = 'YOUR_CHAT_ID';
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
```dart
|
||||
// Протестировать сбор диагностики
|
||||
final service = ref.read(bugReportServiceProvider);
|
||||
final diagnostics = await service.collectDiagnostics();
|
||||
print(diagnostics.connectionStatus);
|
||||
print(diagnostics.pingDelay);
|
||||
|
||||
// Протестировать отправку
|
||||
final result = await service.sendBugReport(
|
||||
userDescription: 'Test report',
|
||||
includeLogs: true,
|
||||
);
|
||||
print(result.isSuccess);
|
||||
```
|
||||
|
||||
## 📈 Метрики для отслеживания
|
||||
|
||||
- Количество отправленных багрепортов
|
||||
- Среднее время ответа поддержки
|
||||
- Процент решённых проблем
|
||||
- Соотношение багрепортов к негативным отзывам
|
||||
|
||||
## 🎉 Результат
|
||||
|
||||
Вместо:
|
||||
> ⭐☆☆☆☆ "Не работает, не подключается" — в Play Store
|
||||
|
||||
Получаем:
|
||||
> 🐛 Багрепорт в Telegram → быстрая помощь → довольный пользователь → ⭐⭐⭐⭐⭐
|
||||
|
||||
---
|
||||
|
||||
**Готово к использованию!** 🚀
|
||||
36
GIT_SAFETY.txt
Normal file
36
GIT_SAFETY.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
🔒 БЕЗОПАСНОСТЬ GIT - ВАЖНО!
|
||||
|
||||
❌ НИКОГДА НЕ ПУШИТЬ В GITHUB HIDDIFY!
|
||||
|
||||
✅ ПРАВИЛЬНЫЕ КОМАНДЫ:
|
||||
|
||||
Основной репозиторий:
|
||||
git push gitea main ← ТОЛЬКО ТАК!
|
||||
|
||||
Libcore:
|
||||
cd libcore
|
||||
git push gitea main ← ТОЛЬКО ТАК!
|
||||
git push origin main ← Тоже можно (origin = gitea)
|
||||
|
||||
❌ ОПАСНЫЕ КОМАНДЫ (НЕ ИСПОЛЬЗОВАТЬ):
|
||||
git push origin main ← В основном репо это GitHub!
|
||||
git push hiddify-upstream ← Заблокировано
|
||||
|
||||
📋 ПРОВЕРКА ПЕРЕД PUSH:
|
||||
git remote -v ← Проверить куда пушим
|
||||
git log --oneline -5 ← Проверить что пушим
|
||||
|
||||
🔍 ТЕКУЩИЕ НАСТРОЙКИ:
|
||||
|
||||
Основной репозиторий:
|
||||
origin = GitHub Hiddify (fetch only) ← READ ONLY!
|
||||
gitea = Наш Gitea ✅
|
||||
|
||||
libcore/:
|
||||
origin = Наш Gitea ✅
|
||||
gitea = Наш Gitea ✅
|
||||
hiddify-upstream = GitHub Hiddify (fetch only) ← READ ONLY!
|
||||
|
||||
✅ ЕСЛИ НУЖНО ОБНОВИТЬСЯ ОТ HIDDIFY:
|
||||
git fetch origin ← Безопасно (только чтение)
|
||||
git merge origin/main ← Потом мержим если нужно
|
||||
125
Logs/umbrix_bug_report_1769058542221.txt
Normal file
125
Logs/umbrix_bug_report_1769058542221.txt
Normal file
@@ -0,0 +1,125 @@
|
||||
🐛 БАГРЕПОРТ UMBRIX
|
||||
═══════════════════════════════
|
||||
|
||||
📝 ОПИСАНИЕ ПРОБЛЕМЫ:
|
||||
test3
|
||||
|
||||
═══════════════════════════════
|
||||
💻 УСТРОЙСТВО:
|
||||
Android AP3A.240905.015.A2.G998BXXSIHYK1
|
||||
|
||||
🔌 СТАТУС ПОДКЛЮЧЕНИЯ:
|
||||
CONNECTED
|
||||
|
||||
🌐 ПРОКСИ:
|
||||
Тип: ProxyType.urltest, Тег: auto, Пинг: 48ms
|
||||
Задержка: 48ms (✅ Отлично)
|
||||
|
||||
🕐 ВРЕМЯ:
|
||||
2026-01-22T08:09:01.776559
|
||||
|
||||
═══════════════════════════════
|
||||
📋 CORE LOGS (ПОЛНЫЕ):
|
||||
|
||||
|
||||
📋 APP LOGS (ПОЛНЫЕ):
|
||||
08:07:54.148257 - [D] PreferencesEntry<bool, bool>: getting persisted preference [auto_check_ip](bool)
|
||||
08:08:07.932215 - [D] PreferencesEntry<bool, dynamic>: getting persisted preference [disable_experimental_feature_notice](bool)
|
||||
08:08:07.939310 - [D] PreferencesEntry<bool, bool>: getting persisted preference [started_by_user](bool)
|
||||
08:08:07.939579 - [D] PreferencesEntry<bool, bool>: updating preference [started_by_user](bool) to [true]
|
||||
08:08:07.948091 - [D] PreferencesEntry<bool, bool>: getting persisted preference [disable_memory_limit](bool)
|
||||
08:08:07.958412 - [I] ConnectionRepositoryImpl: config options: {
|
||||
"region": "ru",
|
||||
"block-ads": false,
|
||||
"use-xray-core-when-possible": false,
|
||||
"execute-config-as-is": false,
|
||||
"log-level": "warn",
|
||||
"resolve-destination": false,
|
||||
"ipv6-mode": "ipv4_only",
|
||||
"remote-dns-address": "udp://1.1.1.1",
|
||||
"remote-dns-domain-strategy": "",
|
||||
"direct-dns-address": "1.1.1.1",
|
||||
"direct-dns-domain-strategy": "",
|
||||
"mixed-port": 12334,
|
||||
"tproxy-port": 12335,
|
||||
"local-dns-port": 16450,
|
||||
"tun-implementation": "gvisor",
|
||||
"mtu": 9000,
|
||||
"strict-route": true,
|
||||
"connection-test-url": "http://cp.cloudflare.com",
|
||||
"url-test-interval": 600,
|
||||
"enable-clash-api": true,
|
||||
"clash-api-port": 16756,
|
||||
"enable-tun": true,
|
||||
"enable-tun-service": false,
|
||||
"set-system-proxy": false,
|
||||
"bypass-lan": false,
|
||||
"allow-connection-from-lan": false,
|
||||
"enable-fake-dns": false,
|
||||
"enable-dns-routing": true,
|
||||
"independent-dns-cache": true,
|
||||
"rules": [],
|
||||
"mux": {
|
||||
"enable": false,
|
||||
"padding": false,
|
||||
"max-streams": 8,
|
||||
"protocol": "h2mux"
|
||||
},
|
||||
"tls-tricks": {
|
||||
"enable-fragment": false,
|
||||
"fragment-size": "10-30",
|
||||
"fragment-sleep": "2-8",
|
||||
"mixed-sni-case": false,
|
||||
"enable-padding": false,
|
||||
"padding-size": "1-1500"
|
||||
},
|
||||
"warp": {
|
||||
"enable": false,
|
||||
"mode": "proxy_over_warp",
|
||||
"wireguard-config": "",
|
||||
"license-key": "",
|
||||
"account-id": "",
|
||||
"access-token": "",
|
||||
"clean-ip": "auto",
|
||||
"clean-port": 0,
|
||||
"noise": "1-3",
|
||||
"noise-size": "10-30",
|
||||
"noise-delay": "10-30",
|
||||
"noise-mode": "m4"
|
||||
},
|
||||
"warp2": {
|
||||
"enable": false,
|
||||
"mode": "proxy_over_warp",
|
||||
"wireguard-config": "",
|
||||
"license-key": "",
|
||||
"account-id": "",
|
||||
"access-token": "",
|
||||
"clean-ip": "auto",
|
||||
"clean-port": 0,
|
||||
"noise": "1-3",
|
||||
"noise-size": "10-30",
|
||||
"noise-delay": "10-30",
|
||||
"noise-mode": "m4"
|
||||
},
|
||||
"per-app-proxy-mode": "off",
|
||||
"included-applications": [],
|
||||
"excluded-applications": []
|
||||
}
|
||||
Memory Limit: true
|
||||
08:08:07.963095 - [D] ConnectionRepositoryImpl: setting up singbox
|
||||
08:08:07.966368 - [D] PlatformSingboxService: changing options
|
||||
08:08:07.969445 - [D] PlatformSingboxService: starting
|
||||
08:08:09.624780 - [I] ConnectionNotifier: connection status: DISCONNECTED
|
||||
08:08:10.488389 - [I] ConnectionNotifier: connection status: CONNECTING
|
||||
08:08:11.299915 - [I] ConnectionNotifier: connection status: CONNECTED
|
||||
08:08:11.304006 - [D] IpInfoNotifier: disposing
|
||||
08:08:11.319044 - [D] ProxyRepositoryImpl: getting current ip info using [https://ipwho.is/]
|
||||
08:08:11.321248 - [D] PlatformSingboxService: watching active groups
|
||||
08:08:11.328357 - [D] PlatformSingboxService: watching stats
|
||||
08:08:11.335624 - [D] PreferencesEntry<bool, bool>: getting persisted preference [store_reviewed_by_user](bool)
|
||||
08:08:11.601832 - [D] PreferencesEntry<bool, bool>: updating preference [store_reviewed_by_user](bool) to [true]
|
||||
08:08:28.427748 - [E] app: Flutter Error: A RenderFlex overflowed by 31 pixels on the right.
|
||||
A RenderFlex overflowed by 31 pixels on the right.
|
||||
08:08:29.908294 - [E] app: Flutter Error: A RenderFlex overflowed by 60 pixels on the bottom.
|
||||
A RenderFlex overflowed by 60 pixels on the bottom.
|
||||
08:08:54.476924 - [D] AddProfile: disposing
|
||||
254
TELEGRAM_BOT_SETUP_RU.md
Normal file
254
TELEGRAM_BOT_SETUP_RU.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# 🤖 Настройка Telegram бота для багрепортов
|
||||
|
||||
## Шаг 1: Создать бота
|
||||
|
||||
1. Откройте Telegram
|
||||
2. Найдите **@BotFather**
|
||||
3. Отправьте команду: `/newbot`
|
||||
4. Введите имя бота: `Umbrix Bug Report Bot`
|
||||
5. Введите username: `@Dorod_bug_bot` (или любой свободный)
|
||||
6. **Скопируйте TOKEN** — это строка типа `7987728101:AAGYUWTeYfFANhBA9-C3dZCjGOSwByAWCaA`
|
||||
|
||||
```
|
||||
📋 Пример токена:
|
||||
7987728101:AAGYUWTeYfFANhBA9-C3dZCjGOSwByAWCaA
|
||||
```
|
||||
|
||||
## Шаг 2: Создать ПРИВАТНЫЙ канал для логов
|
||||
|
||||
### 📱 В мобильном приложении Telegram:
|
||||
|
||||
1. **Откройте Telegram** на телефоне
|
||||
2. **Нажмите на ☰ меню** (три полоски слева вверху)
|
||||
3. **Выберите "Новый канал"**
|
||||
- Если не видите — нажмите на карандаш ✏️ справа внизу, там будет "Новый канал"
|
||||
4. **Введите название:** `Umbrix Bug Reports` (или любое)
|
||||
5. **Введите описание** (необязательно)
|
||||
6. **Нажмите "Создать"**
|
||||
7. **‼️ ВАЖНО: Выберите тип канала:**
|
||||
- ❌ НЕ нажимайте "Публичный канал"
|
||||
- ✅ Просто нажмите "Пропустить" или "Далее"
|
||||
- Это сделает канал **ПРИВАТНЫМ** по умолчанию
|
||||
8. **Нажмите "Сохранить"**
|
||||
|
||||
### 💻 В десктопном приложении Telegram:
|
||||
|
||||
1. **Откройте Telegram** на компьютере
|
||||
2. **Нажмите ☰ меню** (слева вверху)
|
||||
3. **Выберите "Новый канал"**
|
||||
4. **Введите название:** `Umbrix Bug Reports`
|
||||
5. **Нажмите "Далее"**
|
||||
6. **‼️ ВАЖНО:** На вопросе "Публичный или приватный?"
|
||||
- ✅ Выберите **"Приватный канал"**
|
||||
- ❌ НЕ выбирайте "Публичный"
|
||||
7. **Нажмите "Сохранить"**
|
||||
|
||||
### 🤖 Добавить бота как администратора:
|
||||
|
||||
1. **Откройте ваш новый канал** `Umbrix Bug Reports`
|
||||
2. **Нажмите на название канала** вверху
|
||||
3. **Выберите "Администраторы"** (или ⚙️ → "Управление каналом" → "Администраторы")
|
||||
4. **Нажмите "Добавить администратора"**
|
||||
5. **В поиске введите:** `@Dorod_bug_bot` (имя вашего бота)
|
||||
6. **Выберите бота** из списка
|
||||
7. **Дайте права:**
|
||||
- ✅ **"Публикация сообщений"** — ОБЯЗАТЕЛЬНО включите!
|
||||
- Остальное можно оставить выключенным
|
||||
8. **Нажмите "Сохранить"** или "Готово"
|
||||
|
||||
✅ **Готово!** Теперь ваш бот может отправлять сообщения в канал.
|
||||
|
||||
## Шаг 3: Получить Chat ID
|
||||
|
||||
### Способ 1 (Простой):
|
||||
|
||||
1. Отправьте любое сообщение в канал
|
||||
2. Перешлите это сообщение боту **@userinfobot**
|
||||
3. Он покажет Chat ID канала
|
||||
|
||||
### Способ 2 (Через API):
|
||||
|
||||
1. Отправьте любое сообщение в канал
|
||||
2. Откройте в браузере:
|
||||
```
|
||||
https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates
|
||||
```
|
||||
Замените `<YOUR_BOT_TOKEN>` на токен из Шага 1
|
||||
|
||||
3. Найдите в ответе `"chat":{"id":-1001234567890}`
|
||||
4. Это ваш Chat ID (обычно начинается с `-100`)
|
||||
|
||||
```json
|
||||
📋 Пример Chat ID для канала:
|
||||
-1001234567890
|
||||
```
|
||||
|
||||
## Шаг 4: Настроить в приложении
|
||||
|
||||
Откройте файл и замените значения:
|
||||
|
||||
```dart
|
||||
// lib/core/telegram_config.dart
|
||||
|
||||
class TelegramConfig {
|
||||
/// Токен из @BotFather
|
||||
static const String botToken = '6789012345:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw';
|
||||
|
||||
/// Chat ID вашего приватного канала
|
||||
static const String chatId = '-1001234567890';
|
||||
|
||||
static bool get isConfigured {
|
||||
return botToken != 'YOUR_BOT_TOKEN_HERE' && chatId != 'YOUR_CHAT_ID_HERE';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ⚠️ ВАЖНО для безопасности!
|
||||
|
||||
### ✅ ПРАВИЛЬНО:
|
||||
- Используйте **ПРИВАТНЫЙ** канал (никто не видит кроме вас)
|
||||
- Добавьте `.gitignore` чтобы не залить токен в git:
|
||||
```
|
||||
lib/core/telegram_config.dart
|
||||
```
|
||||
|
||||
### ❌ НЕПРАВИЛЬНО:
|
||||
- Публичный канал (все смогут читать баги)
|
||||
- Коммитить токен в git (украдут бота)
|
||||
|
||||
## 🔐 Про анонимность
|
||||
|
||||
### Что мы НЕ собираем автоматически:
|
||||
|
||||
❌ Имя пользователя
|
||||
❌ Email
|
||||
❌ Телефон
|
||||
❌ IP адрес
|
||||
❌ Конфигурацию серверов (URL, пароли)
|
||||
❌ Историю посещений
|
||||
|
||||
### Что собираем:
|
||||
|
||||
✅ Тип ОС (Android/iOS/Windows) — **анонимно**
|
||||
✅ Версия ОС — **анонимно**
|
||||
✅ Статус подключения
|
||||
✅ Задержка пинга (число)
|
||||
✅ Технические логи (без IP, без паролей)
|
||||
|
||||
### Код для проверки:
|
||||
|
||||
Посмотрите в [bug_report_service.dart](lib/features/bug_report/data/bug_report_service.dart#L120):
|
||||
|
||||
```dart
|
||||
/// Получить информацию об устройстве для логов (анонимно)
|
||||
static String getAnonymousDeviceInfo() {
|
||||
// Только общая информация без идентификаторов
|
||||
if (Platform.isAndroid) {
|
||||
return 'Android ${Platform.operatingSystemVersion}';
|
||||
} else if (Platform.isIOS) {
|
||||
return 'iOS ${Platform.operatingSystemVersion}';
|
||||
}
|
||||
// ... и т.д.
|
||||
// НЕТ никаких device ID, IMEI, или других идентификаторов!
|
||||
}
|
||||
```
|
||||
|
||||
## 📱 Как сделать ЕЩЁ более анонимным?
|
||||
|
||||
Если хотите убрать даже версию ОС:
|
||||
|
||||
```dart
|
||||
static String getAnonymousDeviceInfo() {
|
||||
if (Platform.isAndroid) return 'Android';
|
||||
if (Platform.isIOS) return 'iOS';
|
||||
if (Platform.isWindows) return 'Windows';
|
||||
if (Platform.isMacOS) return 'macOS';
|
||||
if (Platform.isLinux) return 'Linux';
|
||||
return 'Unknown';
|
||||
}
|
||||
```
|
||||
|
||||
Тогда будет только `"Android"` без версии.
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
После настройки проверьте:
|
||||
|
||||
```dart
|
||||
import 'package:umbrix/core/telegram_config.dart';
|
||||
|
||||
void main() {
|
||||
print('Configured: ${TelegramConfig.isConfigured}');
|
||||
print('Bot: ${TelegramConfig.botToken}');
|
||||
print('Chat: ${TelegramConfig.chatId}');
|
||||
}
|
||||
```
|
||||
|
||||
Должно вывести:
|
||||
```
|
||||
Configured: true
|
||||
Bot: 6789012345:AAHdqTcvCH...
|
||||
Chat: -1001234567890
|
||||
```
|
||||
|
||||
## 📨 Пример отчёта в Telegram
|
||||
|
||||
Вот что придёт в ваш канал:
|
||||
|
||||
```
|
||||
📱 Umbrix Logs
|
||||
Device: Android 12
|
||||
📅 2026-01-22T15:30:00.000Z
|
||||
━━━━━━━━━━━━━━━━
|
||||
🐛 БАГРЕПОРТ UMBRIX
|
||||
═══════════════════════════════
|
||||
|
||||
📝 ОПИСАНИЕ ПРОБЛЕМЫ:
|
||||
Не могу подключиться, постоянно таймаут
|
||||
|
||||
💻 УСТРОЙСТВО:
|
||||
Android 12
|
||||
|
||||
🔌 СТАТУС ПОДКЛЮЧЕНИЯ:
|
||||
DISCONNECTED
|
||||
Ошибка: Connection timeout
|
||||
|
||||
🌐 ПРОКСИ:
|
||||
Тип: vmess, Тег: server-1, Пинг: 450ms
|
||||
Задержка: 450ms (🟠 Медленно)
|
||||
|
||||
📋 ЛОГИ:
|
||||
[ERROR] Failed to connect...
|
||||
[WARN] Timeout exceeded...
|
||||
```
|
||||
|
||||
## ❓ FAQ
|
||||
|
||||
**Q: Обязательно ли канал? Может группу?**
|
||||
A: Лучше канал — он проще и не спамит уведомлениями других участников.
|
||||
|
||||
**Q: Можно несколько каналов для разных устройств?**
|
||||
A: Да, создайте несколько конфигов или используйте разные билды.
|
||||
|
||||
**Q: Что если не хочу использовать Telegram?**
|
||||
A: Можно заменить `TelegramLogger` на отправку email, webhook, или другой сервис.
|
||||
|
||||
**Q: Бот будет постить от имени пользователя?**
|
||||
A: Нет, бот постит от своего имени. Пользователи останутся анонимными.
|
||||
|
||||
**Q: Можно добавить больше информации?**
|
||||
A: Да, смотрите [BUG_REPORT_INTEGRATION.md](BUG_REPORT_INTEGRATION.md)
|
||||
|
||||
## ✅ Готово!
|
||||
|
||||
Теперь запустите приложение:
|
||||
1. Зайдите в Настройки → Логи
|
||||
2. Нажмите "Сообщить о проблеме"
|
||||
3. Напишите тестовый отчёт
|
||||
4. Отправьте
|
||||
|
||||
Отчёт должен прийти в ваш канал! 🎉
|
||||
|
||||
---
|
||||
|
||||
**Безопасно. Анонимно. Работает.** 🔐
|
||||
685
WINDOWS_SPLIT_TUNNELING.md
Normal file
685
WINDOWS_SPLIT_TUNNELING.md
Normal file
@@ -0,0 +1,685 @@
|
||||
# 🔀 Split Tunneling для Windows (Per-App Proxy)
|
||||
|
||||
## 📋 Что такое Split Tunneling?
|
||||
|
||||
**Split Tunneling** (раздельное туннелирование) - функция которая позволяет:
|
||||
- ✅ **Исключать** определённые приложения из VPN (они идут напрямую)
|
||||
- ✅ **Включать** только выбранные приложения в VPN (остальные напрямую)
|
||||
|
||||
### Примеры использования:
|
||||
- Исключить банковские приложения из VPN
|
||||
- Исключить локальные приложения (Steam, торренты)
|
||||
- Пускать через VPN только браузер
|
||||
- Пускать через VPN только мессенджеры
|
||||
|
||||
---
|
||||
|
||||
## ✅ Текущее состояние
|
||||
|
||||
### Android - ✅ ПОЛНОСТЬЮ РАБОТАЕТ
|
||||
|
||||
```kotlin
|
||||
// android/app/src/main/kotlin/com/umbrix/app/bg/VPNService.kt
|
||||
|
||||
fun addIncludePackage(builder: Builder, packageName: String) {
|
||||
builder.addAllowedApplication(packageName) // Android VPNService API
|
||||
}
|
||||
|
||||
fun addExcludePackage(builder: Builder, packageName: String) {
|
||||
builder.addDisallowedApplication(packageName) // Android VPNService API
|
||||
}
|
||||
```
|
||||
|
||||
**UI готов:** `/settings/per-app-proxy`
|
||||
- Список установленных приложений
|
||||
- Переключатель режимов: Все / Включить / Исключить
|
||||
- Фильтр системных приложений
|
||||
- Поиск по названию
|
||||
|
||||
### Windows - ⚠️ НЕ РЕАЛИЗОВАНО
|
||||
|
||||
**Проблема:** На Windows нет аналога `addAllowedApplication()` из Android API.
|
||||
|
||||
**Что есть:**
|
||||
- ✅ UI уже готов (та же страница `/settings/per-app-proxy`)
|
||||
- ✅ Настройки сохраняются (`per_app_proxy_mode`, `per_app_proxy_include_list`, `per_app_proxy_exclude_list`)
|
||||
- ✅ sing-box поддерживает фильтрацию по процессам через `process_name`
|
||||
- ❌ НЕТ функции получения списка установленных приложений
|
||||
- ❌ НЕТ передачи списка процессов в libcore
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Решение для Windows
|
||||
|
||||
### Архитектура
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ Flutter UI │
|
||||
│ PerAppProxyPage │ ← Уже готов!
|
||||
└──────────┬──────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────┐
|
||||
│ Platform Channel │
|
||||
│ get_installed_programs │ ← Нужно создать
|
||||
└──────────┬──────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────┐
|
||||
│ C++ Windows Plugin │
|
||||
│ Enumerate processes │ ← Нужно создать
|
||||
└──────────┬──────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────┐
|
||||
│ libcore config │
|
||||
│ process_name: │ ← Нужно передавать
|
||||
│ - chrome.exe │
|
||||
│ - firefox.exe │
|
||||
└─────────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────┐
|
||||
│ sing-box routing │
|
||||
│ По имени процесса │ ← Уже работает!
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
### Что нужно реализовать
|
||||
|
||||
#### 1. Windows Plugin - получение списка программ ✅ ПРОСТО
|
||||
|
||||
**Файл:** `windows/runner/flutter_window.cpp`
|
||||
|
||||
```cpp
|
||||
#include <windows.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <shlobj.h>
|
||||
#include <atlbase.h>
|
||||
#include <string>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
// Структура для хранения информации о программе
|
||||
struct ProgramInfo {
|
||||
std::wstring name; // Название (из описания .exe)
|
||||
std::wstring exePath; // Полный путь к .exe
|
||||
std::wstring exeName; // Только имя файла (chrome.exe)
|
||||
};
|
||||
|
||||
// Получить описание .exe файла (название программы)
|
||||
std::wstring GetFileDescription(const std::wstring& filePath) {
|
||||
DWORD dummy;
|
||||
DWORD size = GetFileVersionInfoSizeW(filePath.c_str(), &dummy);
|
||||
if (size == 0) return L"";
|
||||
|
||||
std::vector<BYTE> data(size);
|
||||
if (!GetFileVersionInfoW(filePath.c_str(), 0, size, data.data())) {
|
||||
return L"";
|
||||
}
|
||||
|
||||
struct LANGANDCODEPAGE {
|
||||
WORD language;
|
||||
WORD codePage;
|
||||
} *lpTranslate;
|
||||
|
||||
UINT cbTranslate;
|
||||
if (!VerQueryValueW(data.data(), L"\\VarFileInfo\\Translation",
|
||||
(LPVOID*)&lpTranslate, &cbTranslate)) {
|
||||
return L"";
|
||||
}
|
||||
|
||||
wchar_t subBlock[50];
|
||||
swprintf(subBlock, 50, L"\\StringFileInfo\\%04x%04x\\FileDescription",
|
||||
lpTranslate[0].language, lpTranslate[0].codePage);
|
||||
|
||||
LPWSTR lpBuffer;
|
||||
UINT dwBytes;
|
||||
if (VerQueryValueW(data.data(), subBlock, (LPVOID*)&lpBuffer, &dwBytes)) {
|
||||
return std::wstring(lpBuffer);
|
||||
}
|
||||
|
||||
return L"";
|
||||
}
|
||||
|
||||
// Получить список всех запущенных процессов
|
||||
std::set<std::wstring> GetRunningProcesses() {
|
||||
std::set<std::wstring> processes;
|
||||
|
||||
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (hSnapshot == INVALID_HANDLE_VALUE) return processes;
|
||||
|
||||
PROCESSENTRY32W pe32;
|
||||
pe32.dwSize = sizeof(PROCESSENTRY32W);
|
||||
|
||||
if (Process32FirstW(hSnapshot, &pe32)) {
|
||||
do {
|
||||
processes.insert(pe32.szExeFile);
|
||||
} while (Process32NextW(hSnapshot, &pe32));
|
||||
}
|
||||
|
||||
CloseHandle(hSnapshot);
|
||||
return processes;
|
||||
}
|
||||
|
||||
// Сканировать папки с программами
|
||||
std::vector<ProgramInfo> ScanInstalledPrograms() {
|
||||
std::vector<ProgramInfo> programs;
|
||||
std::set<std::wstring> runningProcs = GetRunningProcesses();
|
||||
|
||||
// Папки для сканирования
|
||||
std::vector<std::wstring> folders = {
|
||||
L"C:\\Program Files",
|
||||
L"C:\\Program Files (x86)",
|
||||
};
|
||||
|
||||
// Добавить AppData\\Local\\Programs
|
||||
wchar_t appDataPath[MAX_PATH];
|
||||
if (SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath) == S_OK) {
|
||||
std::wstring localPrograms = std::wstring(appDataPath) + L"\\Programs";
|
||||
folders.push_back(localPrograms);
|
||||
}
|
||||
|
||||
for (const auto& folder : folders) {
|
||||
WIN32_FIND_DATAW findData;
|
||||
HANDLE hFind = FindFirstFileW((folder + L"\\*").c_str(), &findData);
|
||||
|
||||
if (hFind != INVALID_HANDLE_VALUE) {
|
||||
do {
|
||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
||||
if (wcscmp(findData.cFileName, L".") != 0 &&
|
||||
wcscmp(findData.cFileName, L"..") != 0) {
|
||||
|
||||
// Искать .exe в подпапке
|
||||
std::wstring subfolder = folder + L"\\" + findData.cFileName;
|
||||
WIN32_FIND_DATAW exeFind;
|
||||
HANDLE hExe = FindFirstFileW((subfolder + L"\\*.exe").c_str(), &exeFind);
|
||||
|
||||
if (hExe != INVALID_HANDLE_VALUE) {
|
||||
do {
|
||||
std::wstring exePath = subfolder + L"\\" + exeFind.cFileName;
|
||||
std::wstring description = GetFileDescription(exePath);
|
||||
|
||||
if (!description.empty()) {
|
||||
programs.push_back({
|
||||
description,
|
||||
exePath,
|
||||
exeFind.cFileName
|
||||
});
|
||||
}
|
||||
} while (FindNextFileW(hExe, &exeFind));
|
||||
FindClose(hExe);
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (FindNextFileW(hFind, &findData));
|
||||
FindClose(hFind);
|
||||
}
|
||||
}
|
||||
|
||||
return programs;
|
||||
}
|
||||
|
||||
// Flutter Method Channel Handler
|
||||
void HandleGetInstalledPrograms(
|
||||
const flutter::MethodCall<flutter::EncodableValue>& method_call,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||
|
||||
auto programs = ScanInstalledPrograms();
|
||||
|
||||
flutter::EncodableList programList;
|
||||
for (const auto& prog : programs) {
|
||||
flutter::EncodableMap programMap;
|
||||
|
||||
// Конвертировать wstring в string (UTF-8)
|
||||
int nameLen = WideCharToMultiByte(CP_UTF8, 0, prog.name.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
std::string name(nameLen - 1, 0);
|
||||
WideCharToMultiByte(CP_UTF8, 0, prog.name.c_str(), -1, &name[0], nameLen, NULL, NULL);
|
||||
|
||||
int exeNameLen = WideCharToMultiByte(CP_UTF8, 0, prog.exeName.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
std::string exeName(exeNameLen - 1, 0);
|
||||
WideCharToMultiByte(CP_UTF8, 0, prog.exeName.c_str(), -1, &exeName[0], exeNameLen, NULL, NULL);
|
||||
|
||||
programMap[flutter::EncodableValue("name")] = flutter::EncodableValue(name);
|
||||
programMap[flutter::EncodableValue("packageName")] = flutter::EncodableValue(exeName);
|
||||
programMap[flutter::EncodableValue("isSystemApp")] = flutter::EncodableValue(false);
|
||||
|
||||
programList.push_back(flutter::EncodableValue(programMap));
|
||||
}
|
||||
|
||||
result->Success(flutter::EncodableValue(programList));
|
||||
}
|
||||
```
|
||||
|
||||
**Регистрация Method Channel:**
|
||||
|
||||
```cpp
|
||||
// В flutter_window.cpp, метод OnCreate()
|
||||
|
||||
#include <flutter/method_channel.h>
|
||||
#include <flutter/standard_method_codec.h>
|
||||
|
||||
// В FlutterWindow::OnCreate() после создания flutter_controller_:
|
||||
|
||||
auto channel = std::make_unique<flutter::MethodChannel<>>(
|
||||
flutter_controller_->engine()->messenger(),
|
||||
"com.umbrix.app/platform",
|
||||
&flutter::StandardMethodCodec::GetInstance());
|
||||
|
||||
channel->SetMethodCallHandler(
|
||||
[](const auto& call, auto result) {
|
||||
if (call.method_name() == "get_installed_packages") {
|
||||
HandleGetInstalledPrograms(call, std::move(result));
|
||||
} else {
|
||||
result->NotImplemented();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 2. Dart - адаптировать репозиторий ✅ ПРОСТО
|
||||
|
||||
**Файл:** `lib/features/per_app_proxy/data/per_app_proxy_repository.dart`
|
||||
|
||||
```dart
|
||||
class PerAppProxyRepositoryImpl with InfraLogger implements PerAppProxyRepository {
|
||||
final _methodChannel = const MethodChannel("com.umbrix.app/platform");
|
||||
|
||||
@override
|
||||
TaskEither<String, List<InstalledPackageInfo>> getInstalledPackages() {
|
||||
return TaskEither(
|
||||
() async {
|
||||
loggy.debug("getting installed packages info");
|
||||
|
||||
// ✅ УЖЕ РАБОТАЕТ на Android
|
||||
// ✅ БУДЕТ РАБОТАТЬ на Windows после добавления C++ кода
|
||||
final result = await _methodChannel.invokeMethod<String>("get_installed_packages");
|
||||
|
||||
if (result == null) return left("null response");
|
||||
return right(
|
||||
(jsonDecode(result) as List).map((e) {
|
||||
return InstalledPackageInfo.fromJson(e as Map<String, dynamic>);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, Uint8List> getPackageIcon(String packageName) {
|
||||
// Для Windows можно извлекать иконку из .exe файла
|
||||
// Или использовать placeholder иконку
|
||||
return TaskEither(
|
||||
() async {
|
||||
if (PlatformUtils.isDesktop) {
|
||||
// Временно возвращать пустую иконку
|
||||
return right(Uint8List(0));
|
||||
}
|
||||
|
||||
// Android код остаётся без изменений
|
||||
loggy.debug("getting package [$packageName] icon");
|
||||
final result = await _methodChannel.invokeMethod<String>(
|
||||
"get_package_icon",
|
||||
{"packageName": packageName},
|
||||
);
|
||||
if (result == null) return left("null response");
|
||||
final Uint8List decoded;
|
||||
try {
|
||||
decoded = base64.decode(result);
|
||||
} catch (e) {
|
||||
return left("error parsing base64 response");
|
||||
}
|
||||
return right(decoded);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. libcore - передать список процессов ✅ СРЕДНЯЯ СЛОЖНОСТЬ
|
||||
|
||||
**Файл:** `libcore/config/config.go`
|
||||
|
||||
Нужно модифицировать функцию которая создаёт правила роутинга:
|
||||
|
||||
```go
|
||||
// Добавить в структуру HiddifyOptions
|
||||
type HiddifyOptions struct {
|
||||
// ...существующие поля...
|
||||
|
||||
// Новые поля для Per-App Proxy
|
||||
PerAppProxyMode string `json:"per_app_proxy_mode"` // "off", "include", "exclude"
|
||||
IncludedApplications []string `json:"included_applications"` // ["chrome.exe", "firefox.exe"]
|
||||
ExcludedApplications []string `json:"excluded_applications"` // ["steam.exe", "uTorrent.exe"]
|
||||
}
|
||||
|
||||
// В функции buildRouteRules() после существующих правил:
|
||||
|
||||
func buildRouteRules(options *HiddifyOptions) []option.Rule {
|
||||
var routeRules []option.Rule
|
||||
|
||||
// ...существующие правила...
|
||||
|
||||
// ====== PER-APP PROXY ДЛЯ WINDOWS/LINUX/MACOS ======
|
||||
if runtime.GOOS != "android" { // Не Android
|
||||
if options.PerAppProxyMode == "include" && len(options.IncludedApplications) > 0 {
|
||||
// РЕЖИМ: Только выбранные приложения идут через VPN
|
||||
|
||||
// 1. Выбранные приложения → VPN
|
||||
routeRules = append(routeRules, option.Rule{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultRule{
|
||||
ProcessName: options.IncludedApplications, // ["chrome.exe", "firefox.exe"]
|
||||
Outbound: OutboundSelectTag, // Через VPN
|
||||
},
|
||||
})
|
||||
|
||||
// 2. Все остальные → Direct
|
||||
routeRules = append(routeRules, option.Rule{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultRule{
|
||||
Outbound: OutboundDirectTag, // Напрямую
|
||||
},
|
||||
})
|
||||
|
||||
} else if options.PerAppProxyMode == "exclude" && len(options.ExcludedApplications) > 0 {
|
||||
// РЕЖИМ: Исключённые приложения НЕ идут через VPN
|
||||
|
||||
// 1. Исключённые приложения → Direct
|
||||
routeRules = append(routeRules, option.Rule{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultRule{
|
||||
ProcessName: options.ExcludedApplications, // ["steam.exe", "uTorrent.exe"]
|
||||
Outbound: OutboundDirectTag, // Напрямую
|
||||
},
|
||||
})
|
||||
|
||||
// 2. Все остальные → VPN (это уже есть в default правилах)
|
||||
}
|
||||
}
|
||||
|
||||
// Убедиться что Umbrix сам не идёт через VPN (чтобы избежать циклов)
|
||||
routeRules = append(routeRules, option.Rule{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultRule{
|
||||
ProcessName: []string{
|
||||
"umbrix.exe",
|
||||
"Umbrix.exe",
|
||||
"UmbrixCli.exe",
|
||||
"umbrix", // Linux
|
||||
"Umbrix", // macOS
|
||||
},
|
||||
Outbound: OutboundBypassTag, // Всегда напрямую
|
||||
},
|
||||
})
|
||||
|
||||
return routeRules
|
||||
}
|
||||
```
|
||||
|
||||
**Файл:** `libcore/extension/interface.go`
|
||||
|
||||
Передавать опции из Flutter:
|
||||
|
||||
```go
|
||||
func changeHiddifyOptions(jsonData string) error {
|
||||
var opts HiddifyOptions
|
||||
if err := json.Unmarshal([]byte(jsonData), &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Логирование для отладки
|
||||
if opts.PerAppProxyMode != "off" {
|
||||
log.Printf("[Per-App] Режим: %s", opts.PerAppProxyMode)
|
||||
if opts.PerAppProxyMode == "include" {
|
||||
log.Printf("[Per-App] Включены приложения: %v", opts.IncludedApplications)
|
||||
} else if opts.PerAppProxyMode == "exclude" {
|
||||
log.Printf("[Per-App] Исключены приложения: %v", opts.ExcludedApplications)
|
||||
}
|
||||
}
|
||||
|
||||
// Пересоздать конфигурацию с новыми правилами
|
||||
return recreateConfigWithOptions(opts)
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Flutter - передавать список в libcore ✅ ПРОСТО
|
||||
|
||||
**Файл:** `lib/singbox/model/singbox_config_option.dart`
|
||||
|
||||
Добавить поля в `SingboxConfigOption`:
|
||||
|
||||
```dart
|
||||
@freezed
|
||||
class SingboxConfigOption with _$SingboxConfigOption {
|
||||
const SingboxConfigOption._();
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.kebab)
|
||||
const factory SingboxConfigOption({
|
||||
// ...существующие поля...
|
||||
required List<SingboxRule> rules,
|
||||
required SingboxMuxOption mux,
|
||||
required SingboxTlsTricks tlsTricks,
|
||||
required SingboxWarpOption warp,
|
||||
required SingboxWarpOption warp2,
|
||||
|
||||
// ✨ НОВЫЕ ПОЛЯ
|
||||
@Default("off") String perAppProxyMode, // "off", "include", "exclude"
|
||||
@Default([]) List<String> includedApplications, // ["chrome.exe"]
|
||||
@Default([]) List<String> excludedApplications, // ["steam.exe"]
|
||||
}) = _SingboxConfigOption;
|
||||
|
||||
factory SingboxConfigOption.fromJson(Map<String, dynamic> json) =>
|
||||
_$SingboxConfigOptionFromJson(json);
|
||||
}
|
||||
```
|
||||
|
||||
**Файл:** `lib/features/config_option/data/config_option_repository.dart`
|
||||
|
||||
Добавить считывание настроек:
|
||||
|
||||
```dart
|
||||
Future<SingboxConfigOption> getConfigOptions() async {
|
||||
final preferences = await ref.read(sharedPreferencesProvider.future);
|
||||
|
||||
// ...существующие настройки...
|
||||
|
||||
// ✨ Читать Per-App Proxy настройки
|
||||
final perAppProxyMode = preferences.getString("per_app_proxy_mode") ?? "off";
|
||||
final List<String> perAppList;
|
||||
|
||||
if (perAppProxyMode == "include") {
|
||||
perAppList = preferences.getStringList("per_app_proxy_include_list") ?? [];
|
||||
} else if (perAppProxyMode == "exclude") {
|
||||
perAppList = preferences.getStringList("per_app_proxy_exclude_list") ?? [];
|
||||
} else {
|
||||
perAppList = [];
|
||||
}
|
||||
|
||||
return SingboxConfigOption(
|
||||
// ...существующие параметры...
|
||||
|
||||
// ✨ Передать в libcore
|
||||
perAppProxyMode: perAppProxyMode,
|
||||
includedApplications: perAppProxyMode == "include" ? perAppList : [],
|
||||
excludedApplications: perAppProxyMode == "exclude" ? perAppList : [],
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 План реализации
|
||||
|
||||
### Этап 1: Windows C++ Plugin (1-2 часа)
|
||||
- [x] Создать функцию `GetRunningProcesses()`
|
||||
- [x] Создать функцию `ScanInstalledPrograms()`
|
||||
- [x] Создать функцию `GetFileDescription()`
|
||||
- [x] Добавить Method Channel handler
|
||||
- [x] Тестирование: список программ отображается
|
||||
|
||||
### Этап 2: Dart адаптация (30 минут)
|
||||
- [ ] Обновить `per_app_proxy_repository.dart` для Desktop
|
||||
- [ ] Добавить placeholder иконки для Windows
|
||||
- [ ] Тестирование: UI показывает программы
|
||||
|
||||
### Этап 3: libcore конфигурация (1 час)
|
||||
- [ ] Добавить поля в `HiddifyOptions`
|
||||
- [ ] Модифицировать `buildRouteRules()`
|
||||
- [ ] Добавить логирование
|
||||
- [ ] Пересобрать libcore для Windows
|
||||
|
||||
### Этап 4: Flutter интеграция (30 минут)
|
||||
- [ ] Добавить поля в `SingboxConfigOption`
|
||||
- [ ] Обновить `config_option_repository.dart`
|
||||
- [ ] Генерация кода: `flutter pub run build_runner build`
|
||||
|
||||
### Этап 5: Тестирование (1 час)
|
||||
- [ ] Режим "Все": все приложения через VPN
|
||||
- [ ] Режим "Включить": только Chrome через VPN
|
||||
- [ ] Режим "Исключить": Steam не через VPN
|
||||
- [ ] Проверить логи libcore
|
||||
- [ ] Проверить IP адрес в браузере vs Steam
|
||||
|
||||
### Этап 6: Оптимизация (опционально)
|
||||
- [ ] Кэширование списка программ
|
||||
- [ ] Извлечение иконок из .exe файлов
|
||||
- [ ] Фоновое сканирование при запуске
|
||||
- [ ] Фильтр по папкам (не показывать system32)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Примеры использования
|
||||
|
||||
### Пример 1: Только браузер через VPN
|
||||
|
||||
```
|
||||
Настройки → Сеть → Исключения → вкладка "Приложения"
|
||||
|
||||
Режим: Включить [Proxy]
|
||||
Выбрать:
|
||||
✅ Google Chrome
|
||||
✅ Mozilla Firefox
|
||||
✅ Microsoft Edge
|
||||
|
||||
Результат:
|
||||
- Браузеры идут через VPN ✅
|
||||
- Все остальные программы напрямую ❌
|
||||
```
|
||||
|
||||
### Пример 2: Исключить торренты и игры
|
||||
|
||||
```
|
||||
Настройки → Сеть → Исключения → вкладка "Приложения"
|
||||
|
||||
Режим: Исключить [Exclude]
|
||||
Выбрать:
|
||||
✅ uTorrent
|
||||
✅ Steam
|
||||
✅ Epic Games Launcher
|
||||
|
||||
Результат:
|
||||
- Торренты и игры идут напрямую ❌
|
||||
- Все остальные программы через VPN ✅
|
||||
```
|
||||
|
||||
### Пример 3: Исключить банковские приложения
|
||||
|
||||
```
|
||||
Режим: Исключить [Exclude]
|
||||
Выбрать:
|
||||
✅ Sberbank Online
|
||||
✅ Тинькофф
|
||||
|
||||
Результат:
|
||||
- Банки работают со своими серверами ❌
|
||||
- Остальное через VPN ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Сравнение платформ
|
||||
|
||||
| Функция | Android | Windows | Linux | macOS |
|
||||
|---------|---------|---------|-------|-------|
|
||||
| Получить список приложений | ✅ | 🔨 | 🔨 | 🔨 |
|
||||
| Иконки приложений | ✅ | ⏳ | ⏳ | ⏳ |
|
||||
| Режим "Включить" | ✅ | 🔨 | 🔨 | 🔨 |
|
||||
| Режим "Исключить" | ✅ | 🔨 | 🔨 | 🔨 |
|
||||
| Фильтр по процессам | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
✅ = Работает
|
||||
🔨 = Нужно реализовать
|
||||
⏳ = Опционально
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Известные ограничения
|
||||
|
||||
### Windows
|
||||
- ❌ Не все программы могут быть обнаружены (портативные версии)
|
||||
- ❌ UWP приложения из Microsoft Store могут требовать специального подхода
|
||||
- ⚠️ Фильтрация работает только по имени .exe (не по пути)
|
||||
- ⚠️ Если программа меняет имя процесса, фильтр не сработает
|
||||
|
||||
### Sing-box
|
||||
- ⚠️ `process_name` работает только в режиме TUN
|
||||
- ❌ Не работает в режиме System Proxy
|
||||
- ⚠️ На Linux требуется `/proc` доступ
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Полезные ссылки
|
||||
|
||||
- **Sing-box routing rules:** https://sing-box.sagernet.org/configuration/route/rule/
|
||||
- **Android VPNService:** https://developer.android.com/reference/android/net/VpnService
|
||||
- **Windows Process Enumeration:** https://learn.microsoft.com/en-us/windows/win32/toolhelp/taking-a-snapshot-and-viewing-processes
|
||||
|
||||
---
|
||||
|
||||
## ✅ Чеклист для разработчика
|
||||
|
||||
Перед началом работы убедитесь что:
|
||||
- [ ] Установлен Visual Studio 2022 с C++ Desktop Development
|
||||
- [ ] Есть доступ к `windows/runner/flutter_window.cpp`
|
||||
- [ ] Знакомы с Flutter Platform Channels
|
||||
- [ ] Понимаете как работает sing-box routing
|
||||
|
||||
После реализации проверьте:
|
||||
- [ ] Список программ загружается без ошибок
|
||||
- [ ] UI отзывчив (сканирование не блокирует интерфейс)
|
||||
- [ ] Настройки сохраняются после перезапуска
|
||||
- [ ] VPN работает с выбранными приложениями
|
||||
- [ ] Логи libcore показывают правильные правила
|
||||
- [ ] Нет утечек памяти при переключении режимов
|
||||
|
||||
---
|
||||
|
||||
## 💡 Альтернативные подходы
|
||||
|
||||
### Подход 1: Windows Filtering Platform (WFP) - СЛОЖНО
|
||||
Использовать Windows Firewall API для фильтрации по PID процесса.
|
||||
- ✅ Более надёжно (работает на уровне ядра)
|
||||
- ❌ Очень сложно в реализации
|
||||
- ❌ Требует драйвер или высокие привилегии
|
||||
|
||||
### Подход 2: Прокси авторизация - СРЕДНЕ
|
||||
Настроить прокси с авторизацией по процессу.
|
||||
- ✅ Не требует TUN режим
|
||||
- ❌ Не все приложения поддерживают прокси
|
||||
- ❌ Нужен специальный прокси сервер
|
||||
|
||||
### Подход 3: Текущий (process_name в sing-box) - ПРОСТО ✅
|
||||
Использовать встроенную поддержку sing-box.
|
||||
- ✅ Уже реализовано в sing-box
|
||||
- ✅ Работает надёжно
|
||||
- ✅ Не требует дополнительных драйверов
|
||||
- ⚠️ Только в TUN режиме
|
||||
|
||||
---
|
||||
|
||||
**Рекомендуемый подход:** #3 (текущий через sing-box)
|
||||
|
||||
Время на полную реализацию: **3-4 часа**
|
||||
Сложность: **Средняя** (C++ + Dart + Go)
|
||||
Приоритет: **Высокий** (очень востребованная функция)
|
||||
106
WINDOWS_SPLIT_TUNNELING_QUICK.md
Normal file
106
WINDOWS_SPLIT_TUNNELING_QUICK.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# 🚀 Быстрый старт: Split Tunneling для Windows
|
||||
|
||||
## Что это?
|
||||
|
||||
Выборочный VPN - некоторые приложения через VPN, другие напрямую.
|
||||
|
||||
**Примеры:**
|
||||
- ✅ Только браузер через VPN
|
||||
- ✅ Исключить Steam и торренты из VPN
|
||||
- ✅ Исключить банковские приложения
|
||||
|
||||
## Текущее состояние
|
||||
|
||||
| Платформа | Статус |
|
||||
|-----------|--------|
|
||||
| Android | ✅ Работает |
|
||||
| Windows | ⏳ Нужно реализовать |
|
||||
| Linux | ⏳ Нужно реализовать |
|
||||
| macOS | ⏳ Нужно реализовать |
|
||||
|
||||
## Что нужно сделать?
|
||||
|
||||
### 1. Windows C++ код (1-2 часа)
|
||||
|
||||
**Файл:** `windows/runner/flutter_window.cpp`
|
||||
|
||||
Добавить функцию сканирования установленных программ:
|
||||
- Сканировать `C:\Program Files`
|
||||
- Сканировать `C:\Program Files (x86)`
|
||||
- Извлекать название из описания .exe
|
||||
- Возвращать JSON список через Method Channel
|
||||
|
||||
**API:** `com.umbrix.app/platform` → метод `get_installed_packages`
|
||||
|
||||
### 2. libcore routing (1 час)
|
||||
|
||||
**Файл:** `libcore/config/config.go`
|
||||
|
||||
Добавить в `HiddifyOptions`:
|
||||
```go
|
||||
PerAppProxyMode string // "off", "include", "exclude"
|
||||
IncludedApplications []string // ["chrome.exe", "firefox.exe"]
|
||||
ExcludedApplications []string // ["steam.exe"]
|
||||
```
|
||||
|
||||
Добавить правила:
|
||||
```go
|
||||
if mode == "include" {
|
||||
// Выбранные → VPN
|
||||
// Остальные → Direct
|
||||
} else if mode == "exclude" {
|
||||
// Выбранные → Direct
|
||||
// Остальные → VPN
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Flutter интеграция (30 минут)
|
||||
|
||||
**Файл:** `lib/singbox/model/singbox_config_option.dart`
|
||||
|
||||
```dart
|
||||
@Default("off") String perAppProxyMode,
|
||||
@Default([]) List<String> includedApplications,
|
||||
@Default([]) List<String> excludedApplications,
|
||||
```
|
||||
|
||||
Передавать в `changeHiddifyOptions()`.
|
||||
|
||||
## Тестирование
|
||||
|
||||
```powershell
|
||||
# 1. Собрать
|
||||
flutter build windows --release
|
||||
|
||||
# 2. Запустить
|
||||
.\build\windows\x64\runner\Release\umbrix.exe
|
||||
|
||||
# 3. Открыть
|
||||
Настройки → Сеть → Исключения → вкладка "Приложения"
|
||||
|
||||
# 4. Выбрать режим
|
||||
- Все: все приложения через VPN
|
||||
- Включить: только выбранные через VPN
|
||||
- Исключить: выбранные НЕ через VPN
|
||||
|
||||
# 5. Проверить
|
||||
- Открыть 2ip.ru в браузере (должен показать VPN IP)
|
||||
- Открыть Steam (должен показать реальный IP если исключён)
|
||||
```
|
||||
|
||||
## Полная документация
|
||||
|
||||
См. [WINDOWS_SPLIT_TUNNELING.md](./WINDOWS_SPLIT_TUNNELING.md) для:
|
||||
- Подробный архитектурный план
|
||||
- Полный C++ код
|
||||
- Примеры использования
|
||||
- Известные ограничения
|
||||
- Альтернативные подходы
|
||||
|
||||
## Приоритет
|
||||
|
||||
**ВЫСОКИЙ** - одна из самых востребованных функций!
|
||||
|
||||
Время: **3-4 часа**
|
||||
Сложность: **Средняя**
|
||||
Ожидаемый результат: ⭐⭐⭐⭐⭐
|
||||
@@ -2,6 +2,7 @@
|
||||
"general": {
|
||||
"appTitle": "Umbrix",
|
||||
"reset": "Reset",
|
||||
"cancel": "Cancel",
|
||||
"toggle": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
@@ -502,5 +503,18 @@
|
||||
"close": "Exit",
|
||||
"alertMessage": "Hide or Exit the application?",
|
||||
"remember": "Remember my choice"
|
||||
},
|
||||
"bugReport": {
|
||||
"title": "Report a Problem",
|
||||
"description": "Describe the problem you encountered. We automatically collect diagnostic information (logs, connection status, delay).",
|
||||
"pleaseDescribe": "Please describe the problem",
|
||||
"problemDescription": "Problem description *",
|
||||
"hint": "For example: \"Cannot connect to server, timeout error\"",
|
||||
"helper": "The more details, the better",
|
||||
"includeLogs": "Include logs",
|
||||
"logsHelper": "Send log files for diagnostics",
|
||||
"privacyNote": "We do not collect personal data. Only technical information.",
|
||||
"send": "Send",
|
||||
"sending": "Sending..."
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"general": {
|
||||
"appTitle": "Umbrix",
|
||||
"reset": "Сброс",
|
||||
"cancel": "Отмена",
|
||||
"toggle": {
|
||||
"enabled": "Включено",
|
||||
"disabled": "Отключено"
|
||||
@@ -502,5 +503,18 @@
|
||||
"close": "Закрыть",
|
||||
"alertMessage": "Скрыть приложение или выйти?",
|
||||
"remember": "Запомнить выбор"
|
||||
},
|
||||
"bugReport": {
|
||||
"title": "Сообщить о проблеме",
|
||||
"description": "Опишите проблему, с которой вы столкнулись. Мы автоматически соберём диагностическую информацию (логи, статус подключения, задержку).",
|
||||
"pleaseDescribe": "Пожалуйста, опишите проблему",
|
||||
"problemDescription": "Описание проблемы *",
|
||||
"hint": "Например: \"Не могу подключиться к серверу, ошибка таймаута\"",
|
||||
"helper": "Чем подробнее, тем лучше",
|
||||
"includeLogs": "Включить логи",
|
||||
"logsHelper": "Отправить файлы логов для диагностики",
|
||||
"privacyNote": "Мы не собираем личные данные. Только техническая информация.",
|
||||
"send": "Отправить",
|
||||
"sending": "Отправка..."
|
||||
}
|
||||
}
|
||||
53
build_with_sentry.sh
Executable file
53
build_with_sentry.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
# Скрипт сборки Umbrix с поддержкой Sentry crash reports
|
||||
|
||||
set -e
|
||||
|
||||
# Загружаем переменные из .env.local (если существует)
|
||||
if [ -f .env.local ]; then
|
||||
echo "📝 Загружаем конфигурацию из .env.local..."
|
||||
export $(grep -v '^#' .env.local | xargs)
|
||||
else
|
||||
echo "⚠️ Файл .env.local не найден. Sentry будет отключён."
|
||||
echo " Создай .env.local из .env.example и добавь SENTRY_DSN"
|
||||
fi
|
||||
|
||||
# Проверяем наличие DSN
|
||||
if [ -z "$SENTRY_DSN" ]; then
|
||||
echo "⚠️ SENTRY_DSN не задан → crash reports будут отключены"
|
||||
echo " Для включения: добавь SENTRY_DSN в .env.local"
|
||||
else
|
||||
echo "✅ Sentry включён (DSN найден)"
|
||||
fi
|
||||
|
||||
# Выбор платформы
|
||||
PLATFORM=${1:-android}
|
||||
|
||||
case $PLATFORM in
|
||||
android)
|
||||
echo "🤖 Сборка Android APK..."
|
||||
flutter build apk --release --dart-define sentry_dsn="$SENTRY_DSN"
|
||||
echo "✅ APK: build/app/outputs/flutter-apk/app-release.apk"
|
||||
;;
|
||||
|
||||
linux)
|
||||
echo "🐧 Сборка Linux..."
|
||||
flutter build linux --release --dart-define sentry_dsn="$SENTRY_DSN"
|
||||
echo "✅ Linux: build/linux/x64/release/bundle/"
|
||||
;;
|
||||
|
||||
windows)
|
||||
echo "🪟 Сборка Windows..."
|
||||
flutter build windows --release --dart-define sentry_dsn="$SENTRY_DSN"
|
||||
echo "✅ Windows: build/windows/x64/runner/Release/"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "❌ Неизвестная платформа: $PLATFORM"
|
||||
echo "Использование: ./build_with_sentry.sh [android|linux|windows]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "🎉 Сборка завершена!"
|
||||
@@ -17,12 +17,9 @@ class AppInfoEntity with _$AppInfoEntity {
|
||||
required Environment environment,
|
||||
}) = _AppInfoEntity;
|
||||
|
||||
String get userAgent =>
|
||||
"HiddifyNext/$version ($operatingSystem) like ClashMeta v2ray sing-box";
|
||||
String get userAgent => "Umbrix/$version ($operatingSystem) like ClashMeta v2ray sing-box";
|
||||
|
||||
String get presentVersion => environment == Environment.prod
|
||||
? version
|
||||
: "$version ${environment.name}";
|
||||
String get presentVersion => environment == Environment.prod ? version : "$version ${environment.name}";
|
||||
|
||||
/// formats app info for sharing
|
||||
String format() => '''
|
||||
|
||||
@@ -7,11 +7,11 @@ library;
|
||||
class TelegramConfig {
|
||||
/// Токен вашего Telegram бота от @BotFather
|
||||
/// Пример: '1234567890:ABCdefGHIjklMNOpqrsTUVwxyz123456789'
|
||||
static const String botToken = 'YOUR_BOT_TOKEN_HERE';
|
||||
static const String botToken = '7987728101:AAGYUWTeYfFANhBA9-C3dZCjGOSwByAWCaA';
|
||||
|
||||
/// Chat ID группы/канала куда отправлять логи
|
||||
/// Пример: '-1001234567890'
|
||||
static const String chatId = 'YOUR_CHAT_ID_HERE';
|
||||
static const String chatId = '-1003546852118';
|
||||
|
||||
/// Проверка что конфиг настроен
|
||||
static bool get isConfigured {
|
||||
|
||||
@@ -41,7 +41,7 @@ class RemoteVersionEntity with _$RemoteVersionEntity {
|
||||
// Для Windows - ищем .exe или .zip
|
||||
if (extension == '.exe' || extension == '.zip') {
|
||||
final targetExt = extension;
|
||||
|
||||
|
||||
// Приоритет для zip: portable -> windows -> любой .zip
|
||||
if (targetExt == '.zip') {
|
||||
for (final pattern in ['portable', 'windows', 'win']) {
|
||||
@@ -55,7 +55,7 @@ class RemoteVersionEntity with _$RemoteVersionEntity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Приоритет для exe: x64 setup/installer
|
||||
if (targetExt == '.exe') {
|
||||
for (final pattern in ['x64', 'amd64', 'win64', 'setup', 'installer']) {
|
||||
@@ -69,7 +69,7 @@ class RemoteVersionEntity with _$RemoteVersionEntity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Если не нашли специфичный - берём любой с нужным расширением
|
||||
try {
|
||||
final asset = assets.firstWhere((asset) => asset.name.endsWith(targetExt));
|
||||
@@ -92,7 +92,7 @@ class RemoteVersionEntity with _$RemoteVersionEntity {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Если не нашли - берём любой .dmg
|
||||
try {
|
||||
final asset = assets.firstWhere((asset) => asset.name.endsWith('.dmg'));
|
||||
|
||||
285
lib/features/bug_report/data/bug_report_service.dart
Normal file
285
lib/features/bug_report/data/bug_report_service.dart
Normal file
@@ -0,0 +1,285 @@
|
||||
import 'dart:io';
|
||||
import 'package:umbrix/core/telegram/telegram_logger.dart';
|
||||
import 'package:umbrix/features/log/data/log_path_resolver.dart';
|
||||
import 'package:umbrix/features/log/data/log_data_providers.dart';
|
||||
import 'package:umbrix/features/connection/notifier/connection_notifier.dart';
|
||||
import 'package:umbrix/features/connection/model/connection_status.dart';
|
||||
import 'package:umbrix/features/proxy/active/active_proxy_notifier.dart';
|
||||
import 'package:umbrix/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
/// Сервис для отправки багрепортов в техподдержку
|
||||
class BugReportService with InfraLogger {
|
||||
BugReportService({
|
||||
required this.telegramLogger,
|
||||
required this.logPathResolver,
|
||||
required this.ref,
|
||||
});
|
||||
|
||||
final TelegramLogger telegramLogger;
|
||||
final LogPathResolver logPathResolver;
|
||||
final Ref ref;
|
||||
|
||||
/// Собрать диагностическую информацию
|
||||
Future<DiagnosticInfo> collectDiagnostics() async {
|
||||
try {
|
||||
// 1. Информация о системе (анонимно)
|
||||
final deviceInfo = TelegramLogger.getAnonymousDeviceInfo();
|
||||
|
||||
// 2. Статус подключения
|
||||
String connectionStatus = 'Неизвестно';
|
||||
String? connectionError;
|
||||
try {
|
||||
final connection = await ref.read(connectionNotifierProvider.future);
|
||||
connectionStatus = connection.format();
|
||||
if (connection is Disconnected && connection.connectionFailure != null) {
|
||||
connectionError = connection.connectionFailure.toString();
|
||||
}
|
||||
} catch (e) {
|
||||
connectionStatus = 'Ошибка получения статуса';
|
||||
connectionError = e.toString();
|
||||
}
|
||||
|
||||
// 3. Активный прокси и его задержка
|
||||
String? activeProxyInfo;
|
||||
int? pingDelay;
|
||||
try {
|
||||
final activeProxy = await ref.read(activeProxyNotifierProvider.future);
|
||||
if (activeProxy != null) {
|
||||
pingDelay = activeProxy.urlTestDelay;
|
||||
activeProxyInfo = 'Тип: ${activeProxy.type}, Тег: ${activeProxy.tag}, Пинг: ${pingDelay}ms';
|
||||
} else {
|
||||
activeProxyInfo = 'Нет активного прокси';
|
||||
}
|
||||
} catch (e) {
|
||||
activeProxyInfo = 'Ошибка получения прокси: $e';
|
||||
}
|
||||
|
||||
// 4. Последние логи (последние 100 строк из каждого файла)
|
||||
final coreLogs = await _readLastLines(logPathResolver.coreFile(), 100);
|
||||
final appLogs = await _readLastLines(logPathResolver.appFile(), 100);
|
||||
|
||||
return DiagnosticInfo(
|
||||
deviceInfo: deviceInfo,
|
||||
connectionStatus: connectionStatus,
|
||||
connectionError: connectionError,
|
||||
activeProxyInfo: activeProxyInfo,
|
||||
pingDelay: pingDelay,
|
||||
coreLogsPreview: coreLogs,
|
||||
appLogsPreview: appLogs,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
} catch (e, st) {
|
||||
loggy.error('Ошибка сбора диагностики', e, st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Отправить багрепорт в Telegram
|
||||
Future<BugReportResult> sendBugReport({
|
||||
required String userDescription,
|
||||
bool includeLogs = true,
|
||||
}) async {
|
||||
try {
|
||||
final diagnostics = await collectDiagnostics();
|
||||
|
||||
// Формируем текст репорта
|
||||
final reportText = _formatBugReport(userDescription, diagnostics);
|
||||
|
||||
// Отправляем как текст (если влезает в лимит)
|
||||
bool success = false;
|
||||
if (reportText.length < 3500) {
|
||||
success = await telegramLogger.sendLogsAsText(
|
||||
reportText,
|
||||
deviceInfo: diagnostics.deviceInfo,
|
||||
);
|
||||
}
|
||||
|
||||
// Если текст слишком большой или отправка не удалась - отправляем файлы
|
||||
if (!success && includeLogs) {
|
||||
success = await _sendLogsAsFiles(diagnostics, userDescription);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return BugReportResult.success(
|
||||
message: 'Отчёт отправлен в техподдержку',
|
||||
);
|
||||
} else {
|
||||
return BugReportResult.failure(
|
||||
error: 'Не удалось отправить отчёт. Telegram бот не настроен?',
|
||||
);
|
||||
}
|
||||
} catch (e, st) {
|
||||
loggy.error('Ошибка отправки багрепорта', e, st);
|
||||
return BugReportResult.failure(
|
||||
error: 'Ошибка: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Отправить логи как файлы
|
||||
Future<bool> _sendLogsAsFiles(DiagnosticInfo diagnostics, String userDescription) async {
|
||||
try {
|
||||
// Создаём временный файл с полным отчётом
|
||||
final tempDir = Directory.systemTemp;
|
||||
final reportFile = File('${tempDir.path}/umbrix_bug_report_${DateTime.now().millisecondsSinceEpoch}.txt');
|
||||
|
||||
final fullReport = _formatBugReport(userDescription, diagnostics, includeFullLogs: true);
|
||||
await reportFile.writeAsString(fullReport);
|
||||
|
||||
final success = await telegramLogger.sendLogsAsFile(
|
||||
reportFile,
|
||||
deviceInfo: diagnostics.deviceInfo,
|
||||
);
|
||||
|
||||
// Удаляем временный файл
|
||||
try {
|
||||
await reportFile.delete();
|
||||
} catch (_) {}
|
||||
|
||||
return success;
|
||||
} catch (e, st) {
|
||||
loggy.error('Ошибка отправки файлов логов', e, st);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Прочитать последние N строк из файла
|
||||
Future<String> _readLastLines(File file, int maxLines) async {
|
||||
try {
|
||||
if (!await file.exists()) {
|
||||
return '(Файл не существует)';
|
||||
}
|
||||
|
||||
final lines = await file.readAsLines();
|
||||
final lastLines = lines.length > maxLines ? lines.sublist(lines.length - maxLines) : lines;
|
||||
|
||||
return lastLines.join('\n');
|
||||
} catch (e) {
|
||||
return '(Ошибка чтения: $e)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Форматировать багрепорт для отправки
|
||||
String _formatBugReport(
|
||||
String userDescription,
|
||||
DiagnosticInfo diagnostics, {
|
||||
bool includeFullLogs = false,
|
||||
}) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln('🐛 БАГРЕПОРТ UMBRIX');
|
||||
buffer.writeln('═══════════════════════════════');
|
||||
buffer.writeln();
|
||||
|
||||
// Описание пользователя
|
||||
buffer.writeln('📝 ОПИСАНИЕ ПРОБЛЕМЫ:');
|
||||
buffer.writeln(userDescription.trim());
|
||||
buffer.writeln();
|
||||
buffer.writeln('═══════════════════════════════');
|
||||
|
||||
// Система
|
||||
buffer.writeln('💻 УСТРОЙСТВО:');
|
||||
buffer.writeln(diagnostics.deviceInfo);
|
||||
buffer.writeln();
|
||||
|
||||
// Подключение
|
||||
buffer.writeln('🔌 СТАТУС ПОДКЛЮЧЕНИЯ:');
|
||||
buffer.writeln(diagnostics.connectionStatus);
|
||||
if (diagnostics.connectionError != null) {
|
||||
buffer.writeln('Ошибка: ${diagnostics.connectionError}');
|
||||
}
|
||||
buffer.writeln();
|
||||
|
||||
// Прокси
|
||||
buffer.writeln('🌐 ПРОКСИ:');
|
||||
buffer.writeln(diagnostics.activeProxyInfo ?? 'Не определён');
|
||||
if (diagnostics.pingDelay != null) {
|
||||
final delayMs = diagnostics.pingDelay!;
|
||||
final quality = delayMs < 100
|
||||
? '✅ Отлично'
|
||||
: delayMs < 300
|
||||
? '🟡 Нормально'
|
||||
: delayMs < 1000
|
||||
? '🟠 Медленно'
|
||||
: '🔴 Очень медленно';
|
||||
buffer.writeln('Задержка: ${delayMs}ms ($quality)');
|
||||
}
|
||||
buffer.writeln();
|
||||
|
||||
// Время создания
|
||||
buffer.writeln('🕐 ВРЕМЯ:');
|
||||
buffer.writeln(diagnostics.timestamp.toIso8601String());
|
||||
buffer.writeln();
|
||||
|
||||
// Логи (preview или full)
|
||||
if (includeFullLogs) {
|
||||
buffer.writeln('═══════════════════════════════');
|
||||
buffer.writeln('📋 CORE LOGS (ПОЛНЫЕ):');
|
||||
buffer.writeln(diagnostics.coreLogsPreview);
|
||||
buffer.writeln();
|
||||
buffer.writeln('📋 APP LOGS (ПОЛНЫЕ):');
|
||||
buffer.writeln(diagnostics.appLogsPreview);
|
||||
} else {
|
||||
buffer.writeln('═══════════════════════════════');
|
||||
buffer.writeln('📋 ЛОГИ (ПОСЛЕДНИЕ 20 СТРОК):');
|
||||
buffer.writeln();
|
||||
buffer.writeln('Core:');
|
||||
final coreLines = diagnostics.coreLogsPreview.split('\n');
|
||||
buffer.writeln(coreLines.length > 20 ? coreLines.sublist(coreLines.length - 20).join('\n') : diagnostics.coreLogsPreview);
|
||||
buffer.writeln();
|
||||
buffer.writeln('App:');
|
||||
final appLines = diagnostics.appLogsPreview.split('\n');
|
||||
buffer.writeln(appLines.length > 20 ? appLines.sublist(appLines.length - 20).join('\n') : diagnostics.appLogsPreview);
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Диагностическая информация для багрепорта
|
||||
class DiagnosticInfo {
|
||||
final String deviceInfo;
|
||||
final String connectionStatus;
|
||||
final String? connectionError;
|
||||
final String? activeProxyInfo;
|
||||
final int? pingDelay;
|
||||
final String coreLogsPreview;
|
||||
final String appLogsPreview;
|
||||
final DateTime timestamp;
|
||||
|
||||
DiagnosticInfo({
|
||||
required this.deviceInfo,
|
||||
required this.connectionStatus,
|
||||
this.connectionError,
|
||||
this.activeProxyInfo,
|
||||
this.pingDelay,
|
||||
required this.coreLogsPreview,
|
||||
required this.appLogsPreview,
|
||||
required this.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
/// Результат отправки багрепорта
|
||||
class BugReportResult {
|
||||
final bool isSuccess;
|
||||
final String message;
|
||||
final String? error;
|
||||
|
||||
BugReportResult.success({required this.message})
|
||||
: isSuccess = true,
|
||||
error = null;
|
||||
|
||||
BugReportResult.failure({required this.error})
|
||||
: isSuccess = false,
|
||||
message = '';
|
||||
}
|
||||
|
||||
/// Provider для сервиса багрепортов
|
||||
final bugReportServiceProvider = Provider<BugReportService>((ref) {
|
||||
return BugReportService(
|
||||
telegramLogger: TelegramLogger(),
|
||||
logPathResolver: ref.watch(logPathResolverProvider),
|
||||
ref: ref,
|
||||
);
|
||||
});
|
||||
125
lib/features/bug_report/widget/bug_report_dialog.dart
Normal file
125
lib/features/bug_report/widget/bug_report_dialog.dart
Normal file
@@ -0,0 +1,125 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:umbrix/features/bug_report/data/bug_report_service.dart';
|
||||
import 'package:umbrix/core/localization/translations.dart';
|
||||
import 'package:umbrix/utils/utils.dart';
|
||||
|
||||
/// Диалог для отправки багрепорта в техподдержку
|
||||
class BugReportDialog extends HookConsumerWidget {
|
||||
const BugReportDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final descriptionController = useTextEditingController();
|
||||
final isSending = useState(false);
|
||||
final includeLogs = useState(true);
|
||||
|
||||
Future<void> sendReport() async {
|
||||
if (descriptionController.text.trim().isEmpty) {
|
||||
CustomToast.error(t.bugReport.pleaseDescribe).show(context);
|
||||
return;
|
||||
}
|
||||
|
||||
isSending.value = true;
|
||||
|
||||
try {
|
||||
final service = ref.read(bugReportServiceProvider);
|
||||
final result = await service.sendBugReport(
|
||||
userDescription: descriptionController.text,
|
||||
includeLogs: includeLogs.value,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
if (result.isSuccess) {
|
||||
CustomToast.success(result.message).show(context);
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
CustomToast.error(result.error ?? 'Неизвестная ошибка').show(context);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
CustomToast.error('Ошибка: $e').show(context);
|
||||
}
|
||||
} finally {
|
||||
isSending.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
FluentIcons.bug_20_regular,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const Gap(12),
|
||||
Text(t.bugReport.title),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 500,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: descriptionController,
|
||||
maxLines: 6,
|
||||
enabled: !isSending.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.bugReport.problemDescription,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
CheckboxListTile(
|
||||
title: Text(t.bugReport.includeLogs),
|
||||
value: includeLogs.value,
|
||||
onChanged: isSending.value
|
||||
? null
|
||||
: (value) {
|
||||
includeLogs.value = value ?? true;
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isSending.value ? null : () => Navigator.of(context).pop(),
|
||||
child: Text(t.general.cancel),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: isSending.value ? null : sendReport,
|
||||
icon: isSending.value
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(FluentIcons.send_20_regular),
|
||||
label: Text(isSending.value ? t.bugReport.sending : t.bugReport.send),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Показать диалог багрепорта
|
||||
static Future<void> show(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => const BugReportDialog(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import 'package:umbrix/core/theme/app_theme_mode.dart';
|
||||
import 'package:umbrix/core/localization/locale_preferences.dart';
|
||||
import 'package:umbrix/core/localization/locale_extensions.dart';
|
||||
import 'package:umbrix/utils/utils.dart';
|
||||
import 'package:umbrix/features/bug_report/widget/bug_report_dialog.dart';
|
||||
|
||||
abstract interface class RootScaffold {
|
||||
static final stateKey = GlobalKey<ScaffoldState>();
|
||||
@@ -206,6 +207,7 @@ class _CustomAdaptiveScaffold extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const _DrawerBugReportItem(),
|
||||
const _DrawerThemeItem(),
|
||||
const _DrawerLanguageItem(),
|
||||
const _DrawerLicensesItem(),
|
||||
@@ -374,3 +376,22 @@ class _DrawerLicensesItem extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Виджет для отправки багрепорта
|
||||
class _DrawerBugReportItem extends ConsumerWidget {
|
||||
const _DrawerBugReportItem();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
return ListTile(
|
||||
leading: const Icon(FluentIcons.bug_20_regular, size: 24),
|
||||
title: Text(t.bugReport.title),
|
||||
onTap: () {
|
||||
RootScaffold.stateKey.currentState?.closeDrawer();
|
||||
BugReportDialog.show(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,6 +334,32 @@ abstract class ConfigOptions {
|
||||
"",
|
||||
);
|
||||
|
||||
// Per-App Proxy Settings (Desktop platforms)
|
||||
static final perAppProxyMode = PreferencesNotifier.create<String, String>(
|
||||
"per-app-proxy-mode",
|
||||
"off",
|
||||
);
|
||||
|
||||
static final perAppProxyIncludeList = PreferencesNotifier.create<List<String>, String>(
|
||||
"per-app-proxy-include-list",
|
||||
[],
|
||||
mapFrom: (value) {
|
||||
if (value.isEmpty) return [];
|
||||
return value.split(',').map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
|
||||
},
|
||||
mapTo: (value) => value.join(','),
|
||||
);
|
||||
|
||||
static final perAppProxyExcludeList = PreferencesNotifier.create<List<String>, String>(
|
||||
"per-app-proxy-exclude-list",
|
||||
[],
|
||||
mapFrom: (value) {
|
||||
if (value.isEmpty) return [];
|
||||
return value.split(',').map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
|
||||
},
|
||||
mapTo: (value) => value.join(','),
|
||||
);
|
||||
|
||||
static final hasExperimentalFeatures = Provider.autoDispose<bool>(
|
||||
(ref) {
|
||||
final mode = ref.watch(serviceMode);
|
||||
@@ -557,6 +583,10 @@ abstract class ConfigOptions {
|
||||
noiseSize: ref.watch(warpNoiseSize),
|
||||
noiseDelay: ref.watch(warpNoiseDelay),
|
||||
),
|
||||
// Per-App Proxy
|
||||
perAppProxyMode: ref.watch(perAppProxyMode),
|
||||
includedApplications: ref.watch(perAppProxyMode) == 'include' ? ref.watch(perAppProxyIncludeList) : [],
|
||||
excludedApplications: ref.watch(perAppProxyMode) == 'exclude' ? ref.watch(perAppProxyExcludeList) : [],
|
||||
// geoipPath: ref.watch(geoAssetPathResolverProvider).relativePath(
|
||||
// geoAssets.geoip.providerName,
|
||||
// geoAssets.geoip.fileName,
|
||||
|
||||
@@ -77,7 +77,7 @@ class AddProfile extends _$AddProfile with AppLogger {
|
||||
loggy.debug("adding profile, content");
|
||||
var name = parsed.name;
|
||||
final oldItem = await _profilesRepo.getByName(name);
|
||||
if (name == "Hiddify WARP" && oldItem != null) {
|
||||
if (name == "Umbrix WARP" && oldItem != null) {
|
||||
_profilesRepo.deleteById(oldItem.id).run();
|
||||
}
|
||||
while (await _profilesRepo.getByName(name) != null) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:umbrix/core/localization/translations.dart';
|
||||
import 'package:umbrix/core/router/router.dart';
|
||||
import 'package:umbrix/features/bug_report/widget/bug_report_dialog.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
/// Секция "Логи и отладка" в настройках
|
||||
@@ -14,6 +15,20 @@ class LogsSettingTiles extends HookConsumerWidget {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Кнопка "Сообщить о проблеме"
|
||||
ListTile(
|
||||
title: const Text('Сообщить о проблеме'),
|
||||
subtitle: const Text('Отправить багрепорт в техподдержку с автоматическими логами'),
|
||||
leading: Icon(
|
||||
FluentIcons.bug_20_regular,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
trailing: const Icon(FluentIcons.send_20_regular),
|
||||
onTap: () {
|
||||
BugReportDialog.show(context);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
// Переход на страницу логов
|
||||
ListTile(
|
||||
title: Text(t.logs.pageTitle),
|
||||
|
||||
@@ -52,6 +52,9 @@ class SingboxConfigOption with _$SingboxConfigOption {
|
||||
required SingboxTlsTricks tlsTricks,
|
||||
required SingboxWarpOption warp,
|
||||
required SingboxWarpOption warp2,
|
||||
@JsonKey(name: 'per-app-proxy-mode') @Default('off') String perAppProxyMode,
|
||||
@JsonKey(name: 'included-applications') @Default([]) List<String> includedApplications,
|
||||
@JsonKey(name: 'excluded-applications') @Default([]) List<String> excludedApplications,
|
||||
}) = _SingboxConfigOption;
|
||||
|
||||
String format() {
|
||||
|
||||
2
libcore
2
libcore
Submodule libcore updated: 6dfe63ea76...85db0efc59
@@ -1,7 +1,7 @@
|
||||
name: umbrix
|
||||
description: Cross Platform Multi Protocol Proxy Frontend.
|
||||
publish_to: "none"
|
||||
version: 1.7.5+175
|
||||
version: 1.7.9+179
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.0 <4.0.0"
|
||||
|
||||
87
update-server/WINDOWS_PORTABLE_QUICK_START.md
Normal file
87
update-server/WINDOWS_PORTABLE_QUICK_START.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# 🚀 Быстрая сборка Windows Portable ZIP
|
||||
|
||||
## Проблема решена! ✅
|
||||
|
||||
Теперь в ZIP будут файлы которые помогут пользователям не запутаться:
|
||||
|
||||
```
|
||||
umbrix-portable/
|
||||
├── 📄 README.txt ⭐ ПЕРВЫЙ в списке - ясная инструкция!
|
||||
├── 🚀 Запустить Umbrix.bat ⭐ Удобный запуск одним кликом
|
||||
├── 📱 Umbrix.exe ⭐ Основное приложение
|
||||
├── ⚙️ UmbrixCli.exe ⚠️ Служебная утилита
|
||||
└── ...DLL и папки...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Команда для сборки на Windows
|
||||
|
||||
```powershell
|
||||
# 1. Собрать Release
|
||||
flutter build windows --release
|
||||
|
||||
# 2. Скопировать файлы для пользователей + создать ZIP
|
||||
$buildPath = "build\windows\x64\runner\Release"
|
||||
Copy-Item "windows\packaging\portable\README.txt" -Destination "$buildPath\" -Force
|
||||
Copy-Item "windows\packaging\portable\Запустить Umbrix.bat" -Destination "$buildPath\" -Force
|
||||
|
||||
$zipName = "umbrix-1.7.5-portable-windows-x64.zip"
|
||||
Compress-Archive -Path "$buildPath\*" -DestinationPath $zipName -Force
|
||||
|
||||
# 3. Проверить
|
||||
Get-Item $zipName | Select-Object Name, @{N="MB";E={[math]::Round($_.Length/1MB,2)}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Что увидит пользователь после распаковки
|
||||
|
||||
1. **README.txt** - первый файл (по алфавиту с символом '═')
|
||||
- Большой заголовок с инструкцией
|
||||
- Указывает запускать **Umbrix.exe**
|
||||
- Предупреждает не запускать UmbrixCli.exe
|
||||
|
||||
2. **Запустить Umbrix.bat** - второй файл
|
||||
- Русское название понятное всем
|
||||
- Запускает Umbrix.exe автоматически
|
||||
|
||||
3. **Umbrix.exe** - основное приложение
|
||||
- Яркая цветная иконка
|
||||
- Выделяется визуально
|
||||
|
||||
4. **UmbrixCli.exe** - служебная утилита
|
||||
- Название "Cli" намекает что это командная строка
|
||||
- Большинство не будет трогать
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Альтернативы (если не понравится)
|
||||
|
||||
### Вариант 1: Скрыть CLI в подпапку
|
||||
```powershell
|
||||
# При создании ZIP переместить CLI в tools/
|
||||
New-Item -Path "$buildPath\tools" -ItemType Directory -Force
|
||||
Move-Item "$buildPath\UmbrixCli.exe" "$buildPath\tools\" -Force
|
||||
Compress-Archive -Path "$buildPath\*" -DestinationPath $zipName -Force
|
||||
```
|
||||
|
||||
### Вариант 2: Переименовать основное приложение
|
||||
Изменить в `windows/packaging/exe/make_config.yaml`:
|
||||
```yaml
|
||||
executable_name: Start-Umbrix.exe # Без пробела!
|
||||
```
|
||||
⚠️ Но это сломает update скрипты которые ищут `umbrix.exe`
|
||||
|
||||
### Вариант 3: Только батник
|
||||
Удалить README.txt, оставить только `Запустить Umbrix.bat` как единственный понятный способ запуска.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Полная документация
|
||||
|
||||
См. [WINDOWS_PORTABLE_ZIP.md](./WINDOWS_PORTABLE_ZIP.md) для:
|
||||
- Создания релиза в Gitea
|
||||
- Загрузки ZIP через API
|
||||
- Тестирования auto-update
|
||||
- Сравнения ZIP vs EXE
|
||||
@@ -29,12 +29,23 @@ flutter build windows --release
|
||||
# Путь к build папке
|
||||
$buildPath = "build\windows\x64\runner\Release"
|
||||
|
||||
# ✨ НОВОЕ: Добавить файлы для пользователей
|
||||
Copy-Item "windows\packaging\portable\README.txt" -Destination "$buildPath\" -Force
|
||||
Copy-Item "windows\packaging\portable\Запустить Umbrix.bat" -Destination "$buildPath\" -Force
|
||||
|
||||
# Создать ZIP архив
|
||||
$zipName = "umbrix-1.7.5-portable-windows-x64.zip"
|
||||
Compress-Archive -Path "$buildPath\*" -DestinationPath $zipName -Force
|
||||
|
||||
# Проверить размер (~50-60MB)
|
||||
Get-Item $zipName | Select-Object Name, Length
|
||||
|
||||
Write-Host "`n✅ ZIP содержит:" -ForegroundColor Green
|
||||
Write-Host " • README.txt (инструкция - ПЕРВЫЙ файл в списке)" -ForegroundColor Cyan
|
||||
Write-Host " • Запустить Umbrix.bat (удобный запуск)" -ForegroundColor Cyan
|
||||
Write-Host " • Umbrix.exe (основное приложение)" -ForegroundColor Yellow
|
||||
Write-Host " • UmbrixCli.exe (служебная утилита)" -ForegroundColor Gray
|
||||
Write-Host " • Все DLL и папки" -ForegroundColor Gray
|
||||
```
|
||||
|
||||
### Шаг 3: Создать релиз в Gitea
|
||||
@@ -176,7 +187,33 @@ Get-Item $zipName | Select-Object Name, @{N="Size (MB)";E={[math]::Round($_.Leng
|
||||
|
||||
---
|
||||
|
||||
## 💡 Советы
|
||||
## <EFBFBD> Структура Portable ZIP (что видят пользователи)
|
||||
|
||||
Когда пользователь распакует ZIP, он увидит:
|
||||
|
||||
```
|
||||
umbrix-portable/
|
||||
├── 📄 README.txt ⭐ ПЕРВЫЙ в списке - инструкция!
|
||||
├── 🚀 Запустить Umbrix.bat ⭐ Удобный способ запуска
|
||||
├── 📱 Umbrix.exe ⭐ Основное приложение (267 KB)
|
||||
├── ⚙️ UmbrixCli.exe ⚠️ Служебная утилита (1.8 MB)
|
||||
├── 1.7.4/ 📂 Папка с данными
|
||||
├── data/ 📂 Ресурсы Flutter
|
||||
├── dynamic_color_plugin.dll 🔧 DLL плагины
|
||||
├── flutter_windows.dll 🔧 Flutter (17 MB)
|
||||
├── libcore.dll 🔧 Либкор (45 MB)
|
||||
└── ...другие DLL...
|
||||
```
|
||||
|
||||
**Почему пользователи не запутаются:**
|
||||
1. ✅ **README.txt** - первый файл (по алфавиту), ясная инструкция
|
||||
2. ✅ **Запустить Umbrix.bat** - второй файл, понятное название
|
||||
3. ✅ **Umbrix.exe** с красивой иконкой - визуально выделяется
|
||||
4. ⚠️ **UmbrixCli.exe** - название намекает что это утилита командной строки
|
||||
|
||||
---
|
||||
|
||||
## <20>💡 Советы
|
||||
|
||||
**Для разработки:**
|
||||
- ✅ Используйте Portable ZIP - быстрее тестировать
|
||||
|
||||
19
windows/packaging/portable/README.txt
Normal file
19
windows/packaging/portable/README.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
═══════════════════════════════════════════════════════════
|
||||
UMBRIX - Portable Version
|
||||
═══════════════════════════════════════════════════════════
|
||||
|
||||
🚀 ДЛЯ ЗАПУСКА ПРИЛОЖЕНИЯ:
|
||||
► Откройте файл: Umbrix.exe
|
||||
|
||||
📦 Portable версия:
|
||||
• Не требует установки
|
||||
• Не требует прав администратора
|
||||
• Все настройки сохраняются в папке приложения
|
||||
|
||||
⚠️ НЕ ЗАПУСКАЙТЕ:
|
||||
• UmbrixCli.exe - это служебная утилита командной строки
|
||||
• Файлы .dll - это системные библиотеки
|
||||
|
||||
═══════════════════════════════════════════════════════════
|
||||
https://umbrix.net
|
||||
═══════════════════════════════════════════════════════════
|
||||
8
windows/packaging/portable/Запустить Umbrix.bat
Normal file
8
windows/packaging/portable/Запустить Umbrix.bat
Normal file
@@ -0,0 +1,8 @@
|
||||
@echo off
|
||||
chcp 65001 >nul 2>&1
|
||||
echo.
|
||||
echo ═══════════════════════════════════════════════
|
||||
echo Запуск Umbrix...
|
||||
echo ═══════════════════════════════════════════════
|
||||
echo.
|
||||
start "" "%~dp0Umbrix.exe"
|
||||
Reference in New Issue
Block a user