Files
AI/server2.js

1346 lines
40 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// server.js
const express = require("express");
const multer = require("multer");
const cors = require("cors");
const fs = require("fs");
const path = require("path");
const OpenAI = require("openai");
const xml2js = require('xml2js');
const xmlparser = require('express-xml-bodyparser'); // 引入XML解析中间件
const https = require("https");
// 强制 IPv4 的 https agent
const ipv4Agent = new https.Agent({
family: 4, // 最关键:强制 IPv4
keepAlive: true,
maxSockets: 100,
keepAliveMsecs: 10000,
});
// 包一个强制 IPv4 的 fetch
const fetchIPv4 = (url, options = {}) => {
return fetch(url, {
...options,
agent: ipv4Agent, // 👈 加上 IPv4 专用 agent
});
};
const bodyParser = require("body-parser");
const axios = require('axios');
const crypto = require('crypto');
require('dotenv').config();
const app = express();
const port = 30009;
const TOKEN = 'my_token_123'; // 与公众号后台配置一致
const appid = process.env.WECHAT_APPID;
const secret = process.env.WECHAT_SECRET;
const accessTokenUrl = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`;
const mysql = require("mysql2/promise");
// 初始化数据库连接池
const pool = mysql.createPool({
host: "1.95.137.212",
user: "fastbee",
password: "maibu520",
database: "fastbee",
waitForConnections: true,
connectionLimit: 10,
});
// 允许大文件请求体
app.use(bodyParser.json({ limit: "500mb" }));
app.use(bodyParser.urlencoded({ limit: "500mb", extended: true }));
app.use(xmlparser({
explicitArray: false, // 禁用默认的数组格式,和你后续xml2js配置一致
trim: true // 去除字符串前后空格
}));
// Multer 上传目录
const upload = multer({
dest: "uploads/",
limits: { fileSize: 500 * 1024 * 1024 }, // 500MB
});
// Node <20 兼容 File
if (typeof File === "undefined") {
globalThis.File = require("node:buffer").File;
}
// 启用 CORS
app.use(cors());
// app.use(cors({
// origin: ["https://ai.satabot.com"], // 允许的域
// methods: "GET,POST,PUT,DELETE,OPTIONS",
// allowedHeaders: "Content-Type,Authorization",
// credentials: true
// }));
// app.options('*', cors());
app.use(express.json({ limit: "200mb" })); // 支持大 JSON
app.use(express.urlencoded({ extended: true })); // 支持 form-data 字段
// 初始化混元客户端
// const client = new OpenAI({
// apiKey: "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",
// baseURL: "https://openrouter.ai/api/v1",
// timeout: 120000,
// });
// const client = new OpenAI({
// baseURL:"https://openrouter.ai/api/v1",
// apiKey:"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",
// timeout: 120000
// });
// const client = new OpenAI({
// baseURL:"https://satabot.openai.azure.com/",
// apiKey:"DxvRiQWROzLTapiSHcK0FXKAAXDBlhci1XaqTTpr50sgUsAkmxfUJQQJ99BIACYeBjFXJ3w3AAABACOGZdHp",
// timeout: 120000
// });
//https://satabot.openai.azure.com/
// const client = new OpenAI({
// apiKey: "DxvRiQWROzLTapiSHcK0FXKAAXDBlhci1XaqTTpr50sgUsAkmxfUJQQJ99BIACYeBjFXJ3w3AAABACOGZdHp", // 推荐放环境变量
// baseURL: "https://satabot.openai.azure.com/openai/deployments/gpt-4.1/chat/completions?api-version=2025-01-01-preview",
// defaultQuery: { "api-version": "2024-02-15-preview" },
// defaultHeaders: { "api-key": process.env.AZURE_OPENAI_KEY },
// timeout: 120000
// });
// https://satabot.openai.azure.com/openai/deployments/gpt-4.1/
// https://satabot.openai.azure.com/openai/deployments/gpt-5-mini/
// 初始化客户端
const client = new OpenAI({
apiKey: "DxvRiQWROzLTapiSHcK0FXKAAXDBlhci1XaqTTpr50sgUsAkmxfUJQQJ99BIACYeBjFXJ3w3AAABACOGZdHp",
baseURL: "https://satabot.openai.azure.com/openai/deployments/gpt-4.1",
defaultQuery: { "api-version": "2024-04-01-preview" },
timeout: 120000,
fetch: fetchIPv4, // 关键点:把自定义 fetch 注入 SDK
// 确保底层也传 agent(可选,但保险)
baseOptions: {
agent: ipv4Agent
}
});
const client1 = new OpenAI({
apiKey: "DxvRiQWROzLTapiSHcK0FXKAAXDBlhci1XaqTTpr50sgUsAkmxfUJQQJ99BIACYeBjFXJ3w3AAABACOGZdHp",
baseURL: "https://satabot.openai.azure.com/openai/deployments/gpt-5-mini/",
defaultQuery: { "api-version": "2024-04-01-preview" },
timeout: 120000,
fetch: fetchIPv4, // 关键点:把自定义 fetch 注入 SDK
// 确保底层也传 agent(可选,但保险)
baseOptions: {
agent: ipv4Agent
}
});
async function test1()
{
console.time("test");
await client.chat.completions.create({
model: "gpt-4.1", // 注意:这里必须是你在 Azure portal 里创建的“部署名称”
messages: [
{ role: "user", content: "ping" }
]
});
console.timeEnd("test");
}
async function directCall()
{
console.time("directCall");
await client1.chat.completions.create({ model:"gpt-5-mini", messages:[{ role: "user", content: "ping" }] });
console.timeEnd("directCall");
}
test1();
directCall();
// 获取 access_token
// getAccessToken();
async function getAccessToken() {
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`;
const res = await axios.get(url);
if (res.data.access_token) {
console.log('✅ 获取 access_token 成功',res.data);
return res.data.access_token;
} else {
throw new Error('❌ 获取 access_token 失败: ' + JSON.stringify(res.data));
}
}
const WECHAT_OAUTH_TOKEN_URL = 'https://api.weixin.qq.com/sns/oauth2/access_token';
app.get('/wechat/oauth/access-token', async (req, res) => {
try {
console.log(req.query)
// 1. 接收并校验前端传递的 code 参数
const { code } = req.query; // 从URL查询参数中获取 code
if (!code) {
return res.status(400).json({
code: -1,
message: '参数错误:缺少 code(微信授权临时票据)'
});
}
// 3. 调用微信 OAuth2 接口,传递必要参数
const response = await axios.get(WECHAT_OAUTH_TOKEN_URL, {
params: {
appid: "wx364b8ebf17fe45fe", // 你的微信appid
secret: "9b7476020991a2c71f1416da7f011d31", // 你的微信secret
code: code, // 前端传递的授权code
grant_type: 'authorization_code' // 固定值
}
});
// 4. 处理微信返回的结果
const wechatData = response.data;
// 微信接口失败(返回 errcode 字段)
if (wechatData.errcode) {
throw new Error(`微信接口错误:${wechatData.errmsg}(错误码:${wechatData.errcode})`);
}
// 5. 成功响应:返回微信返回的所有信息(包含 access_token、openid 等)
res.status(200).json({
code: 0,
message: '获取 OAuth2 access_token 成功',
data: wechatData // 包含 access_token、expires_in、refresh_token、openid 等
});
} catch (error) {
// 异常处理:捕获网络错误、微信接口错误等
console.error('获取 OAuth2 access_token 失败:', error.message);
res.status(500).json({
code: -3,
message: '获取 access_token 失败:' + error.message
});
}
});
/*菜单部分 */
async function createMenu() {
const accessToken = await getAccessToken();
const url = `https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${accessToken}`;
const target = encodeURIComponent("https://ai.satabot.com/?from=weixin");
const menuData = {
"button": [
{
"name": "产品中心",
"sub_button": [
{
"type": "view",
"name": "整机产品",
"url": "http://www.jsmaibu.com/products/"
},
{
"type": "view",
"name": "智能电控",
"url": "http://www.jsmaibu.com/Products/%E6%8E%A7%E5%88%B6%E5%99%A8%20MC100.html"
},
{
"type": "view",
"name": "资料下载",
"url": "http://www.jsmaibu.com/Support/Download.html"
},
// {
// "type": "click",
// "name": "AI智能体",
// "key": "AI_INTELLIGENT"
// }
{
"type": "view",
"name": "AI智能体",
// "url":`https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx364b8ebf17fe45fe&redirect_uri= encodeURIComponent("https://ai.satabot.com/?from=weixin")&response_type=code&scope=snsapi_login&state=123#wechat_redirect`,
"url": "https://ai.satabot.com/middle.html"
}
]
},
{
"name": "服务中心",
"sub_button": [
{
"type": "view",
"name": "解决方案",
"url": "http://www.jsmaibu.com/Solution.html"
},
{
"type": "view",
"name": "培训视频",
"url": "http://www.jsmaibu.com/Support/index.html"
},
{
"type": "view",
"name": "一键呼叫",
"url": "http://www.jsmaibu.com/contact/"
}
]
},
{
"name": "飒沓智能",
"sub_button": [
{
"type": "view",
"name": "关于我们",
"url": "http://www.jsmaibu.com/about-us.html#start"
},
{
"type": "view",
"name": "加入我们",
"url": "https://www.zhipin.com/gongsi/job/d1e30046e726d2d01nN72Ni4EFQ~.html?ka=all-jobs-hot"
}
]
}
]
};
const res = await axios.post(url, menuData);
if (res.data.errcode === 0) {
console.log('✅ 菜单创建成功');
} else {
console.error('❌ 菜单创建失败:', res.data);
}
}
// ========== 查询菜单 ==========
async function getMenu() {
const accessToken = await getAccessToken();
const url = `https://api.weixin.qq.com/cgi-bin/menu/get?access_token=${accessToken}`;
const res = await axios.get(url);
return res.data;
}
// ========== 删除菜单(可选)==========
async function deleteMenu() {
const accessToken = await getAccessToken();
const url = `https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=${accessToken}`;
const res = await axios.get(url);
return res.data;
}
app.get('/createMenu', async (req, res) => {
try {
await createMenu();
res.send('✅ 菜单创建成功!');
} catch (err) {
res.send('❌ 创建失败: ' + err.message);
}
});
app.get('/getMenu', async (req, res) => {
try {
const data = await getMenu();
res.json(data);
} catch (err) {
res.send('❌ 查询失败: ' + err.message);
}
});
app.get('/deleteMenu', async (req, res) => {
try {
const data = await deleteMenu();
res.json(data);
} catch (err) {
res.send('❌ 删除失败: ' + err.message);
}
});
app.post('/wechat', async (req, res) => {
try {
console.log("收到请求:", req.body);
const xml = req.body;
const message = xml.xml;
// 修复语法错误:移除msgtype前的空格
const msgType = message.msgtype;
const fromUser = message.fromusername; // openid
const toUser = message.tousername; // 公众号原始ID
console.log("消息类型:", msgType);
console.log("用户OpenID:", fromUser);
// 菜单点击事件
if (msgType === 'event' && message.event === 'CLICK') {
const key = message.eventkey;
console.log("菜单点击Key:", key);
if (key === 'AI_INTELLIGENT') {
try {
const timestamp = Date.now();
// 方案一:简单文本回复(推荐,避免超时)
const reply = `
<xml>
<ToUserName><![CDATA[${fromUser}]]></ToUserName>
<FromUserName><![CDATA[${toUser}]]></FromUserName>
<CreateTime>${Date.now()}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[欢迎使用AI智能体!正在为您准备服务...<a href="https://ai.satabot.com?userId=${fromUser}&from=weixin&ts=${timestamp}" target="_blank">立即进入AI智能体</a>]]></Content>
</xml>
`;
res.send(reply);
return;
// 启动异步任务处理耗时操作
setTimeout(async () => {
try {
const token = await getAccessToken();
console.log("获取到Token:", token);
// 获取用户信息
const userRes = await axios.get('https://api.weixin.qq.com/sns/userinfo', {
params: {
access_token:token,
fromUser,
lang: 'zh_CN'
}
});
console.log(userRes, 'user000000000000');
// res.redirect('https://ai.satabot.com');
const { nickname, errcode: userErrCode } = userRes.data;
console.log(nickname, 'nickname');
return;
if (userErrCode) return res.status(500).send(`获取用户信息失败:${userErrCode}`);
const userInfoRes = await axios.get(
`https://api.weixin.qq.com/cgi-bin/user/info?access_token=${token}&openid=${fromUser}&lang=zh_CN`
);
const user = userInfoRes.data;
console.log("用户信息:", user);
// 使用客服消息接口发送详细回复
if (user && user.nickname) {
await sendCustomerServiceMessage(fromUser, `你好,${user.nickname}!欢迎使用AI智能体`);
}
} catch (err) {
console.error("异步处理错误:", err);
}
}, 0);
return;
// 方案二:直接回复(如果能确保5秒内完成)
/*
const token = await getAccessToken();
console.log("Token:", token);
const userInfoRes = await axios.get(
`https://api.weixin.qq.com/cgi-bin/user/info?access_token=${token}&openid=${fromUser}&lang=zh_CN`
);
const user = userInfoRes.data;
console.log("用户信息:", user);
const reply = `
<xml>
<ToUserName><![CDATA[${fromUser}]]></ToUserName>
<FromUserName><![CDATA[${toUser}]]></FromUserName>
<CreateTime>${Date.now()}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好,${user.nickname || '用户'}!欢迎使用AI智能体]]></Content>
</xml>
`;
res.send(reply);
return;
*/
} catch (err) {
console.error("处理AI_INTELLIGENT错误:", err);
// 出错时返回标准回复
const errorReply = `
<xml>
<ToUserName><![CDATA[${fromUser}]]></ToUserName>
<FromUserName><![CDATA[${toUser}]]></FromUserName>
<CreateTime>${Date.now()}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[服务暂时不可用,请稍后再试]]></Content>
</xml>
`;
res.send(errorReply);
return;
}
}
}
// 默认回复空串(表示不处理)
res.send('');
} catch (err) {
console.error("服务器错误:", err);
// 异常情况下回复空串
res.send('');
}
});
// app.post('/wechat', async (req, res) => {
// try {
// console.log(req,"收到恢复")
// const xml = req.body;
// console.log(xml);
// const message = xml.xml;
// const msgType = message.msgtype;
// const fromUser = message.fromusername; // openid
// // 菜单点击事件
// if (msgType === 'event' && message.event === 'CLICK') {
// const key = message.eventkey;
// if (key === 'AI_INTELLIGENT') {
// const redirectUri = encodeURIComponent(`https://ai.satabot.com/api/wechat/oauth/callback`);
// const state = fromUser; // 可用 openid 做状态标识
// console.log(state,"state")
// // const oauthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`;
// // console.log("oauthUrl",oauthUrl)
// // // 回复用户点击菜单提示,引导跳转
// // const reply = `
// // <xml>
// // <ToUserName><![CDATA[${fromUser}]]></ToUserName>
// // <FromUserName><![CDATA[${message.ToUserName}]]></FromUserName>
// // <CreateTime>${Date.now()}</CreateTime>
// // <MsgType><![CDATA[text]]></MsgType>
// // <Content><![CDATA[请点击以下链接授权获取昵称:${oauthUrl}]]></Content>
// // </xml>
// // `;
// // res.send(reply);
// // return;
// const token = await getAccessToken();
// console.log(token,"token")
// // 获取用户信息
// // https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=o6_bmjrPTlm6_2sgVt7hMZOPfL2M&lang=zh_CN
// const userInfoRes = await axios.get(`https://api.weixin.qq.com/cgi-bin/user/info?access_token=${token}&openid=${fromUser}&lang=zh_CN`);
// // https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=ENCODED_URL&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect
// const user = userInfoRes.data;
// console.log(userInfoRes,'用户信息')
// // 回复文本消息
// const reply = `
// <xml>
// <ToUserName><![CDATA[${fromUser}]]></ToUserName>
// <FromUserName><![CDATA[${message.fromUser}]]></FromUserName>
// <CreateTime>${Date.now()}</CreateTime>
// <MsgType><![CDATA[text]]></MsgType>
// <Content><![CDATA[你好,${user.nickname}!欢迎使用 AI智能体]]></Content>
// </xml>
// `;
// res.send(xml);
// return;
// }
// }
// // 默认回复空
// res.send('success');
// } catch (err) {
// console.error(err);
// res.send('success');
// }
// });
// OAuth2 回调
app.get('/wechat/oauth/callback', async (req, res) => {
try {
const { code, state } = req.query;
console.log(code,"code")
// 用 code 换取 access_token 和 openid
const tokenResp = await axios.get(
`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appid}&secret=${secret}&code=${code}&grant_type=authorization_code`
);
const { access_token, openid } = tokenResp.data;
// 拉取用户信息
const userInfoResp = await axios.get(
`https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=zh_CN`
);
const user = userInfoResp.data;
console.log(user, "获取到用户信息");
// 你可以直接跳转到前端页面并带上用户信息
res.redirect(`https://ai.satabot.com?nickname=${encodeURIComponent(user.nickname)}&openid=${openid}`);
} catch (err) {
console.error(err);
res.send("获取用户信息失败");
}
});
app.get("/test-db", async (req, res) => {
try {
await pool.query(
"INSERT INTO ai_chat_messages (session_id, role, content) VALUES (?, 'user', ?)",
["sess1", "hello"]
);
res.send("OK");
} catch (err) {
console.error(err);
res.send("FAIL");
}
});
app.get("/wechat/userinfo", async (req, res) => {
const { code } = req.query;
if (!code) return res.status(400).send("缺少 code");
try {
// 1. 用 code 换取 access_token 和 openid
const tokenResp = await axios.get(
`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appid}&secret=${secret}&code=${code}&grant_type=authorization_code`
);
const { access_token, openid } = tokenResp.data;
// 2. 拉取用户信息
const userRes = await axios.get(
`https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=zh_CN`
);
const userInfo = userRes.data;
// 3. 返回给前端
res.json({ code: 0, data: userInfo });
} catch (err) {
console.error(err);
res.status(500).send("获取用户信息失败");
}
});
app.post("/user", async (req, res) => {
try {
const { userId } = req.body; // 从请求体里拿到 user_id
if (!userId&&userId!="NaN") {
res.json({
code: 300,
error: "http://ri.satabot.com/#/login?goRemote=1",
msg:"未找到该用户"
});
return;
}
// 查询 user_name
const [rows] = await pool.query(
"SELECT user_name FROM sys_user WHERE user_id = ?",
[userId]
);
if (rows.length === 0) {
res.json({
code: 300,
error: "http://ri.satabot.com/#/login?goRemote=1",
msg:"未找到该用户"
});
return;
}
res.json({ code:0,userId, userName: rows[0].user_name });
} catch (err) {
console.error(err);
res.status(500).send("服务器错误");
}
});
app.post("/getuserByopenid", async (req, res) => {
try {
const { userId } = req.body; // 从请求体里拿到 user_id
if (!userId) {
return res.status(400).send("缺少 user_id");
}
// 查询 user_name
const [rows] = await pool.query(
"SELECT * FROM sys_user WHERE open_id = ?",
[userId]
);
if (rows.length > 0) {
// 用户存在,直接返回
res.json({ code: 0, msg: "找到该用户",userId:rows[0].user_id });
return
} else {
// 用户不存在,插入新用户
const [result] = await pool.query(
"INSERT INTO sys_user (open_id,user_name,nick_name) VALUES (?,?,?)",
[userId,userId,userId]
);
console.log(result,"udgwehvdh")
res.json({code:0,msg:"新增该用户",userId:result.insertId});
return ;
}
} catch (err) {
console.error(err);
res.status(500).send("服务器错误");
}
});
app.get('/wechat', (req, res) => {
const { signature, timestamp, nonce, echostr } = req.query;
console.log(req, "req");
const arr = [TOKEN, timestamp, nonce].sort();
const str = arr.join('');
const hash = crypto.createHash('sha1').update(str).digest('hex');
if (hash === signature) res.send(echostr);
else res.send('验证失败');
});
// 查询用户所有会话列表
app.get("/sessions", async (req, res) => {
console.log("收到的 userId:", req.query.userId);
try {
const userId = req.query.userId;
if (!userId || userId == "NaN" || isNaN(Number(userId)))
{
console.log("userId:", req.query.userId);
res.json({
code: 300,
// error:'https://gsgg.com'
// error: "http://ri.satabot.com/#/dhhdghgbshwadhg"
error: "http://ri.satabot.com/#/login?goRemote=1"
});
return;
// return res.status(500).json({ code: 505, error: "http://1.95.137.212/#/login?redirect=%2Findex&goRemote=1" });
// return res.redirect('http://1.95.137.212/#/login?redirect=%2Findex&goRemote=1');
}
// return;
// 查该用户所有的会话,按最后活跃时间倒序
// const [rows] = await pool.query(
// `
// SELECT session_id,title,MAX(created_at) AS last_time
// FROM ai_chat_messages
// WHERE user_id = ?
// GROUP BY session_id
// ORDER BY last_time DESC
// `,
// [userId]
// );
// const [rows] = await pool.query(`
// SELECT m.session_id, m.title
// FROM ai_chat_messages m
// INNER JOIN (
// SELECT session_id, MAX(created_at) AS last_time
// FROM ai_chat_messages
// WHERE user_id = ?
// GROUP BY session_id
// ) t ON m.session_id = t.session_id AND m.created_at = t.last_time
// WHERE m.user_id = ?
// ORDER BY t.last_time ASC
// `, [userId, userId]);
const [rows] = await pool.query(`
SELECT session_id, title
FROM ai_chat_messages
WHERE user_id = ?
AND title IS NOT NULL
AND title != ''
GROUP BY session_id
ORDER BY MIN(created_at) ASC
`, [userId]);
res.json({
code: 0,
sessions: rows.map(r => ({
sessId: r.session_id,
title: r.title
}))
});
}
catch (err)
{
console.error("查询会话列表失败:", err);
res.status(500).json({ code: 1, error: err.message });
}
});
// 查询会话历史
app.get("/history", async (req, res) => {
try
{
if(!req.query?.sessionId || req.query?.sessionId == "NaN")
{
return res.status(400).json({ code: 1, error: "sessionId 必填" });
}
const { sessionId } = req.query;
// 查询数据库
const [rows] = await pool.query(
"SELECT user_id, role, content, image_base64, created_at,title FROM ai_chat_messages WHERE session_id = ? ORDER BY id ASC",
[sessionId]
);
res.json({
code: 0,
sessionId,
messages: rows,
});
} catch (err) {
console.error("查询会话历史失败:", err);
res.status(500).json({ code: 1, error: err.message });
}
});
// 删除指定会话及其消息
app.delete("/session", async (req, res) => {
try
{
console.log("会话删除:", req.query);
// if (!sessionId || !userId)
if (req.query?.userId == "NaN" || !req.query?.userId || !req.query?.sessionId || req.query?.sessionId == "NaN")
{
return res.status(400).json({ code: 1, error: "sessionId 和 userId 必填" });
}
const { sessionId, userId } = req.query;
// 确保只能删除自己的会话
const [rows] = await pool.query(
"SELECT COUNT(*) as cnt FROM ai_chat_messages WHERE session_id = ? AND user_id = ?",
[sessionId, userId]
);
if (rows[0].cnt === 0) {
return res.status(404).json({ code: 1, error: "会话不存在或无权限" });
}
// 删除该 session 的所有消息
await pool.query(
"DELETE FROM ai_chat_messages WHERE session_id = ? AND user_id = ?",
[sessionId, userId]
);
res.json({
code: 0,
message: `会话 ${sessionId} 已删除`,
});
}
catch (err)
{
console.error("删除会话失败:", err);
res.status(500).json({ code: 1, error: err.message });
}
});
// 新建会话(实际上就是往消息表里插入一条记录)
app.post("/session", async (req, res) => {
try
{
console.log("新建会话: ",req.body)
if (req.body?.userId == "NaN" || !req.body?.userId || !req.body?.sessionId || req.body?.sessionId == "NaN")
{
console.log("=== userId: ",req.body?.userId)
return res.status(400).json({ code: 1, error: "userId 和 sessionId 必填" });
}
const { userId, sessionId } = req.body;
// 插入一条“新会话”的首条消息,可以是空内容
await pool.query(
"INSERT INTO ai_chat_messages (user_id, session_id, role, content) VALUES (?, ?, 'system', '')",
[userId, sessionId]
);
res.json({
code: 0,
message: `会话 ${sessionId} 已创建`,
});
}
catch (err)
{
console.error("创建会话失败:", err);
res.status(500).json({ code: 1, error: err.message });
}
});
const stopFlags = new Map();
const { Mutex } = require('async-mutex');
const aiChatNum = new Map(); // userId -> number
const mutexes = new Map(); // userId -> Mutex
function getMutex(userId) {
if (!mutexes.has(userId)) mutexes.set(userId, new Mutex());
return mutexes.get(userId);
}
async function setQuota(userId,num) {
const m = getMutex(userId);
return await m.runExclusive(async () => {
console.log("更新用户对话次数:", userId, num);
aiChatNum.set(userId, num);
});
}
async function hasQuota(userId) {
const m = getMutex(userId);
return await m.runExclusive(async () => {
console.log("更新用户对话次数:", userId, num);
return aiChatNum.has(userId);
});
}
async function getQuota(userId) {
const m = getMutex(userId);
return await m.runExclusive(async () => {
console.log("更新用户对话次数:", userId, num);
return aiChatNum.get(userId);
});
}
async function removeQuota(userId) {
const m = getMutex(userId);
return await m.runExclusive(async () => {
aiChatNum.delete(userId);
});
}
// ====================== API ======================
app.post("/updateAiChatNum", async (req, res) => {
const { userId } = req.body;
console.log(userId,98888888888);
if(!hasQuota(userId)){
return res.status(400).json({ error: "用户不存在" });
}else{
// 删除当前内存map存放的可用数量
removeQuota(userId);
await pool.query(
"update sys_user set ai_chat_num = ? where user_id= ?",
[aiChatNum.get(userId),userId]
);
res.json({ code: 0, message: "更新成功" });
}
})
app.post("/analyze", upload.single("file"), async (req, res) => {
try
{
const { userId } = req.body;
if(!hasQuota(userId)){
const [userRemainChatNum] = await pool.query(
"SELECT ai_chat_num from sys_user where user_id= ?",
[userId]
);
if (userRemainChatNum.length>0) {
setQuota(userId,userRemainChatNum[0].ai_chat_num);
}
}else{
if(getQuota(userId)<=0 && getQuota(userId)!==-1){
return res.status(400).json({ error: "对话次数已用完,请完成账号注册" });
}
}
const { requestId } = req.body;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.setHeader("Transfer-Encoding", "chunked");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
let aborted = false;
req.on("close", () => {
console.log("🔴 前端断开连接,停止整个分析流程");
aborted = true;
});
console.time("analyze");
const logBody = { ...req.body };
if(req.body?.imageBase64)
{
delete logBody.imageBase64;
}
console.log("analyze: ", logBody);
// console.log("analyze: ",req.body)
if (!req.body?.userId || !req.body?.sessionId)
{
console.log("缺少必要参数");
return res.status(400).json({ error: "缺少必要参数" });
}
const { userPrompt, imageBase64, sessionId } = req.body;
const [rows] = await pool.query(
`
SELECT EXISTS(
SELECT 1
FROM ai_chat_messages
WHERE session_id = ?
AND user_id = ?
AND title IS NOT NULL
AND title != ''
) AS hasTitle
`,
[sessionId, userId]
);
const existingTitle = rows[0].hasTitle === 1; // true / false
console.log(existingTitle, rows?.[0], 9999, rows)
// 如果前端没有传 sessionId,生成一个
const sessId = sessionId || Date.now().toString();
// ------------------ 获取历史对话(上下文) ------------------
const [historyRows] = await pool.query(
"SELECT role, content, image_base64 FROM ai_chat_messages WHERE session_id = ? AND user_id = ? ORDER BY created_at ASC",
[sessId, userId || null]
);
// 构建历史消息数组
const historyMessages = historyRows.map(row => {
const message = {
role: row.role,
content: []
};
// 添加文本内容
if (row.content)
{
message.content.push({ type: "text", text: row.content });
}
// 添加图片内容
if (row.image_base64)
{
message.content.push({
type: "image_url",
image_url: {
url: row.image_base64.startsWith("data:")
? row.image_base64
: `data:image/png;base64,${row.image_base64}`,
}
});
}
return message;
});
// ------------------ 保存用户提问 ------------------
await pool.query(
"INSERT INTO ai_chat_messages (session_id, user_id, role, content, image_base64) VALUES (?, ?, 'user', ?, ?)",
[sessId, userId || null, userPrompt || "", imageBase64 || null]
);
// console.log("userPrompt: ",userPrompt);
// console.log("imageBase64: ",imageBase64);
// console.log("userId: ",userId);
// console.log("sessionId: ",sessionId);
// ------------------ 图像分析 ------------------
// 构建包含上下文的消息数组
const imageAnalysisMessages = [
{
role: "system",
content: "你是一名图像分析专家,需要结合历史对话内容分析当前图像。"
},
...historyMessages, // 添加历史对话作为上下文
{
role: "user",
content: [
{ type: "text", text: userPrompt || "请描述图片内容。" },
imageBase64
? {
type: "image_url",
image_url: {
url: imageBase64.startsWith("data:")
? imageBase64
: `data:image/png;base64,${imageBase64}`,
},
}
: null,
].filter(Boolean),
},
];
const completion = await client.chat.completions.create({
model: "gpt-4.1",
stream: true,
messages: imageAnalysisMessages
});
console.log("====== 开始图像分析... ======")
let sceneText = "";
/* TLS 加密缓冲,小块数据可能先缓存在服务器端,不立即发送,会导致图像分析模型的结果不能立即返回 */
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
res.socket.setNoDelay(true);
for await (const chunk of completion)
{
if (stopFlags.get(requestId)) {
console.log("⛔ 图像分析被前端中断");
res.end();
stopFlags.delete(requestId); // 用完清理
return;
}
const delta = chunk.choices[0]?.delta?.content || "";
sceneText += delta;
if (delta)
{
res.write(
`data: ${JSON.stringify({ type: "scene", content: delta })}\n\n`
);
}
// if (delta)
// {
// for (const ch of delta)
// {
// res.write(`data: ${JSON.stringify({ type: "scene", content: ch })}\n\n`);
// await new Promise(r => setTimeout(r, 0)); // 控制渲染节奏
// }
// }
}
console.log("====== 图像分析结束 ======")
// 保存图像分析结果到数据库
// await pool.query(
// "INSERT INTO ai_chat_messages (session_id, user_id, role, content) VALUES (?, ?, 'assistant', ?)",
// [sessId, userId || null, sceneText]
// );
// ------------------ 3️⃣ LLM 分析 ------------------
// console.log("LLM 分析 sceneText: ", sceneText)
// const preText = "详细分析这个光伏场站,根据图片给出详细的运维方案"
// + "如果这个光伏场站需要割草怎么做,给出一份详细的计划"
// + "如果这个光伏场站需要用无人机清洗怎么做,给出一份详细的计划"
// + "如果这个光伏场站需要巡检怎么做,给出一份详细的计划"
// 构建包含完整上下文的LLM消息数组
const llmMessages = [
{
role: "system",
content: "你是一名新能源项目的图像分析AI分析师,需要结合历史对话提供专业建议。"
},
...historyMessages, // 添加历史对话作为上下文
{
role: "user",
content: userPrompt || "请基于图片分析提供建议。"
},
{
role: "assistant",
content: sceneText // 添加当前图像分析结果
}
];
const llmStream = await client1.chat.completions.create({
model: "gpt-5-mini",
stream: true,
messages: llmMessages
});
console.log("====== 开始深度思考... ======")
let llmText = "";
for await (const chunk of llmStream)
{
if (stopFlags.get(requestId)) {
console.log("⛔ 深度分析被前端中断");
res.end();
stopFlags.delete(requestId);
return;
}
const delta = chunk.choices[0]?.delta?.content || "";
llmText += delta;
if (delta)
res.write(
`data: ${JSON.stringify({ type: "llm", content: delta})}\n\n`
);
/* delta 很长,这会 人为拉长处理时间。 */
// if (delta)
// {
// for (const ch of delta)
// {
// res.write(`data: ${JSON.stringify({ type: "llm", content: ch })}\n\n`);
// await new Promise(r => setTimeout(r, 0)); // 控制渲染节奏
// }
// }
}
// 保存 LLM 输出到数据库
/* 加标题需要异步,且在会话开始时就进行 */
if (existingTitle) {
console.log("uidh")
await pool.query(
"INSERT INTO ai_chat_messages (session_id, user_id, role, content) VALUES (?, ?, 'assistant', ?)",
[sessId, userId || null, llmText]
);
} else {
console.log("uidh1111")
try
{
const titlePrompt = `请根据以下对话内容生成一个简短的标题(不超过15字):
用户问题: ${userPrompt}
AI回答: ${llmText}`;
const titleResp = await client1.chat.completions.create({
model: "gpt-5-mini",
messages: [
{ role: "system", content: "你是标题生成助手,只返回标题,不要解释。" },
{ role: "user", content: titlePrompt }
]
});
const title = titleResp.choices[0]?.message?.content?.trim() || "未命名会话";
res.write(
`data: ${JSON.stringify({ type: "title", content: title })}\n\n`
);
console.log("生成的标题:", title);
// 保存到数据库(建议你给 ai_chat_messages 表新增一列 title,或者单独建一张会话表)
await pool.query(
"INSERT INTO ai_chat_messages (session_id, user_id, role, content,title) VALUES (?, ?, 'assistant', ?,?)",
[sessId, userId || null, llmText,title]
);
}
catch (err)
{
console.error("生成标题失败:", err.message);
}
}
res.write("data: [DONE]\n\n");
res.end();
// 删除上传文件
if (req.file && req.file.path)
{
fs.unlinkSync(req.file.path);
}
console.log("====== 分析完成 ======");
console.timeEnd("analyze");
// 更新用户剩余对话次数
let userChatNum = aiChatNum.get(userId);
if(userChatNum!==-1){
userChatNum = Math.max(0, userChatNum - 1); // 防止变成负数
setQuota(userId,userChatNum);
console.log(`用户剩余AI 对话次数: ${userChatNum}`);
}
}
catch (err)
{
console.error("❌ 调用失败:", err.response?.data || err.message);
res.status(500).json({ code: 1, error: err.response?.data || err.message });
}
});
app.post("/stop", express.json(), (req, res) => {
const { requestId } = req.body;
console.log("🔴 收到停止信号:", requestId);
stopFlags.set(requestId, true);
console.log(stopFlags,"消息id");
res.json({ ok: true });
});
app.post('/submit',async (req, res) => {
const { company, scenes, needs, userId } = req.body;
console.log("收到数据:", company, scenes, needs, userId);
if (!company || !needs || !userId) {
return res.status(400).json({ message: "缺少必要字段" });
}
try {
const sql =
"INSERT INTO pv_form (company, scenes, needs, userId) VALUES (?, ?, ?, ?)";
await pool.query(sql, [company, scenes, needs, userId || null]);
res.json({ code:200,message: "提交成功!" });
} catch (err) {
console.error("数据库插入失败:", err);
res.status(500).json({ message: "数据库写入失败" });
}
});
// // 获取微信授权回调接口
// app.get("/callback", async (req, res) => {
// const code = req.query.code;
// try {
// // 用 code 换 openid
// const resp = await axios.get("https://api.weixin.qq.com/sns/oauth2/access_token", {
// params: {
// appid: APPID,
// secret: APPSECRET,
// code: code,
// grant_type: "authorization_code",
// },
// });
// const data = resp.data;
// console.log("微信授权返回:", data);
// if (data.openid) {
// res.json({
// code: 0,
// openid:data.openid
// })
// // 判断数据库里是否已经有这个 openid
// // 没有 → 注册;有 → 登录
// // res.send(`授权成功,openid: ${data.openid}`);
// } else {
// res.json({
// code: 500,
// msg:"微信授权失败"+ JSON.stringify(data)
// })
// // res.send("授权失败: " + JSON.stringify(data));
// }
// } catch (err) {
// console.error(err);
// res.send("服务器错误");
// }
// });
// 静态文件服务
app.use("/uploads", express.static("uploads"));
// 启动服务
app.listen(port, () => {
console.log(`✅ 服务器运行:http://localhost:${port}`);
});