Skip to content

Latest commit

 

History

History
261 lines (207 loc) · 34.9 KB

File metadata and controls

261 lines (207 loc) · 34.9 KB

Архитектура проекта WinSpector Pro

Версия документа: 1.1.0

English: this document is in Russian. A short English overview of the data flow and the safety model is in the README.

Этот документ описывает высокоуровневую архитектуру, ключевые проектные решения и потоки данных в приложении WinSpector Pro.

1. Философия и цели

WinSpector Pro — это интеллектуальный инструмент для оптимизации Windows, построенный на двух ключевых принципах:

  1. Персонализация через ИИ: Вместо статических правил, ядро приложения использует генеративный ИИ (Google Gemini) для анализа уникального "цифрового отпечатка" системы и формирования контекстно-зависимого плана действий.
  2. Безопасность по умолчанию: Приоритетом является стабильность системы. Все операции выполняются только после создания точки восстановления, а решения ИИ проходят через многоуровневую систему валидации.

Проект следует многослойной архитектуре (Layered Architecture) для обеспечения низкой связанности компонентов (Low Coupling) и высокой степени их зацепления (High Cohesion).

2. Технологический стек

  • Язык: Python 3.12+
  • GUI: PyQt6
  • Асинхронность: asyncio в связке с qasync для интеграции с циклом событий PyQt.
  • Системное взаимодействие: psutil, pywin32 (SCM, DPAPI, WMI через win32com).
  • Изоляция процессов: долгоживущий WorkerPool поверх ProcessPoolExecutor для единственного WMI-вызова (сведения об оборудовании). Службы читаются через psutil, а управляются через API SCM (win32service) — без WMI и без PowerShell.
  • ИИ-интеграция: Google Gemini через SDK google-genai (пакет google-generativeai снят с поддержки).
  • Данные и конфигурация: PyYAML для модульной "Базы Знаний".
  • Сборка: PyInstaller, управляемый через динамически генерируемый .spec файл (scripts/build.py). Результат — один файл dist/WinSpectorPro.exe и его контрольная сумма dist/WinSpectorPro.exe.sha256. EXE не подписывается цифровой подписью.
  • Логотип: исходники assets/logo.svg и упрощённый assets/logo-small.svg (для 16–24 px); scripts/make_logo.py собирает из них assets/app.ico (16–256 px) и логотип с названием для README в тёмном и светлом вариантах.
  • Качество: pytest (+pytest-asyncio, pytest-qt), ruff, GitHub Actions.

3. Структура слоев

Проект логически разделен на четыре основных слоя, каждый со своей зоной ответственности.

3.1. Слой Представления (Presentation Layer)

  • Расположение: src/winspector/gui/
  • Ответственность: Все, что касается отрисовки интерфейса и обработки действий пользователя. Этот слой не содержит бизнес-логики.
  • Ключевые компоненты:
    • MainWindow: QMainWindow, выступающий в роли контроллера для страниц (виджетов).
    • settings_dialog.py и api_key_dialog.py: настройки Gemini открываются из главного окна; запрос ключа при запуске не показывается. Ключ уходит в core/credentials.py и в журнал не попадает. Перед сохранением он проверяется коротким запросом (ai_base.check_api_key) в фоновом потоке.
    • theme.py: единое оформление — цвета, шрифт, тёмная палитра и стиль Fusion. Цвета подставляются в resources/styles/main.qss вместо {имя}. У диалогов рамка системная, её заголовок окрашивается в цвет приложения через DWM.
    • frameless.py: главное окно без системного заголовка, которое ведёт себя как обычное окно Windows. Окну возвращаются системные стили рамки, на WM_NCCALCSIZE вся площадь отдаётся содержимому, а на WM_NCHITTEST Windows узнаёт, где края, заголовок и кнопка «Развернуть». Поэтому перетаскивание, двойной щелчок, меню окна, привязка к краям, тень и макеты привязки Windows 11 работают средствами системы.
    • message_dialog.py: сообщения и подтверждения вместо QMessageBox — значок вида сообщения, заголовок, пояснение и кнопки с названием действия; у необратимого действия красная кнопка, а Enter и Esc выбирают безопасный вариант. QMessageBox остался только в src/main.py — для сбоя до загрузки интерфейса.
    • widgets/report_view.py: Страница отчёта из ReportData — итоговые плитки и карточки (ждут закрытия программ, изменения, карантин, Gemini) со ссылками в нужный раздел настроек. Без данных показывает Markdown-отчёт.
    • widgets/title_bar.py: свой заголовок главного окна — прозрачная полоса с названием, уведомлением об обновлении, шестерёнкой настроек и кнопками окна в стиле Windows 11.
    • widgets/progress.py: тонкая полоса прогресса и отметки этапов на экране оптимизации.
    • widgets/setting_row.py: строка-карточка (значок, название, пояснение, действие справа), из которой собраны окно настроек и окно ключа Gemini.
    • widgets/backdrop.py: фон главного окна — сетка едва заметных точек, по которой от логотипа (во время работы — от полосы прогресса) расходится мягкая волна; во время оптимизации волны идут чаще, на экране отчёта фон замирает.

3.2. Слой Приложения (Application Layer)

  • Расположение: src/winspector/application.py, src/main.py
  • Ответственность: "Клей", соединяющий все части приложения. Управляет жизненным циклом, инициализирует сервисы и обрабатывает глобальные события.
  • Ключевые компоненты:
    • Application: Класс, инкапсулирующий всю логику запуска: настройка логирования, проверка прав администратора, защита от повторного запуска, создание асинхронного цикла и обработка необработанных исключений.
    • main.py: Минималистичная точка входа, которая определяет системные пути и передает управление классу Application.

3.3. Слой Ядра и Бизнес-логики (Core / Business Logic Layer)

  • Расположение: src/winspector/core/
  • Ответственность: Оркестрация всего процесса оптимизации. Это "мозг" приложения.
  • Ключевые компоненты:
    • analyzer.py (WinSpectorCore): Реализует паттерн Фасад, предоставляя один публичный метод (run_autonomous_optimization) для запуска сложного многошагового сценария. Управляет всеми аналитическими модулями.
    • report_data.py (ReportData): Итог прогона в виде полей для окна отчёта — освобождённое место, изменения, карантин, состояние ИИ и программы, из-за которых очистку отложили (сгруппированы по программе, с понятными названиями и размером). Ядро кладёт его в WinSpectorCore.last_report; Markdown-отчёт по-прежнему сохраняется в файл и копируется в буфер.
    • modules/: Набор независимых, узкоспециализированных модулей:
      • user_profiler.py: Собирает многогранный "цифровой отпечаток" системы (ПО, оборудование, ярлыки, переменные окружения).
      • windows_optimizer.py: Собирает службы (psutil) и UWP-пакеты (PowerShell), выполняет план и создаёт точки восстановления.
      • service_control.py: Остановка и смена типа запуска служб через API диспетчера служб. Имя службы уходит в вызов аргументом — командной строки, куда его можно было бы подставить, больше нет.
      • smart_cleaner.py: Реализует гибридную очистку (стандартную и интеллектуальную), а также удаление пустых директорий. Пути пользовательских данных (Documents, Downloads, Recent, карантин Defender, кеши установщиков) закрыты кодом целиком; категории с requires_closed откладываются, пока работает их программа; во временных каталогах не трогаются файлы моложе суток.
      • leftover_scanner.py: Остатки удалённых программ. Кандидат — только при улике удаления (призрачная запись Uninstall, битый ярлык, мёртвый путь в реестре или в следах запуска MuiCache/UserAssist/FeatureUsage) и только если ничто не говорит, что программа жива (имя не среди установленных, внутри нет exe, не запущен процесс, файлы не менялись месяц). Каталоги в ProgramData и Program Files требуют улик уровня машины. Каталог в Program Files считается установленной программой, только если в нём есть exe/dll. Отдельно find_empty_app_dirs ищет каталоги программ без единого файла (верхний уровень корней; в Program Files — ещё и дети каталога издателя): они остаются, если менялись за неделю, в AppData/ProgramData совпадают по имени с установленной программой, а в Program Files/ProgramData принадлежат TrustedInstaller (в ProgramData — и SYSTEM) или имеют явно заданные права. Пустым каталогам карантин не нужен — их удаляет финальный проход SmartCleaner.
      • installer_leftovers.py: Остатки установщиков — пакеты %WINDIR%\Installer, на которые не ссылается ни один продукт или патч (MSI API, все пользователи; без прав администратора перечисление неполно и поиск не выполняется), каталоги Package Cache\{GUID}v… удалённых продуктов и bundle, предыдущие app-* приложений на Squirrel (новая версия старше недели, из старой ничего не запущено). Детекторы — чистые функции: состояние системы передаётся параметрами.
      • quarantine.py: Остатки не удаляются, а переносятся (переименованием, без копирования) в %LOCALAPPDATA%\WinSpectorPro\Quarantine\<дата> с манифестом; хранятся 30 дней, возвращаются scripts/quarantine_tool.py restore.
      • Правила очистки (cleanup_rules.yaml) понимают ключи: min_age_hours (журналы — неделя, кеши шейдеров — месяц, temp — сутки), requires_closed (категория ждёт закрытия программы), atomic_subdirs (подкаталог temp со свежим или занятым файлом не трогается целиком), skip_if_busy/busy_siblings (кеш Electron/WebView2 пропускается, если занят любой его файл или GPUCache/Local Storage рядом), exclude_under; * в пути каталога подставляет профиль или игру. Каталог принадлежит первой нашедшей его категории — общие правила стоят в конце. Без ИИ сканируются только категории high.
      • cleanup_engine.py: Один обход каталога в трёх режимах — scan (размер), audit (сухой прогон: занят ли файл, хватает ли прав, кто держит блокировку) и apply. Порядок всегда один: сначала файлы, затем rmdir опустевших каталогов снизу вверх; junction-точки и симлинки снимаются как ссылки и никогда не раскрываются. Считаются только реально удалённые байты.
      • ai_base.py: Общий клиент Gemini: кеш, повторы при временных сбоях, структурированный JSON через response_schema. Модель по умолчанию — gemini-3.8-flash (GEMINI_MODEL переопределяет; модели 2.5 Google открывает только прежним пользователям). Gemini 3 рассуждает перед ответом, и рассуждения входят в max_output_tokens, поэтому лимиты взяты с запасом, а для простых задач задан thinking_level="low"; температура для Gemini 3 не передаётся — Google советует оставлять 1.0. Сетевые сбои (httpx.TransportError, SDK их не оборачивает) считаются временными и после повторов превращаются в AIUnavailableError.
      • ai_analyzer.py: Формирует промпт и получает план оптимизации.
      • plan_validator.py: Слой безопасности. Отдельный модуль, решающий, что из ответа модели допустимо исполнить.
      • ai_communicator.py: Отвечает за "диалоговые" задачи ИИ (определение профилей, генерация отчетов). Без ИИ отчёт собирается локально (build_offline_report). redact_for_ai заменяет полные пути на имена папок перед отправкой сводок модели.
      • dynamic_scan.py (DynamicAnalyzer): Анализ запущенных процессов и сверка с telemetry_domains.yaml. Пока не подключён к сценарию оптимизации: модуль экспортируется, но WinSpectorCore его не вызывает.
    • offline_planner.py: План без ИИ — только правила safety: high, службам назначается ручной запуск вместо отключения, UWP-приложения не удаляются, профиль HomeUser. Используется, когда ключа нет или ИИ не ответил.
    • credentials.py: Хранение ключа Gemini, зашифрованного DPAPI, в %LOCALAPPDATA%\WinSpectorPro\credentials.dat. Переменная окружения GEMINI_API_KEY имеет приоритет (удобно для разработки и CI). Открытый текст ключа не логируется.
    • config.py: Резервная конфигурация модулей на случай, если в базе знаний нет соответствующей секции.
    • wmi_workers.py: Сбор сведений об оборудовании в изолированном процессе. Синхронен намеренно: COM-объект привязан к апартаменту потока, и обращение из чужого потока падает с CO_E_NOTINITIALIZED.
    • worker_pool.py: Долгоживущий пул процессов. Создание пула в блоке with внутри корутины блокировало бы поток event loop на shutdown(wait=True) и подвешивало интерфейс.
    • exceptions.py: Иерархия исключений, позволяющая GUI отличать ожидаемые проблемы от багов.

3.4. Слой Данных (Data Layer)

  • Расположение: src/winspector/data/
  • Ответственность: Хранение и предоставление статических данных.
  • Ключевые компоненты:
    • data/knowledge_base/: Модульная База Знаний. Директория с YAML-файлами, которые содержат правила и эвристики для ИИ и модулей. Это позволяет обновлять "интеллект" приложения без изменения кода.

4. Поток данных в сценарии автономной оптимизации

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

  1. Инициация (GUI):

    • Пользователь нажимает «Оптимизировать» (start_button) на главном экране.
    • MainWindow вызывает метод WinSpectorCore.run_autonomous_optimization().
  2. Подготовка и Профилирование (Core):

    • Создается точка восстановления Windows.
    • UserProfiler асинхронно собирает полный "снимок" системы (оборудование, ПО, ярлыки и т.д.).
    • AICommunicator отправляет эти данные в ИИ и получает в ответ список профилей пользователя (например, ["Gamer", "Developer"]). Без ИИ профиль — HomeUser, самый осторожный.
  3. Сбор данных для Плана (Core):

    • Параллельно запускаются три задачи:
      • WindowsOptimizer собирает список активных служб и UWP-приложений.
      • SmartCleaner сканирует диски и формирует отчет о потенциальном "мусоре". Без ИИ сканируются только категории safety: high — остальные всё равно не будут очищены.
      • leftover_scanner ищет остатки удалённых программ (только чтение).
  4. Генерация и Валидация Плана (AIAnalyzer -> PlanValidator):

    • Все собранные данные, профили пользователя и отфильтрованная "База Знаний" отправляются в ИИ.
    • Схема ответа (response_schema) переводит модель в режим строгого JSON: action_plan (действия со службами/UWP) и cleanup_decisions (решения по очистке).
    • PlanValidator пропускает план через независимые барьеры (см. раздел 5).
  5. Выполнение (Core):

    • Параллельно идут две ветки:
      • WindowsOptimizer выполняет одобренные действия из action_plan.
      • SmartCleaner последовательно выполняет стандартную очистку, затем очистку по решениям ИИ (или офлайн-плана). Последовательно — потому что обе ветки затрагивают %TEMP%, и параллельный запуск порождал гонки за одни и те же файлы.
    • Пути к удаляемым файлам берутся только из отчёта нашего сканера; ответ модели содержит лишь решение «чистить или нет».
    • Затем остатки удалённых программ с оценкой high переносятся в карантин (quarantine.py), а просроченные партии карантина удаляются. Решение принимает код, не ИИ.
    • В конце SmartCleaner удаляет опустевшие каталоги во временных директориях и пустые каталоги удалённых программ из отчёта сканера. Перед удалением дерево проверяется заново: появился хоть один файл — каталог остаётся целиком; удаление — только rmdir снизу вверх.
  6. Отчет (AICommunicator -> GUI):

    • Сводка по всем выполненным действиям отправляется в ИИ: освобождено (только реально удалённые байты), пропущено с причиной, отложенные категории, перенесённые в карантин остатки. Модели уходят только имена папок остатков — полные пути содержат имя учётной записи.
    • ИИ генерирует персонализированный отчет в формате Markdown с учетом профилей пользователя. Без ИИ отчёт по той же сводке собирается локально (AICommunicator.build_offline_report).
    • Отчет отображается в QTextBrowser на главном окне.
  7. Саморефлексия (Core, фоновая задача, только с ИИ):

    • В фоновом режиме ИИ получает профили, план и сводку сессии и предлагает улучшения самого приложения. Перед отправкой данные проходят через redact_for_ai: полные пути заменяются именами папок, путь профиля — на %USERPROFILE%. Ответ пишется только в журнал и ни на что не влияет.

4.1. Сухой прогон очистки

python scripts/audit_cleanup.py --out отчёт.md повторяет офлайн-сценарий, ничего не удаляя: для каждой цели показывает, что было бы удалено, что пропущено как свежее, занятое или «в работе», какие категории отложены до закрытия программ, где не хватает прав (с владельцем и правами администраторов) и какой процесс держит занятые файлы. Отдельный раздел — найденные остатки удалённых программ с уликами. Права администратора не требуются; отчёт отдельно оценивает, хватит ли их приложению. Флаг --no-medium скрывает категории, которые без ИИ не очищаются.

python scripts/quarantine_tool.py list | restore <партия> | purge — просмотр карантина остатков, возврат партии на исходные места и удаление просроченных партий.


5. Слой безопасности

plan_validator.py исходит из того, что ответ модели не является доверенным. План проходит независимые барьеры, ни один из которых не опирается на данные, сгенерированные ИИ:

  1. Шаблон идентификатора. ^[A-Za-z0-9_.\-+]{1,256}$. Кавычки, пробелы и метасимволы PowerShell отсекаются на уровне данных, поэтому инъекция в команду невозможна в принципе. Проверка продублирована в windows_optimizer — модуль не доверяет вызывающей стороне. Для служб командной строки нет вовсе: имя передаётся в API SCM аргументом.
  2. Жёсткий денилист. Список критических служб и UWP-пакетов зашит в код. Он не редактируется базой знаний и включает зависимости самого приложения (vss, swprv — на них держатся точки восстановления).
  3. Правила базы знаний. safety: critical и protected_for_profiles.
  4. Ограничение очистки. Разрешены только категории, которые нашёл наш сканер. Пути из ответа модели игнорируются полностью.
  5. Защита путей. SmartCleaner.is_safe_to_delete отклоняет корни дисков, системные каталоги и любой путь, являющийся предком защищённого. Кроме того, закрыты целиком поддеревья с данными пользователя: личные папки профиля, Recent (списки переходов), карантин Defender, кеши установщиков, история IRC- и почтовых клиентов.
  6. Только восстановимое. Правило safety: high допустимо лишь для каталогов, чьё имя говорит «кеш, журнал, временное, дамп» (проверяется тестом test_knowledge_base.py). Профиль Chromium целиком никогда не является целью — только его каталоги кеша: рядом лежат cookies и сессии входа.

Семантика полей базы знаний

Поле Значение
relevant_profiles Для каких профилей правило уместно. Влияет только на отбор правил в промпт.
protected_for_profiles Для каких профилей компонент трогать нельзя.
safety critical | low | medium | high.
targets Реальные имена служб. Если не задано, берётся id без префикса (Svc_MapsBroker -> mapsbroker).

Имена профилей нормализуются (PowerUser и power_user — одно и то же), поэтому расхождение стиля записи между базой знаний и ответом модели больше не отключает проверки.


6. Тестирование

python -m venv .venv
.venv\Scripts\pip install -r requirements-dev.txt
.venv\Scripts\pytest tests/ --cov

Приоритет покрытия отдан тому, что может навредить системе: валидатору плана, генератору команд, защите путей и оркестратору. Тесты не выполняют настоящих команд PowerShell, не меняют службы (SCM подменяется дублёром) и не обращаются к сети. Поиск остатков и карантин в тестах изолированы автофикстурой conftest.py: сканер ядра возвращает пустой отчёт, а корень карантина живёт во временном каталоге pytest. Вся очистка идёт в tmp_path.

Маркер windows отмечает тесты, которые читают настоящую систему (WMI, реестр, ярлыки) и ничего в ней не меняют. Pre-commit их пропускает: pytest -m "not slow and not windows".

Отдельный набор (test_knowledge_base.py) проверяет сами данные: YAML — это фактически конфигурация поведения продукта, и опечатка в safety или неизвестный профиль меняют работу валидатора молча.


7. Производительность

Замеры на реальной системе (SSD, 32 ГБ ОЗУ, дерево кеша ~85 ГБ).

Место Было Стало Причина
Подсчёт размера каталога 148.6 с 9.7 с os.scandir вместо os.walk + os.path.getsize
Полное сканирование мусора 4.4 с 3.0 с то же, плюс поиск по маске на scandir
Кадр фона с двумя волнами (окно 1900×1000) 16 мс 3.6 мс сетка — готовая картинка, точки под волной — готовые спрайты

Ключевые решения:

  • os.scandir вместо os.walk. Windows возвращает размер файла уже в результате перечисления каталога, и entry.stat() берёт его из кеша. os.path.getsize делал отдельный системный вызов на каждый файл — на больших деревьях это давало пятнадцатикратную разницу.
  • Фон перерисовывает только то, что меняется. Сетка рисуется один раз в картинку под размер окна. Точки отсортированы по расстоянию от источника волны, поэтому точки под фронтом находятся двоичным поиском, сила волны берётся из таблицы, а сами точки — готовые картинки на десять ступеней яркости вместо сглаженного круга на каждую точку в каждом кадре.
  • Анимация фона работает, только пока её видно. Таймер стоит, пока виджет скрыт, окно свёрнуто или открыт отчёт, и не запускается вовсе, если в Windows отключена анимация интерфейса.
  • Обход дерева итеративный, а не рекурсивный — кеши бывают вложены достаточно глубоко, чтобы упереться в лимит стека.

Версия 1.1.0

Место Было Стало Причина
Импорт приложения на старте 830 мс 190 мс google.genai загружается при первом запросе к ИИ
Первый WMI-запрос (запуск процесса) 1190 мс 490 мс ленивый core/__init__.py, прямой win32com вместо пакета wmi
Список служб 3100 мс 320 мс psutil.win_service_iter() вместо WMI в отдельном процессе
Проверка существования служб перед планом 965 мс 1 мс SCM вместо powershell Get-Service
Одно действие над службой ~900 мс и ~190 МБ < 1 мс вызовы win32service вместо процесса PowerShell
Проход по пустым папкам %TEMP% (43 тыс. каталогов) 23.7 с 10.8 с проверка пустоты раньше resolve()
Поиск мусора без ИИ — 4 с сканируются только категории high