1103 字
6 分钟
本地AI搭建-nodejs
Random Cover
关于如何使用DeepSeek API,可以查看本地AI搭建
Node.js 实现的局域网AI对话Web应用
以下是用Node.js实现的版本,使用Express作为Web框架:
项目结构
ai-chat-app/├── public/│ ├── styles.css│ └── script.js├── views/│ └── index.html├── app.js├── package.json└── .env1. 安装依赖
首先创建package.json:
{ "name": "ai-chat-app", "version": "1.0.0", "description": "局域网AI对话应用", "main": "app.js", "scripts": { "start": "node app.js", "dev": "nodemon app.js" }, "dependencies": { "express": "^4.18.2", "openai": "^4.28.0", "dotenv": "^16.3.1", "cors": "^2.8.5" }, "devDependencies": { "nodemon": "^3.0.2" }}运行 npm install 安装依赖。
2. 主程序 (app.js)
require('dotenv').config()const express = require('express')const {OpenAI} = require('openai')const path = require('path')const cors = require('cors')
const app = express()const port = process.env.PORT || 5000
// 中间件app.use(cors())app.use(express.json())app.use(express.static(path.join(__dirname, 'public')))
// 初始化对话历史let messages = [{role: 'system', content: '你是一个乐于助人的AI助手'}]
// 初始化OpenAI客户端const openai = new OpenAI({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: 'https://api.deepseek.com',})
// 路由app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'views', 'index.html'))})
app.post('/chat', async (req, res) => { const userMessage = req.body.message if (!userMessage) { return res.status(400).json({error: '消息不能为空'}) }
// 添加用户消息到历史 messages.push({role: 'user', content: userMessage})
try { // 获取AI回复 const response = await openai.chat.completions.create({ model: 'deepseek-reasoner', messages: messages, stream: false, })
const assistantOutput = response.choices[0].message.content
// 添加AI回复到历史 messages.push({role: 'assistant', content: assistantOutput})
return res.json({response: assistantOutput}) } catch (error) { console.error('Error:', error) return res.status(500).json({error: error.message}) }})
// 启动服务器app.listen(port, '0.0.0.0', () => { console.log(`Server running at http://localhost:${port}`) console.log(`局域网访问: http://${getIPAddress()}:${port}`)})
// 获取本机IP地址function getIPAddress() { const interfaces = require('os').networkInterfaces() for (const devName in interfaces) { const iface = interfaces[devName] for (let i = 0; i < iface.length; i++) { const alias = iface[i] if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) { return alias.address } } } return 'localhost'}3. 前端HTML (views/index.html)
<!doctype html><html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>AI对话助手</title> <link rel="stylesheet" href="/styles.css" /> </head> <body> <div class="container"> <h1>AI对话助手</h1> <div class="chat-container" id="chat-container"> <!-- 消息将在这里动态添加 --> </div> <div class="input-area"> <input type="text" id="user-input" placeholder="输入你的问题..." autocomplete="off" /> <button id="send-button">发送</button> </div> </div> <script src="/script.js"></script> </body></html>4. CSS样式 (public/styles.css)
body { font-family: 'Arial', sans-serif; background-color: #f5f5f5; margin: 0; padding: 20px; display: flex; justify-content: center;}
.container { width: 100%; max-width: 800px; background-color: white; border-radius: 10px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); padding: 20px;}
h1 { text-align: center; color: #333;}
.chat-container { height: 500px; overflow-y: auto; padding: 15px; margin-bottom: 20px; background-color: #f9f9f9; border-radius: 8px;}
.message { margin-bottom: 15px; padding: 10px 15px; border-radius: 18px; max-width: 70%; word-wrap: break-word;}
.user-message { background-color: #e3f2fd; margin-left: auto; border-bottom-right-radius: 5px;}
.assistant-message { background-color: #f1f1f1; margin-right: auto; border-bottom-left-radius: 5px;}
.input-area { display: flex; gap: 10px;}
#user-input { flex-grow: 1; padding: 10px 15px; border-radius: 20px; border: 1px solid #ddd; font-size: 16px;}
#send-button { padding: 10px 20px; background-color: #4caf50; color: white; border: none; border-radius: 20px; cursor: pointer; font-size: 16px;}
#send-button:hover { background-color: #45a049;}
.typing-indicator { display: inline-block; padding: 10px 15px; background-color: #f1f1f1; border-radius: 18px; margin-bottom: 15px; border-bottom-left-radius: 5px;}
.typing-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background-color: #888; margin: 0 2px; animation: typing-animation 1.4s infinite ease-in-out;}
.typing-dot:nth-child(1) { animation-delay: 0s;}.typing-dot:nth-child(2) { animation-delay: 0.2s;}.typing-dot:nth-child(3) { animation-delay: 0.4s;}
@keyframes typing-animation { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-5px); }}5. 前端JavaScript (public/script.js)
document.addEventListener('DOMContentLoaded', () => { const chatContainer = document.getElementById('chat-container') const userInput = document.getElementById('user-input') const sendButton = document.getElementById('send-button')
// 发送消息函数 async function sendMessage() { const message = userInput.value.trim() if (!message) return
// 显示用户消息 displayMessage(message, 'user') userInput.value = ''
// 显示打字指示器 showTypingIndicator()
try { // 发送到服务器 const response = await fetch('/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({message}), })
if (!response.ok) { throw new Error('网络响应不正常') }
const data = await response.json()
// 隐藏打字指示器 hideTypingIndicator()
// 显示AI回复 if (data.response) { displayMessage(data.response, 'assistant') } else if (data.error) { displayMessage(`错误: ${data.error}`, 'assistant') } } catch (error) { hideTypingIndicator() displayMessage(`抱歉,发生了错误: ${error.message}`, 'assistant') console.error('Error:', error) } }
// 显示消息 function displayMessage(content, role) { const messageDiv = document.createElement('div') messageDiv.classList.add('message', `${role}-message`) messageDiv.textContent = content chatContainer.appendChild(messageDiv) chatContainer.scrollTop = chatContainer.scrollHeight }
// 显示打字指示器 function showTypingIndicator() { const typingDiv = document.createElement('div') typingDiv.id = 'typing-indicator' typingDiv.className = 'typing-indicator' typingDiv.innerHTML = ` <span class="typing-dot"></span> <span class="typing-dot"></span> <span class="typing-dot"></span> ` chatContainer.appendChild(typingDiv) chatContainer.scrollTop = chatContainer.scrollHeight }
// 隐藏打字指示器 function hideTypingIndicator() { const typingDiv = document.getElementById('typing-indicator') if (typingDiv) { typingDiv.remove() } }
// 事件监听 sendButton.addEventListener('click', sendMessage) userInput.addEventListener('keypress', e => { if (e.key === 'Enter') { sendMessage() } })})6. 环境变量 (.env)
DEEPSEEK_API_KEY=sk-134d0ba94323448ba8f715f1970d7c18PORT=5000运行说明
- 安装依赖:
npm install - 启动开发服务器:
npm run dev(使用nodemon自动重启) - 或者启动生产服务器:
npm start - 访问:
- 本地:
http://localhost:5000 - 局域网:
http://[你的IP地址]:5000(控制台会显示具体地址)
- 本地:
功能特点
- 使用Express构建的Node.js后端
- 分离的前端HTML/CSS/JavaScript
- 响应式设计,适配各种设备
- 实时打字指示器
- 完整的对话历史记录
- 错误处理和用户反馈
- 自动获取并显示局域网访问地址
这个实现与之前的Python版本功能相同,但使用了Node.js技术栈,更适合JavaScript开发者。
参考资料
本地AI搭建-nodejs
https://march7th.online/blog/posts/0031-本地ai搭建-nodejs/ 最后更新于 2025-05-25,距今已过 168 天
部分内容可能已过时
March7th