Files
obsidian-vault/personal/projects/psychologist-app/dev-setup.md
T

237 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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**