Версия документа: 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.
WinSpector Pro — это интеллектуальный инструмент для оптимизации Windows, построенный на двух ключевых принципах:
- Персонализация через ИИ: Вместо статических правил, ядро приложения использует генеративный ИИ (Google Gemini) для анализа уникального "цифрового отпечатка" системы и формирования контекстно-зависимого плана действий.
- Безопасность по умолчанию: Приоритетом является стабильность системы. Все операции выполняются только после создания точки восстановления, а решения ИИ проходят через многоуровневую систему валидации.
Проект следует многослойной архитектуре (Layered Architecture) для обеспечения низкой связанности компонентов (Low Coupling) и высокой степени их зацепления (High Cohesion).
- Язык: 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.
Проект логически разделен на четыре основных слоя, каждый со своей зоной ответственности.
- Расположение:
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_NCHITTESTWindows узнаёт, где края, заголовок и кнопка «Развернуть». Поэтому перетаскивание, двойной щелчок, меню окна, привязка к краям, тень и макеты привязки 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: фон главного окна — сетка едва заметных точек, по которой от логотипа (во время работы — от полосы прогресса) расходится мягкая волна; во время оптимизации волны идут чаще, на экране отчёта фон замирает.
- Расположение:
src/winspector/application.py,src/main.py - Ответственность: "Клей", соединяющий все части приложения. Управляет жизненным циклом, инициализирует сервисы и обрабатывает глобальные события.
- Ключевые компоненты:
Application: Класс, инкапсулирующий всю логику запуска: настройка логирования, проверка прав администратора, защита от повторного запуска, создание асинхронного цикла и обработка необработанных исключений.main.py: Минималистичная точка входа, которая определяет системные пути и передает управление классуApplication.
- Расположение:
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 отличать ожидаемые проблемы от багов.
- Расположение:
src/winspector/data/ - Ответственность: Хранение и предоставление статических данных.
- Ключевые компоненты:
data/knowledge_base/: Модульная База Знаний. Директория с YAML-файлами, которые содержат правила и эвристики для ИИ и модулей. Это позволяет обновлять "интеллект" приложения без изменения кода.
Основной сценарий работы приложения разделен на четкие асинхронные шаги, управляемые WinSpectorCore.
-
Инициация (GUI):
- Пользователь нажимает «Оптимизировать» (
start_button) на главном экране. MainWindowвызывает методWinSpectorCore.run_autonomous_optimization().
- Пользователь нажимает «Оптимизировать» (
-
Подготовка и Профилирование (Core):
- Создается точка восстановления Windows.
UserProfilerасинхронно собирает полный "снимок" системы (оборудование, ПО, ярлыки и т.д.).AICommunicatorотправляет эти данные в ИИ и получает в ответ список профилей пользователя (например,["Gamer", "Developer"]). Без ИИ профиль —HomeUser, самый осторожный.
-
Сбор данных для Плана (Core):
- Параллельно запускаются три задачи:
WindowsOptimizerсобирает список активных служб и UWP-приложений.SmartCleanerсканирует диски и формирует отчет о потенциальном "мусоре". Без ИИ сканируются только категорииsafety: high— остальные всё равно не будут очищены.leftover_scannerищет остатки удалённых программ (только чтение).
- Параллельно запускаются три задачи:
-
Генерация и Валидация Плана (AIAnalyzer -> PlanValidator):
- Все собранные данные, профили пользователя и отфильтрованная "База Знаний" отправляются в ИИ.
- Схема ответа (
response_schema) переводит модель в режим строгого JSON:action_plan(действия со службами/UWP) иcleanup_decisions(решения по очистке). PlanValidatorпропускает план через независимые барьеры (см. раздел 5).
-
Выполнение (Core):
- Параллельно идут две ветки:
WindowsOptimizerвыполняет одобренные действия изaction_plan.SmartCleanerпоследовательно выполняет стандартную очистку, затем очистку по решениям ИИ (или офлайн-плана). Последовательно — потому что обе ветки затрагивают%TEMP%, и параллельный запуск порождал гонки за одни и те же файлы.
- Пути к удаляемым файлам берутся только из отчёта нашего сканера; ответ модели содержит лишь решение «чистить или нет».
- Затем остатки удалённых программ с оценкой
highпереносятся в карантин (quarantine.py), а просроченные партии карантина удаляются. Решение принимает код, не ИИ. - В конце
SmartCleanerудаляет опустевшие каталоги во временных директориях и пустые каталоги удалённых программ из отчёта сканера. Перед удалением дерево проверяется заново: появился хоть один файл — каталог остаётся целиком; удаление — толькоrmdirснизу вверх.
- Параллельно идут две ветки:
-
Отчет (AICommunicator -> GUI):
- Сводка по всем выполненным действиям отправляется в ИИ: освобождено (только реально удалённые байты), пропущено с причиной, отложенные категории, перенесённые в карантин остатки. Модели уходят только имена папок остатков — полные пути содержат имя учётной записи.
- ИИ генерирует персонализированный отчет в формате Markdown с учетом профилей пользователя. Без ИИ отчёт по той же сводке собирается локально (
AICommunicator.build_offline_report). - Отчет отображается в
QTextBrowserна главном окне.
-
Саморефлексия (Core, фоновая задача, только с ИИ):
- В фоновом режиме ИИ получает профили, план и сводку сессии и предлагает улучшения самого приложения. Перед отправкой данные проходят через
redact_for_ai: полные пути заменяются именами папок, путь профиля — на%USERPROFILE%. Ответ пишется только в журнал и ни на что не влияет.
- В фоновом режиме ИИ получает профили, план и сводку сессии и предлагает улучшения самого приложения. Перед отправкой данные проходят через
python scripts/audit_cleanup.py --out отчёт.md повторяет офлайн-сценарий, ничего не удаляя:
для каждой цели показывает, что было бы удалено, что пропущено как свежее, занятое или
«в работе», какие категории отложены до закрытия программ, где не хватает прав (с
владельцем и правами администраторов) и какой процесс держит занятые файлы. Отдельный
раздел — найденные остатки удалённых программ с уликами. Права администратора не
требуются; отчёт отдельно оценивает, хватит ли их приложению. Флаг --no-medium скрывает
категории, которые без ИИ не очищаются.
python scripts/quarantine_tool.py list | restore <партия> | purge — просмотр карантина
остатков, возврат партии на исходные места и удаление просроченных партий.
plan_validator.py исходит из того, что ответ модели не является доверенным.
План проходит независимые барьеры, ни один из которых не опирается на данные,
сгенерированные ИИ:
- Шаблон идентификатора.
^[A-Za-z0-9_.\-+]{1,256}$. Кавычки, пробелы и метасимволы PowerShell отсекаются на уровне данных, поэтому инъекция в команду невозможна в принципе. Проверка продублирована вwindows_optimizer— модуль не доверяет вызывающей стороне. Для служб командной строки нет вовсе: имя передаётся в API SCM аргументом. - Жёсткий денилист. Список критических служб и UWP-пакетов зашит в код.
Он не редактируется базой знаний и включает зависимости самого приложения
(
vss,swprv— на них держатся точки восстановления). - Правила базы знаний.
safety: criticalиprotected_for_profiles. - Ограничение очистки. Разрешены только категории, которые нашёл наш сканер. Пути из ответа модели игнорируются полностью.
- Защита путей.
SmartCleaner.is_safe_to_deleteотклоняет корни дисков, системные каталоги и любой путь, являющийся предком защищённого. Кроме того, закрыты целиком поддеревья с данными пользователя: личные папки профиля,Recent(списки переходов), карантин Defender, кеши установщиков, история IRC- и почтовых клиентов. - Только восстановимое. Правило
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 — одно и то же),
поэтому расхождение стиля записи между базой знаний и ответом модели больше
не отключает проверки.
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 или
неизвестный профиль меняют работу валидатора молча.
Замеры на реальной системе (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 отключена анимация интерфейса.
- Обход дерева итеративный, а не рекурсивный — кеши бывают вложены достаточно глубоко, чтобы упереться в лимит стека.
| Место | Было | Стало | Причина |
|---|---|---|---|
| Импорт приложения на старте | 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 |