190 lines
9.8 KiB
Markdown
190 lines
9.8 KiB
Markdown
---
|
|
title: "naisei / reflect-skip — проект"
|
|
aliases:
|
|
- "naisei"
|
|
- "nolvu"
|
|
- "reflect-skip"
|
|
- "Naisei проект"
|
|
- "Reflect Skip"
|
|
tags:
|
|
- personal
|
|
- project
|
|
- app
|
|
- psychology
|
|
- skip-tools
|
|
created: '2026-05-17'
|
|
updated: '2026-05-28'
|
|
related:
|
|
- "[[personal/projects/psychologist-app/overview]]"
|
|
- "[[personal/projects/psychologist-app/dev-setup]]"
|
|
---
|
|
# naisei / reflect-skip — проект
|
|
|
|
## Статус
|
|
- Имя продукта: **naisei**
|
|
- Активная кодовая база: `~/Developer/reflect-skip/`
|
|
- Android: публикуется на `https://reflect.qentra.top` (F-Droid)
|
|
- iOS: собирается через `Project.xcworkspace` → `Darwin/ReflectApp.xcodeproj`
|
|
- Текущая версия: v0.0.92 (Android APK), v0.0.25 (iOS)
|
|
|
|
## Структура проекта
|
|
|
|
### Два источника Swift-кода в reflect-skip
|
|
- `Sources/ReflectApp/` — **актуальный** Skip-источник. Компилируется Darwin-проектом. Используется для iOS и Android.
|
|
- `ReflectApp/` — **legacy** iOS-only код, существовавший до перехода на Skip. Компилируется `ReflectApp.xcodeproj` в корне. Не используется в продакшне.
|
|
|
|
### Как открывать iOS-проект
|
|
Всегда через `Project.xcworkspace`, не через `ReflectApp.xcodeproj` напрямую.
|
|
|
|
### Как собирается Android
|
|
`build-and-publish.sh` → Skip компилирует `Sources/ReflectApp/` → APK → F-Droid repo.
|
|
|
|
## Прокси
|
|
|
|
- LaunchAgent: `ai.reflect-proxy` → `localhost:8765`
|
|
- Cloudflare tunnel: `ai.qentra.top` → `localhost:8765`
|
|
- Бэкенд: `claude-proxy` на `localhost:3456`
|
|
- Лог сессий: `~/Developer/reflect-skip/sessions.log`
|
|
|
|
## Агенты
|
|
|
|
- **NarratorBot** — нейтрализует ввод пользователя (убирает эмоциональную окраску, 3-е лицо)
|
|
- **AnalystBot** — задаёт 2-3 глубоких вопроса по профилю + контексту сессии
|
|
- **SessionManager** — управляет историей сессий
|
|
|
|
## Исправления (v0.0.92, 28 мая 2026) — Android vault validation
|
|
|
|
### Веха: Android onboarding работает
|
|
✅ Алерт валидации vault отображается корректно
|
|
✅ Кнопки появились после выбора папки
|
|
✅ Дошли до экрана "Доброе утро"
|
|
|
|
### Fix — @AppStorage вместо @State для UI-обновлений на Android
|
|
**Проблема:** `@State var folderPickerError: String?` и `@State var vaultFound: Int?` не триггерили Compose recompose в SkipFuse bridge.
|
|
**Симптом:** алерт не появлялся, кнопки не обновлялись, хотя логика SAF выполнялась.
|
|
**Исправление:** заменили на `@AppStorage("_vaultErr")` и `@AppStorage("_vaultFound")` — идут через SharedPreferences → всегда триггерят Compose recompose.
|
|
**Файл:** `Sources/naisei/Views/OnboardingView.swift`
|
|
|
|
### Открытые проблемы (v0.0.92)
|
|
- ❌ На главном экране "0 сессий", "0 дней"
|
|
- ❌ Профиль и Инсайты пустые
|
|
- ❌ Агент при начале сессии говорит "нет данных" (не видит vault)
|
|
- **Гипотеза:** vault URI сохраняется, но ProfileManager/SessionManager читают данные не через SAF URI, а по прямому пути — на Android без SAF доступа путь недоступен
|
|
|
|
## Исправления (v0.0.25, май 2026)
|
|
|
|
### Fix 1 — дублирование нарратора
|
|
`buildSessionContext()` вызывался после `addMessage()` → нарратор видел своё же сообщение в контексте.
|
|
Исправлено: поменяли порядок в `Sources/SessionManager.swift`.
|
|
|
|
### Fix 2 — AnalystBot не видел текущий диалог
|
|
`ProfileManager.loadRecentSessions(count: 3)` загружал прошлые ФАЙЛЫ, не текущую сессию.
|
|
Исправлено: добавили `buildCurrentSessionDialogue()` (последние 8 пар user/analyst), передаём в `analyze()`.
|
|
|
|
### Fix 3 — нарратору уходили вопросы аналитика
|
|
`sendBatchSummary()` отправлял весь диалог включая вопросы аналитика.
|
|
Исправлено: фильтруем только user answers.
|
|
|
|
## Obsidian-совместимость
|
|
|
|
Приложение читает сессии из папки `observations/` (формат vault), а не `sessions/`.
|
|
|
|
- **Папка сессий:** `observations/` в корне импортируемой директории
|
|
- **Именование файлов:** ISO week — `2026-W20.md`, `2026-W21.md`
|
|
- **Запись сессий:** аппендит `## Сессия DD.MM.YYYY HH:mm` в текущий недельный файл
|
|
- **Счётчик сессий:** считается напрямую как количество `.md` файлов в `observations/` (не хранится в profile.md)
|
|
- **Профиль:** `profile.md` — строка `**Имя:** Алексей` нужна для `parseName()`
|
|
- **Инсайты:** `insights.md` — произвольный формат, передаётся напрямую в Claude
|
|
|
|
Vault путь для импорта: `personal/psychology/` (на Taiga).
|
|
|
|
## Pitfalls
|
|
|
|
- Если на iOS-симуляторе не появляется `DevSetupView` (ввод токена) — значит `dev_setup_done = true` в UserDefaults. Удалить приложение с симулятора и запустить заново.
|
|
- `ReflectApp/` в корне — legacy, не трогать. Реальные правки только в `Sources/ReflectApp/`.
|
|
|
|
## Plan: Session Pacing & Closing (не реализовано)
|
|
|
|
### Проблема
|
|
|
|
`SessionViewModel.endSession()` существует и реализует полный closing-флоу (инсайт → warm phrase → oneLastThing → vault save), но **нигде не вызывается из UI**.
|
|
`dismissSession()` (кнопка X) отменяет сессию без сохранения.
|
|
Пейсинга нет: агент не знает сколько оборотов прошло, нет правил завершения в systemPrompt, нет сигнала `session_complete` в `AnalystJSON`.
|
|
|
|
### Решение: Вариант Б + hard limit
|
|
|
|
**Шаг 1 — Quick-fix: X-кнопка вызывает `endSession()` (даёт рабочий closing уже сейчас)**
|
|
|
|
Вместо `dismissSession()` при нажатии X — вызывать `endSession()`.
|
|
Пользователь сам решает когда нажать, но closing-флоу отработает корректно.
|
|
|
|
**Шаг 2 — Turn count в SessionManager**
|
|
|
|
Добавить `var userTurnCount: Int = 0`, инкрементировать в `processUserInput()`.
|
|
Передавать в `AnalystBot.analyze()` как параметр `turnCount`.
|
|
|
|
**Шаг 3 — `session_complete` в AnalystJSON**
|
|
|
|
```swift
|
|
struct AnalystJSON: Codable {
|
|
let questions: [String]
|
|
let observation: String?
|
|
let crisisSignal: Bool
|
|
let suggestedInsight: String?
|
|
let sessionComplete: Bool // новое поле
|
|
}
|
|
```
|
|
|
|
**Шаг 4 — Пейсинг-правила в systemPrompt AnalystBot**
|
|
|
|
```
|
|
Правила ведения сессии (параметр turn_count передаётся в каждом запросе):
|
|
- После 4+ оборотов: если пользователь пришёл к пониманию, тема исчерпана
|
|
или повторяется — установи session_complete: true
|
|
- Если открылась новая важная тема — продолжай (session_complete: false)
|
|
- После 7+ оборотов: session_complete: true независимо от темы
|
|
- session_complete: true — предложение закрыть, не принудительное завершение
|
|
```
|
|
|
|
**Шаг 5 — `.readyToClose` в ProcessingResult**
|
|
|
|
```swift
|
|
enum ProcessingResult {
|
|
case questions([String], observation: String?)
|
|
case readyToClose([String]?, observation: String?)
|
|
case crisis(CrisisDetectionResult)
|
|
case error(String)
|
|
}
|
|
```
|
|
|
|
SessionManager возвращает `.readyToClose` когда `analystResponse.sessionComplete == true` ИЛИ `userTurnCount >= 8`.
|
|
|
|
**Шаг 6 — UI карточка в SessionViewModel**
|
|
|
|
При `.readyToClose`: показать вопросы (если есть) → после ответа добавить карточку
|
|
"Кажется, мы хорошо прошлись по теме. Завершить сессию?"
|
|
Кнопки: "Завершить" → `endSession()` | "Продолжить" → сбросить сигнал на текущий turn.
|
|
|
|
**Шаг 7 — Интеграционные тесты (TDD: писать до реализации шагов 2-6)**
|
|
|
|
- `testAnalystReturnsSessionComplete_afterExhaustedTopic()`
|
|
- `testSessionManager_readyToClose_onSessionCompleteSignal()`
|
|
- `testSessionManager_hardLimit_at8Turns()`
|
|
- `testSessionViewModel_showsCompletionCard_onReadyToClose()`
|
|
- `testSessionViewModel_continueAfterReadyToClose_resetsSignal()`
|
|
|
|
### Порядок реализации
|
|
|
|
1. Тесты (шаг 7) — все RED
|
|
2. Quick-fix X-кнопки (шаг 1) — немедленно даёт рабочий closing
|
|
3. Шаги 2-5 — данные + логика
|
|
4. Шаг 6 — UI карточка
|
|
5. Все тесты GREEN
|
|
|
|
---
|
|
|
|
## Связанные заметки
|
|
|
|
- [[personal/projects/psychologist-app/overview]]
|
|
- [[personal/projects/psychologist-app/dev-setup]]
|