--- created: '2026-05-16' updated: '2026-05-16' tags: - project - dev - ios - swift --- # Reflect App — Dev Setup & Architecture > Путь: `~/Developer/reflect-app/` > Статус: BUILD SUCCEEDED, 30/30 тестов --- ## Структура проекта ``` reflect-app/ ├── project.yml ← XcodeGen spec (генерирует .xcodeproj) ├── proxy_server.py ← Local AI proxy server (Mac → iPhone) ├── ReflectApp/ │ ├── Models/ │ │ ├── CrisisKeywords.swift │ │ ├── Message.swift │ │ ├── Session.swift │ │ └── UserProfile.swift │ ├── Services/ │ │ ├── ClaudeService.swift ← AI бэкенд (cloud + local proxy) │ │ ├── AnalystBot.swift ← Бот 2 (Sonnet, CBT вопросы) │ │ ├── NarratorBot.swift ← Бот 1 (Haiku, 3rd-person reframe) │ │ ├── CrisisDetector.swift ← Hardcoded SI/SH детекция (RU+EN) │ │ ├── ProfileManager.swift ← Obsidian-like on-device vault │ │ └── SessionScheduler.swift ← Напоминания (UNUserNotificationCenter) │ ├── Views/ │ │ ├── OnboardingView.swift ← 3 слайда + 5 вопросов + инсайт │ │ ├── HomeView.swift │ │ ├── SessionView.swift │ │ ├── CrisisView.swift │ │ ├── ProfileView.swift ← Obsidian-like markdown viewer │ │ ├── SettingsView.swift ← Выбор бэкенда + API key + server URL │ │ ├── CardView.swift │ │ └── OrbCharacter.swift ← SwiftUI анимированный орб │ └── ViewModels/ │ ├── HomeViewModel.swift │ └── SessionViewModel.swift └── ReflectAppTests/ └── ReflectAppTests.swift ← 30 тестов ``` --- ## Генерация проекта ```bash # Установить XcodeGen (один раз) brew install xcodegen # Регенерировать .xcodeproj после изменения project.yml cd ~/Developer/reflect-app xcodegen generate ``` --- ## Сборка и тесты ```bash # Узнать ID симулятора xcrun simctl list devices | grep "iPhone 16 Pro" # Сборка xcodebuild -project ReflectApp.xcodeproj -scheme ReflectApp \ -destination "id=9FF2D6A8-E0DF-4157-9FA5-463E45BB5FAD" build # Тесты (30 тестов, ~0.1 сек) xcodebuild test -project ReflectApp.xcodeproj -scheme ReflectApp \ -destination "id=9FF2D6A8-E0DF-4157-9FA5-463E45BB5FAD" ``` --- ## Локальный AI сервер (режим без ключа) ### Запуск ```bash # claude-code — OAuth через Hermes (нет API ключа, читает CLAUDE_CODE_OAUTH_TOKEN из ~/.hermes/.env) cd ~/Developer/reflect-app python3 proxy_server.py --backend claude-code # Ollama — локальная модель python3 proxy_server.py --backend ollama --ollama-model qwen2.5:7b # Claude API key export ANTHROPIC_API_KEY=sk-ant-... python3 proxy_server.py --backend claude ``` **claude-code бэкенд** — приоритет источников токена: 1. `CLAUDE_CODE_OAUTH_TOKEN` из `~/.hermes/.env` (Hermes-managed, наиболее надёжный) 2. macOS Keychain (`Claude Code-credentials`) 3. `~/.claude/.credentials.json` (с авто-обновлением если есть refreshToken) ### Настройка на iPhone 1. Открыть приложение → шестерёнка (Settings) 2. AI Backend → "Локальный сервер (Mac)" 3. URL сервера: скопировать из вывода `proxy_server.py` 4. Нажать "Проверить" — должно показать "Доступен · qwen2.5:7b" ### Архитектура proxy ``` iPhone (ClaudeService) → POST /v1/messages → proxy_server.py (8765) → Anthropic API format → Ollama (11434) ← response ← Ollama response (конвертируется в Anthropic формат) ← response ← proxy_server.py ``` --- ## Архитектура двойного бота ``` User input (1-е лицо) ↓ CrisisDetector.detect() — hardcoded, <5ms ↓ (если .critical → CrisisView) NarratorBot (Haiku) — переформулировка в 3-е лицо ↓ AnalystBot (Sonnet) — читает profile.md + вопросы ↓ CardView с вопросами ``` --- ## On-device хранилище (Obsidian-like) ``` Documents/UserProfile/ ├── profile.md ← паттерны, наблюдения, прогресс ├── insights.md ← кристаллизованные инсайты └── sessions/ ├── 2026-05-16.md └── ... ``` --- ## App Lock (Face ID / Touch ID / Passcode) **Файлы:** - `Services/AppLockService.swift` — `@MainActor ObservableObject`, `LAContext`, состояние - `Views/LockView.swift` — экран блокировки, автоматически запускает биометрию при появлении - `ReflectApp.swift` — `checkOnLaunch()` при старте, `scenePhase.background → lock()` - `SettingsView.swift` — секция "Безопасность" с Toggle **Поведение:** - Включение/выключение требует подтверждения биометрией - При уходе в фон (`scenePhase == .background`) — блокируется - При возврате — LockView показывается поверх всего, сразу запрашивает биометрию - Симулятор: биометрия эмулируется через Features → Face ID → Enrolled --- ## Голосовой ввод **`Services/VoiceInputService.swift`** — `@MainActor ObservableObject`: - `AVAudioEngine` + `SFSpeechRecognizer` (locale: ru-RU с fallback на системный) - Push-to-talk: `startRecording()` / `stopRecording()` - `onTranscript: ((String) -> Void)?` — коллбэк с финальным текстом - `audioLevel: Float` — уровень звука в реальном времени (→ орб анимация) - `requestPermissions()` — запрашивает mic + speech recognition --- ## Адаптивное расписание (ScheduleNegotiationService) **`Services/ScheduleNegotiationService.swift`**: - `nextSessionDate` — следующая запланированная сессия (UserDefaults) - `buildSuggestion()` → предлагает время на основе паттерна прошлых встреч - `acceptSchedule(date)` → сохраняет и ставит разовое `UNCalendarNotificationTrigger` - `postpone(by: PostponeOption)` → переносит на +1ч / +3ч / завтра / skip - Уведомление содержит action-кнопки прямо из шторы (без открытия приложения) **Флоу в конце сессии:** 1. Агент завершает сессию → `SessionManager.endSession()` → `clearSchedule()` 2. `SessionViewModel.endSession()` → показывает `CardData` с предложением времени 3. Пользователь: "Да, подходит" / "Другое время" / "Без расписания" 4. При выборе времени → `ScheduleNegotiationService.acceptSchedule(date)` → уведомление **HomeView:** - Если есть расписание → показывает badge "Следующая: пятница 20:00" + кнопка "Перенести" - Если нет расписания → "Назначить следующую встречу" (кнопка открывает `PostponeSheetView`) --- ## Тулинг агента (AnalystBot) Аналитик использует tool-use loop (`ClaudeService.completeWithTools`). **4 инструмента:** | Tool | Описание | |------|----------| | `get_index` | Индекс сессий — таблица дат/тем/тегов, < 1K токенов | | `search_sessions` | Полнотекстовый поиск по архиву | | `read_session` | Прочитать полную сессию по дате | | `read_profile` | Прочитать profile.md | Агент сам решает когда обращаться к хранилищу — когда видит паттерн или нужен контекст прошлых встреч. --- ## Session Index **`ProfileManager.updateSessionIndex()`** обновляет `sessions/index.md`: ``` | Дата | Темы | Теги | Содержание | |------------|------------------------|-------------------|-------------------| | 2026-05-16 | работа, усталость | #сессия #инсайт | Говорит что устал | ``` Заполняется автоматически при `endSession()`. Агент читает через `get_index` — без засорения контекста полными файлами. --- ## Тест-результаты (2026-05-16) - ClaudeServiceTests: 4/4 - CrisisDetectorTests: 17/17 (включая RU+EN, нормализация) - ProfileManagerTests: 6/6 (включая onboarding init) - SessionSchedulerTests: 3/3 - **ИТОГО: 30/30, 0 failures**