Spaces:
Paused
Paused
| 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(`=======================`); | |
| }); | |