跳过

本地AI搭建-nodejs

发布时间: at 15:41
本文收录在以下合集中:

关于如何使用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

运行说明

  1. 安装依赖:npm install
  2. 启动开发服务器:npm run dev (使用nodemon自动重启)
  3. 或者启动生产服务器:npm start
  4. 访问:
    • 本地:http://localhost:5000
    • 局域网:http://[你的IP地址]:5000 (控制台会显示具体地址)

功能特点

  1. 使用Express构建的Node.js后端
  2. 分离的前端HTML/CSS/JavaScript
  3. 响应式设计,适配各种设备
  4. 实时打字指示器
  5. 完整的对话历史记录
  6. 错误处理和用户反馈
  7. 自动获取并显示局域网访问地址

这个实现与之前的Python版本功能相同,但使用了Node.js技术栈,更适合JavaScript开发者。

参考资料