9.3 KiB
9.3 KiB
created, updated, tags
| created | updated | tags | ||||
|---|---|---|---|---|---|---|
| 2026-05-16 | 2026-05-16 |
|
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 тестов
Генерация проекта
# Установить XcodeGen (один раз)
brew install xcodegen
# Регенерировать .xcodeproj после изменения project.yml
cd ~/Developer/reflect-app
xcodegen generate
Сборка и тесты
# Узнать 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 сервер (режим без ключа)
Запуск
# 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 бэкенд — приоритет источников токена:
CLAUDE_CODE_OAUTH_TOKENиз~/.hermes/.env(Hermes-managed, наиболее надёжный)- macOS Keychain (
Claude Code-credentials) ~/.claude/.credentials.json(с авто-обновлением если есть refreshToken)
Настройка на iPhone
- Открыть приложение → шестерёнка (Settings)
- AI Backend → "Локальный сервер (Mac)"
- URL сервера: скопировать из вывода
proxy_server.py - Нажать "Проверить" — должно показать "Доступен · 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)→ сохраняет и ставит разовоеUNCalendarNotificationTriggerpostpone(by: PostponeOption)→ переносит на +1ч / +3ч / завтра / skip- Уведомление содержит action-кнопки прямо из шторы (без открытия приложения)
Флоу в конце сессии:
- Агент завершает сессию →
SessionManager.endSession()→clearSchedule() SessionViewModel.endSession()→ показываетCardDataс предложением времени- Пользователь: "Да, подходит" / "Другое время" / "Без расписания"
- При выборе времени →
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