237 lines
9.3 KiB
Markdown
237 lines
9.3 KiB
Markdown
---
|
||
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**
|