ponshu-room-lite/tools/proxy/server.js

130 lines
3.8 KiB
JavaScript
Raw Normal View History

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const { GoogleGenerativeAI } = require('@google/generative-ai');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 8080;
const API_KEY = process.env.GEMINI_API_KEY;
// Rate Limiting (Simple In-Memory)
const DAILY_LIMIT = parseInt(process.env.DAILY_LIMIT || '50', 10);
const usageStore = {}; // { deviceId: { date: 'YYYY-MM-DD', count: 0 } }
app.use(cors());
app.use(bodyParser.json({ limit: '10mb' })); // Allow large image payloads
// Gemini Client
const genAI = new GoogleGenerativeAI(API_KEY);
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); // Flutter側(gemini_service.dart)と統一
// Helper: Get Today's Date String (YYYY-MM-DD)
function getTodayString() {
return new Date().toISOString().split('T')[0];
}
// Helper: Check & Update Rate Limit
function checkRateLimit(deviceId) {
const today = getTodayString();
if (!usageStore[deviceId]) {
usageStore[deviceId] = { date: today, count: 0 };
}
// 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 {
allowed: remaining > 0,
current: currentUsage.count,
limit: DAILY_LIMIT,
remaining: remaining
};
}
// API Endpoint
app.post('/analyze', async (req, res) => {
const { device_id, images, prompt } = req.body;
if (!device_id) {
return res.status(400).json({ success: false, error: 'Device ID is required' });
}
// 1. Check Rate Limit
const limitStatus = checkRateLimit(device_id);
if (!limitStatus.allowed) {
console.log(`[Limit Reached] Device: ${device_id} (${limitStatus.current}/${limitStatus.limit})`);
return res.status(429).json({
success: false,
error: 'Daily limit reached',
usage: { today: limitStatus.current, limit: limitStatus.limit }
});
}
console.log(`[Request] Device: ${device_id} | Images: ${images ? images.length : 0}`);
try {
// 2. Prepare Gemini Request
// Base64 images to GenerativeContentBlob
const imageParts = (images || []).map(base64 => ({
inlineData: {
data: base64,
mimeType: "image/jpeg"
}
}));
const result = await model.generateContent([prompt, ...imageParts]);
const response = await result.response;
const text = response.text();
// 3. Parse JSON from Markdown (e.g. ```json ... ```)
const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/) || text.match(/```([\s\S]*?)```/);
let jsonData;
if (jsonMatch) {
jsonData = JSON.parse(jsonMatch[1]);
} else {
// Try parsing raw text if no code blocks
jsonData = JSON.parse(text);
}
// 4. Update Usage
usageStore[device_id].count++;
console.log(`[Success] Device: ${device_id} | Count: ${usageStore[device_id].count}/${DAILY_LIMIT}`);
// 5. Send Response
res.json({
success: true,
data: jsonData,
usage: {
today: usageStore[device_id].count,
limit: DAILY_LIMIT
}
});
} catch (error) {
console.error('Gemini API Error:', error);
res.status(500).json({
success: false,
error: error.message || 'Internal Server Error'
});
}
});
// Health Check
app.get('/health', (req, res) => {
res.send('OK');
});
// Start Server
app.listen(PORT, () => {
console.log(`Proxy Server running on port ${PORT}`);
if (!API_KEY) console.error('WARNING: GEMINI_API_KEY is not set!');
});