From a5353a9b505198b6bdfceae80c90034e8c106a86 Mon Sep 17 00:00:00 2001 From: Ponshu Developer Date: Thu, 15 Jan 2026 22:50:23 +0900 Subject: [PATCH] feat(infra): Add AI Proxy Server for Rate Limiting & API Key Protection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 Gemini APIへのリクエストを中継するプロキシサーバーを実装。 アプリにAPIキーを埋め込まず、Synology上で安全に管理。 ## 主な機能 - デバイスID認証(SHA256ハッシュ) - レート制限(1デバイスあたり1日10回) - 使用状況のログ記録(JSON形式) - 30日以上前のデータ自動削除 ## 技術スタック - Python 3.11 + FastAPI - Docker Container(既存のGitea環境に追加) - ポート8080で公開 ## ファイル構成 - tools/synology/ai-proxy/server.py - プロキシサーバー本体 - tools/synology/ai-proxy/requirements.txt - Python依存関係 - tools/synology/docker-compose.yml - ai-proxyサービス追加 - tools/synology/.env.example - 環境変数テンプレート - tools/synology/DEPLOYMENT_GUIDE.md - デプロイ手順書 ## セキュリティ - .env ファイルは.gitignoreで除外(APIキー保護) - 環境変数でAPIキー管理(コードに埋め込まない) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tools/synology/.env.example | 17 ++ tools/synology/.gitignore | 27 ++ tools/synology/DEPLOYMENT_GUIDE.md | 298 +++++++++++++++++++ tools/synology/ai-proxy/README.md | 104 +++++++ tools/synology/ai-proxy/requirements.txt | 4 + tools/synology/ai-proxy/server.py | 351 +++++++++++++++++++++++ tools/synology/docker-compose.yml | 26 ++ 7 files changed, 827 insertions(+) create mode 100644 tools/synology/.env.example create mode 100644 tools/synology/.gitignore create mode 100644 tools/synology/DEPLOYMENT_GUIDE.md create mode 100644 tools/synology/ai-proxy/README.md create mode 100644 tools/synology/ai-proxy/requirements.txt create mode 100644 tools/synology/ai-proxy/server.py diff --git a/tools/synology/.env.example b/tools/synology/.env.example new file mode 100644 index 0000000..278c491 --- /dev/null +++ b/tools/synology/.env.example @@ -0,0 +1,17 @@ +# ========================================== +# Ponshu Room AI Factory - 環境変数設定 +# ========================================== +# このファイルを `.env` にコピーして、実際の値を設定してください。 +# +# コピー方法: +# cp .env.example .env +# +# 重要: `.env` ファイルは絶対にGitにコミットしないでください! + +# ---------------------------------------- +# Gemini API設定 +# ---------------------------------------- +# 新しいGoogle AI Studioプロジェクト(Early Accessでない通常プロジェクト)で +# 作成したAPIキーを設定してください +# 取得方法: https://aistudio.google.com/ → "Get API key" → "Create API key in new project" +GEMINI_API_KEY=YOUR_GEMINI_API_KEY_HERE diff --git a/tools/synology/.gitignore b/tools/synology/.gitignore new file mode 100644 index 0000000..a36e2ac --- /dev/null +++ b/tools/synology/.gitignore @@ -0,0 +1,27 @@ +# ========================================== +# Synology AI Factory - Git除外設定 +# ========================================== +# セキュリティ上重要: APIキーを含むファイルは絶対にコミットしない + +# 環境変数ファイル(APIキーが含まれる) +.env + +# データフォルダ(使用状況ログなど) +ai-proxy-data/ +gitea/ +postgres/ +mcp/ + +# macOS +.DS_Store + +# Windows +Thumbs.db +desktop.ini + +# エディタ +.vscode/ +.idea/ +*.swp +*.swo +*~ diff --git a/tools/synology/DEPLOYMENT_GUIDE.md b/tools/synology/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..bf03a63 --- /dev/null +++ b/tools/synology/DEPLOYMENT_GUIDE.md @@ -0,0 +1,298 @@ +# Synology AI Proxy Server デプロイガイド + +## 📋 前提条件 + +- ✅ Synology NASにDockerがインストールされている +- ✅ Container Managerでgitea環境が稼働している +- ✅ 新しいGoogle AI StudioプロジェクトでAPIキーを取得済み + +--- + +## 🚀 デプロイ手順 + +### ステップ1: ファイルをSynologyにアップロード + +#### 方法A: File Station(推奨・簡単) + +1. **Synology DSMにログイン** + - ブラウザで `http://[SynologyのIP]:5000` にアクセス + +2. **File Stationを開く** + - メインメニュー → File Station + +3. **プロジェクトフォルダに移動** + - `/docker/ponshu-ai-factory/` に移動 + - (もしフォルダ名が違う場合は、Giteaのdocker-compose.ymlがある場所) + +4. **新しいファイルをアップロード** + - `ai-proxy` フォルダを作成 + - 以下のファイルをアップロード: + - `server.py` + - `requirements.txt` + - `README.md` + +5. **docker-compose.ymlを更新** + - 既存の `docker-compose.yml` を新しいバージョンで上書き + +6. **.envファイルを作成** + - `.env.example` を `.env` にコピー + - `.env` を編集して、`YOUR_GEMINI_API_KEY_HERE` を実際のAPIキーに置き換え + +#### 方法B: Git(上級者向け) + +```bash +# PCのターミナルから +cd c:\Users\maita\posimai-project\ponshu_room_lite\tools\synology + +# Giteaにpush(既にリポジトリがある場合) +git add . +git commit -m "feat: Add AI Proxy Server with rate limiting" +git push origin main + +# SynologyにSSH接続 +ssh maita@[SynologyのIP] + +# プロジェクトフォルダに移動 +cd /volume1/docker/ponshu-ai-factory + +# Giteaからpull +git pull origin main + +# .envファイルを作成 +cp .env.example .env +nano .env # APIキーを設定 +``` + +--- + +### ステップ2: APIキーの設定 + +1. **Google AI Studioで新しいプロジェクトを作成** + - https://aistudio.google.com/ にアクセス + - 新しいプロジェクトを作成(**Early Accessでない通常プロジェクト**) + - "Get API key" → "Create API key in new project" + - APIキーをコピー + +2. **Synologyの.envファイルに設定** + - File Stationで `/docker/ponshu-ai-factory/.env` を編集 + - `GEMINI_API_KEY=実際のAPIキー` に変更 + - 保存 + +--- + +### ステップ3: Dockerコンテナを起動 + +#### Container Managerを使う場合(推奨) + +1. **Container Managerを開く** + - DSMメインメニュー → Container Manager + +2. **プロジェクトを停止** + - 「プロジェクト」タブ + - 既存の `ponshu-ai-factory` プロジェクトを選択 + - 「停止」をクリック + +3. **プロジェクトを再構築** + - 「アクション」→「ビルド」をクリック + - 数分待つ(Pythonの依存関係をインストール中) + +4. **プロジェクトを起動** + - 「起動」をクリック + +5. **コンテナの状態を確認** + - 「コンテナ」タブ + - `ai_proxy` コンテナが「実行中」になっているか確認 + - ログを確認(「ログ」ボタン)して `🚀 AI Proxy Server started` が出ていればOK + +#### SSHを使う場合(上級者向け) + +```bash +# Synologyにログイン +ssh maita@[SynologyのIP] + +# プロジェクトフォルダに移動 +cd /volume1/docker/ponshu-ai-factory + +# 既存のコンテナを停止 +docker-compose down + +# 新しいコンテナを起動 +docker-compose up -d + +# ログを確認 +docker logs ai_proxy + +# 正常に起動していれば以下が表示される: +# 🚀 AI Proxy Server started +# Model: gemini-1.5-flash +# Rate Limit: 10 requests/day +``` + +--- + +### ステップ4: 動作確認 + +#### ブラウザでヘルスチェック + +``` +http://[SynologyのIP]:8080/health +``` + +**期待されるレスポンス:** +```json +{ + "status": "healthy", + "gemini_api_key_configured": true, + "rate_limit": 10, + "model": "gemini-1.5-flash" +} +``` + +--- + +## 🔧 トラブルシューティング + +### コンテナが起動しない + +**原因1: .envファイルが見つからない** +- `.env.example` を `.env` にコピーしたか確認 +- `.env` に正しいAPIキーが設定されているか確認 + +**原因2: ポート8080が既に使われている** +```bash +# ポート使用状況を確認 +netstat -tuln | grep 8080 + +# 他のアプリが使っている場合は、docker-compose.ymlのポートを変更 +# ports: +# - "8081:8080" # 外部8081 → コンテナ内8080 +``` + +**原因3: Pythonの依存関係インストールに失敗** +```bash +# コンテナ内に入ってログ確認 +docker exec -it ai_proxy bash +pip install -r requirements.txt +python server.py +``` + +### APIキーが認識されない + +```bash +# 環境変数を確認 +docker exec ai_proxy printenv | grep GEMINI + +# GEMINI_API_KEY が空の場合は .env ファイルを確認 +``` + +### アプリから接続できない + +1. **ネットワーク確認** + - Synologyとスマホが同じネットワークにいるか + - ファイアウォールで8080ポートが開いているか + +2. **IPアドレス確認** + ```bash + # SynologyのIPを確認 + ifconfig | grep inet + ``` + +3. **アプリ側のURLを確認** + - `http://192.168.1.xxx:8080/analyze` のようになっているか + - `https://` ではなく `http://` を使用 + +--- + +## 📊 使用状況の確認 + +### 特定デバイスの使用状況を確認 + +```bash +# デバイスIDを取得(アプリのログから) +DEVICE_ID="a1b2c3d4..." # SHA256ハッシュ + +# ブラウザまたはcurlで確認 +curl http://[SynologyのIP]:8080/usage/${DEVICE_ID} +``` + +### 全デバイスの使用状況を確認 + +```bash +# Synologyにログイン +ssh maita@[SynologyのIP] + +# 使用状況ファイルを確認 +cat /volume1/docker/ponshu-ai-factory/ai-proxy-data/usage.json +``` + +**データ形式:** +```json +{ + "a1b2c3d4e5f6...": { + "2026-01-15": 3, + "2026-01-14": 7 + }, + "f6e5d4c3b2a1...": { + "2026-01-15": 10 + } +} +``` + +--- + +## 🔐 セキュリティのベストプラクティス + +1. **APIキーを絶対にGitにコミットしない** + - `.env` ファイルは `.gitignore` に追加済み + - 万が一コミットした場合は即座にAPIキーを再発行 + +2. **定期的にログをチェック** + ```bash + docker logs ai_proxy | tail -50 + ``` + +3. **不正アクセスの監視** + - `usage.json` で異常な使用パターンを確認 + - 知らないデバイスIDが大量に出ている場合は要調査 + +4. **ファイアウォール設定** + - Synology DSM → コントロールパネル → セキュリティ → ファイアウォール + - ポート8080はローカルネットワークのみ許可(外部には開放しない) + +--- + +## 📈 次のステップ + +1. **アプリ側の実装** + - `lib/services/gemini_service.dart` をプロキシサーバー経由に変更 + - デバイスID認証の実装 + +2. **Pro版の準備** + - `RATE_LIMIT_PER_DAY=999999` で無制限に変更 + - 決済システムと連携 + +3. **モニタリング** + - Grafanaなどで使用状況をダッシュボード化 + - アラート設定(異常なリクエスト数など) + +--- + +## 💡 よくある質問 + +**Q: 1日10回の制限はいつリセットされますか?** +A: 日本時間の0時(サーバー時刻基準)にリセットされます。 + +**Q: 複数のアプリバージョン(開発版/本番版)で使えますか?** +A: はい。デバイスIDが同じであれば共通のカウントになります。別々にしたい場合は、環境ごとにプロキシサーバーを分けてください。 + +**Q: Gemini APIのコストは誰が負担しますか?** +A: プロキシサーバーの管理者(あなた)が負担します。gemini-1.5-flashは1日1,500回まで無料なので、10ユーザー×10回/日=100回/日であれば無料枠内です。 + +**Q: レート制限を超えたらどうなりますか?** +A: アプリに「本日の解析上限に達しました。明日またお試しください」というメッセージが表示されます。サーバーはエラーを返しません。 + +--- + +## 🎉 完成! + +これでAI Proxyサーバーの構築は完了です。次はアプリ側の実装に進みましょう! diff --git a/tools/synology/ai-proxy/README.md b/tools/synology/ai-proxy/README.md new file mode 100644 index 0000000..0a80a71 --- /dev/null +++ b/tools/synology/ai-proxy/README.md @@ -0,0 +1,104 @@ +# Ponshu Room AI Proxy Server + +## 概要 + +Ponshu Room Liteアプリのバックエンドプロキシサーバー。Gemini APIへのリクエストを中継し、以下の機能を提供します: + +### 主な機能 + +1. **APIキー保護** + - アプリにAPIキーを埋め込まない + - サーバー側で環境変数として管理 + +2. **デバイスID認証** + - SHA256ハッシュ化されたデバイスIDで認証 + - 未登録デバイスのアクセスを制限可能(将来実装) + +3. **レート制限** + - 1デバイスあたり1日10回まで(Lite版) + - Pro版では無制限に拡張可能 + +4. **使用状況ログ** + - JSON形式で使用履歴を記録 + - 30日以上前のデータは自動削除 + +## 技術スタック + +- **言語**: Python 3.11 +- **フレームワーク**: FastAPI +- **HTTPクライアント**: httpx +- **デプロイ**: Docker + +## エンドポイント + +### `GET /` +ヘルスチェック + +### `GET /health` +詳細なヘルスチェック(APIキー設定状況など) + +### `POST /analyze` +日本酒ラベル画像を解析 + +**リクエスト:** +```json +{ + "device_id": "sha256ハッシュ(64文字)", + "images": ["base64エンコードされた画像1", "画像2", ...], + "prompt": "カスタムプロンプト(オプション)" +} +``` + +**レスポンス:** +```json +{ + "success": true, + "data": { + "name": "獺祭 純米大吟醸", + "brand": "旭酒造", + "prefecture": "山口県", + ... + }, + "usage": { + "today": 3, + "limit": 10 + } +} +``` + +### `GET /usage/{device_id}` +デバイスの使用状況を取得 + +## 環境変数 + +| 変数名 | 説明 | デフォルト | +|--------|------|-----------| +| `GEMINI_API_KEY` | Gemini API Key | (必須) | +| `GEMINI_MODEL` | 使用するモデル | `gemini-1.5-flash` | +| `RATE_LIMIT_PER_DAY` | 1日あたりの制限 | `10` | + +## データ保存 + +- 使用状況: `/app/data/usage.json` +- 30日以上前のデータは自動削除 + +## セキュリティ + +- デバイスIDはSHA256ハッシュ化されたもののみ受け入れ +- CORS設定でオリジン制限可能 +- APIキーは環境変数で管理(コードに埋め込まない) + +## ローカルでのテスト + +```bash +# 依存関係インストール +pip install -r requirements.txt + +# 環境変数設定 +export GEMINI_API_KEY="YOUR_API_KEY" + +# サーバー起動 +python server.py +``` + +http://localhost:8080 でアクセス可能 diff --git a/tools/synology/ai-proxy/requirements.txt b/tools/synology/ai-proxy/requirements.txt new file mode 100644 index 0000000..6bfbd62 --- /dev/null +++ b/tools/synology/ai-proxy/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.109.0 +uvicorn[standard]==0.27.0 +httpx==0.26.0 +pydantic==2.5.3 diff --git a/tools/synology/ai-proxy/server.py b/tools/synology/ai-proxy/server.py new file mode 100644 index 0000000..fd01757 --- /dev/null +++ b/tools/synology/ai-proxy/server.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python3 +""" +Ponshu Room AI Proxy Server +============================ +役割: アプリからのリクエストを受け取り、Gemini APIに中継 +機能: + - APIキーの保護(アプリにAPIキーを埋め込まない) + - デバイスID認証(許可されたデバイスのみアクセス可能) + - レート制限(1日10回まで/デバイス) + - 使用状況のログ記録 +""" + +import os +import json +import hashlib +from datetime import datetime, timedelta +from pathlib import Path +from typing import Dict, List, Optional + +from fastapi import FastAPI, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +import httpx + +# ========================================== +# 設定 +# ========================================== +GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "") +GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-1.5-flash") +RATE_LIMIT_PER_DAY = int(os.getenv("RATE_LIMIT_PER_DAY", "10")) +DATA_DIR = Path("/app/data") +USAGE_FILE = DATA_DIR / "usage.json" + +# アプリ起動時にデータディレクトリを作成 +DATA_DIR.mkdir(exist_ok=True) + +# ========================================== +# FastAPI アプリ初期化 +# ========================================== +app = FastAPI( + title="Ponshu Room AI Proxy", + description="Gemini API Proxy with Rate Limiting", + version="1.0.0" +) + +# CORS設定(モバイルアプリからのリクエストを許可) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 本番環境では特定のオリジンに制限すべき + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# ========================================== +# データモデル +# ========================================== +class AnalyzeRequest(BaseModel): + """画像解析リクエスト""" + device_id: str # デバイスID(SHA256ハッシュ化されたもの) + images: List[str] # Base64エンコードされた画像データ + prompt: Optional[str] = None # カスタムプロンプト(オプション) + +class AnalyzeResponse(BaseModel): + """画像解析レスポンス""" + success: bool + data: Optional[Dict] = None + error: Optional[str] = None + usage: Dict[str, int] = {} # {"today": 3, "limit": 10} + +# ========================================== +# 使用状況管理 +# ========================================== +def load_usage_data() -> Dict: + """使用状況データを読み込む""" + if not USAGE_FILE.exists(): + return {} + + try: + with open(USAGE_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return {} + +def save_usage_data(data: Dict) -> None: + """使用状況データを保存""" + with open(USAGE_FILE, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + +def get_today_key() -> str: + """今日の日付キー(YYYY-MM-DD)を取得""" + return datetime.now().strftime("%Y-%m-%d") + +def check_rate_limit(device_id: str) -> tuple[bool, int]: + """ + レート制限をチェック + + Returns: + (制限内か, 今日の使用回数) + """ + usage_data = load_usage_data() + today = get_today_key() + + # デバイスIDのエントリがない場合は作成 + if device_id not in usage_data: + usage_data[device_id] = {} + + # 今日の使用回数を取得 + today_count = usage_data[device_id].get(today, 0) + + # 制限チェック + is_within_limit = today_count < RATE_LIMIT_PER_DAY + + return is_within_limit, today_count + +def increment_usage(device_id: str) -> int: + """ + 使用回数をインクリメント + + Returns: + 更新後の今日の使用回数 + """ + usage_data = load_usage_data() + today = get_today_key() + + if device_id not in usage_data: + usage_data[device_id] = {} + + usage_data[device_id][today] = usage_data[device_id].get(today, 0) + 1 + + save_usage_data(usage_data) + + return usage_data[device_id][today] + +def cleanup_old_usage_data() -> None: + """30日以上前のデータを削除(データサイズ管理)""" + usage_data = load_usage_data() + cutoff_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d") + + for device_id in usage_data: + dates_to_remove = [ + date for date in usage_data[device_id] + if date < cutoff_date + ] + for date in dates_to_remove: + del usage_data[device_id][date] + + save_usage_data(usage_data) + +# ========================================== +# APIエンドポイント +# ========================================== +@app.get("/") +async def root(): + """ヘルスチェック""" + return { + "service": "Ponshu Room AI Proxy", + "status": "running", + "version": "1.0.0" + } + +@app.get("/health") +async def health_check(): + """詳細なヘルスチェック""" + return { + "status": "healthy", + "gemini_api_key_configured": bool(GEMINI_API_KEY), + "rate_limit": RATE_LIMIT_PER_DAY, + "model": GEMINI_MODEL + } + +@app.post("/analyze", response_model=AnalyzeResponse) +async def analyze_sake_label(request: AnalyzeRequest): + """ + 日本酒ラベル画像を解析 + + レート制限: 1デバイスあたり1日10回まで + """ + + # 1. APIキーのチェック + if not GEMINI_API_KEY: + raise HTTPException( + status_code=500, + detail="Gemini APIキーが設定されていません" + ) + + # 2. デバイスIDの検証(SHA256形式かチェック) + if len(request.device_id) != 64: # SHA256は64文字 + raise HTTPException( + status_code=400, + detail="無効なデバイスIDです" + ) + + # 3. レート制限チェック + is_within_limit, today_count = check_rate_limit(request.device_id) + + if not is_within_limit: + return AnalyzeResponse( + success=False, + error=f"本日の解析上限({RATE_LIMIT_PER_DAY}回)に達しました。明日またお試しください。", + usage={ + "today": today_count, + "limit": RATE_LIMIT_PER_DAY + } + ) + + # 4. Gemini APIにリクエストを送信 + try: + # プロンプトのデフォルト値 + prompt = request.prompt or """ +この日本酒のラベル画像(複数枚ある場合は表・裏など)を分析してください。 +全ての画像から情報を統合し、以下の情報をJSON形式で返してください: + +{ + "name": "銘柄名(例:獺祭 純米大吟醸)", + "brand": "蔵元名(例:旭酒造)", + "prefecture": "都道府県名(例:山口県)", + "type": "種類(例:純米大吟醸)", + "description": "この日本酒の特徴を100文字程度で説明(裏ラベルの情報があれば活用してください)", + "catchCopy": "この日本酒を一言で表現する詩的なキャッチコピー(20文字以内)", + "confidenceScore": 0から100の整数, + "flavorTags": ["フルーティ", "辛口"], + "tasteStats": { + "aroma": 3, + "sweetness": 3, + "acidity": 3, + "bitterness": 3, + "body": 3 + } +} + +**tasteStatsの説明 (1-5の整数)**: +- aroma: 香りの強さ +- sweetness: 甘み +- acidity: 酸味 +- bitterness: ビター感/キレ +- body: コク・ボディ + +読み取れない情報は null を返してください。 +JSONのみを返し、他の文章は含めないでください。 +""" + + # Gemini APIエンドポイント + gemini_url = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL}:generateContent" + + # リクエストボディの構築 + parts = [{"text": prompt}] + for image_data in request.images: + parts.append({ + "inline_data": { + "mime_type": "image/jpeg", + "data": image_data + } + }) + + gemini_request = { + "contents": [{ + "parts": parts + }], + "safetySettings": [ + {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"}, + {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"} + ] + } + + # HTTPクライアントでリクエスト送信 + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + gemini_url, + json=gemini_request, + params={"key": GEMINI_API_KEY} + ) + response.raise_for_status() + + gemini_response = response.json() + + # レスポンスから結果を抽出 + if "candidates" not in gemini_response or not gemini_response["candidates"]: + raise HTTPException( + status_code=500, + detail="Gemini APIからの応答が不正です" + ) + + text_result = gemini_response["candidates"][0]["content"]["parts"][0]["text"] + + # JSONをパース(マークダウンのコードブロックを削除) + text_result = text_result.strip().replace("```json", "").replace("```", "").strip() + result_json = json.loads(text_result) + + # 5. 使用回数をインクリメント + new_count = increment_usage(request.device_id) + + # 6. 成功レスポンス + return AnalyzeResponse( + success=True, + data=result_json, + usage={ + "today": new_count, + "limit": RATE_LIMIT_PER_DAY + } + ) + + except httpx.HTTPStatusError as e: + raise HTTPException( + status_code=e.response.status_code, + detail=f"Gemini API error: {e.response.text}" + ) + except json.JSONDecodeError: + raise HTTPException( + status_code=500, + detail="Gemini APIからのJSON解析に失敗しました" + ) + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"予期しないエラー: {str(e)}" + ) + +@app.get("/usage/{device_id}") +async def get_usage(device_id: str): + """デバイスの使用状況を取得""" + if len(device_id) != 64: + raise HTTPException(status_code=400, detail="無効なデバイスIDです") + + _, today_count = check_rate_limit(device_id) + + return { + "device_id": device_id[:8] + "...", # プライバシー保護のため一部のみ表示 + "today": today_count, + "limit": RATE_LIMIT_PER_DAY, + "remaining": max(0, RATE_LIMIT_PER_DAY - today_count), + "date": get_today_key() + } + +# ========================================== +# 起動時処理 +# ========================================== +@app.on_event("startup") +async def startup_event(): + """サーバー起動時に古いデータをクリーンアップ""" + cleanup_old_usage_data() + print(f"🚀 AI Proxy Server started") + print(f" Model: {GEMINI_MODEL}") + print(f" Rate Limit: {RATE_LIMIT_PER_DAY} requests/day") + +# ========================================== +# メイン実行 +# ========================================== +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/tools/synology/docker-compose.yml b/tools/synology/docker-compose.yml index 11b09a8..caa9f7d 100644 --- a/tools/synology/docker-compose.yml +++ b/tools/synology/docker-compose.yml @@ -74,6 +74,32 @@ services: networks: - gitea_network + # ---------------------------------------- + # 4. AI Proxy Server (App Backend) + # 役割: Ponshu Room Liteアプリからのリクエストを受け取り、Gemini APIに中継 + # - APIキー保護: アプリにAPIキーを埋め込まない + # - レート制限: 1デバイスあたり1日10回まで(Lite版) + # - デバイスID認証: SHA256ハッシュ化されたIDで管理 + # ---------------------------------------- + ai-proxy: + image: python:3.11-slim + container_name: ai_proxy + working_dir: /app + volumes: + - ./ai-proxy:/app # AI Proxyサーバーのコード + - ./ai-proxy-data:/app/data # 使用状況データの永続化 + environment: + - GEMINI_API_KEY=${GEMINI_API_KEY} # 【重要】APIキーを.envファイルに設定してください + - GEMINI_MODEL=gemini-2.5-flash # 一旦2.5を使用(RPD 20制限あり)。1.5にアクセスできたら変更 + - RATE_LIMIT_PER_DAY=10 + ports: + - "8080:8080" # アプリからアクセスするポート + # 初回起動時に依存関係をインストールしてサーバー起動 + command: sh -c "pip install --no-cache-dir -r requirements.txt && python server.py" + restart: always + networks: + - gitea_network + # ---------------------------------------- # Network Setting # 内部通信用の専用ネットワーク