node.js + html调用ChatGPTApi实现Ai网站demo(带源码)

文章目录


前言

关注博主,学习每天一个小demo 今天是Ai对话网站

又到了每天一个小demo的时候咯,前面我写了多人实时对话demo、和视频转换demo,今天我来使用 node.js + html 调用chatGpt Api实现一个Ai 流式对话小demo,当然现在主流的各种APi调用方式一致,你也可以接入deepseek,或者第三方的接口,效果一样


一、demo演示

下面是一个简单的demo演示,并且是支持流式的

二、node.js 使用步骤

1.引入库

代码如下:

先初始化一个package.js 文件。

c 复制代码
npm init -y

我们需要安装 express、cors、openAi、dotenv三方库

c 复制代码
{
  "name": "web",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "axios": "^1.7.9",
    "cors": "^2.8.5",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "openai": "^4.0.0"
  }
}

2.引入包

创建server.js 引入我们的第三方包 编写接口逻辑

**特别关注:

  1. baseURL: 这里大家可以替换其他的APi 比如deepseek(好像目前不能充值了),或者一些第三方的API地址。
  2. apiKey: 这里大家把key可以直接填写上,我这里为了学习知识,新建了一个.env 文件。**
c 复制代码
const express = require('express');
const cors = require('cors');
const OpenAI = require('openai');
require('dotenv').config();

const app = express();
app.use(express.json());
app.use(cors());

// 初始化OpenAI客户端
const openai = new OpenAI({
    apiKey: process.env.DEEPSEEK_API_KEY, 
    baseURL: "https://api.openai.com/v1"  // 使用OpenAI官方API地址  这里可以可以使用别的Api地址
    // 比如 deepseek 或者其他的第三方的一些
    // baseURL: "https://api.deepseek.com/v1"
});

let conversationHistory = [];

app.post('/chat', async (req, res) => {
    try {
        const { message } = req.body;
        
        // 设置响应头,支持流式输出
        res.setHeader('Content-Type', 'text/event-stream');
        res.setHeader('Cache-Control', 'no-cache');
        res.setHeader('Connection', 'keep-alive');
        res.setHeader('Access-Control-Allow-Origin', '*');  // 添加CORS支持
        
        // 添加用户消息到历史记录
        conversationHistory.push({ role: "user", content: message });
        
        const stream = await openai.chat.completions.create({
            model: "gpt-3.5-turbo",
            messages: [
                { role: "system", content: "You are a helpful assistant." },
                ...conversationHistory
            ],
            temperature: 0.7,
            max_tokens: 1000,
            stream: true,
        });

        let fullResponse = '';
        
        // 确保每次写入后立即刷新缓冲区
        for await (const chunk of stream) {
            const content = chunk.choices[0]?.delta?.content || '';
            if (content) {
                fullResponse += content;
                const dataToSend = JSON.stringify({ content });
                console.log('Sending to client:', dataToSend);
                res.write(`data: ${dataToSend}\n\n`);
                // 强制刷新缓冲区
                if (res.flush) {
                    res.flush();
                }
            }
        }

        // 将完整回复添加到对话历史
        conversationHistory.push({ role: "assistant", content: fullResponse });
        
        if (conversationHistory.length > 10) {
            conversationHistory = conversationHistory.slice(-10);
        }

        res.write('data: [DONE]\n\n');
        if (res.flush) {
            res.flush();
        }
        res.end();

    } catch (error) {
        console.error('Error:', error);
        res.write(`data: ${JSON.stringify({ error: '服务器错误' })}\n\n`);
        res.end();
    }
});

const PORT = 3000;
app.listen(PORT, () => {
    console.log(`服务器运行在端口 ${PORT}`);
}); 

在根目录执行 node server.js 启动服务

前端HTML调用接口和UI

不墨迹哈,直接把所有代码贴过来 大家好直接研究代码,我就不用一行一行解读了,没啥东西,难处就是对流式数据的一个处理

特别注意, 不要直接点击html打开页面,在vscode里面安装扩展Live Server,然后点击右下角 Go live启动一个微服务。

c 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ChatGPT 聊天</title>
    <style>
        #chat-container {
            width: 80%;
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }
        #chat-messages {
            height: 400px;
            overflow-y: auto;
            margin-bottom: 20px;
            padding: 10px;
            border: 1px solid #eee;
        }
        .message {
            margin: 10px 0;
            padding: 10px;
            border-radius: 5px;
        }
        .user-message {
            background-color: #e3f2fd;
            margin-left: 20%;
        }
        .bot-message {
            background-color: #f5f5f5;
            margin-right: 20%;
        }
        #message-form {
            display: flex;
            gap: 10px;
        }
        #message-input {
            flex: 1;
            padding: 8px;
        }
        .typing {
            opacity: 0.5;
        }
        .cursor {
            display: inline-block;
            width: 2px;
            height: 15px;
            background: #000;
            margin-left: 2px;
            animation: blink 1s infinite;
        }
        @keyframes blink {
            0%, 100% { opacity: 1; }
            50% { opacity: 0; }
        }
    </style>
</head>
<body>
    <div id="chat-container">
        <div id="chat-messages"></div>
        <form id="message-form">
            <input type="text" id="message-input" placeholder="输入消息..." required>
            <button type="submit" id="submit-btn">发送</button>
        </form>
    </div>

    <script>
        const messageForm = document.getElementById('message-form');
        const messageInput = document.getElementById('message-input');
        const chatMessages = document.getElementById('chat-messages');
        const submitBtn = document.getElementById('submit-btn');

        messageForm.addEventListener('submit', async (e) => {
            e.preventDefault();
            const message = messageInput.value.trim();
            if (!message) return;

            // 禁用输入和发送按钮
            messageInput.disabled = true;
            submitBtn.disabled = true;

            // 显示用户消息
            addMessage(message, 'user');
            messageInput.value = '';

            // 创建机器人回复的消息框
            const botMessageDiv = document.createElement('div');
            botMessageDiv.className = 'message bot-message typing';
            chatMessages.appendChild(botMessageDiv);
            
            // 添加光标
            const cursor = document.createElement('span');
            cursor.className = 'cursor';
            botMessageDiv.appendChild(cursor);

            try {
                const response = await fetch('http://localhost:3000/chat', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Accept': 'text/event-stream'
                    },
                    body: JSON.stringify({ message })
                });

                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }

                const reader = response.body.getReader();
                const decoder = new TextDecoder();
                let botResponse = '';
                let buffer = '';

                try {
                    while (true) {
                        const { value, done } = await reader.read();
                        // 如果流结束了,就退出循环
                        if (done) {
                            console.log('Stream complete');
                            break;
                        }

                        // 确保有值才处理
                        if (value) {
                            buffer += decoder.decode(value, { stream: true });
                            const lines = buffer.split('\n');
                            
                            // 保留最后一个不完整的行
                            buffer = lines.pop() || '';

                            for (const line of lines) {
                                if (line.trim() === '') continue;
                                if (!line.startsWith('data: ')) continue;

                                const data = line.slice(6).trim();

                                if (data === '[DONE]') {
                                    botMessageDiv.classList.remove('typing');
                                    cursor.remove();
                                    continue;
                                }

                                try {
                                    const parsedData = JSON.parse(data);
                                    if (parsedData.content) {
                                        botResponse += parsedData.content;
                                        botMessageDiv.textContent = botResponse;
                                        botMessageDiv.appendChild(cursor);
                                        chatMessages.scrollTop = chatMessages.scrollHeight;
                                    }
                                } catch (e) {
                                    console.error('JSON解析错误:', e, 'Raw data:', data);
                                }
                            }
                        }
                    }

                    // 处理最后可能残留的数据
                    if (buffer.trim()) {
                        const finalText = decoder.decode(); // 完成流的解码
                        if (finalText) {
                            buffer += finalText;
                            const lines = buffer.split('\n');
                            
                            for (const line of lines) {
                                if (line.trim() === '' || !line.startsWith('data: ')) continue;
                                
                                const data = line.slice(6).trim();
                                if (data === '[DONE]') continue;
                                
                                try {
                                    const parsedData = JSON.parse(data);
                                    if (parsedData.content) {
                                        botResponse += parsedData.content;
                                        botMessageDiv.textContent = botResponse;
                                    }
                                } catch (e) {
                                    console.error('最终解析错误:', e, 'Raw data:', data);
                                }
                            }
                        }
                    }
                } catch (streamError) {
                    console.error('Stream processing error:', streamError);
                    throw streamError;
                }
            } catch (error) {
                console.error('Error:', error);
                botMessageDiv.textContent = '抱歉,发生错误。';
                botMessageDiv.classList.remove('typing');
                cursor.remove();
            } finally {
                messageInput.disabled = false;
                submitBtn.disabled = false;
                messageInput.focus();
            }
        });

        function addMessage(text, sender) {
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${sender}-message`;
            messageDiv.textContent = text;
            chatMessages.appendChild(messageDiv);
            chatMessages.scrollTop = chatMessages.scrollHeight;
        }
    </script>
</body>
</html> 

所有文件

  1. .env 存储了APi-key
  2. index.html 前端代码
  3. server.js 后段代码

总结

尽可能的多学习一些知识,或许以后用不到,关注我每天练习一个小demo。

相关推荐
workflower1 小时前
装备企业的 AI 路线图
人工智能·深度学习·机器学习·设计模式·机器人
AKAMAI1 小时前
Linode接口及默认防火墙现已全面开放使用
人工智能·云计算
前端开发江鸟2 小时前
RAG 回答错了,问题到底出在召回、重排,还是生成?
人工智能
美狐美颜SDK开放平台2 小时前
直播app开发如何实现主播级美颜效果?美颜sdk开发方案详解
人工智能·音视频·美颜sdk·视频美颜sdk·美颜api
东方小月2 小时前
从零开发一个 Coding Agent(四):使用状态机校验大模型事件流
前端·人工智能·后端
大龄码农有梦想2 小时前
使用大模型服务如何保证数据安全?企业 AI 安全、私有化部署与治理实践
人工智能·私有化部署·数据安全·信创·ai agent·智能体·智能体开发平台
冬奇Lab2 小时前
每日一个开源项目(第170篇):CodeWiki - ACL 2026 论文级代码库自动文档生成,递归多 Agent 架构
人工智能·开源·资讯
W_326002 小时前
VSCode 进阶
c语言·vscode
想做后端的前端2 小时前
WebGL实现FPS游戏
vue.js·游戏·webgl
Csvn2 小时前
🧩 ESM vs CJS 混用的 7 个「天坑」——从 TypeScript 编译到 Node 与浏览器
前端