Files
AI/server2.js

687 lines
20 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 bodyParser = require("body-parser");
const axios = require('axios');
require('dotenv').config();
const app = express();
const port = 30009;
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 }));
// 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(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
});
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
});
// 公众号
function getMenuForPlatform(req) {
const userAgent = req.headers['user-agent'];
// 判断请求是否来自微信客户端
if (/MicroMessenger/i.test(userAgent)) {
// 微信菜单
return {
button: [
{
type: 'click',
name: '获取 OpenID',
key: 'GET_OPENID',
},
{
type: 'view',
name: '访问官网',
url: 'https://www.example.com',
sub_button: [ // 二级菜单
{
type: 'view',
name: '关于我们',
url: 'https://www.example.com/about',
},
{
type: 'view',
name: '联系我们',
url: 'https://www.example.com/contact',
}
]
}
]
};
}
}
// 获取 access_token
async function getAccessToken() {
try {
const res = await axios.get(accessTokenUrl);
return res.data.access_token;
} catch (error) {
console.error('获取 access_token 失败:', error);
}
}
// 创建菜单的接口
app.post('/create-menu', async (req, res) => {
const accessToken = await getAccessToken();
const menuData = getMenuForPlatform(req); // 根据请求判断平台并获取菜单
const createMenuUrl = `https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${accessToken}`;
try {
const response = await axios.post(createMenuUrl, menuData);
res.send(`菜单创建成功: ${JSON.stringify(response.data)}`);
} catch (error) {
res.status(500).send(`菜单创建失败: ${error}`);
}
});
// 处理微信自定义菜单点击事件
app.post('/wechat', (req, res) => {
const xmlData = req.body.xml;
if (xmlData.Event === 'CLICK' && xmlData.EventKey === 'GET_OPENID') {
const openid = xmlData.FromUserName;
console.log(`用户的 openid: ${openid}`);
res.send(`<xml>
<ToUserName><![CDATA[${openid}]]></ToUserName>
<FromUserName><![CDATA[${xmlData.ToUserName}]]></FromUserName>
<CreateTime>${Date.now()}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[您的 OpenID 是:${openid}]]></Content>
</xml>`);
} else {
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) {
return res.status(400).send("缺少 user_id");
}
// 查询 user_name
const [rows] = await pool.query(
"SELECT user_name FROM sys_user WHERE user_id = ?",
[userId]
);
if (rows.length === 0) {
return res.status(404).send("未找到该用户");
}
res.json({ code:0,userId, userName: rows[0].user_name });
} catch (err) {
console.error(err);
res.status(500).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: "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]);
res.json({
code: 0,
userId,
sessions: rows.map((r) => ({ sessId: r.session_id, title: r.title })), // 只返回 session_id 列表
});
}
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 });
}
});
// ====================== API ======================
app.post("/analyze", upload.single("file"), async (req, res) => {
try
{
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");
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, userId, sessionId } = req.body;
// 如果前端没有传 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 = "";
for await (const chunk of completion)
{
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)
{
const delta = chunk.choices[0]?.delta?.content || "";
llmText += delta;
// if (delta)
// res.write(
// `data: ${JSON.stringify({ type: "llm", content: delta})}\n\n`
// );
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 输出到数据库
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("====== 分析完成 ======")
}
catch (err)
{
console.error("❌ 调用失败:", err.response?.data || err.message);
res.status(500).json({ code: 1, error: err.response?.data || err.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}`);
});