Compare commits
No commits in common. "da05455e7c03c97f3e47e5904b0e324273c7a34b" and "aded5562cd4eb40988bfedb8c64671bf7e75a311" have entirely different histories.
da05455e7c
...
aded5562cd
|
|
@ -1,85 +1,145 @@
|
||||||
# 📋 Ponshu Room Lite - Current TODO List & Roadmap
|
# 📋 Ponshu Room Lite - Current TODO List
|
||||||
|
|
||||||
**Last Updated**: 2026-02-21 (v1.0.16)
|
**Last Updated**: 2026-01-21
|
||||||
**Status**: Active Task Tracking (Based on Claude's Comprehensive Review)
|
**Status**: Active Task Tracking
|
||||||
**For**: Multi-AI Collaboration (Claude + Antigravity + Gemini)
|
**For**: Multi-AI Collaboration (Claude + Antigravity + Gemini)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔴 High Priority (品質・配布に直結 / 即時対応推奨)
|
## 🔴 CRITICAL: Deferred Tasks (DO NOT FORGET)
|
||||||
|
|
||||||
- [ ] **H1. ダークモード完全対応** (推定: 6-8時間)
|
### Dark Mode Visibility Fixes (Phase 1 - INCOMPLETE)
|
||||||
- **対象**: `sake_detail_screen.dart:794` - 9箇所の `Theme.of(context).primaryColor` を `colorScheme` に置換。コアスクリーンでの視認性問題のため優先。
|
|
||||||
- [ ] **H2. 大規模ファイルのリファクタリング** (推定: 8-12時間)
|
- [x] ✅ Create Dark Mode guidelines ([DARK_MODE_COLOR_GUIDELINES.md](DARK_MODE_COLOR_GUIDELINES.md))
|
||||||
- `camera_screen.dart` (966行): `CameraControls`, `PhotoPreview`, `GalleryPicker` に分割。
|
- [x] ✅ Redesign app_theme.dart to use Material 3 ColorScheme
|
||||||
- `sake_detail_screen.dart` (794行): 分割済みだが監視継続 (`BasicInfo`, `TasteChart`, `Sakenowa`, `Pricing`)。
|
- [x] ✅ Fix guide_screen.dart (section headers to secondary color)
|
||||||
- `menu_pricing_screen.dart` (650行): `PricingInputSection`, `PricingPreview` に分割。
|
- [x] ✅ Fix soul_screen.dart (10+ manual dark mode checks removed)
|
||||||
- [ ] **H3. Synology Proxy永続化対応** (推定: 4-6時間)
|
- [ ] ⏸️ **DEFERRED**: Fix [sake_detail_screen.dart](../lib/screens/sake_detail_screen.dart)
|
||||||
- **現状**: In-Memoryのため、再起動で利用回数クォータがリセットされる。
|
- **Why Deferred**: 1500+ lines, recently modified, needs careful review
|
||||||
- **解決策**: Redis導入(推奨)またはファイルベースの永続化。
|
- **Issues**: 9 instances of `Theme.of(context).primaryColor` usage
|
||||||
|
- **Location**: Lines need investigation with `Grep` tool
|
||||||
|
- **Priority**: High (core feature screen)
|
||||||
|
- **Estimated Time**: 2-3 hours
|
||||||
|
- **Action Plan**:
|
||||||
|
1. Search for all `primaryColor` instances in file
|
||||||
|
2. Replace with appropriate `colorScheme` properties
|
||||||
|
3. Test thoroughly in both Light and Dark modes
|
||||||
|
4. Verify all UI elements (buttons, icons, charts, dialogs)
|
||||||
|
|
||||||
|
- [ ] ⏸️ **DEFERRED**: Fix shop_settings_screen.dart
|
||||||
|
- **Issues**: Manual dark mode checks present
|
||||||
|
- **Priority**: Medium
|
||||||
|
- **Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
- [ ] ⏸️ **DEFERRED**: Fix remaining widgets with `primaryColor` usage
|
||||||
|
- **Files**: badge_case.dart, level_title_card.dart, sake_grid_item.dart, etc.
|
||||||
|
- **Count**: ~40 instances across multiple files
|
||||||
|
- **Priority**: Low-Medium
|
||||||
|
- **Estimated Time**: 4-6 hours total
|
||||||
|
- **Strategy**: Fix incrementally during feature work
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🟡 Medium Priority (UX改善・機能追加)
|
## 🟡 Phase 2: Planned Features (READY TO START)
|
||||||
|
|
||||||
- [ ] **M1. AI「あわせて飲みたい」機能実装** (推定: 12-16時間)
|
See detailed implementation plan: [PHASE_2_IMPLEMENTATION_PLAN.md](PHASE_2_IMPLEMENTATION_PLAN.md)
|
||||||
- **要件**: 履歴分析、未登録銘柄からの推薦文生成。ソムリエタブのプレースホルダーを実稼働させる。
|
|
||||||
- [ ] **M2. チャート手動編集UIの完成** (推定: 8-10時間)
|
**Summary**:
|
||||||
- **内容**: AI生成の5軸チャートをユーザーが微調整できる機能。
|
1. **Help Button Placement** (Pattern C: Hybrid approach) - 6 hours
|
||||||
- [ ] **M3. ヘルプボタンの配置改善** (推定: 6時間)
|
2. **AI-Powered "あわせて飲みたい" Recommendations** - 12 hours
|
||||||
- **方針**: Pattern C (Hybrid approach) - 各画面の状況に応じて配置。
|
3. **AI Analysis Info Editing** - 8 hours
|
||||||
- [ ] **M4. Pro版機能の実装** (推定: 20-24時間)
|
|
||||||
- **要件**: QRスキャン連携、Instagram投稿生成機能、売上アナリティクスダッシュボード等。
|
**Total Estimated Time**: 26 hours
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🟢 Low Priority (技術的負債・長期改善)
|
## 🟢 Phase 3: Technical Debt & Cleanup
|
||||||
|
|
||||||
- [ ] **L1. debugPrint整理** (推定: 3-4時間)
|
### Coach Mark / Tutorial Cleanup
|
||||||
- **対象**: 221箇所(23ファイル)。`if (kDebugMode)` で囲む(主に `gemini_service.dart`, `analysis_cache_service.dart`)。
|
- [ ] Remove `hasSeenTutorial` from UserProfile model
|
||||||
- [ ] **L2. ハードコード色の段階的修正** (推定: 10-15時間)
|
- [ ] Remove `hasSeenCoachMarks` from UserProfile model
|
||||||
- **対象**: 意図的な例外(カメラビューなど)を除くハードコード色。`onboarding_dialog.dart` や `pending_analysis_screen.dart` などから優先。
|
- [ ] Remove tutorial-related methods from theme_provider.dart
|
||||||
- [ ] **L3. 真の画像圧縮実装** (推定: 3-4時間)
|
- [ ] Remove tutorial-related images/assets
|
||||||
- **現状**: コピーのみの `ImageCompressionService` を `flutter_image_compress` などの圧縮処理に置き換え。
|
- **Why**: Tutorial service was deleted due to persistence bugs
|
||||||
- [ ] **L4. Tutorial/CoachMark残存コードの削除** (推定: 1-2時間)
|
- **Priority**: Low (not breaking anything, just cluttering code)
|
||||||
- **対象**: `UserProfile` に存在したチュートリアル系不要フィールドと関連アセットの削除の完全完了。
|
- **Estimated Time**: 1 hour
|
||||||
- [ ] **L5. テストコード導入** (推定: 8-12時間)
|
|
||||||
- `SakeItem` JSON変換、`LevelCalculator`、`GeminiService` 等々のカバレッジ向上。
|
### Flutter Analyze Warnings
|
||||||
- [ ] **L6. UI/UXのさらなる改善**
|
- [ ] Fix 49 warnings (see `flutter analyze` output)
|
||||||
- バッジアンロックモーダル、スキャン時のアニメーション、タブレット対応(レスポンシブ)等。
|
- Unused imports
|
||||||
|
- Deprecated API usage
|
||||||
|
- Type warnings
|
||||||
|
- **Priority**: Medium
|
||||||
|
- **Estimated Time**: 2 hours
|
||||||
|
|
||||||
|
### Image Compression
|
||||||
|
- [ ] Implement real image compression (not just file copy)
|
||||||
|
- **Current Issue**: ImageCompressionService just copies files
|
||||||
|
- **Solution**: Use flutter_image_compress or similar
|
||||||
|
- **Priority**: Medium (affects storage usage)
|
||||||
|
- **Estimated Time**: 3 hours
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔮 Future / 長期構想 (Phase 3+)
|
## 📚 Documentation Status
|
||||||
|
|
||||||
- [ ] **F1. Firebase同期** (16-20時間): クラウドバックアップ、複数デバイス同期。
|
### ✅ Complete & Up-to-date
|
||||||
- [ ] **F2. Posimai Core Platform構築** (40時間〜): 日本酒・香道アプリの共通基盤化。
|
- [DARK_MODE_COLOR_GUIDELINES.md](DARK_MODE_COLOR_GUIDELINES.md) - Dark Mode implementation guide
|
||||||
- [ ] **F3. 多言語対応** (20時間): 英語・中国語対応(ARBファイル利用)。
|
- [PROJECT_BACKLOG_MASTER.md](PROJECT_BACKLOG_MASTER.md) - High-level roadmap
|
||||||
- [ ] **F4. 酒蔵マップ実データ統合** (12時間): プレースホルダーから実データへの連携、GPS連動等。
|
- [UI_UX_BACKLOG.md](UI_UX_BACKLOG.md) - UI/UX improvement tracking
|
||||||
|
- [CURSOR_CHAT_MASTER_CONTEXT.md](architecture/CURSOR_CHAT_MASTER_CONTEXT.md) - Antigravity handoff
|
||||||
|
|
||||||
|
### 🔄 Needs Update
|
||||||
|
- [PROJECT_BACKLOG_MASTER.md](PROJECT_BACKLOG_MASTER.md) - Mark Dark Mode tasks as partially complete
|
||||||
|
- [UI_UX_BACKLOG.md](UI_UX_BACKLOG.md) - Update Coach Mark status
|
||||||
|
|
||||||
|
### 📝 New Documents Created
|
||||||
|
- This file (PROJECT_TODO.md)
|
||||||
|
- [README.md](README.md) - Documentation index (TO BE CREATED)
|
||||||
|
- [PHASE_2_IMPLEMENTATION_PLAN.md](PHASE_2_IMPLEMENTATION_PLAN.md) (TO BE CREATED)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🏗️ Synology環境の堅牢化 (運用サーバー対応)
|
## 🎯 Recommended Next Steps
|
||||||
|
|
||||||
- [ ] **S1. Proxy Serverのレート制限永続化** (Critical / =H3)
|
For immediate work, choose ONE of the following:
|
||||||
- Container Managerで Redis コンテナを立ち上げ、Node.jsプロキシに接続して利用履歴を永続化。
|
|
||||||
- [ ] **S2. バックアップ戦略の確立** (Critical)
|
### Option A: Complete Dark Mode Fixes
|
||||||
- Hyper Backupでのプロジェクト全体の外部保存、Dokploy / Giteaの定期ボリュームバックアップの設定。
|
1. Fix sake_detail_screen.dart (~2-3 hours)
|
||||||
- [ ] **S3. SSL/TLS対応**
|
2. Fix shop_settings_screen.dart (~1 hour)
|
||||||
- Tailscale Funnel や Synology Application Portal (リバースプロキシ) の構築。
|
3. Mark Dark Mode project as complete
|
||||||
- [ ] **S4. 監視・アラート設定**
|
|
||||||
- Uptime Kuma などを導入し、ダウン時に通知を飛ばす体制づくり。
|
### Option B: Start Phase 2 Features
|
||||||
- [ ] **S5-10. インフラのさらなる拡張** (Important/Nice to Have)
|
1. Implement Help Button Placement (~6 hours)
|
||||||
- CI/CD構築(Gitea Actions, Dokploy連携等)、共有基盤化、Webhook通知など。
|
2. Creates user value immediately
|
||||||
|
3. Defer remaining Dark Mode fixes to incremental work
|
||||||
|
|
||||||
|
### Option C: Quick Wins
|
||||||
|
1. Technical debt cleanup (~3 hours total)
|
||||||
|
- Coach mark removal
|
||||||
|
- Flutter analyze fixes
|
||||||
|
2. Builds confidence, cleans codebase
|
||||||
|
|
||||||
|
**Recommended by Previous Claude Instance**: Option B (Phase 2) - Delivers user value while Dark Mode is "good enough" for now.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📅 推奨作業順序 (今後2週間)
|
## 🚨 Known Issues to Avoid
|
||||||
|
|
||||||
1. **品質安定化 (Week 1)**
|
1. **Image Path Repair Service** - Works correctly after Day 4 bug fix
|
||||||
- Day 1-2: Synology Proxy永続化 (S1 / H3)
|
2. **Backup/Restore** - Functional, tested successfully
|
||||||
- Day 3-4: ダークモード完全対応 (H1)
|
3. **Tutorial Service** - Deleted, do not attempt to restore
|
||||||
- Day 5: バックアップ戦略確立 (S2)
|
4. **Dark Mode** - Fixed for core screens, remaining issues documented above
|
||||||
2. **機能拡張 or インフラ強化 (Week 2)**
|
|
||||||
- *Option A (機能優先)*: 推薦機能(M1) & チャート微調整(M2)
|
---
|
||||||
- *Option B (インフラ優先)*: Dokploy設定 & CI/CDテスト & 監視設定
|
|
||||||
|
## 📞 For Future AI Collaborators
|
||||||
|
|
||||||
|
- **Before modifying Dark Mode code**: Read [DARK_MODE_COLOR_GUIDELINES.md](DARK_MODE_COLOR_GUIDELINES.md)
|
||||||
|
- **Before adding new features**: Check this TODO and [PROJECT_BACKLOG_MASTER.md](PROJECT_BACKLOG_MASTER.md)
|
||||||
|
- **Before committing**: Run `flutter analyze` and fix new warnings
|
||||||
|
- **For documentation questions**: See [docs/README.md](README.md) (index file)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This is a living document. Update status after completing tasks.*
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,10 @@ class Secrets {
|
||||||
|
|
||||||
/// AI Mode: Proxy(Home) vs Direct(Cloud)
|
/// AI Mode: Proxy(Home) vs Direct(Cloud)
|
||||||
/// If false, connects directly to Google Gemini API (Works anywhere).
|
/// If false, connects directly to Google Gemini API (Works anywhere).
|
||||||
/// Development (with Tailscale): --dart-define=USE_PROXY=true
|
/// Release build: --dart-define=USE_PROXY=true
|
||||||
/// General distribution: false (each user provides their own Gemini API key)
|
|
||||||
static const bool useProxy = bool.fromEnvironment(
|
static const bool useProxy = bool.fromEnvironment(
|
||||||
'USE_PROXY',
|
'USE_PROXY',
|
||||||
defaultValue: false, // ← 一般配布用: Direct API(各自がAPIキー設定)
|
defaultValue: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
# Gemini API Key
|
|
||||||
GEMINI_API_KEY=your_gemini_api_key_here
|
|
||||||
|
|
||||||
# Proxy Authentication Token (recommended for security)
|
|
||||||
# Generate a random string: openssl rand -hex 32
|
|
||||||
PROXY_AUTH_TOKEN=your_secure_random_token_here
|
|
||||||
|
|
||||||
# Daily request limit per device
|
|
||||||
DAILY_LIMIT=50
|
|
||||||
|
|
||||||
# Redis connection settings (default values for Docker Compose)
|
|
||||||
REDIS_HOST=redis
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
# Environment variables (contains secrets)
|
|
||||||
.env
|
|
||||||
|
|
||||||
# Node modules
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
|
|
||||||
# Docker volumes
|
|
||||||
data/
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
# Ponshu Room Proxy Server (Redis永続化対応)
|
|
||||||
|
|
||||||
Gemini APIキーを隠蔽し、デバイスごとの利用制限(レート制限)を管理するプロキシサーバー。
|
|
||||||
|
|
||||||
## 🎯 機能
|
|
||||||
|
|
||||||
- **APIキー保護**: クライアントアプリにAPIキーを含めず、プロキシサーバーのみが保持
|
|
||||||
- **レート制限**: デバイスIDごとに1日あたりのリクエスト数を制限
|
|
||||||
- **Redis永続化**: Dockerコンテナ再起動後もカウントを維持
|
|
||||||
- **認証**: Bearer Token認証(オプション)
|
|
||||||
- **Dokploy対応**: 将来のDokploy移行に対応した構成
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 クイックスタート(Synology NAS)
|
|
||||||
|
|
||||||
### 1. 環境変数ファイル作成
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
`.env` を編集してAPIキーと認証トークンを設定:
|
|
||||||
|
|
||||||
```env
|
|
||||||
GEMINI_API_KEY=AIzaSy...(あなたのAPIキー)
|
|
||||||
PROXY_AUTH_TOKEN=$(openssl rand -hex 32) # ランダム生成推奨
|
|
||||||
DAILY_LIMIT=50
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Docker Composeで起動
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 動作確認
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8080/health
|
|
||||||
# 期待される出力: OK
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Redis データ確認
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Redisコンテナに接続
|
|
||||||
docker exec -it ponshu-redis redis-cli
|
|
||||||
|
|
||||||
# 使用状況確認
|
|
||||||
KEYS usage:*
|
|
||||||
GET "usage:device-123:2026-02-21"
|
|
||||||
TTL "usage:device-123:2026-02-21"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 運用コマンド
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# ログ確認
|
|
||||||
docker-compose logs -f proxy
|
|
||||||
|
|
||||||
# 再起動
|
|
||||||
docker-compose restart
|
|
||||||
|
|
||||||
# 停止
|
|
||||||
docker-compose stop
|
|
||||||
|
|
||||||
# 完全削除(データも削除)
|
|
||||||
docker-compose down -v
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 ドキュメント
|
|
||||||
|
|
||||||
- **移行ガイド**: [REDIS_MIGRATION_GUIDE.md](REDIS_MIGRATION_GUIDE.md) - In-MemoryからRedis永続化への移行手順
|
|
||||||
- **セットアップガイド**: [../../docs/SYNOLOGY_PROXY_SETUP.md](../../docs/SYNOLOGY_PROXY_SETUP.md) - 初回構築手順(旧版・参考用)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ アーキテクチャ
|
|
||||||
|
|
||||||
```
|
|
||||||
Flutter App (Client)
|
|
||||||
↓ HTTP POST /analyze
|
|
||||||
↓ Authorization: Bearer <token>
|
|
||||||
Proxy Server (Node.js/Express)
|
|
||||||
↓ Rate Limit Check (Redis)
|
|
||||||
↓ Gemini API Call
|
|
||||||
Gemini 2.5 Flash API
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 セキュリティ
|
|
||||||
|
|
||||||
- `.env` ファイルは **Git管理しない**(`.gitignore`に追加済み)
|
|
||||||
- `PROXY_AUTH_TOKEN` は **長いランダム文字列** を使用
|
|
||||||
- 外部公開する場合は **リバースプロキシ + HTTPS** 推奨
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 環境変数
|
|
||||||
|
|
||||||
| 変数名 | 説明 | デフォルト |
|
|
||||||
|--------|------|-----------|
|
|
||||||
| `GEMINI_API_KEY` | Gemini APIキー(必須) | なし |
|
|
||||||
| `PROXY_AUTH_TOKEN` | Bearer Token認証(推奨) | なし(認証無効) |
|
|
||||||
| `DAILY_LIMIT` | 1日あたりのリクエスト上限 | 50 |
|
|
||||||
| `REDIS_HOST` | Redisホスト名 | `redis` |
|
|
||||||
| `REDIS_PORT` | Redisポート | `6379` |
|
|
||||||
| `PORT` | Proxy Serverポート | `8080` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🆘 トラブルシューティング
|
|
||||||
|
|
||||||
### Q: `Redis connection failed`
|
|
||||||
|
|
||||||
**解決策**: Redisコンテナが起動していることを確認
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose ps
|
|
||||||
# ponshu-redis が "Up (healthy)" になっているか確認
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q: Flutter アプリから接続できない
|
|
||||||
|
|
||||||
**解決策**:
|
|
||||||
1. `.env` の `PROXY_AUTH_TOKEN` を確認
|
|
||||||
2. Flutter の `lib/secrets.local.dart` で同じトークンを設定
|
|
||||||
3. Synology NASのIPアドレス(`100.76.7.3`)が正しいか確認
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated**: 2026-02-21
|
|
||||||
|
|
@ -1,290 +0,0 @@
|
||||||
# Synology Proxy Server - Redis永続化移行ガイド
|
|
||||||
|
|
||||||
## 📋 概要
|
|
||||||
|
|
||||||
このガイドでは、Synology NAS上のProxy Serverを**In-Memory方式からRedis永続化方式**に移行する手順を説明します。
|
|
||||||
|
|
||||||
### 移行の目的
|
|
||||||
- **永続化**: Dockerコンテナ再起動後もレート制限カウントが維持される
|
|
||||||
- **Dokploy対応**: 将来のDokploy導入時にそのまま移行可能な構成
|
|
||||||
- **複数アプリ対応**: ダッシュボードWebシステム等、他プロジェクトでもRedisを共有可能
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ 前提条件
|
|
||||||
|
|
||||||
- Synology NAS(Container Manager インストール済み)
|
|
||||||
- 既存のProxy Serverコンテナが稼働中(`ponshu-proxy-server`)
|
|
||||||
- SSH接続可能な環境(または File Station経由のファイル操作)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📂 ステップ1: 既存コンテナの停止と削除
|
|
||||||
|
|
||||||
### 1-1. Container Managerで既存コンテナを停止
|
|
||||||
|
|
||||||
1. Synology DSM → **Container Manager** を開く
|
|
||||||
2. **コンテナ** タブで `ponshu-proxy-server` を選択
|
|
||||||
3. **アクション** → **停止** をクリック
|
|
||||||
4. 停止後、**削除** をクリック(イメージは削除しない)
|
|
||||||
|
|
||||||
**注意**: 既存のレート制限カウントは削除されますが、これは移行の性質上避けられません。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📂 ステップ2: 新しいファイルをSynologyに配置
|
|
||||||
|
|
||||||
### 2-1. File Stationでファイルをアップロード
|
|
||||||
|
|
||||||
PC上の `tools/proxy/` フォルダから、以下のファイルをSynologyの `/docker/ponshu_proxy/` にアップロードします。
|
|
||||||
|
|
||||||
**アップロード対象**:
|
|
||||||
- `docker-compose.yml` ← **新規追加**
|
|
||||||
- `server.js` ← **更新版**
|
|
||||||
- `package.json` ← **更新版(Redis依存追加)**
|
|
||||||
- `Dockerfile` ← 変更なし
|
|
||||||
- `.env` ← **新規作成(次ステップ)**
|
|
||||||
|
|
||||||
### 2-2. `.env` ファイルを作成
|
|
||||||
|
|
||||||
File Stationで `/docker/ponshu_proxy/.env` を新規作成し、以下の内容を記述します。
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Gemini API Key
|
|
||||||
GEMINI_API_KEY=AIzaSy...(あなたのAPIキー)
|
|
||||||
|
|
||||||
# Proxy認証トークン(必須推奨)
|
|
||||||
PROXY_AUTH_TOKEN=your-secure-token-here
|
|
||||||
|
|
||||||
# 1日あたりの解析回数上限
|
|
||||||
DAILY_LIMIT=50
|
|
||||||
|
|
||||||
# Redis接続設定(Docker Compose使用時はデフォルトでOK)
|
|
||||||
REDIS_HOST=redis
|
|
||||||
REDIS_PORT=6379
|
|
||||||
```
|
|
||||||
|
|
||||||
**セキュリティ注意**:
|
|
||||||
- `.env` ファイルは**Git管理しない**こと(`.gitignore` に追加済み)
|
|
||||||
- `PROXY_AUTH_TOKEN` は推測困難な長いランダム文字列を使用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📂 ステップ3: Docker Composeでコンテナ起動
|
|
||||||
|
|
||||||
### 3-1. SSH接続でSynologyにログイン
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ssh admin@100.76.7.3
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3-2. プロジェクトディレクトリに移動
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /volume1/docker/ponshu_proxy
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3-3. Docker Composeで起動
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
**実行内容**:
|
|
||||||
- `ponshu-redis`: Redisコンテナを起動(ポート6379、永続化有効)
|
|
||||||
- `ponshu-proxy-server`: Proxy Serverコンテナを起動(ポート8080、Redis接続)
|
|
||||||
|
|
||||||
### 3-4. 起動確認
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo docker-compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
**期待される出力**:
|
|
||||||
```
|
|
||||||
NAME IMAGE STATUS
|
|
||||||
ponshu-redis redis:7-alpine Up (healthy)
|
|
||||||
ponshu-proxy-server ponshu_proxy-proxy Up (healthy)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ ステップ4: 動作確認
|
|
||||||
|
|
||||||
### 4-1. ヘルスチェック
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://100.76.7.3:8080/health
|
|
||||||
```
|
|
||||||
|
|
||||||
**期待される出力**: `OK`
|
|
||||||
|
|
||||||
### 4-2. Redis接続確認
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Redisコンテナに接続
|
|
||||||
sudo docker exec -it ponshu-redis redis-cli
|
|
||||||
|
|
||||||
# Redis内でコマンド実行
|
|
||||||
127.0.0.1:6379> PING
|
|
||||||
PONG
|
|
||||||
|
|
||||||
127.0.0.1:6379> KEYS *
|
|
||||||
(empty array) # 初回起動時は空
|
|
||||||
|
|
||||||
127.0.0.1:6379> exit
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4-3. レート制限テスト(Flutter アプリから実行)
|
|
||||||
|
|
||||||
Flutter アプリ(`lib/secrets.local.dart`)で以下を設定:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
static const String aiProxyAnalyzeUrl = 'http://100.76.7.3:8080/analyze';
|
|
||||||
static const bool useProxy = true;
|
|
||||||
static const String proxyAuthToken = 'your-secure-token-here'; // .envと同じ値
|
|
||||||
```
|
|
||||||
|
|
||||||
アプリから日本酒ラベル解析を実行し、成功することを確認。
|
|
||||||
|
|
||||||
### 4-4. Redis内のデータ確認
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo docker exec -it ponshu-redis redis-cli
|
|
||||||
|
|
||||||
# 使用状況を確認(device_idは実際のデバイスIDに置き換え)
|
|
||||||
127.0.0.1:6379> KEYS usage:*
|
|
||||||
1) "usage:device-123:2026-02-21"
|
|
||||||
|
|
||||||
127.0.0.1:6379> GET "usage:device-123:2026-02-21"
|
|
||||||
"1" # 1回解析済み
|
|
||||||
|
|
||||||
127.0.0.1:6379> TTL "usage:device-123:2026-02-21"
|
|
||||||
(integer) 43200 # 残り12時間(秒単位)
|
|
||||||
```
|
|
||||||
|
|
||||||
**重要**: TTL(有効期限)が設定されていることを確認してください。これにより、翌日0時以降に自動的にリセットされます。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 ステップ5: コンテナ再起動テスト(永続化確認)
|
|
||||||
|
|
||||||
### 5-1. コンテナ再起動
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo docker-compose restart
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5-2. 再起動後のデータ確認
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo docker exec -it ponshu-redis redis-cli
|
|
||||||
|
|
||||||
127.0.0.1:6379> GET "usage:device-123:2026-02-21"
|
|
||||||
"1" # 再起動前のカウントが維持されている
|
|
||||||
```
|
|
||||||
|
|
||||||
**成功条件**: 再起動後もカウントが保持されていること。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 運用上の注意点
|
|
||||||
|
|
||||||
### ログの確認方法
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Proxy Serverのログ
|
|
||||||
sudo docker-compose logs -f proxy
|
|
||||||
|
|
||||||
# Redisのログ
|
|
||||||
sudo docker-compose logs -f redis
|
|
||||||
```
|
|
||||||
|
|
||||||
### コンテナの停止・削除
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 停止(データは保持)
|
|
||||||
sudo docker-compose stop
|
|
||||||
|
|
||||||
# 完全削除(データも削除される)
|
|
||||||
sudo docker-compose down -v
|
|
||||||
```
|
|
||||||
|
|
||||||
**警告**: `docker-compose down -v` を実行すると、Redisのデータボリュームも削除されます。
|
|
||||||
|
|
||||||
### Redis データのバックアップ
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Redisのデータをダンプ
|
|
||||||
sudo docker exec ponshu-redis redis-cli SAVE
|
|
||||||
|
|
||||||
# ダンプファイルをコピー
|
|
||||||
sudo docker cp ponshu-redis:/data/dump.rdb /volume1/backups/redis-backup-$(date +%Y%m%d).rdb
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Dokploy移行への準備
|
|
||||||
|
|
||||||
この構成は**Dokploy環境にそのまま移行可能**です。
|
|
||||||
|
|
||||||
### Dokploy移行時の変更点
|
|
||||||
|
|
||||||
1. **Gitea連携**: Gitea pushで自動デプロイ
|
|
||||||
2. **環境変数管理**: `.env` ファイルの代わりにDokployのWeb UIで設定
|
|
||||||
3. **ドメイン設定**: `ponshu-proxy.local` 等のカスタムドメイン自動割り当て
|
|
||||||
4. **スケーリング**: Docker Swarmで複数ノードに分散可能
|
|
||||||
|
|
||||||
**移行時期**: Week 2(日本酒アプリとダッシュボードを同時にDokployへ移行)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🆘 トラブルシューティング
|
|
||||||
|
|
||||||
### Q1: Proxy Serverが起動しない(`Redis connection failed`)
|
|
||||||
|
|
||||||
**原因**: Redisコンテナが起動していない、またはネットワーク設定が誤っている。
|
|
||||||
|
|
||||||
**解決策**:
|
|
||||||
```bash
|
|
||||||
# Redisの状態確認
|
|
||||||
sudo docker-compose ps
|
|
||||||
|
|
||||||
# Redisが "unhealthy" の場合、ログを確認
|
|
||||||
sudo docker-compose logs redis
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q2: `docker-compose` コマンドが見つからない
|
|
||||||
|
|
||||||
**原因**: Synology DSMのバージョンによっては `docker compose`(スペース)を使用。
|
|
||||||
|
|
||||||
**解決策**:
|
|
||||||
```bash
|
|
||||||
sudo docker compose up -d # ハイフンなし
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q3: Flutter アプリから接続できない(認証エラー)
|
|
||||||
|
|
||||||
**原因**: `PROXY_AUTH_TOKEN` が `.env` と `secrets.local.dart` で一致していない。
|
|
||||||
|
|
||||||
**解決策**:
|
|
||||||
1. `.env` の `PROXY_AUTH_TOKEN` を確認
|
|
||||||
2. Flutter の `lib/secrets.local.dart` の `proxyAuthToken` を同じ値に設定
|
|
||||||
3. Flutter アプリを再起動
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ 完了チェックリスト
|
|
||||||
|
|
||||||
- [ ] 既存コンテナを停止・削除
|
|
||||||
- [ ] 新しいファイル(`docker-compose.yml`, 更新版 `server.js`, `package.json`)をアップロード
|
|
||||||
- [ ] `.env` ファイルを作成(APIキー、認証トークン設定)
|
|
||||||
- [ ] `docker-compose up -d` で起動成功
|
|
||||||
- [ ] `curl http://100.76.7.3:8080/health` で `OK` 返却
|
|
||||||
- [ ] Redis内でデータが保存されることを確認(`KEYS usage:*`)
|
|
||||||
- [ ] コンテナ再起動後もデータが維持されることを確認
|
|
||||||
- [ ] Flutter アプリから解析が成功することを確認
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**移行完了後は、この手順書を `docs/` フォルダに移動してアーカイブしてください。**
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# Redis: Rate Limit永続化用
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: ponshu-redis
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
volumes:
|
|
||||||
- redis-data:/data
|
|
||||||
command: redis-server --appendonly yes # AOF永続化有効化
|
|
||||||
healthcheck:
|
|
||||||
test: [ "CMD", "redis-cli", "ping" ]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 3
|
|
||||||
networks:
|
|
||||||
- ponshu-network
|
|
||||||
|
|
||||||
# Proxy Server: Gemini API プロキシ
|
|
||||||
proxy:
|
|
||||||
build: .
|
|
||||||
container_name: ponshu-proxy-server
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
environment:
|
|
||||||
- PORT=8080
|
|
||||||
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
|
||||||
- PROXY_AUTH_TOKEN=${PROXY_AUTH_TOKEN}
|
|
||||||
- DAILY_LIMIT=${DAILY_LIMIT:-50}
|
|
||||||
- REDIS_HOST=redis
|
|
||||||
- REDIS_PORT=6379
|
|
||||||
depends_on:
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
test: [ "CMD-SHELL", "node -e \"require('http').get('http://localhost:8080/health', (r) => { if (r.statusCode !== 200) process.exit(1); }).on('error', () => process.exit(1))\"" ]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
networks:
|
|
||||||
- ponshu-network
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
redis-data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
networks:
|
|
||||||
ponshu-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"@google/generative-ai": "^0.21.0",
|
"@google/generative-ai": "^0.21.0"
|
||||||
"redis": "^4.7.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
||||||
const { createClient } = require('redis');
|
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
@ -9,36 +8,9 @@ const PORT = process.env.PORT || 8080;
|
||||||
const API_KEY = process.env.GEMINI_API_KEY;
|
const API_KEY = process.env.GEMINI_API_KEY;
|
||||||
const AUTH_TOKEN = process.env.PROXY_AUTH_TOKEN;
|
const AUTH_TOKEN = process.env.PROXY_AUTH_TOKEN;
|
||||||
|
|
||||||
// Rate Limiting Configuration
|
// Rate Limiting (Simple In-Memory)
|
||||||
const DAILY_LIMIT = parseInt(process.env.DAILY_LIMIT || '50', 10);
|
const DAILY_LIMIT = parseInt(process.env.DAILY_LIMIT || '50', 10);
|
||||||
const REDIS_HOST = process.env.REDIS_HOST || 'localhost';
|
const usageStore = {}; // { deviceId: { date: 'YYYY-MM-DD', count: 0 } }
|
||||||
const REDIS_PORT = parseInt(process.env.REDIS_PORT || '6379', 10);
|
|
||||||
|
|
||||||
// Redis Client Setup
|
|
||||||
const redisClient = createClient({
|
|
||||||
socket: {
|
|
||||||
host: REDIS_HOST,
|
|
||||||
port: REDIS_PORT
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
redisClient.on('error', (err) => {
|
|
||||||
console.error('[Redis] Connection Error:', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
redisClient.on('connect', () => {
|
|
||||||
console.log(`[Redis] Connected to ${REDIS_HOST}:${REDIS_PORT}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize Redis connection
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
await redisClient.connect();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Redis] Failed to connect:', err);
|
|
||||||
process.exit(1); // Exit if Redis is unavailable
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Authentication Middleware (skip for /health)
|
// Authentication Middleware (skip for /health)
|
||||||
function authMiddleware(req, res, next) {
|
function authMiddleware(req, res, next) {
|
||||||
|
|
@ -63,81 +35,44 @@ function authMiddleware(req, res, next) {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global middleware: Body parser first, then auth (skip /health)
|
// Global middleware: Auth first (skip /health), then body parser
|
||||||
app.use(bodyParser.json({ limit: '10mb' }));
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (req.path === '/health') return next();
|
if (req.path === '/health') return next();
|
||||||
authMiddleware(req, res, next);
|
authMiddleware(req, res, next);
|
||||||
});
|
});
|
||||||
|
app.use(bodyParser.json({ limit: '10mb' }));
|
||||||
|
|
||||||
// Gemini Client with JSON response configuration
|
// Gemini Client
|
||||||
const genAI = new GoogleGenerativeAI(API_KEY);
|
const genAI = new GoogleGenerativeAI(API_KEY);
|
||||||
const model = genAI.getGenerativeModel({
|
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); // Flutter側(gemini_service.dart)と統一
|
||||||
model: "gemini-2.5-flash", // Flutter側(gemini_service.dart)と統一
|
|
||||||
generationConfig: {
|
|
||||||
responseMimeType: "application/json", // Force JSON-only output
|
|
||||||
temperature: 0.2, // チャート一貫性向上のため(Flutter側と統一)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper: Get Today's Date String (YYYY-MM-DD)
|
// Helper: Get Today's Date String (YYYY-MM-DD)
|
||||||
function getTodayString() {
|
function getTodayString() {
|
||||||
return new Date().toISOString().split('T')[0];
|
return new Date().toISOString().split('T')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Check & Update Rate Limit (Redis-based)
|
// Helper: Check & Update Rate Limit
|
||||||
async function checkRateLimit(deviceId) {
|
function checkRateLimit(deviceId) {
|
||||||
const today = getTodayString();
|
const today = getTodayString();
|
||||||
const redisKey = `usage:${deviceId}:${today}`;
|
|
||||||
|
|
||||||
try {
|
if (!usageStore[deviceId]) {
|
||||||
// Get current usage count
|
usageStore[deviceId] = { date: today, count: 0 };
|
||||||
const currentCount = await redisClient.get(redisKey);
|
}
|
||||||
const count = currentCount ? parseInt(currentCount, 10) : 0;
|
|
||||||
const remaining = DAILY_LIMIT - count;
|
// Reset if new day
|
||||||
|
if (usageStore[deviceId].date !== today) {
|
||||||
|
usageStore[deviceId] = { date: today, count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUsage = usageStore[deviceId];
|
||||||
|
const remaining = DAILY_LIMIT - currentUsage.count;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allowed: remaining > 0,
|
allowed: remaining > 0,
|
||||||
current: count,
|
current: currentUsage.count,
|
||||||
limit: DAILY_LIMIT,
|
limit: DAILY_LIMIT,
|
||||||
remaining: remaining,
|
remaining: remaining
|
||||||
redisKey: redisKey
|
|
||||||
};
|
};
|
||||||
} catch (err) {
|
|
||||||
console.error('[Redis] Error checking rate limit:', err);
|
|
||||||
// Fallback: deny request if Redis is down
|
|
||||||
return {
|
|
||||||
allowed: false,
|
|
||||||
current: 0,
|
|
||||||
limit: DAILY_LIMIT,
|
|
||||||
remaining: 0,
|
|
||||||
error: 'Rate limit check failed'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Increment Usage Count (Redis-based)
|
|
||||||
async function incrementUsage(deviceId) {
|
|
||||||
const today = getTodayString();
|
|
||||||
const redisKey = `usage:${deviceId}:${today}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Increment count
|
|
||||||
const newCount = await redisClient.incr(redisKey);
|
|
||||||
|
|
||||||
// Set expiration to end of day (86400 seconds = 24 hours)
|
|
||||||
const now = new Date();
|
|
||||||
const midnight = new Date(now);
|
|
||||||
midnight.setHours(24, 0, 0, 0);
|
|
||||||
const secondsUntilMidnight = Math.floor((midnight - now) / 1000);
|
|
||||||
|
|
||||||
await redisClient.expire(redisKey, secondsUntilMidnight);
|
|
||||||
|
|
||||||
return newCount;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Redis] Error incrementing usage:', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// API Endpoint (authentication enforced by global middleware)
|
// API Endpoint (authentication enforced by global middleware)
|
||||||
|
|
@ -148,20 +83,20 @@ app.post('/analyze', async (req, res) => {
|
||||||
return res.status(400).json({ success: false, error: 'Device ID is required' });
|
return res.status(400).json({ success: false, error: 'Device ID is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// 1. Check Rate Limit
|
||||||
// 1. Check Rate Limit (Redis-based)
|
const limitStatus = checkRateLimit(device_id);
|
||||||
const limitStatus = await checkRateLimit(device_id);
|
|
||||||
if (!limitStatus.allowed) {
|
if (!limitStatus.allowed) {
|
||||||
console.log(`[Limit Reached] Device: ${device_id} (${limitStatus.current}/${limitStatus.limit})`);
|
console.log(`[Limit Reached] Device: ${device_id} (${limitStatus.current}/${limitStatus.limit})`);
|
||||||
return res.status(429).json({
|
return res.status(429).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: limitStatus.error || 'Daily limit reached',
|
error: 'Daily limit reached',
|
||||||
usage: { today: limitStatus.current, limit: limitStatus.limit }
|
usage: { today: limitStatus.current, limit: limitStatus.limit }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Request] Device: ${device_id} | Images: ${images ? images.length : 0} | Current: ${limitStatus.current}/${limitStatus.limit}`);
|
console.log(`[Request] Device: ${device_id} | Images: ${images ? images.length : 0}`);
|
||||||
|
|
||||||
|
try {
|
||||||
// 2. Prepare Gemini Request
|
// 2. Prepare Gemini Request
|
||||||
// Base64 images to GenerativeContentBlob
|
// Base64 images to GenerativeContentBlob
|
||||||
const imageParts = (images || []).map(base64 => ({
|
const imageParts = (images || []).map(base64 => ({
|
||||||
|
|
@ -176,41 +111,32 @@ app.post('/analyze', async (req, res) => {
|
||||||
const text = response.text();
|
const text = response.text();
|
||||||
|
|
||||||
// 3. Parse JSON from Markdown (e.g. ```json ... ```)
|
// 3. Parse JSON from Markdown (e.g. ```json ... ```)
|
||||||
console.log(`[Debug] Gemini raw response (first 200 chars): ${text.substring(0, 200)}`);
|
|
||||||
|
|
||||||
const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/) || text.match(/```([\s\S]*?)```/);
|
const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/) || text.match(/```([\s\S]*?)```/);
|
||||||
let jsonData;
|
let jsonData;
|
||||||
|
|
||||||
if (jsonMatch) {
|
if (jsonMatch) {
|
||||||
console.log('[Debug] Found JSON in code block');
|
|
||||||
jsonData = JSON.parse(jsonMatch[1]);
|
jsonData = JSON.parse(jsonMatch[1]);
|
||||||
} else {
|
} else {
|
||||||
// Try parsing raw text if no code blocks
|
// Try parsing raw text if no code blocks
|
||||||
console.log('[Debug] Attempting to parse raw text as JSON');
|
|
||||||
try {
|
|
||||||
jsonData = JSON.parse(text);
|
jsonData = JSON.parse(text);
|
||||||
} catch (parseError) {
|
|
||||||
console.error('[Error] JSON parse failed. Raw response:', text);
|
|
||||||
throw new Error(`Failed to parse Gemini response as JSON. Response starts with: ${text.substring(0, 100)}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Increment Usage (Redis-based)
|
// 4. Update Usage
|
||||||
const newCount = await incrementUsage(device_id);
|
usageStore[device_id].count++;
|
||||||
console.log(`[Success] Device: ${device_id} | New Count: ${newCount}/${DAILY_LIMIT}`);
|
console.log(`[Success] Device: ${device_id} | Count: ${usageStore[device_id].count}/${DAILY_LIMIT}`);
|
||||||
|
|
||||||
// 5. Send Response
|
// 5. Send Response
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: jsonData,
|
data: jsonData,
|
||||||
usage: {
|
usage: {
|
||||||
today: newCount,
|
today: usageStore[device_id].count,
|
||||||
limit: DAILY_LIMIT
|
limit: DAILY_LIMIT
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Error] Gemini API or Redis Error:', error);
|
console.error('Gemini API Error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message || 'Internal Server Error'
|
error: error.message || 'Internal Server Error'
|
||||||
|
|
@ -224,7 +150,7 @@ app.get('/health', (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start Server
|
// Start Server
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Proxy Server running on port ${PORT}`);
|
console.log(`Proxy Server running on port ${PORT}`);
|
||||||
if (!API_KEY) console.error('WARNING: GEMINI_API_KEY is not set!');
|
if (!API_KEY) console.error('WARNING: GEMINI_API_KEY is not set!');
|
||||||
if (!AUTH_TOKEN) console.error('WARNING: PROXY_AUTH_TOKEN is not set! Authentication is disabled.');
|
if (!AUTH_TOKEN) console.error('WARNING: PROXY_AUTH_TOKEN is not set! Authentication is disabled.');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue