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

9.3 KiB
Raw Blame History

created, updated, tags
created updated tags
2026-05-16 2026-05-16
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 тестов

Генерация проекта

# Установить 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 бэкенд — приоритет источников токена:

  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.swiftcheckOnLaunch() при старте, 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