opensite / server.js
luoluoluo22's picture
直接在服务器提供静态页面,绕过构建问题
987f2a6
import express from "express";
import path from "path";
import { fileURLToPath } from "url";
import dotenv from "dotenv";
import cookieParser from "cookie-parser";
import fs from "fs"; // 使用 ESM 语法导入 fs 模块
import {
createRepo,
uploadFiles,
whoAmI,
spaceInfo,
fileExists,
} from "@huggingface/hub";
import { InferenceClient } from "@huggingface/inference";
import bodyParser from "body-parser";
import checkUser from "./middlewares/checkUser.js";
import {
PROVIDERS,
DEFAULT_SYSTEM_PROMPT,
convertToOpenAIFormat,
convertToHFFormat
} from "./utils/providers.js";
import { COLORS } from "./utils/colors.js";
// Load environment variables from .env file
dotenv.config();
// 设置环境变量如果不存在
process.env.APP_PORT = 7860;
// 打印环境变量(排除敏感信息)
console.log('===== 环境变量 =====');
console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('APP_PORT:', process.env.APP_PORT);
console.log('DEFAULT_MODEL_ID:', process.env.DEFAULT_MODEL_ID);
console.log('REDIRECT_URI:', process.env.REDIRECT_URI);
console.log('OPENAI_COMPATIBLE_ENDPOINT:', process.env.OPENAI_COMPATIBLE_ENDPOINT ? '已设置' : '未设置');
console.log('OPENAI_COMPATIBLE_MODEL_ID:', process.env.OPENAI_COMPATIBLE_MODEL_ID);
console.log('===================');
const app = express();
const ipAddresses = new Map();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Hugging Face Spaces 使用 7860 端口
const PORT = 7860;
const REDIRECT_URI =
process.env.REDIRECT_URI || `http://localhost:${PORT}/auth/login`;
const MODEL_ID = process.env.DEFAULT_MODEL_ID || "deepseek-ai/DeepSeek-V3-0324";
const MAX_REQUESTS_PER_IP = 4;
app.use((req, res, next) => {
console.log(`收到请求: ${req.method} ${req.url}`);
next();
});
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "dist"), {
index: false,
setHeaders: (res, filePath) => {
console.log(`提供静态文件: ${filePath}`);
// 移除可能导致问题的安全头部
res.removeHeader('Feature-Policy');
res.removeHeader('Permissions-Policy');
// 设置适当的内容安全策略
res.setHeader('Content-Security-Policy', "default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval';");
}
}));
app.use(bodyParser.json());
const getPTag = (repoId) => {
return `<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=${repoId}" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p>`;
};
app.get("/api/login", (_req, res) => {
res.redirect(
302,
`https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`
);
});
app.get("/auth/login", async (req, res) => {
const { code } = req.query;
if (!code) {
return res.redirect(302, "/");
}
const Authorization = `Basic ${Buffer.from(
`${process.env.OAUTH_CLIENT_ID}:${process.env.OAUTH_CLIENT_SECRET}`
).toString("base64")}`;
const request_auth = await fetch("https://huggingface.co/oauth/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization,
},
body: new URLSearchParams({
grant_type: "authorization_code",
code: code,
redirect_uri: REDIRECT_URI,
}),
});
const response = await request_auth.json();
if (!response.access_token) {
return res.redirect(302, "/");
}
res.cookie("hf_token", response.access_token, {
httpOnly: false,
secure: true,
sameSite: "none",
maxAge: 30 * 24 * 60 * 60 * 1000,
});
return res.redirect(302, "/");
});
app.get("/auth/logout", (req, res) => {
res.clearCookie("hf_token", {
httpOnly: false,
secure: true,
sameSite: "none",
});
return res.redirect(302, "/");
});
app.get("/api/@me", checkUser, async (req, res) => {
const { hf_token } = req.cookies;
try {
const request_user = await fetch("https://huggingface.co/oauth/userinfo", {
headers: {
Authorization: `Bearer ${hf_token}`,
},
});
const user = await request_user.json();
res.send(user);
} catch (err) {
res.clearCookie("hf_token", {
httpOnly: false,
secure: true,
sameSite: "none",
});
res.status(401).send({
ok: false,
message: err.message,
});
}
});
app.post("/api/deploy", checkUser, async (req, res) => {
const { html, title, path } = req.body;
if (!html || !title) {
return res.status(400).send({
ok: false,
message: "Missing required fields",
});
}
const { hf_token } = req.cookies;
try {
const repo = {
type: "space",
name: path ?? "",
};
let readme;
let newHtml = html;
if (!path || path === "") {
const { name: username } = await whoAmI({ accessToken: hf_token });
const newTitle = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.split("-")
.filter(Boolean)
.join("-")
.slice(0, 96);
const repoId = `${username}/${newTitle}`;
repo.name = repoId;
await createRepo({
repo,
accessToken: hf_token,
});
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
readme = `---
title: ${newTitle}
emoji: 🐳
colorFrom: ${colorFrom}
colorTo: ${colorTo}
sdk: static
pinned: false
tags:
- deepsite
---
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
}
newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
const file = new Blob([newHtml], { type: "text/html" });
file.name = "index.html"; // Add name property to the Blob
const files = [file];
if (readme) {
const readmeFile = new Blob([readme], { type: "text/markdown" });
readmeFile.name = "README.md"; // Add name property to the Blob
files.push(readmeFile);
}
await uploadFiles({
repo,
files,
accessToken: hf_token,
});
return res.status(200).send({ ok: true, path: repo.name });
} catch (err) {
return res.status(500).send({
ok: false,
message: err.message,
});
}
});
// 添加OpenAI兼容的流式响应处理函数
const handleOpenAIStream = async (response, res) => {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let completeResponse = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.trim() === '') continue;
if (line.trim() === 'data: [DONE]') break;
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
const content = data.choices[0]?.delta?.content;
if (content) {
res.write(content);
completeResponse += content;
if (completeResponse.includes("</html>")) {
return;
}
}
} catch (e) {
console.error('Error parsing SSE message:', e);
}
}
}
}
} catch (error) {
console.error('Stream reading error:', error);
throw error;
}
};
app.post("/api/ask-ai", async (req, res) => {
const { prompt, html, previousPrompt, provider, customConfig } = req.body;
if (!prompt) {
return res.status(400).send({
ok: false,
message: "Missing required fields",
});
}
const { hf_token } = req.cookies;
let token = hf_token;
const ip =
req.headers["x-forwarded-for"]?.split(",")[0].trim() ||
req.headers["x-real-ip"] ||
req.socket.remoteAddress ||
req.ip ||
"0.0.0.0";
if (!hf_token) {
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
return res.status(429).send({
ok: false,
openLogin: true,
message: "Log In to continue using the service",
});
}
token = process.env.DEFAULT_HF_TOKEN;
}
// Set up response headers for streaming
res.setHeader("Content-Type", "text/plain");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
let client;
let selectedProvider;
try {
if (provider === "custom" && customConfig) {
selectedProvider = {
...PROVIDERS.custom,
...customConfig,
};
const messages = [
{
role: "system",
content: selectedProvider.systemPrompt || DEFAULT_SYSTEM_PROMPT,
},
...(previousPrompt
? [
{
role: "user",
content: previousPrompt,
},
]
: []),
...(html
? [
{
role: "assistant",
content: `The current code is: ${html}.`,
},
]
: []),
{
role: "user",
content: prompt,
},
];
if (selectedProvider.apiType === 'openai') {
// 使用OpenAI兼容的API格式
const response = await fetch(`${selectedProvider.endpoint}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${selectedProvider.apiKey}`,
},
body: JSON.stringify(convertToOpenAIFormat(messages)),
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
await handleOpenAIStream(response, res);
} else {
// 使用HuggingFace格式
client = new InferenceClient({
endpoint: selectedProvider.endpoint,
token: selectedProvider.apiKey || token,
});
const chatCompletion = client.chatCompletionStream(convertToHFFormat(messages));
let completeResponse = "";
while (true) {
const { done, value } = await chatCompletion.next();
if (done) break;
const chunk = value.choices[0]?.delta?.content;
if (chunk) {
res.write(chunk);
completeResponse += chunk;
if (completeResponse.includes("</html>")) {
break;
}
}
}
}
} else {
// 使用默认配置
client = new InferenceClient(token);
selectedProvider = provider === "auto"
? PROVIDERS.novita
: PROVIDERS[provider] ?? PROVIDERS.novita;
let TOKENS_USED = prompt?.length;
if (previousPrompt) TOKENS_USED += previousPrompt.length;
if (html) TOKENS_USED += html.length;
if (provider !== "auto" && TOKENS_USED >= selectedProvider.max_tokens) {
return res.status(400).send({
ok: false,
openSelectProvider: true,
message: `Context is too long. ${selectedProvider.name} allow ${selectedProvider.max_tokens} max tokens.`,
});
}
const chatCompletion = client.chatCompletionStream({
model: selectedProvider.model_id || MODEL_ID,
provider: selectedProvider.id,
messages: [
{
role: "system",
content: selectedProvider.systemPrompt || DEFAULT_SYSTEM_PROMPT,
},
...(previousPrompt
? [
{
role: "user",
content: previousPrompt,
},
]
: []),
...(html
? [
{
role: "assistant",
content: `The current code is: ${html}.`,
},
]
: []),
{
role: "user",
content: prompt,
},
],
...(selectedProvider.id !== "sambanova" && selectedProvider.max_tokens
? {
max_tokens: selectedProvider.max_tokens,
}
: {}),
});
let completeResponse = "";
while (true) {
const { done, value } = await chatCompletion.next();
if (done) {
break;
}
const chunk = value.choices[0]?.delta?.content;
if (chunk) {
if (provider !== "sambanova") {
res.write(chunk);
completeResponse += chunk;
if (completeResponse.includes("</html>")) {
break;
}
} else {
let newChunk = chunk;
if (chunk.includes("</html>")) {
newChunk = newChunk.replace(/<\/html>[\s\S]*/, "</html>");
}
completeResponse += newChunk;
res.write(newChunk);
if (newChunk.includes("</html>")) {
break;
}
}
}
}
}
res.end();
} catch (error) {
if (!res.headersSent) {
res.status(500).send({
ok: false,
message: error.message || "An error occurred while processing your request.",
});
} else {
res.end();
}
}
});
app.post("/api/custom-model", checkUser, async (req, res) => {
const {
endpoint,
apiKey,
model_id,
max_tokens,
name,
systemPrompt,
apiType = 'hf' // 默认使用HuggingFace格式
} = req.body;
if (!endpoint || !model_id) {
return res.status(400).send({
ok: false,
message: "Missing required fields: endpoint and model_id",
});
}
try {
// 验证端点可用性
if (apiType === 'openai') {
// 验证OpenAI兼容的API端点
const response = await fetch(`${endpoint}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: model_id,
messages: [{ role: "system", content: "Hello" }],
max_tokens: 1,
}),
});
if (!response.ok) {
throw new Error(`API endpoint validation failed: ${response.statusText}`);
}
} else {
// 验证HuggingFace格式的端点
const client = new InferenceClient({
endpoint,
token: apiKey,
});
await client.ping();
}
// 更新自定义提供商配置
PROVIDERS.custom = {
...PROVIDERS.custom,
endpoint,
model_id,
apiKey,
apiType,
max_tokens: max_tokens || 0,
name: name || "自定义模型",
systemPrompt: systemPrompt || DEFAULT_SYSTEM_PROMPT,
};
res.status(200).send({
ok: true,
message: "Custom model configured successfully",
provider: PROVIDERS.custom,
});
} catch (error) {
res.status(500).send({
ok: false,
message: error.message || "Failed to configure custom model",
});
}
});
app.get("/api/remix/:username/:repo", async (req, res) => {
const { username, repo } = req.params;
const { hf_token } = req.cookies;
const token = hf_token || process.env.DEFAULT_HF_TOKEN;
const repoId = `${username}/${repo}`;
const space = await spaceInfo({
name: repoId,
});
console.log(space);
if (!space || space.sdk !== "static" || space.private) {
return res.status(404).send({
ok: false,
message: "Space not found",
});
}
const url = `https://huggingface.co/spaces/${repoId}/raw/main/index.html`;
const response = await fetch(url);
if (!response.ok) {
return res.status(404).send({
ok: false,
message: "Space not found",
});
}
let html = await response.text();
// remove the last p tag including this url https://enzostvs-deepsite.hf.space
html = html.replace(getPTag(repoId), "");
res.status(200).send({
ok: true,
html,
});
});
// 添加根路由的专门处理
app.get("/", (req, res) => {
console.log("接收到根路径请求");
console.log("请求头:", req.headers);
// 直接提供简单的页面而不是依赖构建的应用
res.send(`
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeepSite - AI驱动的前端助手</title>
<style>
html, body {
height: 100%;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f0f2f5;
color: #333;
text-align: center;
padding: 20px;
}
.container {
background: white;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 40px;
max-width: 700px;
width: 100%;
}
h1 {
margin-top: 0;
color: #0066cc;
font-size: 32px;
}
p {
line-height: 1.6;
font-size: 16px;
}
.features {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
margin: 30px 0;
}
.feature {
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
width: 180px;
}
.feature h3 {
margin-top: 0;
color: #0066cc;
}
.buttons {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
}
.button {
display: inline-block;
background-color: #0066cc;
color: white;
padding: 12px 24px;
border-radius: 6px;
text-decoration: none;
font-weight: bold;
transition: background-color 0.3s;
}
.button:hover {
background-color: #0052a3;
}
.button.secondary {
background-color: #f0f2f5;
color: #333;
}
.button.secondary:hover {
background-color: #e0e2e5;
}
.footer {
margin-top: 40px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h1>DeepSite</h1>
<p>一个强大的 AI 驱动的前端开发助手,帮助你快速创建和优化网页。现在支持自定义模型和 OpenAI 兼容 API。</p>
<div class="features">
<div class="feature">
<h3>🎨 智能UI生成</h3>
<p>快速生成美观的用户界面</p>
</div>
<div class="feature">
<h3>🔧 代码优化</h3>
<p>智能优化和改进代码</p>
</div>
<div class="feature">
<h3>🚀 实时预览</h3>
<p>即时查看设计效果</p>
</div>
<div class="feature">
<h3>🔄 自定义模型</h3>
<p>支持接入自定义AI模型</p>
</div>
</div>
<div class="buttons">
<a href="/api/login" class="button">登录使用</a>
<a href="https://github.com/luoluoluo22/deepsite-space" class="button secondary">查看源码</a>
</div>
<div class="footer">
<p>当前服务器时间: ${new Date().toLocaleString('zh-CN')}</p>
<p>服务器状态: 正常运行中</p>
<p>系统版本: DeepSite 1.0.0</p>
</div>
</div>
</body>
</html>
`);
});
// 将 /debug 路由放到开头部分(在其他路由之前)
// 添加调试路由
app.get("/debug", (req, res) => {
console.log("处理 /debug 请求");
const info = {
env: process.env.NODE_ENV,
port: PORT,
time: new Date().toISOString(),
static_dir: path.join(__dirname, "dist"),
files: fs.readdirSync(path.join(__dirname, "dist")),
headers: req.headers,
cookies: req.cookies
};
// 禁用缓存
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.setHeader('Surrogate-Control', 'no-store');
// 设置内容类型为 JSON
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(info, null, 2));
});
// 修改通配符路由,排除 /debug 路径
app.get("*", (req, res) => {
// 跳过 /debug 请求
if (req.url === '/debug') {
return;
}
console.log(`通配符路由请求: ${req.url}`);
const indexPath = path.join(__dirname, "dist", "index.html");
if (fs.existsSync(indexPath)) {
console.log(`通配符路由提供 index.html: ${indexPath}`);
res.sendFile(indexPath);
} else {
console.error(`通配符路由错误: index.html 文件不存在: ${indexPath}`);
res.status(404).send("找不到页面文件");
}
});
// 在 app.listen 之前添加
// 确保 dist 目录和 index.html 存在
const distDir = path.join(__dirname, "dist");
const indexPath = path.join(distDir, "index.html");
if (!fs.existsSync(distDir)) {
console.log("创建 dist 目录");
fs.mkdirSync(distDir, { recursive: true });
}
// 修改 createBasicHtml 函数,创建一个更基础的 HTML 页面
const createBasicHtml = () => {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeepSite</title>
<style>
body {
font-family: system-ui, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
padding: 20px;
text-align: center;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
padding: 30px;
background-color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
</style>
</head>
<body>
<div class="container">
<h1>欢迎使用 DeepSite</h1>
<p>一个强大的 AI 驱动的前端开发助手</p>
<p>正在加载中...</p>
<p><a href="/" id="refresh">刷新页面</a></p>
<div id="debug"></div>
<script>
// 简单的调试脚本
document.getElementById('debug').innerHTML = '浏览器信息: ' + navigator.userAgent;
// 5秒后自动刷新
setTimeout(() => {
window.location.reload();
}, 5000);
</script>
</div>
</body>
</html>
`;
};
// 在检查 index.html 不存在时使用新的函数
if (!fs.existsSync(indexPath)) {
console.log("创建基本的 index.html 文件");
fs.writeFileSync(indexPath, createBasicHtml());
}
app.listen(PORT, () => {
console.log(`===== 应用启动信息 =====`);
console.log(`启动时间: ${new Date().toISOString()}`);
console.log(`运行环境: ${process.env.NODE_ENV || 'development'}`);
console.log(`服务器启动在端口: ${PORT}`);
console.log(`默认模型: ${MODEL_ID}`);
console.log(`静态文件目录: ${path.join(__dirname, "dist")}`);
// 检查静态文件是否存在
const indexPath = path.join(__dirname, "dist", "index.html");
if (fs.existsSync(indexPath)) {
console.log(`index.html 文件存在: ${indexPath}`);
} else {
console.error(`错误: index.html 文件不存在: ${indexPath}`);
console.log(`目录内容: ${fs.readdirSync(path.join(__dirname, "dist")).join(', ') || '空目录'}`);
}
console.log(`=======================`);
});