15 KiB
created, updated, tags
| created | updated | tags | ||||||
|---|---|---|---|---|---|---|---|---|
| 2026-05-16 | 2026-05-19 |
|
Reflect App — Dev Setup & Architecture
iOS (reflect-app):
~/Desktop/reflect-app/— нативный SwiftUI, не трогать Cross-platform (reflect-skip):~/Developer/reflect-skip/— Skip Fuse, iOS + Android Gitea:git.mallexxx.duckdns.org/git_admin/reflect-app
F-Droid Internal Repo
| Параметр | Значение |
|---|---|
| URL | https://reflect.qentra.top |
| CF Tunnel | mbp black (same as zulip.qentra.top), hostname reflect → localhost:8766 |
| CF Token (Zone:DNS) | cfat_iKrPBaTAqlL8Zo7Ss4EHaiGqRGxBK4RDVX78jhgD51fdd7a1 |
| Сервер | python3 ~/Developer/reflect-skip/scripts/fdroid-server.py (LaunchAgent: com.reflect.fdroid-server) |
| Workspace | ~/Developer/reflect-skip/fdroid-workspace/ — подписанный репо, генерируется fdroid update |
Как устроен деплой
Репо генерируется через fdroidserver (pip3 install fdroidserver), а не вручную.
HTTP-сервер (fdroid-server.py) раздаёт содержимое fdroid-workspace/repo/ — там лежат подписанные index-v1.jar, entry.jar, index-v2.json.
Ключевые файлы workspace:
fdroid-workspace/
├── config.yml ← repo_url, keystore, имя репо
├── keystore.p12 ← подписывающий ключ (не коммитить)
└── repo/ ← раздаётся HTTP-сервером
├── index-v1.jar ← подписанный JAR (Droid-ify требует его)
├── entry.jar ← новый формат entry point
├── index-v2.json
└── reflect-app-debug.apk
Сборка и публикация APK
cd ~/Developer/reflect-skip
# Полная сборка (Xcode + Gradle) + публикация
bash scripts/build-and-publish.sh
# Быстрая (только Gradle, если Swift не менялся)
bash scripts/build-and-publish.sh --quick
build-and-publish.sh вызывает generate-index.sh, который:
- Копирует APK в
fdroid-workspace/repo/ - Запускает
fdroid update -c(JAVA_HOME=/opt/homebrew/opt/openjdk@21) - Генерирует подписанные JAR и JSON индексы
Первоначальная настройка (один раз)
cd ~/Developer/reflect-skip/fdroid-workspace
# Удалить старый keystore если есть, создать новый
keytool -genkey -v \
-keystore keystore.p12 -storetype PKCS12 \
-alias fdroid -storepass reflect123 -keypass reflect123 \
-keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=Reflect App, O=Dev, C=US"
# Сгенерировать репо
ANDROID_HOME=~/Library/Android/sdk \
JAVA_HOME=/opt/homebrew/opt/openjdk@21 \
PATH="/opt/homebrew/opt/openjdk@21/bin:$PATH" \
fdroid update -c
Добавить репо в Droid-ify
Settings → Repositories → + → https://reflect.qentra.top
⚠️ URL без
/repo— сервер уже раздаёт содержимоеfdroid-workspace/repo/напрямую с корня.
Статус (2026-05-19)
Что реализовано
| Компонент | Статус |
|---|---|
| iOS build (reflect-app) | ✅ Build succeeded, 30/30 тестов |
| Android build (reflect-skip) | ✅ APK собирается, запускается на эмуляторе |
| Экран 0: Privacy intro | ✅ Личный текст, GDPR opt-out, дисклеймер |
| Онбординг | ✅ 3 info-слайда + 5 вопросов + AI инсайт |
| Сессия: dynamic intro | ✅ AI-генерированное вступление + max 2 крючка из истории |
| Сессия: one-by-one вопросы | ✅ Вопросы по одному, батч-подбивка агенту |
| Сессия: closing phrase | ✅ Тёплая фраза от Narrator'а |
| Сессия: one last thing | ✅ Маленький input + неакцентная кнопка Завершить |
| Dual-bot pipeline | ✅ Narrator (Haiku) → Analyst (Sonnet) |
| Crisis detection | ✅ Hardcoded RU+EN, CrisisView |
| Profile vault | ✅ profile.md, sessions/, insights.md |
| App Lock (Face ID) | ✅ BiometricAuthentication |
| Settings: API key, proxy | ✅ Keychain + LocalProxy |
| Settings: Сбросить данные | ✅ Алерт + полный сброс + онбординг заново |
| Session scheduler | ✅ UNCalendarNotification |
Что не реализовано (план)
| Компонент | Приоритет |
|---|---|
| ProfileView: история сессий | 🟠 Средний |
| Session recovery (незавершённые) | 🟠 Средний |
| Полноценный sessions/YYYY-MM-DD.md | 🟡 Низкий |
| RevenueCat paywall | 🔴 Нужен до публикации |
| Голосовой ввод (iOS) | 🟡 Phase 2 |
| Push-уведомления opt-in | 🟡 Phase 2 |
Android / Skip Fuse — сборка
Требования
- Xcode 16+,
xcodebuild - Java 21:
/opt/homebrew/opt/openjdk@21 - Android SDK:
~/Library/Android/sdk - Android Emulator:
reflect-android(Pixel 6, API 35) - Skip CLI:
skip(из Homebrew или из Xcode DerivedData)
Полная сборка (первый раз или после новых Swift файлов)
cd ~/Developer/reflect-skip
export ANDROID_HOME=~/Library/Android/sdk
export JAVA_HOME=/opt/homebrew/opt/openjdk@21
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:/opt/homebrew/bin:$PATH"
# Шаг 1: Xcode генерирует Kotlin стабы (skipstone) + собирает Android
xcodebuild \
-workspace Project.xcworkspace \
-scheme "ReflectApp App" \
-destination "generic/platform=iOS Simulator" \
-configuration Debug \
-skipPackagePluginValidation \
SKIP_ACTION=build
# Шаг 2: Линкуем symlinks субмодулей (один раз после очистки)
# Делается скриптом или вручную — см. раздел ниже
# Шаг 3: Gradle собирает APK
cd Darwin
skip gradle -p ../Android assembleDebug
Быстрая пересборка (инкрементальная)
cd ~/Developer/reflect-skip/Darwin
export ANDROID_HOME=~/Library/Android/sdk JAVA_HOME=/opt/homebrew/opt/openjdk@21
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:/opt/homebrew/bin:$PATH"
skip gradle -p ../Android assembleDebug
Деплой на эмулятор
export ANDROID_HOME=~/Library/Android/sdk
export PATH="$ANDROID_HOME/platform-tools:$PATH"
# Запустить эмулятор если не запущен
$ANDROID_HOME/emulator/emulator -avd reflect-android &
APK=~/Developer/reflect-skip/.build/Android/app/outputs/apk/debug/app-debug.apk
adb -s emulator-5554 install -r "$APK"
adb -s emulator-5554 shell am start -n com.reflect.app/reflect.app.MainActivity
⚠️ Критические особенности Skip Fuse
1. Новые Swift View файлы требуют ручного Kotlin стаба
При добавлении нового *.swift файла с View в Sources/ReflectApp/Views/:
skipstone не всегда автоматически генерирует .kt стаб. Нужно создать вручную.
Шаблон (копируем OnboardingView.kt, меняем класс):
package reflect.app
import skip.lib.*
import skip.ui.*
import skip.foundation.*
import skip.model.*
@androidx.annotation.Keep
internal class MyNewView: skip.ui.View, skip.bridge.SwiftPeerBridged, skip.lib.SwiftProjecting {
var Swift_peer: skip.bridge.SwiftObjectPointer = skip.bridge.SwiftObjectNil
constructor(Swift_peer: skip.bridge.SwiftObjectPointer, marker: skip.bridge.SwiftPeerMarker?) {
this.Swift_peer = Swift_peer
}
fun finalize() { Swift_release(Swift_peer); Swift_peer = skip.bridge.SwiftObjectNil }
private external fun Swift_release(Swift_peer: skip.bridge.SwiftObjectPointer)
override fun Swift_peer(): skip.bridge.SwiftObjectPointer = Swift_peer
override fun equals(other: Any?): Boolean {
if (other !is skip.bridge.SwiftPeerBridged) return false
return Swift_peer == other.Swift_peer()
}
override fun hashCode(): Int = Swift_peer.hashCode()
override fun body(): skip.ui.View {
return skip.ui.ComposeBuilder { ctx -> Swift_composableBody(Swift_peer)?.Compose(ctx) ?: skip.ui.ComposeResult.ok }
}
private external fun Swift_composableBody(Swift_peer: skip.bridge.SwiftObjectPointer): skip.ui.View?
constructor(/* params */) { Swift_peer = Swift_constructor_0(/* params */) }
private external fun Swift_constructor_0(/* params */): skip.bridge.SwiftObjectPointer
override fun Swift_projection(options: Int): () -> Any = Swift_projectionImpl(options)
private external fun Swift_projectionImpl(options: Int): () -> Any
@androidx.annotation.Keep companion object {}
}
Файл кладём в оба места:
~/Library/Developer/Xcode/DerivedData/Project-bpfpf.../Build/Intermediates.noindex/BuildToolPluginIntermediates/reflect-skip.output/ReflectApp/skipstone/ReflectApp/src/main/kotlin/reflect/app/
~/Developer/reflect-skip/.build/plugins/outputs/reflect-skip/ReflectApp/destination/skipstone/ReflectApp/src/main/kotlin/reflect/app/
2. Kotlin субмодули — symlinks
После очистки кеша/DerivedData Kotlin исходники субмодулей (SkipLib, SkipBridge, etc.) перестают быть видны Gradle. Нужно перелинковать:
DERIVED="$HOME/Library/Developer/Xcode/DerivedData/Project-bpfpf..."
SKIPSTONE="$HOME/Developer/reflect-skip/.build/plugins/outputs/reflect-skip/ReflectApp/destination/skipstone"
for MODULE in SkipLib SkipBridge SkipFoundation SkipUI SkipAndroidBridge SkipModel SkipSwiftUI SkipUnit; do
case "$MODULE" in
SkipLib) PKG="skip-lib" ;;
SkipBridge) PKG="skip-bridge" ;;
SkipFoundation) PKG="skip-foundation" ;;
SkipUI) PKG="skip-ui" ;;
SkipAndroidBridge) PKG="skip-android-bridge" ;;
SkipModel) PKG="skip-model" ;;
SkipSwiftUI) PKG="skip-fuse-ui" ;;
SkipUnit) PKG="skip-unit" ;;
esac
REAL_KT="$DERIVED/Build/Intermediates.noindex/BuildToolPluginIntermediates/${PKG}.output/${MODULE}/skipstone/${MODULE}/src/main/kotlin"
mv "$SKIPSTONE/$MODULE/src/main/kotlin" "$SKIPSTONE/$MODULE/src/main/kotlin.bak" 2>/dev/null || true
ln -sfn "$REAL_KT" "$SKIPSTONE/$MODULE/src/main/kotlin"
done
3. AndroidBridge.initBridge — обязательно до первого JNI вызова
В Android/app/src/main/kotlin/Main.kt порядок в onCreate:
ProcessInfo.launch(applicationContext)
AndroidBridge.initBridge("ReflectApp") // ← ДО AppDelegate.shared.onInit()
AppDelegate.shared.onInit()
Структура проекта (reflect-skip)
reflect-skip/
├── Package.swift ← SPM, SkipFuseUI зависимость
├── Skip.env ← ANDROID_PACKAGE_NAME=reflect.app
├── Project.xcworkspace/ ← Workspace для xcodebuild
├── Darwin/ ← Xcode проект (iOS таргет + Skip build phases)
│ ├── ReflectApp.xcodeproj/
│ └── Sources/Main.swift
├── Android/ ← Gradle Android проект
│ ├── app/src/main/
│ │ ├── AndroidManifest.xml
│ │ └── kotlin/Main.kt ← Entry point, AndroidBridge.initBridge
│ └── settings.gradle.kts
└── Sources/ReflectApp/
├── ReflectAppMain.swift ← App root + PrivacyIntroView → Onboarding → Home
├── Models/
├── Services/
│ ├── ClaudeService.swift ← Claude API (cloud + local proxy)
│ ├── AnalystBot.swift ← Бот 2 (Sonnet, CBT вопросы + tools)
│ ├── NarratorBot.swift ← Бот 1 (Haiku, 3rd-person reframe)
│ ├── CrisisDetector.swift ← Hardcoded SI/SH
│ ├── ProfileManager.swift ← On-device vault (profile.md, sessions/)
│ ├── SessionManager.swift ← Оркестратор: intro/closing/pipeline
│ └── ScheduleNegotiationService.swift
└── Views/
├── PrivacyIntroView.swift ← Экран 0: личный текст + GDPR
├── OnboardingView.swift ← Онбординг
├── HomeView.swift
├── SessionView.swift ← Фазы: loading/intro/dialogue/closing/oneLastThing
├── CardView.swift
├── SettingsView.swift ← AI backend + Сбросить данные
└── OrbCharacter.swift
Архитектура флоу онбординга
Первый запуск
→ PrivacyIntroView (privacy_accepted)
→ OnboardingView (5 вопросов + AI инсайт → profile.md)
→ HomeView
→ SessionView (loading → intro → dialogue → closing → oneLastThing → done)
On-device хранилище
Documents/UserProfile/
├── profile.md ← паттерны, наблюдения, счётчик сессий
├── insights.md ← кристаллизованные инсайты
└── sessions/
├── index.md ← таблица дат/тем для быстрого поиска
└── 2026-05-19.md
Сброс: Settings → Данные → Сбросить все данные → удаляет UserProfile/, сбрасывает privacy_accepted, onboarding_completed, API key.
Локальный AI прокси
cd ~/Developer/reflect-app # iOS проект
python3 proxy_server.py --backend claude-code # OAuth через Hermes
# или
python3 proxy_server.py --backend ollama --ollama-model qwen2.5:7b
В Settings приложения: AI Backend → Локальный сервер → URL сервера.
Архитектура двойного бота
User input (1-е лицо)
↓
CrisisDetector.detect() — hardcoded, <5ms
↓ (если .critical → CrisisView)
NarratorBot (Haiku) — переформулировка в 3-е лицо
↓
AnalystBot (Sonnet) — читает profile.md + инструменты (search_sessions, read_profile...)
↓
1 вопрос из батча → ответ → следующий вопрос → нумерованная подбивка → AnalystBot
Закрытие сессии:
endSession()→ AnalystBot генерирует наблюдение (3-е лицо)- NarratorBot генерирует тёплую closing phrase
- One last thing: маленький input
- Если нет расписания → предложить дату следующей сессии