关于如何使用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
└── .env
1. 安装依赖
首先创建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-134d0ba94323448ba8f715f1970d7c18
PORT=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开发者。