Files
2026-07-13 13:30:30 +08:00

398 lines
14 KiB
JavaScript

/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const express = require('express');
const path = require('path');
const fs = require('fs');
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
// Import the new Google Gen AI SDK
const { GoogleGenAI } = require('@google/genai');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 8080;
// Middleware & Static File Serving
app.use(express.json({ limit: '50mb' }));
app.use(express.static(path.join(__dirname)));
// --- API ENDPOINTS ---
// 1. Endpoint to securely load the YouTube study data from a private file
app.get('/api/study/veo-youtube-study', (req, res) => {
const filePath = path.join(__dirname, 'data', 'veo-youtube-study.json');
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error("Error reading study file:", err);
return res.status(500).json({ error: "Could not load the study data." });
}
try {
const jsonData = JSON.parse(data);
res.json(jsonData);
} catch (parseErr) {
console.error("Error parsing study JSON:", parseErr);
return res.status(500).json({ error: "Study data is corrupted." });
}
});
});
// 2. Endpoint to proxy video URLs to avoid CORS issues
app.get('/api/proxy-video', async (req, res) => {
const videoUrl = req.query.url;
if (!videoUrl) {
return res.status(400).json({ error: 'URL query parameter is required.' });
}
try {
console.log(`Proxying video from: ${videoUrl}`);
const videoResponse = await fetch(videoUrl);
if (!videoResponse.ok) {
throw new Error(`Failed to fetch video with status: ${videoResponse.statusText}`);
}
const contentType = videoResponse.headers.get('content-type');
if (contentType) res.setHeader('Content-Type', contentType);
videoResponse.body.pipe(res);
} catch (error) {
console.error('Error proxying video:', error.message);
res.status(500).json({ error: `Failed to proxy video. Reason: ${error.message}` });
}
});
// 3. Fast validation endpoint using Gemini Flash model
app.post('/api/validate-token', async (req, res) => {
const { projectId, accessToken, location } = req.body;
const validationLocation = location || 'us-central1';
console.log(`Fast validation using Flash for project: ${projectId} in location: ${validationLocation}`);
if (projectId && !accessToken) {
return res.status(400).json({
valid: false,
message: 'Access Token is required for validation.'
});
}
// Store original env variable to restore later
const originalAuthToken = process.env.GOOGLE_AUTH_TOKEN;
try {
let ai;
if (accessToken) {
// Access token validation
process.env.GOOGLE_AUTH_TOKEN = accessToken;
ai = new GoogleGenAI({
vertexai: true,
project: projectId,
location: validationLocation
});
} else {
// API key validation
const apiKey = process.env.API_KEY || process.env.GEMINI_API_KEY;
if (!apiKey) {
return res.json({
valid: false,
message: 'No API key configured on server.'
});
}
ai = new GoogleGenAI({
vertexai: false,
apiKey: apiKey
});
}
console.log('Running fast validation with gemini-2.0-flash...');
// Use Flash model for much faster validation
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash',
contents: [{
role: "user",
parts: [{ text: "1+1" }] // Minimal math question
}],
generationConfig: {
maxOutputTokens: 1,
temperature: 0
}
});
// If we get here, credentials are valid
console.log('Flash validation successful');
res.json({
valid: true,
message: 'Credentials validated successfully!'
});
} catch (error) {
console.error('Validation error:', error.message);
// Use a generic error message for all credential-related failures
res.json({
valid: false,
message: 'Invalid credentials. Please check your configuration.'
});
} finally {
// Always restore the original environment variable
if (originalAuthToken !== undefined) {
process.env.GOOGLE_AUTH_TOKEN = originalAuthToken;
} else {
delete process.env.GOOGLE_AUTH_TOKEN;
}
}
});
// 4. Main Gemini API proxy endpoint using SDK
app.post('/api/generate', async (req, res) => {
console.log('========== Generate endpoint called ==========');
const { authMethod, accessToken, projectId, location, systemPrompt, contentParts } = req.body;
const model = 'gemini-2.5-pro';
// Debug logging
console.log('Request received with:', {
authMethod,
hasToken: !!accessToken,
hasProjectId: !!projectId,
location: location || 'us-central1',
systemPromptLength: systemPrompt?.length || 0,
contentPartsCount: contentParts?.length || 0
});
try {
let ai;
let response;
if (authMethod === 'access-token') {
// === ACCESS TOKEN PATH ===
if (!projectId || !accessToken) {
return res.status(400).json({
error: "Project ID and Access Token are required for gcloud auth.",
validationError: true
});
}
const vertexLocation = location || 'us-central1';
console.log(`Using Gen AI SDK with access token for project: ${projectId} in location: ${vertexLocation}`);
// The SDK can use access tokens through environment variables
// Store the original value to restore later
const originalAuthToken = process.env.GOOGLE_AUTH_TOKEN;
try {
// Set the access token as environment variable for the SDK
process.env.GOOGLE_AUTH_TOKEN = accessToken;
// Initialize SDK with Vertex AI configuration
// The SDK will automatically use the GOOGLE_AUTH_TOKEN we just set
ai = new GoogleGenAI({
vertexai: true,
project: projectId,
location: vertexLocation
});
// Build parts array
const parts = [];
// Add text prompt first
if (systemPrompt) {
parts.push({ text: systemPrompt });
console.log('Added system prompt to parts');
}
// Add any images/videos from contentParts
if (contentParts && contentParts.length > 0) {
contentParts.forEach((part, index) => {
if (part.inlineData && part.inlineData.data && part.inlineData.mimeType) {
parts.push({
inlineData: {
mimeType: part.inlineData.mimeType,
data: part.inlineData.data
}
});
console.log(`Added media to parts: ${part.inlineData.mimeType}`);
}
});
}
console.log(`Sending ${parts.length} parts via SDK to Vertex AI in ${vertexLocation}`);
// Generate content using the SDK
response = await ai.models.generateContent({
model: model,
contents: [{
role: "user",
parts: parts
}]
});
const text = response.text;
console.log('Response received, text length:', text.length);
res.json({ text: text.trim() });
} finally {
// Always restore the original environment variable
if (originalAuthToken !== undefined) {
process.env.GOOGLE_AUTH_TOKEN = originalAuthToken;
} else {
delete process.env.GOOGLE_AUTH_TOKEN;
}
}
} else {
// === API KEY PATH ===
const apiKey = process.env.API_KEY || process.env.GEMINI_API_KEY;
if (!apiKey) {
return res.status(500).json({
error: 'API Key is not configured on the server. Please check your .env file.',
validationError: true
});
}
console.log('Using Gen AI SDK with API key');
// Initialize SDK with API key
ai = new GoogleGenAI({
vertexai: false,
apiKey: apiKey
});
// Build parts array
const parts = [];
// Add text prompt first
if (systemPrompt) {
parts.push({ text: systemPrompt });
console.log('Added system prompt to parts');
}
// Add any images/videos from contentParts
if (contentParts && contentParts.length > 0) {
contentParts.forEach((part, index) => {
if (part.inlineData && part.inlineData.data && part.inlineData.mimeType) {
parts.push({
inlineData: {
mimeType: part.inlineData.mimeType,
data: part.inlineData.data
}
});
console.log(`Added media to parts: ${part.inlineData.mimeType}`);
}
});
}
console.log(`Sending ${parts.length} parts via SDK to Gemini API`);
// Generate content using the SDK
response = await ai.models.generateContent({
model: model,
contents: [{
role: "user",
parts: parts
}]
});
const text = response.text;
console.log('Response received, text length:', text.length);
res.json({ text: text.trim() });
}
} catch (error) {
console.error('Generation error:', error);
// Use generic error message for validation errors
if (error.message?.includes('401') || error.message?.includes('Unauthorized') || error.message?.includes('invalid')) {
return res.status(401).json({
error: 'Invalid credentials. Please check your configuration.',
validationError: true
});
}
if (error.message?.includes('API_KEY_INVALID')) {
return res.status(401).json({
error: 'Invalid credentials. Please check your configuration.',
validationError: true
});
}
if (error.message?.includes('403') || error.message?.includes('Permission denied')) {
return res.status(403).json({
error: 'Invalid credentials. Please check your configuration.',
validationError: true
});
}
if (error.message?.includes('404') || error.message?.includes('not found')) {
return res.status(404).json({
error: 'Invalid credentials. Please check your configuration.',
validationError: true
});
}
if (error.message?.includes('quota')) {
return res.status(429).json({
error: 'API quota exceeded. Please try again later.'
});
}
if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
return res.status(500).json({
error: 'Cannot connect to Google services. Check your internet connection.',
validationError: true
});
}
res.status(500).json({
error: 'An internal server error occurred: ' + error.message
});
}
});
// Test endpoint to verify server is running
app.get('/api/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
endpoints: [
'/api/health',
'/api/validate-token',
'/api/generate',
'/api/proxy-video',
'/api/study/veo-youtube-study'
],
sdkVersion: {
'@google/genai': require('@google/genai/package.json').version
}
});
});
// Root route to serve index.html
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
// --- Server Start ---
app.listen(port, '0.0.0.0', () => {
console.log(`Server listening at http://localhost:${port}`);
console.log('Environment variables configured:');
console.log(' API_KEY/GEMINI_API_KEY:', !!(process.env.API_KEY || process.env.GEMINI_API_KEY));
console.log(' GOOGLE_CLOUD_PROJECT:', process.env.GOOGLE_CLOUD_PROJECT || 'Not set');
console.log(' GOOGLE_CLOUD_LOCATION:', process.env.GOOGLE_CLOUD_LOCATION || 'Not set (will use UI selection)');
});