import express from "express";
import path from "path";
import { fileURLToPath } from "url";
//import dotenv from "dotenv";
import cookieParser from "cookie-parser";
import {
createRepo,
uploadFiles,
whoAmI,
spaceInfo,
fileExists,
} from "@huggingface/hub";
import bodyParser from "body-parser";
import { PROVIDERS } from "./utils/providers.js";
import { COLORS } from "./utils/colors.js";
import { TEMPLATES, CDN_URLS } from "./utils/templates.js";
// Load environment variables from .env file
import dotenv from "dotenv";
// Hanya load .env jika BUKAN di environment production
if (process.env.NODE_ENV !== 'production') {
console.log("Memuat environment variables dari .env (non-production)...");
dotenv.config();
} else {
console.log("Mode production, mengharapkan environment variables diset langsung.");
}
// Detect Vercel environment - Vercel automatically sets the VERCEL environment variable
const isVercelEnvironment = process.env.VERCEL === '1' || process.env.VERCEL === 'true' || !!process.env.VERCEL;
// IP access limit - No limit if not configured or value <= 0
const IP_RATE_LIMIT = parseInt(process.env.IP_RATE_LIMIT) || 0;
// Cache for storing IP access records
const ipRequestCache = {};
const app = express();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const MODEL_ID = process.env.OPENAI_MODEL || "gpt-4o";
const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";
const DEFAULT_MAX_TOKENS = process.env.DEFAULT_MAX_TOKENS || 64000;
const DEFAULT_TEMPERATURE = process.env.DEFAULT_TEMPERATURE || 0;
app.use(cookieParser());
app.use(bodyParser.json());
// Optimize static file path handling
const staticPath = isVercelEnvironment ? path.join(process.cwd(), "dist") : path.join(__dirname, "dist");
app.use(express.static(staticPath));
// IP rate limiting middleware - Check the access frequency of each IP
app.use((req, res, next) => {
// If the limit is not configured or the limit value is <= 0, skip the rate limit check
if (IP_RATE_LIMIT <= 0) {
req.rateLimit = { limited: false };
return next();
}
// Get the client IP address
const clientIp = req.headers['x-forwarded-for'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress;
// Static resource requests are not counted towards the limit
if (req.path.startsWith('/assets/') ||
req.path.endsWith('.js') ||
req.path.endsWith('.css') ||
req.path.endsWith('.ico') ||
req.path.endsWith('.png') ||
req.path.endsWith('.jpg') ||
req.path.endsWith('.svg')) {
req.rateLimit = { limited: false };
return next();
}
const now = Date.now();
const hourAgo = now - 3600000; // Timestamp 1 hour ago
// Initialize IP record
if (!ipRequestCache[clientIp]) {
ipRequestCache[clientIp] = [];
}
// Clean up request records older than 1 hour
ipRequestCache[clientIp] = ipRequestCache[clientIp].filter(timestamp => timestamp > hourAgo);
// Calculate the current number of requests and remaining requests
const requestCount = ipRequestCache[clientIp].length;
const remainingRequests = IP_RATE_LIMIT - requestCount;
// Add rate limit information to the request object for subsequent processing
req.rateLimit = {
limited: requestCount >= IP_RATE_LIMIT,
requestCount,
remainingRequests,
clientIp
};
// Check if the limit is exceeded
if (req.rateLimit.limited) {
// Find the earliest request time, calculate when the next request can be made
const oldestRequest = Math.min(...ipRequestCache[clientIp]);
const resetTime = oldestRequest + 3600000; // Earliest request time + 1 hour
const waitTimeMs = resetTime - now;
const waitTimeMinutes = Math.ceil(waitTimeMs / 60000); // Convert to minutes and round up
// Get the client's possible language settings
const clientLang = req.headers['accept-language'] || 'en';
const isIdClient = clientLang.toLowerCase().includes('id');
console.log(`Rate limit exceeded for IP: ${clientIp}, can try again in ${waitTimeMinutes} minutes`);
// Return the appropriate message based on the language
// Note: The Indonesian message remains as requested by the original code logic based on client language.
const message = isIdClient
? `Request frequency exceeds the limit, please try again in ${waitTimeMinutes} minutes` // Translated from Chinese
: `Too many requests. Please try again in ${waitTimeMinutes} minutes.`;
return res.status(429).send({
ok: false,
message: message,
waitTimeMinutes: waitTimeMinutes,
resetTime: resetTime
});
}
// Record the timestamp of this request (only record in middleware to avoid double counting)
ipRequestCache[clientIp].push(now);
// Periodically clean up expired IP records (hourly)
if (!global.ipCacheCleanupInterval) {
global.ipCacheCleanupInterval = setInterval(() => {
const cleanupTime = Date.now() - 3600000;
for (const ip in ipRequestCache) {
ipRequestCache[ip] = ipRequestCache[ip].filter(timestamp => timestamp > cleanupTime);
// If there are no records, delete the cache for this IP
if (ipRequestCache[ip].length === 0) {
delete ipRequestCache[ip];
}
}
console.log(`IP cache cleanup completed. Active IPs: ${Object.keys(ipRequestCache).length}`);
}, 3600000);
}
next();
});
const getPTag = (repoId) => {
return `
Made with
DeepSite - 🧬 Remix
`;
};
// Get all available templates
app.get("/api/templates", (req, res) => {
const templates = Object.keys(TEMPLATES).map(key => ({
id: key,
name: TEMPLATES[key].name,
description: TEMPLATES[key].description,
}));
return res.status(200).send({
ok: true,
templates,
});
});
// Get detailed information for the specified template
app.get("/api/templates/:id", (req, res) => {
const { id } = req.params;
if (!TEMPLATES[id]) {
return res.status(404).send({
ok: false,
message: "Template not found",
});
}
// Templates now directly reference CDN_URLS, no need to replace variables
const html = TEMPLATES[id].html;
return res.status(200).send({
ok: true,
template: {
id,
name: TEMPLATES[id].name,
description: TEMPLATES[id].description,
systemPrompt: TEMPLATES[id].systemPrompt,
html: html
},
});
});
// API to check the configuration status of OpenAI environment variables
app.get("/api/check-env", (req, res) => {
// Check if each environment variable is configured
const apiKeyConfigured = !!process.env.OPENAI_API_KEY;
const baseUrlConfigured = !!process.env.OPENAI_BASE_URL;
const modelConfigured = !!process.env.OPENAI_MODEL;
const ipRateLimitConfigured = !!process.env.IP_RATE_LIMIT && parseInt(process.env.IP_RATE_LIMIT) > 0;
return res.status(200).send({
ok: true,
env: {
apiKey: apiKeyConfigured,
baseUrl: baseUrlConfigured,
model: modelConfigured,
ipRateLimit: ipRateLimitConfigured
},
model: process.env.OPENAI_MODEL || "",
ipRateLimit: parseInt(process.env.IP_RATE_LIMIT) || 0
});
});
// Test API connection
app.post("/api/test-connection", async (req, res) => {
const { api_key, base_url, model } = req.body;
try {
// Prioritize user-provided parameters, otherwise use environment variables
const apiKey = api_key || process.env.OPENAI_API_KEY;
if (!apiKey) {
return res.status(400).send({
ok: false,
message: "API key is required for testing",
});
}
const baseUrl = base_url || OPENAI_BASE_URL;
const modelId = model || MODEL_ID;
// Build a simple test request
const requestOptions = {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`
},
body: JSON.stringify({
model: modelId,
messages: [
{
role: "user",
content: "hi",
},
],
max_tokens: 50, // Limit return length to speed up testing
temperature: 0 // Fixed return result
})
};
console.log("Testing OpenAI API connection");
console.log(`Testing API at: ${baseUrl}`);
console.log(`Testing model: ${modelId}`);
const response = await fetch(`${baseUrl}/chat/completions`, requestOptions);
if (!response.ok) {
const errorData = await response.json();
return res.status(response.status).send({
ok: false,
message: errorData.error?.message || "Connection test failed",
});
}
const data = await response.json();
// Validate if the response contains valid content
if (data && data.choices && data.choices[0] && data.choices[0].message) {
return res.status(200).send({
ok: true,
message: "Connection test successful",
response: data.choices[0].message.content
});
} else {
return res.status(500).send({
ok: false,
message: "Received invalid response format"
});
}
} catch (error) {
console.error("Error testing connection:", error);
return res.status(500).send({
ok: false,
message: error.message || "An error occurred during connection test",
});
}
});
// Interface for optimizing prompts
app.post("/api/optimize-prompt", async (req, res) => {
const {
prompt,
language,
api_key,
base_url,
model
} = req.body;
if (!prompt) {
return res.status(400).send({
ok: false,
message: "Missing prompt field",
});
}
try {
// Prioritize user-provided API KEY, if not provided, use the environment variable
const apiKey = api_key || process.env.OPENAI_API_KEY;
if (!apiKey) {
return res.status(500).send({
ok: false,
message: "OpenAI API key is not configured.",
});
}
// Prioritize user-provided BASE URL and Model
const baseUrl = base_url || OPENAI_BASE_URL;
const modelId = model || MODEL_ID;
// Set system prompt based on language
// Translating the Chinese prompt to English here
const systemPrompt = language === 'id'
? "You are a professional prompt optimization assistant. Your task is to improve the user's prompt to make it clearer, more specific, and more effective. Maintain the user's original intent but make the prompt more structured and easier for AI to understand. Output only the plain text of the optimized prompt without any Markdown syntax, explanations, comments, or additional markers. You may use
and spaces to format the text when necessary to improve readability." // Was originally in Chinese
: "You are a professional prompt optimization assistant. Your task is to improve the user's prompt to make it clearer, more specific, and more effective. Maintain the user's original intent but make the prompt more structured and easier for AI to understand. Output only the plain text of the optimized prompt without any Markdown syntax, explanations, comments, or additional markers. You may use
and spaces to format the text when necessary to improve readability."; // English version was already present
const messages = [
{
role: "system",
content: systemPrompt,
},
{
role: "user",
content: prompt,
},
];
const requestOptions = {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`
},
body: JSON.stringify({
model: modelId,
messages,
temperature: 0.7,
max_tokens: 2000
})
};
console.log("Sending prompt optimization request to OpenAI API");
console.log(`Using API at: ${baseUrl}`);
console.log(`Using model: ${modelId}`);
const response = await fetch(`${baseUrl}/chat/completions`, requestOptions);
if (!response.ok) {
console.error(`OpenAI API error: ${response.status} ${response.statusText}`);
try {
const error = await response.json();
return res.status(response.status).send({
ok: false,
message: error?.message || "Error calling OpenAI API",
});
} catch (parseError) {
return res.status(response.status).send({
ok: false,
message: `OpenAI API error: ${response.status} ${response.statusText}`,
});
}
}
const data = await response.json();
const optimizedPrompt = data.choices?.[0]?.message?.content?.trim();
return res.status(200).send({
ok: true,
optimizedPrompt,
});
} catch (error) {
console.error("Error optimizing prompt:", error);
return res.status(500).send({
ok: false,
message: error.message || "An error occurred while optimizing the prompt",
});
}
});
app.post("/api/deploy", async (req, res) => {
const { html, title } = req.body;
if (!html || !title) {
return res.status(400).send({
ok: false,
message: "Missing required fields",
});
}
return res.status(200).send({
ok: true,
message: "Deployment feature has been removed as it required Hugging Face login",
});
});
app.post("/api/ask-ai", async (req, res) => {
const {
prompt,
html,
previousPrompt,
templateId,
language,
ui,
tools,
max_tokens,
temperature,
api_key,
base_url,
model
} = req.body;
if (!prompt) {
return res.status(400).send({
ok: false,
message: "Missing required fields",
});
}
// Get client IP - for logging purposes
const clientIp = req.headers['x-forwarded-for'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress;
// Use the rate limit information already calculated by the middleware for logging
if (req.rateLimit) {
if (req.rateLimit.limited === false) {
console.log(`API request from IP: ${clientIp}, rate limit: unlimited or not applicable`);
} else {
console.log(`API request from IP: ${clientIp}, requests this hour: ${req.rateLimit.requestCount}/${IP_RATE_LIMIT}, remaining: ${req.rateLimit.remainingRequests}`);
}
} else {
console.log(`API request from IP: ${clientIp}, rate limit information not available`);
}
// Set response headers
res.setHeader("Content-Type", "text/plain");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
// Add the following response headers to optimize streaming
res.setHeader("Transfer-Encoding", "chunked");
res.setHeader("X-Accel-Buffering", "no"); // Disable Nginx buffering
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("Keep-Alive", "timeout=120"); // Keep the connection alive for 120 seconds
res.flushHeaders(); // Send response headers immediately
const selectedProvider = PROVIDERS["openai"];
try {
// Prioritize user-provided API KEY, if not provided, use the environment variable
const apiKey = api_key || process.env.OPENAI_API_KEY;
if (!apiKey) {
return res.status(500).send({
ok: false,
message: "OpenAI API key is not configured.",
});
}
// Prioritize user-provided BASE URL, if not provided, use the environment variable
const baseUrl = base_url || OPENAI_BASE_URL;
// Prioritize user-provided Model, if not provided, use the environment variable
const modelId = model || MODEL_ID;
console.log(`Using OpenAI API at: ${baseUrl}`);
console.log(`Using model: ${modelId}`);
// Get the base system prompt
let systemPrompt = templateId && TEMPLATES[templateId]
? TEMPLATES[templateId].systemPrompt
: TEMPLATES.vanilla.systemPrompt;
// If a component library is selected, add related prompts only when the Vue3 framework is chosen
if (ui && ui !== templateId && templateId === 'vue3') {
const uiTemplate = TEMPLATES[ui];
if (uiTemplate) {
// Extract key parts from the component library prompt and add them to the system prompt
systemPrompt += ` Also, use ${uiTemplate.name} component library with CDN: `;
if (ui === 'elementPlus') {
systemPrompt += `CSS: ${CDN_URLS.ELEMENT_PLUS_CSS}, JS: ${CDN_URLS.ELEMENT_PLUS_JS}, Icons: ${CDN_URLS.ELEMENT_PLUS_ICONS}.`;
} else if (ui === 'naiveUI') {
systemPrompt += `${CDN_URLS.NAIVE_UI}.`;
}
}
}
// If a tool library is selected, add related prompts
if (tools && tools.length > 0) {
systemPrompt += " Include the following additional libraries: ";
tools.forEach((tool, index) => {
if (tool === 'tailwindcss') {
systemPrompt += `Tailwind CSS (use )`;
} else if (tool === 'vueuse') {
systemPrompt += `VueUse (use and )`;
} else if (tool === 'dayjs') {
systemPrompt += `Day.js (use )`;
} else if (tool === 'element-plus-icons') {
systemPrompt += `Element Plus Icons (use )`;
}
if (index < tools.length - 1) {
systemPrompt += ", ";
}
});
systemPrompt += ". Make sure to use the correct syntax for all the frameworks and libraries.";
}
// Add comment language prompt based on language setting
if (language === 'id') {
// Translating the Chinese instruction to English
systemPrompt += " Please write all comments in Indonesian."; // Was originally "Silakan tulis semua komentar dalam Bahasa Indonesia"
} else if (language === 'en') {
systemPrompt += " Please write all comments in English."; // Already in English
}
// Log output for selected configuration
console.log("Template configuration:");
console.log(`- Framework: ${templateId}`);
console.log(`- UI Library: ${ui || 'None'}`);
console.log(`- Tools: ${tools ? tools.join(', ') : 'None'}`);
console.log(`- Language: ${language || 'default'}`);
const messages = [
{
role: "system",
content: systemPrompt,
},
];
if (previousPrompt) {
messages.push({
role: "user",
content: previousPrompt,
});
}
if (html) {
messages.push({
role: "assistant",
content: `The current code is: ${html}.`,
});
}
messages.push({
role: "user",
content: prompt,
});
const requestOptions = {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`
},
body: JSON.stringify({
model: modelId,
messages,
stream: true,
max_tokens: max_tokens || parseInt(DEFAULT_MAX_TOKENS),
temperature: temperature !== undefined ? parseFloat(temperature) : parseFloat(DEFAULT_TEMPERATURE)
})
};
console.log(`Sending request to OpenAI API with model: ${modelId}`);
console.log(`Using max_tokens: ${max_tokens || DEFAULT_MAX_TOKENS}, temperature: ${temperature !== undefined ? temperature : DEFAULT_TEMPERATURE}`);
console.log("Request URL:", `${baseUrl}/chat/completions`);
console.log("Request headers:", {
...requestOptions.headers,
"Authorization": "Bearer [API_KEY_HIDDEN]"
});
console.log("Request body:", JSON.parse(requestOptions.body));
const response = await fetch(`${baseUrl}/chat/completions`, requestOptions);
if (!response.ok) {
console.error(`OpenAI API error: ${response.status} ${response.statusText}`);
try {
// Check if Content-Type is JSON
const contentType = response.headers.get("Content-Type");
console.log(`Response Content-Type: ${contentType}`);
if (contentType && contentType.includes("application/json")) {
const error = await response.json();
console.error("OpenAI API error details:", error);
return res.status(response.status).send({
ok: false,
message: error?.message || "Error calling OpenAI API",
});
} else {
// If not JSON, read text directly
const errorText = await response.text();
console.error("OpenAI API error text:", errorText);
return res.status(response.status).send({
ok: false,
message: errorText || `OpenAI API error: ${response.status} ${response.statusText}`,
});
}
} catch (parseError) {
// Handle JSON parsing errors
console.error("Error parsing API response:", parseError);
return res.status(response.status).send({
ok: false,
message: `OpenAI API error: ${response.status} ${response.statusText}`,
});
}
}
// Process OpenAI stream response
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let completeResponse = "";
console.log("Starting to process stream response");
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log("Stream completed");
break;
}
// Check if the client has already disconnected
if (res.writableEnded) {
console.log("Client disconnected, stopping stream");
reader.cancel("Client disconnected");
break;
}
// Parse SSE format data
const chunk = decoder.decode(value);
// Try to handle different formats of stream responses
const lines = chunk.split("\n");
let processedAnyLine = false;
for (const line of lines) {
// Standard OpenAI format
if (line.startsWith("data: ")) {
processedAnyLine = true;
if (line.includes("[DONE]")) {
console.log("Received [DONE] signal");
continue;
}
try {
const data = JSON.parse(line.replace("data: ", ""));
const content = data.choices?.[0]?.delta?.content || "";
if (content) {
// Check if the connection is interrupted
if (!res.writableEnded) {
res.write(content);
completeResponse += content;
if (completeResponse.includes("